Drag done

This commit is contained in:
Alexander Nozik 2022-08-31 22:48:52 +03:00
parent 5a1d3d701f
commit 491a4e6000
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
9 changed files with 243 additions and 102 deletions

View File

@ -7,7 +7,8 @@ import androidx.compose.runtime.*
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode 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.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.maps.compose.* import center.sciprog.maps.compose.*
@ -51,32 +52,24 @@ fun App() {
val pointOne = 55.568548 to 37.568604 val pointOne = 55.568548 to 37.568604
var pointTwo: Pair<Double, Double> by remember { mutableStateOf(55.929444 to 37.518434) } val pointTwo = 55.929444 to 37.518434
val pointThree = 60.929444 to 37.518434 val pointThree = 60.929444 to 37.518434
val dragPoint = 55.744 to 37.614
MapView( MapView(
mapTileProvider = mapTileProvider, mapTileProvider = mapTileProvider,
initialViewPoint = viewPoint, initialViewPoint = viewPoint,
config = MapViewConfig( config = MapViewConfig(
inferViewBoxFromFeatures = true, inferViewBoxFromFeatures = true,
onViewChange = { centerCoordinates = focus }, 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) image(pointOne, Icons.Filled.Home)
rectangle(dragPoint, id = "dragMe", size = DpSize(10.dp, 10.dp)).draggable()
points( points(
points = listOf( points = listOf(
55.742465 to 37.615812, 55.742465 to 37.615812,
@ -89,7 +82,7 @@ fun App() {
pointMode = PointMode.Polygon pointMode = PointMode.Polygon
) )
//remember feature Id //remember feature ID
val circleId: FeatureId = circle( val circleId: FeatureId = circle(
centerCoordinates = pointTwo, centerCoordinates = pointTwo,
) )

View File

@ -11,16 +11,20 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.maps.coordinates.GeodeticMapCoordinates import center.sciprog.maps.coordinates.*
import center.sciprog.maps.coordinates.GmcRectangle import kotlin.math.floor
import center.sciprog.maps.coordinates.wrapAll
public interface MapFeature { public interface MapFeature {
public val zoomRange: IntRange public val zoomRange: IntRange
public fun getBoundingBox(zoom: Int): GmcRectangle? public fun getBoundingBox(zoom: Double): GmcRectangle?
} }
public fun Iterable<MapFeature>.computeBoundingBox(zoom: Int): GmcRectangle? = public interface DraggableMapFeature : MapFeature {
public fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature
}
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<Double, Double>.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second)
@ -35,18 +39,21 @@ public class MapFeatureSelector(
) : MapFeature { ) : MapFeature {
override val zoomRange: IntRange get() = defaultZoomRange 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 class MapDrawFeature(
public val position: GeodeticMapCoordinates, public val position: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val drawFeature: DrawScope.() -> Unit, public val drawFeature: DrawScope.() -> Unit,
) : MapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Int): GmcRectangle { override fun getBoundingBox(zoom: Double): GmcRectangle {
//TODO add box computation //TODO add box computation
return GmcRectangle(position, position) return GmcRectangle(position, position)
} }
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapDrawFeature(newCoordinates, zoomRange, drawFeature)
} }
public class MapPointsFeature( public class MapPointsFeature(
@ -54,9 +61,9 @@ public class MapPointsFeature(
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val stroke: Float = 2f, public val stroke: Float = 2f,
public val color: Color = Color.Red, public val color: Color = Color.Red,
public val pointMode: PointMode = PointMode.Points public val pointMode: PointMode = PointMode.Points,
) : MapFeature { ) : MapFeature {
override fun getBoundingBox(zoom: Int): GmcRectangle { override fun getBoundingBox(zoom: Double): GmcRectangle {
return GmcRectangle(points.first(), points.last()) return GmcRectangle(points.first(), points.last())
} }
} }
@ -66,8 +73,14 @@ public class MapCircleFeature(
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val size: Float = 5f, public val size: Float = 5f,
public val color: Color = Color.Red, public val color: Color = Color.Red,
) : MapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(center, center) 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( public class MapRectangleFeature(
@ -75,8 +88,14 @@ public class MapRectangleFeature(
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val size: DpSize = DpSize(5.dp, 5.dp), public val size: DpSize = DpSize(5.dp, 5.dp),
public val color: Color = Color.Red, public val color: Color = Color.Red,
) : MapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(center, center) 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( public class MapLineFeature(
@ -85,7 +104,7 @@ public class MapLineFeature(
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val color: Color = Color.Red, public val color: Color = Color.Red,
) : MapFeature { ) : MapFeature {
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(a, b) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(a, b)
} }
public class MapArcFeature( public class MapArcFeature(
@ -95,7 +114,7 @@ public class MapArcFeature(
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val color: Color = Color.Red, public val color: Color = Color.Red,
) : MapFeature { ) : MapFeature {
override fun getBoundingBox(zoom: Int): GmcRectangle = oval override fun getBoundingBox(zoom: Double): GmcRectangle = oval
} }
public class MapBitmapImageFeature( public class MapBitmapImageFeature(
@ -103,8 +122,11 @@ public class MapBitmapImageFeature(
public val image: ImageBitmap, public val image: ImageBitmap,
public val size: IntSize = IntSize(15, 15), public val size: IntSize = IntSize(15, 15),
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
) : MapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(position, position) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapBitmapImageFeature(newCoordinates, image, size, zoomRange)
} }
public class MapVectorImageFeature( public class MapVectorImageFeature(
@ -112,8 +134,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,
) : MapFeature { ) : DraggableMapFeature{
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(position, position) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapVectorImageFeature(newCoordinates,painter, size, zoomRange)
} }
@Composable @Composable
@ -131,7 +156,8 @@ public class MapFeatureGroup(
public val children: Map<FeatureId, MapFeature>, public val children: Map<FeatureId, MapFeature>,
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
) : MapFeature { ) : 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( public class MapTextFeature(
@ -140,6 +166,9 @@ 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,
) : MapFeature { ) : DraggableMapFeature{
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(position, position) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapTextFeature(newCoordinates, text, zoomRange, color, fontConfig)
} }

View File

@ -15,27 +15,53 @@ import center.sciprog.maps.coordinates.GmcRectangle
public typealias FeatureId = String public typealias FeatureId = String
public interface MapFeatureAttributeKey<T>
public class MapFeatureAttributeSet(private val map: Map<MapFeatureAttributeKey<*>, *>) {
public operator fun <T> get(key: MapFeatureAttributeKey<*>): T? = map[key]?.let {
@Suppress("UNCHECKED_CAST")
it as T
}
}
public interface MapFeatureBuilder { public interface MapFeatureBuilder {
public fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId public fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId
public fun build(): SnapshotStateMap<FeatureId, MapFeature> public fun <T> setAttribute(id: FeatureId, key: MapFeatureAttributeKey<T>, value: T)
public val features: MutableMap<FeatureId, MapFeature>
public fun attributes(): Map<FeatureId, MapFeatureAttributeSet>
//TODO use context receiver for that
public fun FeatureId.draggable(enabled: Boolean = true) {
setAttribute(this, DraggableAttribute, enabled)
}
} }
internal class MapFeatureBuilderImpl(initialFeatures: Map<FeatureId, MapFeature>) : MapFeatureBuilder { internal class MapFeatureBuilderImpl(
override val features: SnapshotStateMap<FeatureId, MapFeature>,
) : MapFeatureBuilder {
private val attributes = SnapshotStateMap<FeatureId, SnapshotStateMap<MapFeatureAttributeKey<out Any?>, in Any?>>()
private val content: SnapshotStateMap<FeatureId, MapFeature> = mutableStateMapOf<FeatureId, MapFeature>().apply {
putAll(initialFeatures)
}
private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]" private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]"
override fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId { override fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId {
val safeId = id ?: generateID(feature) val safeId = id ?: generateID(feature)
content[id ?: generateID(feature)] = feature features[id ?: generateID(feature)] = feature
return safeId return safeId
} }
override fun build(): SnapshotStateMap<FeatureId, MapFeature> = content override fun <T> setAttribute(id: FeatureId, key: MapFeatureAttributeKey<T>, value: T) {
attributes.getOrPut(id) { SnapshotStateMap() }[key] = value
}
override fun attributes(): Map<FeatureId, MapFeatureAttributeSet> =
attributes.mapValues { MapFeatureAttributeSet(it.value) }
} }
public fun MapFeatureBuilder.circle( public fun MapFeatureBuilder.circle(
@ -123,7 +149,7 @@ public fun MapFeatureBuilder.points(
stroke: Float = 2f, stroke: Float = 2f,
color: Color = Color.Red, color: Color = Color.Red,
pointMode: PointMode = PointMode.Points, pointMode: PointMode = PointMode.Points,
id: FeatureId? = null id: FeatureId? = null,
): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode)) ): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode))
@Composable @Composable
@ -140,7 +166,7 @@ public fun MapFeatureBuilder.group(
id: FeatureId? = null, id: FeatureId? = null,
builder: MapFeatureBuilder.() -> Unit, builder: MapFeatureBuilder.() -> Unit,
): FeatureId { ): FeatureId {
val map = MapFeatureBuilderImpl(emptyMap()).apply(builder).build() val map = MapFeatureBuilderImpl(mutableStateMapOf()).apply(builder).features
val feature = MapFeatureGroup(map, zoomRange) val feature = MapFeatureGroup(map, zoomRange)
return addFeature(id, feature) return addFeature(id, feature)
} }

View File

@ -1,9 +1,10 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.isPrimaryPressed
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import center.sciprog.maps.coordinates.* import center.sciprog.maps.coordinates.*
import kotlin.math.PI import kotlin.math.PI
@ -19,10 +20,33 @@ public fun interface DragHandle {
* *
* @return true if default event processors should be used after this one * @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 companion object {
public val BYPASS: DragHandle = DragHandle { _, _, _ -> true } 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(), modifier: Modifier = Modifier.fillMaxSize(),
) )
private fun prepareConfig(initialConfig: MapViewConfig, featureBuilder: MapFeatureBuilder): MapViewConfig {
val draggableFeatureIds: Set<FeatureId> = 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 @Composable
public fun MapView( public fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
initialViewPoint: MapViewPoint, initialViewPoint: MapViewPoint,
features: Map<FeatureId, MapFeature> = emptyMap(),
config: MapViewConfig = MapViewConfig(), config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {}, buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
) { ) {
val featuresBuilder = MapFeatureBuilderImpl(features) val featuresBuilder = MapFeatureBuilderImpl(mutableStateMapOf())
featuresBuilder.buildFeatures() featuresBuilder.buildFeatures()
val features = remember { featuresBuilder.features }
val newConfig = remember(features) {
prepareConfig(config, featuresBuilder)
}
MapView( MapView(
mapTileProvider, mapTileProvider,
{ initialViewPoint }, { initialViewPoint },
featuresBuilder.build(), features,
config, newConfig,
modifier modifier
) )
} }
internal fun GmcRectangle.computeViewPoint(mapTileProvider: MapTileProvider): (canvasSize: DpSize) -> MapViewPoint = internal fun GmcRectangle.computeViewPoint(
{ 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(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
box: GmcRectangle, ): (canvasSize: DpSize) -> MapViewPoint = { canvasSize ->
features: Map<FeatureId, MapFeature> = emptyMap(), val zoom = log2(
config: MapViewConfig = MapViewConfig(), min(
modifier: Modifier = Modifier.fillMaxSize(), canvasSize.width.value / longitudeDelta.radians.value,
buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {}, canvasSize.height.value / latitudeDelta.radians.value
) { ) * PI / mapTileProvider.tileSize
val featuresBuilder = MapFeatureBuilderImpl(features)
featuresBuilder.buildFeatures()
MapView(
mapTileProvider,
box.computeViewPoint(mapTileProvider),
featuresBuilder.build(),
config,
modifier
) )
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
// )
//}

View File

@ -0,0 +1,3 @@
package center.sciprog.maps.compose
public object DraggableAttribute: MapFeatureAttributeKey<Boolean>

View File

@ -5,6 +5,7 @@ import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@ -66,7 +67,7 @@ public actual fun MapView(
val viewPoint: MapViewPoint by derivedStateOf { val viewPoint: MapViewPoint by derivedStateOf {
viewPointInternal ?: if (config.inferViewBoxFromFeatures) { viewPointInternal ?: if (config.inferViewBoxFromFeatures) {
features.values.computeBoundingBox(1)?.let { box -> features.values.computeBoundingBox(1.0)?.let { box ->
val zoom = log2( val zoom = log2(
min( min(
canvasSize.width.value / box.longitudeDelta.radians.value, 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()) val dpEnd = DpOffset(dragChange.position.x.toDp(), dragChange.position.y.toDp())
//apply drag handle and check if it prohibits the drag even propagation
if ( if (
!config.dragHandle.drag( !config.dragHandle.handle(
event, event,
MapViewPoint(dpStart.toGeodetic(), viewPoint.zoom), MapViewPoint(dpStart.toGeodetic(), viewPoint.zoom),
MapViewPoint(dpEnd.toGeodetic(), viewPoint.zoom) MapViewPoint(dpEnd.toGeodetic(), viewPoint.zoom)

View File

@ -50,7 +50,7 @@ public class GeodeticMapCoordinates(
/** /**
* Short name for GeodeticMapCoordinates * Short name for GeodeticMapCoordinates
*/ */
public typealias GMC = GeodeticMapCoordinates public typealias Gmc = GeodeticMapCoordinates
//public interface GeoToScreenConversion { //public interface GeoToScreenConversion {
// public fun getScreenX(gmc: GeodeticMapCoordinates): Double // public fun getScreenX(gmc: GeodeticMapCoordinates): Double

View File

@ -57,8 +57,8 @@ public fun GeoEllipsoid.meridianCurve(
} }
return GmcCurve( return GmcCurve(
forward = GmcPose(GMC(fromLatitude, longitude), if (up) zero else pi), forward = GmcPose(Gmc(fromLatitude, longitude), if (up) zero else pi),
backward = GmcPose(GMC(toLatitude, longitude), if (up) pi else zero), backward = GmcPose(Gmc(toLatitude, longitude), if (up) pi else zero),
distance = s 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" } require(latitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
val right = toLongitude > fromLongitude val right = toLongitude > fromLongitude
return GmcCurve( return GmcCurve(
forward = GmcPose(GMC(latitude, fromLongitude), 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), backward = GmcPose(Gmc(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians),
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians.value) distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians.value)
) )
} }
@ -186,7 +186,7 @@ public fun GeoEllipsoid.curveInDirection(
val L = lambda - (1 - C) * f * sinAlpha * val L = lambda - (1 - C) * f * sinAlpha *
(sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2))) (sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)))
val endPoint = GMC(phi2, L.radians) val endPoint = Gmc(phi2, L.radians)
// eq. 12 // eq. 12
@ -212,7 +212,7 @@ public fun GeoEllipsoid.curveInDirection(
* @return solution to the inverse geodetic problem * @return solution to the inverse geodetic problem
*/ */
@Suppress("SpellCheckingInspection", "LocalVariableName") @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: // All equation numbers refer back to Vincenty's publication:
// See http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf // See http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf

View File

@ -9,29 +9,39 @@ package center.sciprog.maps.coordinates
public data class GmcRectangle( public data class GmcRectangle(
public val a: GeodeticMapCoordinates, public val a: GeodeticMapCoordinates,
public val b: GeodeticMapCoordinates, public val b: GeodeticMapCoordinates,
public val ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
) { ) {
public companion object { 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 * A quasi-square section. Note that latitudinal distance could be imprecise for large distances
*/ */
public fun square( public fun square(
center: GeodeticMapCoordinates, center: GeodeticMapCoordinates,
width: Distance,
height: Distance, height: Distance,
width: Distance,
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84, ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
): GmcRectangle { ): GmcRectangle {
val reducedRadius = ellipsoid.reducedRadius(center.latitude) val reducedRadius = ellipsoid.reducedRadius(center.latitude)
val a = GeodeticMapCoordinates( return square(center, (height / ellipsoid.polarRadius).radians, (width / reducedRadius).radians)
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)
} }
} }
} }
@ -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.topLeft: GeodeticMapCoordinates get() = GeodeticMapCoordinates(top, left)
public val GmcRectangle.bottomRight: GeodeticMapCoordinates get() = GeodeticMapCoordinates(bottom, right) 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 * Compute a minimal bounding box including all given boxes. Return null if collection is empty
*/ */