From 491a4e60008497a31d23eb803bd11e7dcb907835 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 31 Aug 2022 22:48:52 +0300 Subject: [PATCH] Drag done --- demo/maps/src/jvmMain/kotlin/Main.kt | 25 ++-- .../center/sciprog/maps/compose/MapFeature.kt | 75 +++++++---- .../sciprog/maps/compose/MapFeatureBuilder.kt | 44 +++++-- .../center/sciprog/maps/compose/MapView.kt | 122 +++++++++++++----- .../center/sciprog/maps/compose/drag.kt | 3 + .../center/sciprog/maps/compose/MapViewJvm.kt | 6 +- .../coordinates/GeodeticMapCoordinates.kt | 2 +- .../sciprog/maps/coordinates/GmcCurve.kt | 12 +- .../sciprog/maps/coordinates/GmcRectangle.kt | 56 ++++++-- 9 files changed, 243 insertions(+), 102 deletions(-) create mode 100644 maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/drag.kt diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index dbcba0a..687602c 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -7,7 +7,8 @@ import androidx.compose.runtime.* import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PointMode -import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import center.sciprog.maps.compose.* @@ -51,32 +52,24 @@ fun App() { val pointOne = 55.568548 to 37.568604 - var pointTwo: Pair by remember { mutableStateOf(55.929444 to 37.518434) } + val pointTwo = 55.929444 to 37.518434 val pointThree = 60.929444 to 37.518434 + + val dragPoint = 55.744 to 37.614 + MapView( mapTileProvider = mapTileProvider, initialViewPoint = viewPoint, config = MapViewConfig( inferViewBoxFromFeatures = true, onViewChange = { centerCoordinates = focus }, - dragHandle = { event, start, end -> - if (!event.buttons.isPrimaryPressed) { - true - } else if (start.focus.latitude.degrees.value in (pointTwo.first - 0.05)..(pointTwo.first + 0.05) && - start.focus.longitude.degrees.value in (pointTwo.second - 0.05)..(pointTwo.second + 0.05) - ) { - pointTwo = pointTwo.first + (end.focus.latitude - start.focus.latitude).degrees.value to - pointTwo.second + (end.focus.longitude - start.focus.longitude).degrees.value - false// returning false, because when we are dragging circle we don't want to drag map - } else { - true - } - } ) ) { image(pointOne, Icons.Filled.Home) + rectangle(dragPoint, id = "dragMe", size = DpSize(10.dp, 10.dp)).draggable() + points( points = listOf( 55.742465 to 37.615812, @@ -89,7 +82,7 @@ fun App() { pointMode = PointMode.Polygon ) - //remember feature Id + //remember feature ID val circleId: FeatureId = circle( centerCoordinates = pointTwo, ) 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 13f09c6..418b25f 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 @@ -11,16 +11,20 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp -import center.sciprog.maps.coordinates.GeodeticMapCoordinates -import center.sciprog.maps.coordinates.GmcRectangle -import center.sciprog.maps.coordinates.wrapAll +import center.sciprog.maps.coordinates.* +import kotlin.math.floor public interface MapFeature { public val zoomRange: IntRange - public fun getBoundingBox(zoom: Int): GmcRectangle? + public fun getBoundingBox(zoom: Double): GmcRectangle? + } -public fun Iterable.computeBoundingBox(zoom: Int): GmcRectangle? = +public interface DraggableMapFeature : MapFeature { + public fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature +} + +public fun Iterable.computeBoundingBox(zoom: Double): GmcRectangle? = mapNotNull { it.getBoundingBox(zoom) }.wrapAll() internal fun Pair.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second) @@ -35,18 +39,21 @@ public class MapFeatureSelector( ) : MapFeature { override val zoomRange: IntRange get() = defaultZoomRange - override fun getBoundingBox(zoom: Int): GmcRectangle? = selector(zoom).getBoundingBox(zoom) + override fun getBoundingBox(zoom: Double): GmcRectangle? = selector(floor(zoom).toInt()).getBoundingBox(zoom) } public class MapDrawFeature( public val position: GeodeticMapCoordinates, override val zoomRange: IntRange = defaultZoomRange, public val drawFeature: DrawScope.() -> Unit, -) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcRectangle { +) : DraggableMapFeature { + override fun getBoundingBox(zoom: Double): GmcRectangle { //TODO add box computation return GmcRectangle(position, position) } + + override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = + MapDrawFeature(newCoordinates, zoomRange, drawFeature) } public class MapPointsFeature( @@ -54,9 +61,9 @@ public class MapPointsFeature( override val zoomRange: IntRange = defaultZoomRange, public val stroke: Float = 2f, public val color: Color = Color.Red, - public val pointMode: PointMode = PointMode.Points + public val pointMode: PointMode = PointMode.Points, ) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcRectangle { + override fun getBoundingBox(zoom: Double): GmcRectangle { return GmcRectangle(points.first(), points.last()) } } @@ -66,8 +73,14 @@ public class MapCircleFeature( override val zoomRange: IntRange = defaultZoomRange, public val size: Float = 5f, public val color: Color = Color.Red, -) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(center, center) +) : DraggableMapFeature { + override fun getBoundingBox(zoom: Double): GmcRectangle { + val scale = WebMercatorProjection.scaleFactor(zoom) + return GmcRectangle.square(center, (size/scale).radians, (size/scale).radians) + } + + override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = + MapCircleFeature(newCoordinates, zoomRange, size, color) } public class MapRectangleFeature( @@ -75,8 +88,14 @@ public class MapRectangleFeature( override val zoomRange: IntRange = defaultZoomRange, public val size: DpSize = DpSize(5.dp, 5.dp), public val color: Color = Color.Red, -) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(center, center) +) : 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) + } + + override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = + MapRectangleFeature(newCoordinates, zoomRange, size, color) } public class MapLineFeature( @@ -85,7 +104,7 @@ public class MapLineFeature( override val zoomRange: IntRange = defaultZoomRange, public val color: Color = Color.Red, ) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(a, b) + override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(a, b) } public class MapArcFeature( @@ -95,7 +114,7 @@ public class MapArcFeature( override val zoomRange: IntRange = defaultZoomRange, public val color: Color = Color.Red, ) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcRectangle = oval + override fun getBoundingBox(zoom: Double): GmcRectangle = oval } public class MapBitmapImageFeature( @@ -103,8 +122,11 @@ public class MapBitmapImageFeature( public val image: ImageBitmap, public val size: IntSize = IntSize(15, 15), override val zoomRange: IntRange = defaultZoomRange, -) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(position, position) +) : DraggableMapFeature { + override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) + + override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = + MapBitmapImageFeature(newCoordinates, image, size, zoomRange) } public class MapVectorImageFeature( @@ -112,8 +134,11 @@ public class MapVectorImageFeature( public val painter: Painter, public val size: DpSize, override val zoomRange: IntRange = defaultZoomRange, -) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(position, position) +) : DraggableMapFeature{ + override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) + + override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = + MapVectorImageFeature(newCoordinates,painter, size, zoomRange) } @Composable @@ -131,7 +156,8 @@ public class MapFeatureGroup( public val children: Map, override val zoomRange: IntRange = defaultZoomRange, ) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcRectangle? = children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll() + override fun getBoundingBox(zoom: Double): GmcRectangle? = + children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll() } public class MapTextFeature( @@ -140,6 +166,9 @@ public class MapTextFeature( override val zoomRange: IntRange = defaultZoomRange, public val color: Color, public val fontConfig: MapTextFeatureFont.() -> Unit, -) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(position, position) +) : DraggableMapFeature{ + override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) + + override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = + MapTextFeature(newCoordinates, text, zoomRange, color, fontConfig) } 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 ebedb83..508385c 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 @@ -15,27 +15,53 @@ import center.sciprog.maps.coordinates.GmcRectangle public typealias FeatureId = String +public interface MapFeatureAttributeKey + + +public class MapFeatureAttributeSet(private val map: Map, *>) { + public operator fun get(key: MapFeatureAttributeKey<*>): T? = map[key]?.let { + @Suppress("UNCHECKED_CAST") + it as T + } +} + public interface MapFeatureBuilder { public fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId - public fun build(): SnapshotStateMap + public fun setAttribute(id: FeatureId, key: MapFeatureAttributeKey, value: T) + + public val features: MutableMap + + public fun attributes(): Map + + //TODO use context receiver for that + public fun FeatureId.draggable(enabled: Boolean = true) { + setAttribute(this, DraggableAttribute, enabled) + } } -internal class MapFeatureBuilderImpl(initialFeatures: Map) : MapFeatureBuilder { +internal class MapFeatureBuilderImpl( + override val features: SnapshotStateMap, +) : MapFeatureBuilder { + + private val attributes = SnapshotStateMap, in Any?>>() - private val content: SnapshotStateMap = mutableStateMapOf().apply { - putAll(initialFeatures) - } private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]" override fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId { val safeId = id ?: generateID(feature) - content[id ?: generateID(feature)] = feature + features[id ?: generateID(feature)] = feature return safeId } - override fun build(): SnapshotStateMap = content + override fun setAttribute(id: FeatureId, key: MapFeatureAttributeKey, value: T) { + attributes.getOrPut(id) { SnapshotStateMap() }[key] = value + } + + override fun attributes(): Map = + attributes.mapValues { MapFeatureAttributeSet(it.value) } + } public fun MapFeatureBuilder.circle( @@ -123,7 +149,7 @@ public fun MapFeatureBuilder.points( stroke: Float = 2f, color: Color = Color.Red, pointMode: PointMode = PointMode.Points, - id: FeatureId? = null + id: FeatureId? = null, ): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode)) @Composable @@ -140,7 +166,7 @@ public fun MapFeatureBuilder.group( id: FeatureId? = null, builder: MapFeatureBuilder.() -> Unit, ): FeatureId { - val map = MapFeatureBuilderImpl(emptyMap()).apply(builder).build() + val map = MapFeatureBuilderImpl(mutableStateMapOf()).apply(builder).features val feature = MapFeatureGroup(map, zoomRange) return addFeature(id, feature) } diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt index 2829891..4ef6320 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt @@ -1,9 +1,10 @@ package center.sciprog.maps.compose import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.isPrimaryPressed import androidx.compose.ui.unit.DpSize import center.sciprog.maps.coordinates.* import kotlin.math.PI @@ -19,10 +20,33 @@ public fun interface DragHandle { * * @return true if default event processors should be used after this one */ - public fun drag(event: PointerEvent, start: MapViewPoint, end: MapViewPoint): Boolean + public fun handle(event: PointerEvent, start: MapViewPoint, end: MapViewPoint): Boolean public companion object { public val BYPASS: DragHandle = DragHandle { _, _, _ -> true } + + /** + * Process only events with primary button pressed + */ + public fun withPrimaryButton( + block: (event: PointerEvent, start: MapViewPoint, end: MapViewPoint) -> Boolean, + ): DragHandle = DragHandle { event, start, end -> + if (event.buttons.isPrimaryPressed) { + block(event, start, end) + } else { + false + } + } + + /** + * Combine several handles into one + */ + public fun combine(vararg handles: DragHandle): DragHandle = DragHandle { event, start, end -> + handles.forEach { + if (!it.handle(event, start, end)) return@DragHandle false + } + return@DragHandle true + } } } @@ -49,53 +73,83 @@ public expect fun MapView( modifier: Modifier = Modifier.fillMaxSize(), ) +private fun prepareConfig(initialConfig: MapViewConfig, featureBuilder: MapFeatureBuilder): MapViewConfig { + val draggableFeatureIds: Set = featureBuilder.attributes().filterValues { + it[DraggableAttribute] ?: false + }.keys + + val features = featureBuilder.features + + val featureDrag = DragHandle.withPrimaryButton { _, start, end -> + val zoom = start.zoom + draggableFeatureIds.forEach { id -> + val feature = features[id] as? DraggableMapFeature ?: return@forEach + //val border = WebMercatorProjection.scaleFactor(zoom) + val boundingBox = feature.getBoundingBox(zoom) ?: return@forEach + if (start.focus in boundingBox) { + features[id] = feature.withCoordinates(end.focus) + return@withPrimaryButton false + } + } + return@withPrimaryButton true + } + return initialConfig.copy( + dragHandle = DragHandle.combine(featureDrag, initialConfig.dragHandle), + ) +} + @Composable public fun MapView( mapTileProvider: MapTileProvider, initialViewPoint: MapViewPoint, - features: Map = emptyMap(), config: MapViewConfig = MapViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {}, ) { - val featuresBuilder = MapFeatureBuilderImpl(features) + val featuresBuilder = MapFeatureBuilderImpl(mutableStateMapOf()) featuresBuilder.buildFeatures() + val features = remember { featuresBuilder.features } + + val newConfig = remember(features) { + prepareConfig(config, featuresBuilder) + } + MapView( mapTileProvider, { initialViewPoint }, - featuresBuilder.build(), - config, + features, + newConfig, modifier ) } -internal fun GmcRectangle.computeViewPoint(mapTileProvider: MapTileProvider): (canvasSize: DpSize) -> MapViewPoint = - { canvasSize -> - val zoom = log2( - min( - canvasSize.width.value / longitudeDelta.radians.value, - canvasSize.height.value / latitudeDelta.radians.value - ) * PI / mapTileProvider.tileSize - ) - MapViewPoint(center, zoom) - } - -@Composable -public fun MapView( +internal fun GmcRectangle.computeViewPoint( mapTileProvider: MapTileProvider, - box: GmcRectangle, - features: Map = emptyMap(), - config: MapViewConfig = MapViewConfig(), - modifier: Modifier = Modifier.fillMaxSize(), - buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {}, -) { - val featuresBuilder = MapFeatureBuilderImpl(features) - featuresBuilder.buildFeatures() - MapView( - mapTileProvider, - box.computeViewPoint(mapTileProvider), - featuresBuilder.build(), - config, - modifier +): (canvasSize: DpSize) -> MapViewPoint = { canvasSize -> + val zoom = log2( + min( + canvasSize.width.value / longitudeDelta.radians.value, + canvasSize.height.value / latitudeDelta.radians.value + ) * PI / mapTileProvider.tileSize ) -} \ No newline at end of file + MapViewPoint(center, zoom) +} +// +//@Composable +//public fun MapView( +// mapTileProvider: MapTileProvider, +// box: GmcRectangle, +// config: MapViewConfig = MapViewConfig(), +// modifier: Modifier = Modifier.fillMaxSize(), +// buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {}, +//) { +// val builder by derivedStateOf { MapFeatureBuilderImpl().apply(buildFeatures) } +// +// MapView( +// mapTileProvider, +// box.computeViewPoint(mapTileProvider), +// builder.features, +// prepareConfig(config, builder), +// modifier +// ) +//} \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/drag.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/drag.kt new file mode 100644 index 0000000..81e117d --- /dev/null +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/drag.kt @@ -0,0 +1,3 @@ +package center.sciprog.maps.compose + +public object DraggableAttribute: MapFeatureAttributeKey \ No newline at end of file diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt index 3dfdadd..b9c195d 100644 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt +++ b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.gestures.drag import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* +import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -66,7 +67,7 @@ public actual fun MapView( val viewPoint: MapViewPoint by derivedStateOf { viewPointInternal ?: if (config.inferViewBoxFromFeatures) { - features.values.computeBoundingBox(1)?.let { box -> + features.values.computeBoundingBox(1.0)?.let { box -> val zoom = log2( min( canvasSize.width.value / box.longitudeDelta.radians.value, @@ -132,8 +133,9 @@ public actual fun MapView( ) val dpEnd = DpOffset(dragChange.position.x.toDp(), dragChange.position.y.toDp()) + //apply drag handle and check if it prohibits the drag even propagation if ( - !config.dragHandle.drag( + !config.dragHandle.handle( event, MapViewPoint(dpStart.toGeodetic(), viewPoint.zoom), MapViewPoint(dpEnd.toGeodetic(), viewPoint.zoom) diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeodeticMapCoordinates.kt b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeodeticMapCoordinates.kt index d12c310..b0d3923 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeodeticMapCoordinates.kt +++ b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeodeticMapCoordinates.kt @@ -50,7 +50,7 @@ public class GeodeticMapCoordinates( /** * Short name for GeodeticMapCoordinates */ -public typealias GMC = GeodeticMapCoordinates +public typealias Gmc = GeodeticMapCoordinates //public interface GeoToScreenConversion { // public fun getScreenX(gmc: GeodeticMapCoordinates): Double diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcCurve.kt b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcCurve.kt index d528831..9ff8575 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcCurve.kt +++ b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcCurve.kt @@ -57,8 +57,8 @@ public fun GeoEllipsoid.meridianCurve( } return GmcCurve( - forward = GmcPose(GMC(fromLatitude, longitude), if (up) zero else pi), - backward = GmcPose(GMC(toLatitude, longitude), if (up) pi else zero), + forward = GmcPose(Gmc(fromLatitude, longitude), if (up) zero else pi), + backward = GmcPose(Gmc(toLatitude, longitude), if (up) pi else zero), distance = s ) } @@ -70,8 +70,8 @@ public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toL require(latitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" } val right = toLongitude > fromLongitude return GmcCurve( - forward = GmcPose(GMC(latitude, fromLongitude), if (right) piDiv2.radians else -piDiv2.radians), - backward = GmcPose(GMC(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians), + forward = GmcPose(Gmc(latitude, fromLongitude), if (right) piDiv2.radians else -piDiv2.radians), + backward = GmcPose(Gmc(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians), distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians.value) ) } @@ -186,7 +186,7 @@ public fun GeoEllipsoid.curveInDirection( val L = lambda - (1 - C) * f * sinAlpha * (sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2))) - val endPoint = GMC(phi2, L.radians) + val endPoint = Gmc(phi2, L.radians) // eq. 12 @@ -212,7 +212,7 @@ public fun GeoEllipsoid.curveInDirection( * @return solution to the inverse geodetic problem */ @Suppress("SpellCheckingInspection", "LocalVariableName") -public fun GeoEllipsoid.curveBetween(start: GMC, end: GMC, precision: Double = 1e-6): GmcCurve { +public fun GeoEllipsoid.curveBetween(start: Gmc, end: Gmc, precision: Double = 1e-6): GmcCurve { // // All equation numbers refer back to Vincenty's publication: // See http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcRectangle.kt b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcRectangle.kt index bb1322c..9181f98 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcRectangle.kt +++ b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcRectangle.kt @@ -9,29 +9,39 @@ package center.sciprog.maps.coordinates public data class GmcRectangle( public val a: GeodeticMapCoordinates, public val b: GeodeticMapCoordinates, - public val ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84, ) { public companion object { + /** + * A quasi-square section. + */ + public fun square( + center: GeodeticMapCoordinates, + height: Angle, + width: Angle, + ): GmcRectangle { + val a = GeodeticMapCoordinates( + center.latitude - (height / 2), + center.longitude - (width / 2) + ) + val b = GeodeticMapCoordinates( + center.latitude + (height / 2), + center.longitude + (width / 2) + ) + return GmcRectangle(a, b) + } + /** * A quasi-square section. Note that latitudinal distance could be imprecise for large distances */ public fun square( center: GeodeticMapCoordinates, - width: Distance, height: Distance, + width: Distance, ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84, ): GmcRectangle { val reducedRadius = ellipsoid.reducedRadius(center.latitude) - val a = GeodeticMapCoordinates( - center.latitude - (height / ellipsoid.polarRadius / 2).radians, - center.longitude - (width / reducedRadius / 2).radians - ) - val b = GeodeticMapCoordinates( - center.latitude + (height / ellipsoid.polarRadius / 2).radians, - center.longitude + (width / reducedRadius / 2).radians - ) - return GmcRectangle(a, b, ellipsoid) + return square(center, (height / ellipsoid.polarRadius).radians, (width / reducedRadius).radians) } } } @@ -68,6 +78,30 @@ public val GmcRectangle.latitudeDelta: Angle get() = abs(a.latitude - b.latitude public val GmcRectangle.topLeft: GeodeticMapCoordinates get() = GeodeticMapCoordinates(top, left) public val GmcRectangle.bottomRight: GeodeticMapCoordinates get() = GeodeticMapCoordinates(bottom, right) +//public fun GmcRectangle.enlarge( +// top: Distance, +// bottom: Distance = top, +// left: Distance = top, +// right: Distance = left, +//): GmcRectangle { +// +//} +// +//public fun GmcRectangle.enlarge( +// top: Angle, +// bottom: Angle = top, +// left: Angle = top, +// right: Angle = left, +//): GmcRectangle { +// +//} + +/** + * Check if coordinate is inside the box + */ +public operator fun GmcRectangle.contains(coordinate: Gmc): Boolean = + coordinate.latitude in (bottom..top) && coordinate.longitude in (left..right) + /** * Compute a minimal bounding box including all given boxes. Return null if collection is empty */