MapCompose icon indicating copy to clipboard operation
MapCompose copied to clipboard

GPS coordinates to normalized coordinates

Open feczkob opened this issue 1 year ago • 8 comments

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:

  • normalized coordinates: 0.5064745545387268, 0.3440358340740204
  • GPS coordinates: 48°51′ N 2°21′ E, that is 48.86 degrees North latitude and 2.35 degrees East longitude

feczkob avatar Mar 16 '24 19:03 feczkob

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

p-lr avatar Mar 17 '24 07:03 p-lr

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)?

feczkob avatar Mar 18 '24 18:03 feczkob

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 :)

MascotWorld avatar Mar 19 '24 13:03 MascotWorld

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.

Nohus avatar Mar 19 '24 14:03 Nohus

@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).

p-lr avatar Mar 19 '24 18:03 p-lr

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)
} 

MascotWorld avatar Mar 19 '24 18:03 MascotWorld

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.

feczkob avatar Mar 22 '24 16:03 feczkob

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

p-lr avatar Mar 22 '24 17:03 p-lr