Add abstraction for coordinate projections

This commit is contained in:
Alexander Nozik 2022-09-10 16:14:30 +03:00
parent cda8d8e76f
commit 42e0a4c46d
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
3 changed files with 35 additions and 15 deletions

View File

@ -27,7 +27,8 @@ public interface DraggableMapFeature : MapFeature {
public fun Iterable<MapFeature>.computeBoundingBox(zoom: Double): GmcRectangle? = public fun Iterable<MapFeature>.computeBoundingBox(zoom: Double): GmcRectangle? =
mapNotNull { it.getBoundingBox(zoom) }.wrapAll() mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
internal fun Pair<Double, Double>.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second) internal fun Pair<Number, Number>.toCoordinates() =
GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble())
internal val defaultZoomRange = 1..18 internal val defaultZoomRange = 1..18
@ -76,7 +77,7 @@ public class MapCircleFeature(
) : DraggableMapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle { override fun getBoundingBox(zoom: Double): GmcRectangle {
val scale = WebMercatorProjection.scaleFactor(zoom) 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 = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
@ -91,7 +92,7 @@ public class MapRectangleFeature(
) : DraggableMapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle { override fun getBoundingBox(zoom: Double): GmcRectangle {
val scale = WebMercatorProjection.scaleFactor(zoom) 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 = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
@ -134,11 +135,11 @@ public class MapVectorImageFeature(
public val painter: Painter, public val painter: Painter,
public val size: DpSize, public val size: DpSize,
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
) : DraggableMapFeature{ ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapVectorImageFeature(newCoordinates,painter, size, zoomRange) MapVectorImageFeature(newCoordinates, painter, size, zoomRange)
} }
@Composable @Composable
@ -166,7 +167,7 @@ public class MapTextFeature(
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val color: Color, public val color: Color,
public val fontConfig: MapTextFeatureFont.() -> Unit, public val fontConfig: MapTextFeatureFont.() -> Unit,
) : DraggableMapFeature{ ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =

View File

@ -171,6 +171,16 @@ public fun MapFeatureBuilder.points(
id: FeatureId? = null, id: FeatureId? = null,
): FeatureId = addFeature(id, MapPointsFeature(points, zoomRange, stroke, color, pointMode)) ): FeatureId = addFeature(id, MapPointsFeature(points, zoomRange, stroke, color, pointMode))
@JvmName("pointsFromPairs")
public fun MapFeatureBuilder.points(
points: List<Pair<Double, Double>>,
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 @Composable
public fun MapFeatureBuilder.image( public fun MapFeatureBuilder.image(
position: Pair<Double, Double>, position: Pair<Double, Double>,

View File

@ -10,7 +10,16 @@ import kotlin.math.atan
import kotlin.math.ln import kotlin.math.ln
import kotlin.math.sinh 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<T: Any>{
public fun toGeodetic(pc: T): GeodeticMapCoordinates
public fun toProjection(gmc: GeodeticMapCoordinates): T
}
/** /**
* @param baseLongitude the longitude offset in radians * @param baseLongitude the longitude offset in radians
@ -21,18 +30,18 @@ public open class MercatorProjection(
public val baseLongitude: Angle = Angle.zero, public val baseLongitude: Angle = Angle.zero,
protected val radius: Distance = DEFAULT_EARTH_RADIUS, protected val radius: Distance = DEFAULT_EARTH_RADIUS,
private val correctedRadius: ((GeodeticMapCoordinates) -> Distance)? = null, private val correctedRadius: ((GeodeticMapCoordinates) -> Distance)? = null,
) { ): MapProjection<ProjectionCoordinates> {
public fun toGeodetic(mc: MercatorCoordinates): GeodeticMapCoordinates { override fun toGeodetic(pc: ProjectionCoordinates): GeodeticMapCoordinates {
val res = GeodeticMapCoordinates.ofRadians( val res = GeodeticMapCoordinates.ofRadians(
atan(sinh(mc.y / radius)), atan(sinh(pc.y / radius)),
baseLongitude.radians.value + (mc.x / radius), baseLongitude.radians.value + (pc.x / radius),
) )
return if (correctedRadius != null) { return if (correctedRadius != null) {
val r = correctedRadius.invoke(res) val r = correctedRadius.invoke(res)
GeodeticMapCoordinates.ofRadians( GeodeticMapCoordinates.ofRadians(
atan(sinh(mc.y / r)), atan(sinh(pc.y / r)),
baseLongitude.radians.value + mc.x / r, baseLongitude.radians.value + pc.x / r,
) )
} else { } else {
res res
@ -42,10 +51,10 @@ public open class MercatorProjection(
/** /**
* https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas * 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" } require(abs(gmc.latitude) <= MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" }
val r: Distance = correctedRadius?.invoke(gmc) ?: radius val r: Distance = correctedRadius?.invoke(gmc) ?: radius
return MercatorCoordinates( return ProjectionCoordinates(
x = r * (gmc.longitude - baseLongitude).radians.value, x = r * (gmc.longitude - baseLongitude).radians.value,
y = r * ln(tan(pi / 4 + gmc.latitude / 2)) y = r * ln(tan(pi / 4 + gmc.latitude / 2))
) )