Add ellipsoid mercator backward transformation
This commit is contained in:
parent
8f489ea0f9
commit
1a7646f311
@ -1,7 +1,9 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package center.sciprog.maps.coordinates
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.jvm.JvmInline
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
|
@Serializable
|
||||||
@JvmInline
|
@JvmInline
|
||||||
public value class Distance internal constructor(public val kilometers: Double) : Comparable<Distance> {
|
public value class Distance internal constructor(public val kilometers: Double) : Comparable<Distance> {
|
||||||
override fun compareTo(other: Distance): Int = this.kilometers.compareTo(other.kilometers)
|
override fun compareTo(other: Distance): Int = this.kilometers.compareTo(other.kilometers)
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package center.sciprog.maps.coordinates
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
import space.kscience.kmath.geometry.tan
|
import space.kscience.kmath.geometry.tan
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
@Serializable
|
||||||
public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRadius: Distance) {
|
public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRadius: Distance) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,8 +19,13 @@ public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRa
|
|||||||
*/
|
*/
|
||||||
public val inverseF: Double = equatorRadius.kilometers / (equatorRadius.kilometers - polarRadius.kilometers)
|
public val inverseF: Double = equatorRadius.kilometers / (equatorRadius.kilometers - polarRadius.kilometers)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eccentricity squared
|
||||||
|
*/
|
||||||
public val eSquared: Double = 2 * f - f * f
|
public val eSquared: Double = 2 * f - f * f
|
||||||
|
|
||||||
|
public val eccentricity: Double = sqrt(eSquared)
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
|
|
||||||
public val WGS84: GeoEllipsoid = GeoEllipsoid(
|
public val WGS84: GeoEllipsoid = GeoEllipsoid(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package center.sciprog.maps.coordinates
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
import space.kscience.kmath.geometry.degrees
|
import space.kscience.kmath.geometry.degrees
|
||||||
import space.kscience.kmath.geometry.normalized
|
import space.kscience.kmath.geometry.normalized
|
||||||
@ -10,6 +11,7 @@ import space.kscience.kmath.geometry.radians
|
|||||||
*
|
*
|
||||||
* @param elevation is optional
|
* @param elevation is optional
|
||||||
*/
|
*/
|
||||||
|
@Serializable
|
||||||
public class GeodeticMapCoordinates(
|
public class GeodeticMapCoordinates(
|
||||||
public val latitude: Angle,
|
public val latitude: Angle,
|
||||||
public val longitude: Angle,
|
public val longitude: Angle,
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package center.sciprog.maps.coordinates
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
import space.kscience.kmath.geometry.normalized
|
import space.kscience.kmath.geometry.normalized
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A coordinate-bearing pair
|
* A coordinate-bearing pair
|
||||||
*/
|
*/
|
||||||
|
@Serializable
|
||||||
public data class GmcPose(val coordinates: GeodeticMapCoordinates, val bearing: Angle) {
|
public data class GmcPose(val coordinates: GeodeticMapCoordinates, val bearing: Angle) {
|
||||||
val latitude: Angle get() = coordinates.latitude
|
val latitude: Angle get() = coordinates.latitude
|
||||||
val longitude: Angle get() = coordinates.longitude
|
val longitude: Angle get() = coordinates.longitude
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
package center.sciprog.maps.coordinates
|
package center.sciprog.maps.coordinates
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.kmath.geometry.*
|
import space.kscience.kmath.geometry.*
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
@ -27,25 +28,42 @@ public interface MapProjection<T : Any> {
|
|||||||
* @param baseLongitude the longitude offset in radians
|
* @param baseLongitude the longitude offset in radians
|
||||||
* @param ellipsoid - a [GeoEllipsoid] to be used for conversion
|
* @param ellipsoid - a [GeoEllipsoid] to be used for conversion
|
||||||
*/
|
*/
|
||||||
|
@Serializable
|
||||||
public open class MercatorProjection(
|
public open class MercatorProjection(
|
||||||
public val baseLongitude: Angle = Angle.zero,
|
public val baseLongitude: Angle = Angle.zero,
|
||||||
public val ellipsoid: GeoEllipsoid = GeoEllipsoid.sphere,
|
public val ellipsoid: GeoEllipsoid = GeoEllipsoid.sphere,
|
||||||
) : MapProjection<ProjectionCoordinates> {
|
) : MapProjection<ProjectionCoordinates> {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taken from https://github.com/geotools/geotools/blob/main/modules/library/referencing/src/main/java/org/geotools/referencing/operation/projection/Mercator.java#L164
|
||||||
|
*/
|
||||||
|
private fun cphi2(ts: Double): Double {
|
||||||
|
val eccnth: Double = 0.5 * ellipsoid.eccentricity
|
||||||
|
var phi: Double = PI / 2 - 2.0 * atan(ts)
|
||||||
|
for (i in 0 until 15) {
|
||||||
|
val con: Double = ellipsoid.eccentricity * sin(phi)
|
||||||
|
val dphi: Double = PI / 2 - 2.0 * atan(ts * ((1 - con) / (1 + con)).pow(eccnth)) - phi
|
||||||
|
phi += dphi
|
||||||
|
if (abs(dphi) <= 1e-10) {
|
||||||
|
return phi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error("Inverse mercator projection transformation failed to converge")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun toGeodetic(pc: ProjectionCoordinates): GeodeticMapCoordinates {
|
override fun toGeodetic(pc: ProjectionCoordinates): GeodeticMapCoordinates {
|
||||||
val res = GeodeticMapCoordinates.ofRadians(
|
return if (ellipsoid === GeoEllipsoid.sphere) {
|
||||||
|
GeodeticMapCoordinates.ofRadians(
|
||||||
atan(sinh(pc.y / ellipsoid.equatorRadius)),
|
atan(sinh(pc.y / ellipsoid.equatorRadius)),
|
||||||
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius),
|
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius),
|
||||||
)
|
)
|
||||||
|
|
||||||
return if (ellipsoid === GeoEllipsoid.sphere) {
|
|
||||||
res
|
|
||||||
} else {
|
} else {
|
||||||
TODO("Elliptical mercator projection not implemented")
|
GeodeticMapCoordinates.ofRadians(
|
||||||
// GeodeticMapCoordinates(
|
cphi2(exp(-(pc.y / ellipsoid.equatorRadius))),
|
||||||
// atan(sinh(pc.y / ellipsoid.polarRadius)).radians,
|
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius)
|
||||||
// res.longitude,
|
)
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,14 +6,24 @@ import kotlin.test.assertEquals
|
|||||||
|
|
||||||
class MercatorTest {
|
class MercatorTest {
|
||||||
@Test
|
@Test
|
||||||
fun forwardBackward(){
|
fun sphereForwardBackward(){
|
||||||
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869)
|
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869)
|
||||||
val mercator = MapProjection.epsg3857.toProjection(moscow)
|
val mercator = MapProjection.epsg3857.toProjection(moscow)
|
||||||
//https://epsg.io/transform#s_srs=4326&t_srs=3857&x=37.6035862&y=55.7605829
|
//https://epsg.io/transform#s_srs=4326&t_srs=3857&x=37.6035862&y=55.7605829
|
||||||
assertEquals(4186.0120709, mercator.x.kilometers, 1e-4)
|
assertEquals(4186.0120709, mercator.x.kilometers, 1e-4)
|
||||||
assertEquals(7510.9013658, mercator.y.kilometers, 1e-4)
|
assertEquals(7510.9013658, mercator.y.kilometers, 1e-4)
|
||||||
val backwards = MapProjection.epsg3857.toGeodetic(mercator)
|
val backwards = MapProjection.epsg3857.toGeodetic(mercator)
|
||||||
assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 0.001)
|
assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 1e-6)
|
||||||
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 0.001)
|
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ellipseForwardBackward(){
|
||||||
|
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869)
|
||||||
|
val projection = MercatorProjection(ellipsoid = GeoEllipsoid.WGS84)
|
||||||
|
val mercator = projection.toProjection(moscow)
|
||||||
|
val backwards = projection.toGeodetic(mercator)
|
||||||
|
assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 1e-6)
|
||||||
|
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6)
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user