Web mercator conversion

This commit is contained in:
Alexander Nozik 2022-06-16 21:37:13 +03:00
parent 081304f110
commit 027ab64989
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
2 changed files with 62 additions and 15 deletions

View File

@ -9,24 +9,55 @@ import space.kscience.kmath.operations.Algebra
import kotlin.math.* import kotlin.math.*
public class GeodeticCoordinates private constructor(public val latitude: Double, public val longitude: Double) { public class GeodeticCoordinates private constructor(public val latitude: Double, public val longitude: Double) {
init {
require(longitude in (-PI)..(PI)) { "Longitude $longitude is not in (-PI)..(PI)" } override fun equals(other: Any?): Boolean {
require(latitude in (-PI / 2)..(PI / 2)) { "Latitude $latitude is not in (-PI/2)..(PI/2)" } if (this === other) return true
if (other == null || this::class != other::class) return false
other as GeodeticCoordinates
if (latitude != other.latitude) return false
if (longitude != other.longitude) return false
return true
} }
public companion object { override fun hashCode(): Int {
public fun ofRadians(longitude: Double, latitude: Double): GeodeticCoordinates = var result = latitude.hashCode()
GeodeticCoordinates(latitude, longitude) result = 31 * result + longitude.hashCode()
return result
}
public fun ofDegrees(longitude: Double, latitude: Double): GeodeticCoordinates = override fun toString(): String {
GeodeticCoordinates(latitude * PI / 180, longitude * PI / 180) return "GeodeticCoordinates(latitude=$latitude, longitude=$longitude)"
}
public companion object {
public fun ofRadians(latitude: Double, longitude: Double): GeodeticCoordinates {
require(longitude in (-PI)..(PI)) { "Longitude $longitude is not in (-PI)..(PI)" }
return GeodeticCoordinates(latitude, longitude.rem(PI / 2))
}
public fun ofDegrees(latitude: Double, longitude: Double): GeodeticCoordinates {
require(latitude in (-90.0)..(90.0)) { "Latitude $latitude is not in -90..90" }
return GeodeticCoordinates(latitude * PI / 180, (longitude.rem(180) * PI / 180))
}
} }
} }
public data class WebMercatorCoordinates(val zoom: Double, val x: Double, val y: Double) public data class WebMercatorCoordinates(val zoom: Double, val x: Double, val y: Double)
public object WebMercatorAlgebra : Algebra<WebMercatorCoordinates> { public object WebMercatorAlgebra : Algebra<WebMercatorCoordinates> {
fun WebMercatorCoordinates.toGeodetic(): GeodeticCoordinates = TODO()
private fun scaleFactor(zoom: Double) = 256.0 / 2 / PI * 2.0.pow(zoom)
public fun WebMercatorCoordinates.toGeodetic(): GeodeticCoordinates {
val scaleFactor = scaleFactor(zoom)
val longitude = x / scaleFactor - PI
val latitude = (atan(exp(PI - y / scaleFactor)) - PI / 4) * 2
return GeodeticCoordinates.ofRadians(latitude, longitude)
}
/** /**
* https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas * https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas
@ -34,7 +65,7 @@ public object WebMercatorAlgebra : Algebra<WebMercatorCoordinates> {
public fun GeodeticCoordinates.toMercator(zoom: Double): WebMercatorCoordinates { public fun GeodeticCoordinates.toMercator(zoom: Double): WebMercatorCoordinates {
require(abs(latitude) <= 2 * atan(E.pow(PI)) - PI / 2) { "Latitude exceeds the maximum latitude for mercator coordinates" } require(abs(latitude) <= 2 * atan(E.pow(PI)) - PI / 2) { "Latitude exceeds the maximum latitude for mercator coordinates" }
val scaleFactor = 256.0 / 2 / PI * 2.0.pow(zoom) val scaleFactor = scaleFactor(zoom)
return WebMercatorCoordinates( return WebMercatorCoordinates(
zoom = zoom, zoom = zoom,
x = scaleFactor * (longitude + PI), x = scaleFactor * (longitude + PI),
@ -56,8 +87,8 @@ public object WebMercatorAlgebra : Algebra<WebMercatorCoordinates> {
tan(PI / 4 + target.latitude / 2) / tan(PI / 4 + base.latitude / 2) tan(PI / 4 + target.latitude / 2) / tan(PI / 4 + base.latitude / 2)
) )
val computedZoom = zoom ?: floor( log2(PI / max(abs(xOffsetUnscaled), abs(yOffsetUnscaled)))) val computedZoom = zoom ?: floor(log2(PI / max(abs(xOffsetUnscaled), abs(yOffsetUnscaled))))
val scaleFactor = 256.0 / 2 / PI * 2.0.pow(computedZoom) val scaleFactor = scaleFactor(computedZoom)
return WebMercatorCoordinates( return WebMercatorCoordinates(
computedZoom, computedZoom,
x = scaleFactor * xOffsetUnscaled, x = scaleFactor * xOffsetUnscaled,

View File

@ -6,12 +6,28 @@
package space.kscience.kmath.geometry package space.kscience.kmath.geometry
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class MercatorTest { class MercatorTest {
@Test @Test
fun mercatorOffset(){ fun boundaries() {
assertEquals(GeodeticCoordinates.ofDegrees(45.0, 20.0), GeodeticCoordinates.ofDegrees(45.0, 200.0))
}
@Test
fun webMercatorConversion() = with(WebMercatorAlgebra) {
val mskCoordinates = GeodeticCoordinates.ofDegrees(55.7558, 37.6173)
val m = mskCoordinates.toMercator(2.0)
val r = m.toGeodetic()
assertEquals(mskCoordinates.longitude, r.longitude,1e-4)
assertEquals(mskCoordinates.latitude, r.latitude,1e-4)
}
@Test
fun webMercatorOffset() {
val mskCoordinates = GeodeticCoordinates.ofDegrees(55.7558, 37.6173) val mskCoordinates = GeodeticCoordinates.ofDegrees(55.7558, 37.6173)
val spbCoordinates = GeodeticCoordinates.ofDegrees(59.9311, 30.3609) val spbCoordinates = GeodeticCoordinates.ofDegrees(59.9311, 30.3609)
@ -19,6 +35,6 @@ class MercatorTest {
assertTrue { offset.x in -127.0..128.0 } assertTrue { offset.x in -127.0..128.0 }
assertTrue { offset.y in -127.0..128.0 } assertTrue { offset.y in -127.0..128.0 }
assertTrue { offset.zoom > 1.0 } assertTrue { offset.zoom > 0.0 }
} }
} }