Closed feczkob closed 6 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
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() }
}
}
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)?
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 :)
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
.
@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).
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)
}
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.
Glad it worked! Your project surely will be useful to others. I'm adding a link to your project in this discussion. Thanks.
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:
0.5064745545387268, 0.3440358340740204
48°51′ N 2°21′ E
, that is48.86
degrees North latitude and2.35
degrees East longitude