p-lr / MapCompose

A fast, memory efficient Jetpack Compose library to display tiled maps, with support for markers, paths, and rotation.
Apache License 2.0
224 stars 20 forks source link

GPS coordinates to normalized coordinates #111

Closed feczkob closed 6 months ago

feczkob commented 8 months ago

This is rather a question than an issue: how can one calculate the normalized coordinates from GPS latitude and longitude? There's an example for Paris:

p-lr commented 8 months ago

You need to convert the GPS coordinates into the projected coordinates. In this case, most map providers (including Google maps) use the "Web Mercator" projection (or, in more technical terms, EPSG 3857).

Here is the formula:

import kotlin.math.*
import java.io.IOException

fun main() {
    val (X, Y) = doProjection(48.86, 2.35)!!  // Paris
    println("projected: X=$X , Y=$Y")

    val x = normalize(X, min = X0, max = -X0)
    val y = normalize(Y, min = -X0, max = X0)
    println("normalized: x=$x , y=$y")
}

fun doProjection(latitude: Double, longitude: Double): Pair<Double, Double>? {
    if (abs(latitude) > 90 || abs(longitude) > 180) {
        return null
    }
    val num = longitude * 0.017453292519943295 // 2*pi / 360
    val X = 6378137.0 * num
    val a = latitude * 0.017453292519943295
    val Y = 3189068.5 * ln((1.0 + sin(a)) / (1.0 - sin(a)))

    return Pair(X, Y)
}

fun normalize(t: Double, min: Double, max: Double): Double {
    return (t - min) / (max - min)
}

private const val X0 = -20037508.3427892476
feczkob commented 8 months ago

Thank you very much!

I have another question, if it's not a problem: I generated some tiles using this project: https://github.com/magellium/osmtilemaker. I rendered only for the zoom levels from 13 to 18, and then I renamed the resulting folders and images to start from 0 (to follow the z/x/y convention). Then I would like to import these tiles into a project that uses MapCompose. For the tile generation I selected a rectanglular area (latMin, latMax, lonMin, lonMax). The generated tiles are of 256 x 256 pixels. I have 57 tiles in a column and 50 in a row in the highest zoom level (18). I initialize the MapState as

MapState(levelCount = 6, fullWidth = 12800, fullHeight = 14592) {
            scale(0f)
            minimumScaleMode(Forced((1 / 2.0.pow(5 - 0)).toFloat()))
//            minimumScaleMode(Fill)
        }.apply {
            addLayer(localTileStreamProvider)
            shouldLoopScale = true
        }

In the app I would like to map GPS coordinates to the generated tiles and then use markers. It seems to be an easy mapping, simply [(latX - latMin) / (latMax - latMin), (lonX - lonMin) / (lonMax - lonMin)] is supposed to give the transformed coordinates. However, for some reason if I plot a marker at [1, 1], it won't be in the bottom right corner of the map. On the picture there's another marker at [0, 0], that's working fine.

fun addMarker() {
        _state.update {
            return it.addMarker(UUID.randomUUID().toString(), 1.0, 1.0) { Marker() }
        }
    }

image

Do you know what may I do wrong?

PS. Can I reach you either in the Kotlin language's Slack workspace (my handler is @feczkob) or in Discord (my name there is @feczkob too)?

MascotWorld commented 8 months ago

It would be nice to add the zoomIn and zoomOut functions (you can implement them yourself, but it’s better to have them initially). Google maps also has LatLngBounds for focusing on a certain set of points that fit on the screen (almost like an open cluster), it would also be good to add a simple function for this (I found an implementation of this behavior for Cluster and used it myself). And when will it be possible to add geozones? (Polygon and Circle in Google map) It would also be nice to add a conditional class LatLong to use GPS and normalized coordinates out of the box :)

Nohus commented 8 months ago

Google maps also has LatLngBounds for focusing on a certain set of points that fit on the screen (almost like an open cluster), it would also be good to add a simple function for this

I've added them already. There are versions of scroll methods that take a BoundingBox.

p-lr commented 8 months ago

@feczkob Your calculation of the map size is good. However, I believe your issue is that the area covered by the level 13 is greater than the area covered by the level 18. In a well formed map, the number of tiles at level n + 1 is exactly 4 times the number of tiles of the level n. When selecting an area, you need to pick tiles depending on the lowest level, not the highest level. In you case, you should pick tiles of level 13 which cover your area, and then pick all tiles beneath those tiles at level 13.

This is an issue that I know well, as in some cases, the area actually downloaded is significantly greater than the selected bounding box (especially when the min level is below 14).

MascotWorld commented 8 months ago

Google maps also has LatLngBounds for focusing on a certain set of points that fit on the screen (almost like an open cluster), it would also be good to add a simple function for this

I've added them already. There are versions of scroll methods that take a BoundingBox.

yes, I already use it. I had in mind that I needed to make it a little more convenient and out of the box. (or i miss that) not like this:

fun  List<Pair<Double,Double>>.getBounds(): BoundingBox? {
    val latLongs = this
    if (latLongs.isEmpty()) return null

    var minX: Double = Double.MAX_VALUE
    var maxX: Double = Double.MIN_VALUE
    var minY: Double = Double.MAX_VALUE
    var maxY: Double = Double.MIN_VALUE
    latLongs.forEach {
        minX = if (it.first < minX) it.first else minX
        maxX = if (it.first > maxX) it.first else maxX
        minY = if (it.second < minY) it.second else minY
        maxY = if (it.second > maxY) it.second else maxY
    }
    return BoundingBox(minX, minY, maxX, maxY)
} 
feczkob commented 8 months ago

Hey @p-lr , I created a project to fetch the tiles correctly: https://github.com/feczkob/osm-tile-manager After downloading they can be renamed to match the zoom/x/y convention too.

Thank you very much for the explanation, now it's clear what went wrong previously.

p-lr commented 8 months ago

Glad it worked! Your project surely will be useful to others. I'm adding a link to your project in this discussion. Thanks.