diff --git a/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/WebMercatorAlgebra.kt b/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/WebMercatorAlgebra.kt index f4ea339b8..959146072 100644 --- a/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/WebMercatorAlgebra.kt +++ b/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/WebMercatorAlgebra.kt @@ -9,24 +9,55 @@ import space.kscience.kmath.operations.Algebra import kotlin.math.* 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)" } - require(latitude in (-PI / 2)..(PI / 2)) { "Latitude $latitude is not in (-PI/2)..(PI/2)" } + + override fun equals(other: Any?): Boolean { + 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 { - public fun ofRadians(longitude: Double, latitude: Double): GeodeticCoordinates = - GeodeticCoordinates(latitude, longitude) + override fun hashCode(): Int { + var result = latitude.hashCode() + result = 31 * result + longitude.hashCode() + return result + } - public fun ofDegrees(longitude: Double, latitude: Double): GeodeticCoordinates = - GeodeticCoordinates(latitude * PI / 180, longitude * PI / 180) + override fun toString(): String { + 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 object WebMercatorAlgebra : Algebra { - 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 @@ -34,7 +65,7 @@ public object WebMercatorAlgebra : Algebra { 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" } - val scaleFactor = 256.0 / 2 / PI * 2.0.pow(zoom) + val scaleFactor = scaleFactor(zoom) return WebMercatorCoordinates( zoom = zoom, x = scaleFactor * (longitude + PI), @@ -56,8 +87,8 @@ public object WebMercatorAlgebra : Algebra { tan(PI / 4 + target.latitude / 2) / tan(PI / 4 + base.latitude / 2) ) - val computedZoom = zoom ?: floor( log2(PI / max(abs(xOffsetUnscaled), abs(yOffsetUnscaled)))) - val scaleFactor = 256.0 / 2 / PI * 2.0.pow(computedZoom) + val computedZoom = zoom ?: floor(log2(PI / max(abs(xOffsetUnscaled), abs(yOffsetUnscaled)))) + val scaleFactor = scaleFactor(computedZoom) return WebMercatorCoordinates( computedZoom, x = scaleFactor * xOffsetUnscaled, diff --git a/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/MercatorTest.kt b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/MercatorTest.kt index 5514e5521..81b74ff6e 100644 --- a/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/MercatorTest.kt +++ b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/MercatorTest.kt @@ -6,19 +6,35 @@ package space.kscience.kmath.geometry import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertTrue class MercatorTest { @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 spbCoordinates = GeodeticCoordinates.ofDegrees(59.9311, 30.3609) - val offset = WebMercatorAlgebra.computeOffset(mskCoordinates, spbCoordinates) + val offset = WebMercatorAlgebra.computeOffset(mskCoordinates, spbCoordinates) assertTrue { offset.x in -127.0..128.0 } assertTrue { offset.y in -127.0..128.0 } - assertTrue { offset.zoom > 1.0 } + assertTrue { offset.zoom > 0.0 } } } \ No newline at end of file