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? =
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
@ -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 =

View File

@ -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<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
public fun MapFeatureBuilder.image(
position: Pair<Double, Double>,

View File

@ -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<T: Any>{
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<ProjectionCoordinates> {
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))
)