From 42e0a4c46d88ae0afbb735b32933cad33199ddd0 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 10 Sep 2022 16:14:30 +0300 Subject: [PATCH] Add abstraction for coordinate projections --- .../center/sciprog/maps/compose/MapFeature.kt | 13 ++++----- .../sciprog/maps/compose/MapFeatureBuilder.kt | 10 +++++++ .../maps/coordinates/MercatorProjection.kt | 27 ++++++++++++------- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt index dbd8412..5562473 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt @@ -27,7 +27,8 @@ public interface DraggableMapFeature : MapFeature { public fun Iterable.computeBoundingBox(zoom: Double): GmcRectangle? = mapNotNull { it.getBoundingBox(zoom) }.wrapAll() -internal fun Pair.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second) +internal fun Pair.toCoordinates() = + GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble()) internal val defaultZoomRange = 1..18 @@ -76,7 +77,7 @@ public class MapCircleFeature( ) : DraggableMapFeature { override fun getBoundingBox(zoom: Double): GmcRectangle { val scale = WebMercatorProjection.scaleFactor(zoom) - return GmcRectangle.square(center, (size/scale).radians, (size/scale).radians) + return GmcRectangle.square(center, (size / scale).radians, (size / scale).radians) } override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = @@ -91,7 +92,7 @@ public class MapRectangleFeature( ) : DraggableMapFeature { override fun getBoundingBox(zoom: Double): GmcRectangle { val scale = WebMercatorProjection.scaleFactor(zoom) - return GmcRectangle.square(center, (size.height.value/scale).radians, (size.width.value/scale).radians) + return GmcRectangle.square(center, (size.height.value / scale).radians, (size.width.value / scale).radians) } override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = @@ -134,11 +135,11 @@ public class MapVectorImageFeature( public val painter: Painter, public val size: DpSize, override val zoomRange: IntRange = defaultZoomRange, -) : DraggableMapFeature{ +) : DraggableMapFeature { override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = - MapVectorImageFeature(newCoordinates,painter, size, zoomRange) + MapVectorImageFeature(newCoordinates, painter, size, zoomRange) } @Composable @@ -166,7 +167,7 @@ public class MapTextFeature( override val zoomRange: IntRange = defaultZoomRange, public val color: Color, public val fontConfig: MapTextFeatureFont.() -> Unit, -) : DraggableMapFeature{ +) : DraggableMapFeature { override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeatureBuilder.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeatureBuilder.kt index 4b2249e..dce18fa 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeatureBuilder.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeatureBuilder.kt @@ -171,6 +171,16 @@ public fun MapFeatureBuilder.points( id: FeatureId? = null, ): FeatureId = addFeature(id, MapPointsFeature(points, zoomRange, stroke, color, pointMode)) +@JvmName("pointsFromPairs") +public fun MapFeatureBuilder.points( + points: List>, + zoomRange: IntRange = defaultZoomRange, + stroke: Float = 2f, + color: Color = Color.Red, + pointMode: PointMode = PointMode.Points, + id: FeatureId? = null, +): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode)) + @Composable public fun MapFeatureBuilder.image( position: Pair, diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/MercatorProjection.kt b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/MercatorProjection.kt index 337a9c3..03d21f5 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/MercatorProjection.kt +++ b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/MercatorProjection.kt @@ -10,7 +10,16 @@ import kotlin.math.atan import kotlin.math.ln import kotlin.math.sinh -public data class MercatorCoordinates(val x: Distance, val y: Distance) +public data class ProjectionCoordinates(val x: Distance, val y: Distance) + +/** + * @param T the type of projection coordinates + */ +public interface MapProjection{ + public fun toGeodetic(pc: T): GeodeticMapCoordinates + public fun toProjection(gmc: GeodeticMapCoordinates): T +} + /** * @param baseLongitude the longitude offset in radians @@ -21,18 +30,18 @@ public open class MercatorProjection( public val baseLongitude: Angle = Angle.zero, protected val radius: Distance = DEFAULT_EARTH_RADIUS, private val correctedRadius: ((GeodeticMapCoordinates) -> Distance)? = null, -) { +): MapProjection { - public fun toGeodetic(mc: MercatorCoordinates): GeodeticMapCoordinates { + override fun toGeodetic(pc: ProjectionCoordinates): GeodeticMapCoordinates { val res = GeodeticMapCoordinates.ofRadians( - atan(sinh(mc.y / radius)), - baseLongitude.radians.value + (mc.x / radius), + atan(sinh(pc.y / radius)), + baseLongitude.radians.value + (pc.x / radius), ) return if (correctedRadius != null) { val r = correctedRadius.invoke(res) GeodeticMapCoordinates.ofRadians( - atan(sinh(mc.y / r)), - baseLongitude.radians.value + mc.x / r, + atan(sinh(pc.y / r)), + baseLongitude.radians.value + pc.x / r, ) } else { res @@ -42,10 +51,10 @@ public open class MercatorProjection( /** * https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas */ - public fun toMercator(gmc: GeodeticMapCoordinates): MercatorCoordinates { + override fun toProjection(gmc: GeodeticMapCoordinates): ProjectionCoordinates { require(abs(gmc.latitude) <= MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" } val r: Distance = correctedRadius?.invoke(gmc) ?: radius - return MercatorCoordinates( + return ProjectionCoordinates( x = r * (gmc.longitude - baseLongitude).radians.value, y = r * ln(tan(pi / 4 + gmc.latitude / 2)) )