From 9b8ba884e1bc2b4a302bbd4ff99b5a7edb6d064c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 23 Dec 2022 22:16:16 +0300 Subject: [PATCH] Generic features --- demo/maps/src/jvmMain/kotlin/Main.kt | 3 +- maps-kt-compose/build.gradle.kts | 1 + .../sciprog/maps/compose/AttributeMap.kt | 43 --- .../center/sciprog/maps/compose/DragHandle.kt | 43 --- .../maps/compose/GmcCoordinateSpace.kt | 71 ++++ .../sciprog/maps/compose/GmcRectangle.kt | 73 +++++ .../center/sciprog/maps/compose/MapFeature.kt | 217 ------------- .../sciprog/maps/compose/MapFeaturesState.kt | 302 ------------------ .../maps/compose/MapTextFeatureFont.kt | 5 - .../center/sciprog/maps/compose/MapView.kt | 22 +- .../sciprog/maps/compose}/MapViewPoint.kt | 10 +- .../sciprog/maps/compose/mapFeatures.kt | 132 ++++++++ .../maps/compose/MapTextFeatureFontJvm.kt | 5 - .../center/sciprog/maps/compose/MapViewJvm.kt | 37 +-- .../sciprog/maps/coordinates/GeoEllipsoid.kt | 3 +- .../sciprog/maps/coordinates/GmcRectangle.kt | 118 ------- .../sciprog/maps/features/AttributeMap.kt | 2 + .../sciprog/maps/features/CoordinateSpace.kt | 22 +- .../sciprog/maps/features/DragHandle.kt | 14 +- .../center/sciprog/maps/features/Feature.kt | 69 ++-- .../sciprog/maps/features/FeaturesState.kt | 224 +++++++++++++ .../sciprog/maps/features/MapFeaturesState.kt | 302 ------------------ 22 files changed, 601 insertions(+), 1117 deletions(-) delete mode 100644 maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/AttributeMap.kt delete mode 100644 maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/DragHandle.kt create mode 100644 maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcCoordinateSpace.kt create mode 100644 maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcRectangle.kt delete mode 100644 maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt delete mode 100644 maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeaturesState.kt delete mode 100644 maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFont.kt rename {maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates => maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose}/MapViewPoint.kt (85%) create mode 100644 maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt delete mode 100644 maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFontJvm.kt delete mode 100644 maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcRectangle.kt create mode 100644 maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeaturesState.kt delete mode 100644 maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/MapFeaturesState.kt diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index 89a820e..3d56780 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import center.sciprog.maps.compose.* import center.sciprog.maps.coordinates.* +import center.sciprog.maps.features.* import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import kotlinx.coroutines.delay @@ -129,7 +130,7 @@ fun App() { centerCoordinates?.let { group(id = "center") { - circle(center = it, color = Color.Blue, id = "circle", size = 1f) + circle(center = it, color = Color.Blue, id = "circle", size = 1.dp) text(position = it, it.toShortString(), id = "text", color = Color.Blue) } } diff --git a/maps-kt-compose/build.gradle.kts b/maps-kt-compose/build.gradle.kts index d326e1b..ecec062 100644 --- a/maps-kt-compose/build.gradle.kts +++ b/maps-kt-compose/build.gradle.kts @@ -16,6 +16,7 @@ kotlin { commonMain { dependencies { api(projects.mapsKtCore) + api(projects.mapsKtFeatures) api(compose.foundation) api(project.dependencies.platform(spclibs.ktor.bom)) api("io.ktor:ktor-client-core") diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/AttributeMap.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/AttributeMap.kt deleted file mode 100644 index 52e6b90..0000000 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/AttributeMap.kt +++ /dev/null @@ -1,43 +0,0 @@ -package center.sciprog.maps.compose - -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.ui.graphics.Color - -public object DraggableAttribute : MapFeature.Attribute -public object SelectableAttribute : MapFeature.Attribute<(FeatureId<*>, SelectableMapFeature) -> Unit> -public object VisibleAttribute : MapFeature.Attribute - -public object ColorAttribute : MapFeature.Attribute - -public class AttributeMap { - public val map: MutableMap, Any> = mutableStateMapOf() - - public fun > setAttribute( - attribute: A, - attrValue: T?, - ) { - if (attrValue == null) { - map.remove(attribute) - } else { - map[attribute] = attrValue - } - } - - @Suppress("UNCHECKED_CAST") - public operator fun get(attribute: MapFeature.Attribute): T? = map[attribute] as? T - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as AttributeMap - - if (map != other.map) return false - - return true - } - - override fun hashCode(): Int = map.hashCode() - - override fun toString(): String = "AttributeMap(value=${map.entries})" -} \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/DragHandle.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/DragHandle.kt deleted file mode 100644 index ef4362a..0000000 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/DragHandle.kt +++ /dev/null @@ -1,43 +0,0 @@ -package center.sciprog.maps.compose - -import androidx.compose.ui.input.pointer.PointerEvent -import androidx.compose.ui.input.pointer.isPrimaryPressed -import center.sciprog.maps.coordinates.MapViewPoint - -public fun interface DragHandle { - /** - * @param event - qualifiers of the event used for drag - * @param start - is a point where drag begins, end is a point where drag ends - * @param end - end point of the drag - * - * @return true if default event processors should be used after this one - */ - 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 { - true - } - } - - /** - * 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 - } - } -} \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcCoordinateSpace.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcCoordinateSpace.kt new file mode 100644 index 0000000..624b46a --- /dev/null +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcCoordinateSpace.kt @@ -0,0 +1,71 @@ +package center.sciprog.maps.compose + +import androidx.compose.ui.unit.DpSize +import center.sciprog.maps.coordinates.* +import center.sciprog.maps.features.CoordinateSpace +import center.sciprog.maps.features.Rectangle + +public object GmcCoordinateSpace : CoordinateSpace { + override fun buildRectangle(first: Gmc, second: Gmc): GmcRectangle = GmcRectangle(first, second) + + override fun buildRectangle(center: Gmc, zoom: Double, size: DpSize): GmcRectangle{ + val scale = WebMercatorProjection.scaleFactor(zoom) + return buildRectangle(center, (size.width.value/scale).radians, (size.height.value / scale).radians) + } + + override fun Rectangle.withCenter(center: Gmc): GmcRectangle { + return buildRectangle(center, height = latitudeDelta, width = longitudeDelta) + } + + override fun Collection>.wrapRectangles(): Rectangle? { + if (isEmpty()) return null + //TODO optimize computation + val minLat = minOf { it.bottom } + val maxLat = maxOf { it.top } + val minLong = minOf { it.left } + val maxLong = maxOf { it.right } + return GmcRectangle(GeodeticMapCoordinates(minLat, minLong), GeodeticMapCoordinates(maxLat, maxLong)) + } + + override fun Collection.wrapPoints(): Rectangle? { + if (isEmpty()) return null + //TODO optimize computation + val minLat = minOf { it.latitude } + val maxLat = maxOf { it.latitude } + val minLong = minOf { it.longitude } + val maxLong = maxOf { it.longitude } + return GmcRectangle(GeodeticMapCoordinates(minLat, minLong), GeodeticMapCoordinates(maxLat, maxLong)) + } +} + +/** + * A quasi-square section. Note that latitudinal distance could be imprecise for large distances + */ +public fun CoordinateSpace.buildRectangle( + center: Gmc, + height: Distance, + width: Distance, + ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84 +): GmcRectangle { + val reducedRadius = ellipsoid.reducedRadius(center.latitude) + return buildRectangle(center, (height / ellipsoid.polarRadius).radians, (width / reducedRadius).radians) +} + +/** + * A quasi-square section. + */ +public fun CoordinateSpace.buildRectangle( + 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) +} \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcRectangle.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcRectangle.kt new file mode 100644 index 0000000..94f3d2c --- /dev/null +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcRectangle.kt @@ -0,0 +1,73 @@ +package center.sciprog.maps.compose + +import center.sciprog.maps.coordinates.Angle +import center.sciprog.maps.coordinates.GeodeticMapCoordinates +import center.sciprog.maps.coordinates.Gmc +import center.sciprog.maps.coordinates.abs +import center.sciprog.maps.features.Rectangle + +/** + * A section of the map between two parallels and two meridians. The figure represents a square in a Mercator projection. + * Params are two opposing "corners" of quasi-square. + * + * Note that this is a rectangle only on a Mercator projection. + */ +public data class GmcRectangle( + override val a: GeodeticMapCoordinates, + override val b: GeodeticMapCoordinates, +) : Rectangle { + + override fun contains(point: Gmc): Boolean = + point.latitude in a.latitude..b.latitude + && point.longitude in a.longitude..b.longitude +} + +public val Rectangle.center: GeodeticMapCoordinates + get() = GeodeticMapCoordinates( + (a.latitude + b.latitude) / 2, + (a.longitude + b.longitude) / 2 + ) + +/** + * Minimum longitude + */ +public val Rectangle.left: Angle get() = minOf(a.longitude, b.longitude) + +/** + * maximum longitude + */ +public val Rectangle.right: Angle get() = maxOf(a.longitude, b.longitude) + +/** + * Maximum latitude + */ +public val Rectangle.top: Angle get() = maxOf(a.latitude, b.latitude) + +/** + * Minimum latitude + */ +public val Rectangle.bottom: Angle get() = minOf(a.latitude, b.latitude) + +public val Rectangle.longitudeDelta: Angle get() = abs(a.longitude - b.longitude) +public val Rectangle.latitudeDelta: Angle get() = abs(a.latitude - b.latitude) + +public val Rectangle.topLeft: GeodeticMapCoordinates get() = GeodeticMapCoordinates(top, left) +public val Rectangle.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 { +// +//} \ No newline at end of file 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 deleted file mode 100644 index aeb8000..0000000 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt +++ /dev/null @@ -1,217 +0,0 @@ -package center.sciprog.maps.compose - -import androidx.compose.runtime.Composable -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.DrawStyle -import androidx.compose.ui.graphics.drawscope.Fill -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.VectorPainter -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.* -import kotlin.math.floor - -public interface MapFeature { - public interface Attribute - - public val zoomRange: ClosedFloatingPointRange - - public var attributes: AttributeMap - - public fun getBoundingBox(zoom: Double): GmcRectangle? -} - -public interface SelectableMapFeature : MapFeature { - public operator fun contains(point: MapViewPoint): Boolean = getBoundingBox(point.zoom)?.let { - point.focus in it - } ?: false -} - -public interface DraggableMapFeature : SelectableMapFeature { - public fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature -} - -public fun Iterable.computeBoundingBox(zoom: Double): GmcRectangle? = - mapNotNull { it.getBoundingBox(zoom) }.wrapAll() - -public fun Pair.toCoordinates(): GeodeticMapCoordinates = - GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble()) - -internal val defaultZoomRange = 1.0..Double.POSITIVE_INFINITY - -/** - * A feature that decides what to show depending on the zoom value (it could change size of shape) - */ -public class MapFeatureSelector( - override var attributes: AttributeMap = AttributeMap(), - public val selector: (zoom: Int) -> MapFeature, -) : MapFeature { - override val zoomRange: ClosedFloatingPointRange get() = defaultZoomRange - - override fun getBoundingBox(zoom: Double): GmcRectangle? = selector(floor(zoom).toInt()).getBoundingBox(zoom) -} - -public class MapDrawFeature( - public val position: GeodeticMapCoordinates, - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - override var attributes: AttributeMap = AttributeMap(), - public val drawFeature: DrawScope.() -> Unit, -) : DraggableMapFeature { - override fun getBoundingBox(zoom: Double): GmcRectangle { - //TODO add box computation - return GmcRectangle(position, position) - } - - override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = - MapDrawFeature(newCoordinates, zoomRange, attributes, drawFeature) -} - -public class MapPathFeature( - public val rectangle: GmcRectangle, - public val path: Path, - public val brush: Brush, - public val style: DrawStyle = Fill, - public val targetRect: Rect = path.getBounds(), - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - override var attributes: AttributeMap = AttributeMap(), -) : DraggableMapFeature { - override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = - MapPathFeature(rectangle.moveTo(newCoordinates), path, brush, style, targetRect, zoomRange) - - override fun getBoundingBox(zoom: Double): GmcRectangle = rectangle - -} - -public class MapPointsFeature( - public val points: List, - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - public val stroke: Float = 2f, - public val color: Color = Color.Red, - public val pointMode: PointMode = PointMode.Points, - override var attributes: AttributeMap = AttributeMap(), -) : MapFeature { - override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(points.first(), points.last()) -} - -public data class MapCircleFeature( - public val center: GeodeticMapCoordinates, - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - public val size: Float = 5f, - public val color: Color = Color.Red, - override var attributes: AttributeMap = AttributeMap(), -) : 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, attributes) -} - -public class MapRectangleFeature( - public val center: GeodeticMapCoordinates, - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - public val size: DpSize = DpSize(5.dp, 5.dp), - public val color: Color = Color.Red, - override var attributes: AttributeMap = AttributeMap(), -) : 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, attributes) -} - -public class MapLineFeature( - public val a: GeodeticMapCoordinates, - public val b: GeodeticMapCoordinates, - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - public val color: Color = Color.Red, - override var attributes: AttributeMap = AttributeMap(), -) : SelectableMapFeature { - override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(a, b) - - override fun contains(point: MapViewPoint): Boolean { - return super.contains(point) - } -} - -/** - * @param startAngle the angle from parallel downwards for the start of the arc - * @param arcLength arc length - */ -public class MapArcFeature( - public val oval: GmcRectangle, - public val startAngle: Angle, - public val arcLength: Angle, - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - public val color: Color = Color.Red, - override var attributes: AttributeMap = AttributeMap(), -) : DraggableMapFeature { - override fun getBoundingBox(zoom: Double): GmcRectangle = oval - - override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = - MapArcFeature(oval.moveTo(newCoordinates), startAngle, arcLength, zoomRange, color, attributes) -} - -public class MapBitmapImageFeature( - public val position: GeodeticMapCoordinates, - public val image: ImageBitmap, - public val size: IntSize = IntSize(15, 15), - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - override var attributes: AttributeMap = AttributeMap(), -) : DraggableMapFeature { - override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) - - override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = - MapBitmapImageFeature(newCoordinates, image, size, zoomRange, attributes) -} - -public class MapVectorImageFeature( - public val position: GeodeticMapCoordinates, - public val image: ImageVector, - public val size: DpSize = DpSize(20.dp, 20.dp), - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - override var attributes: AttributeMap = AttributeMap(), -) : DraggableMapFeature { - override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) - - override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = - MapVectorImageFeature(newCoordinates, image, size, zoomRange, attributes) - - @Composable - public fun painter(): VectorPainter = rememberVectorPainter(image) -} - -/** - * A group of other features - */ -public class MapFeatureGroup( - public val children: Map, MapFeature>, - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - override var attributes: AttributeMap = AttributeMap(), -) : MapFeature { - override fun getBoundingBox(zoom: Double): GmcRectangle? = - children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll() -} - -public class MapTextFeature( - public val position: GeodeticMapCoordinates, - public val text: String, - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - public val color: Color = Color.Black, - override var attributes: AttributeMap = AttributeMap(), - public val fontConfig: MapTextFeatureFont.() -> Unit, -) : DraggableMapFeature { - override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) - - override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = - MapTextFeature(newCoordinates, text, zoomRange, color, attributes, fontConfig) -} diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeaturesState.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeaturesState.kt deleted file mode 100644 index 02e98fe..0000000 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeaturesState.kt +++ /dev/null @@ -1,302 +0,0 @@ -package center.sciprog.maps.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.PointMode -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import center.sciprog.maps.coordinates.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch - -@JvmInline -public value class FeatureId(public val id: String) - -public class MapFeaturesState { - - @PublishedApi - internal val featureMap: MutableMap = mutableStateMapOf() - - //TODO use context receiver for that - public fun FeatureId.draggable( - //TODO add constraints - callback: DragHandle = DragHandle.BYPASS, - ) { - val handle = DragHandle.withPrimaryButton { event, start, end -> - val feature = featureMap[id] as? DraggableMapFeature ?: return@withPrimaryButton true - val boundingBox = feature.getBoundingBox(start.zoom) ?: return@withPrimaryButton true - if (start.focus in boundingBox) { - feature(id, feature.withCoordinates(end.focus)) - callback.handle(event, start, end) - false - } else { - true - } - } - setAttribute(this, DraggableAttribute, handle) - } - - /** - * Cyclic update of a feature. Called infinitely until canceled. - */ - public fun FeatureId.updated( - scope: CoroutineScope, - update: suspend (T) -> T, - ): Job = scope.launch { - while (isActive) { - feature(this@updated, update(getFeature(this@updated))) - } - } - - @Suppress("UNCHECKED_CAST") - public fun FeatureId.selectable( - onSelect: (FeatureId, T) -> Unit, - ) { - setAttribute(this, SelectableAttribute) { id, feature -> onSelect(id as FeatureId, feature as T) } - } - - - public val features: Map, MapFeature> - get() = featureMap.mapKeys { FeatureId(it.key) } - - @Suppress("UNCHECKED_CAST") - public fun getFeature(id: FeatureId): T = featureMap[id.id] as T - - - private fun generateID(feature: MapFeature): String = "@feature[${feature.hashCode().toUInt()}]" - - public fun feature(id: String?, feature: T): FeatureId { - val safeId = id ?: generateID(feature) - featureMap[safeId] = feature - return FeatureId(safeId) - } - - public fun feature(id: FeatureId?, feature: T): FeatureId = feature(id?.id, feature) - - public fun setAttribute(id: FeatureId, key: MapFeature.Attribute, value: T?) { - getFeature(id).attributes.setAttribute(key, value) - } - - @Suppress("UNCHECKED_CAST") - public fun getAttribute(id: FeatureId, key: MapFeature.Attribute): T? = - getFeature(id).attributes[key] - - -// @Suppress("UNCHECKED_CAST") -// public fun findAllWithAttribute(key: Attribute, condition: (T) -> Boolean): Set { -// return attributes.filterValues { -// condition(it[key] as T) -// }.keys -// } - - public inline fun forEachWithAttribute( - key: MapFeature.Attribute, - block: (id: FeatureId<*>, attributeValue: T) -> Unit, - ) { - featureMap.forEach { (id, feature) -> - feature.attributes[key]?.let { - block(FeatureId(id), it) - } - } - } - - public companion object { - - /** - * Build, but do not remember map feature state - */ - public fun build( - builder: MapFeaturesState.() -> Unit = {}, - ): MapFeaturesState = MapFeaturesState().apply(builder) - - /** - * Build and remember map feature state - */ - @Composable - public fun remember( - builder: MapFeaturesState.() -> Unit = {}, - ): MapFeaturesState = remember(builder) { - build(builder) - } - - } -} - -public fun MapFeaturesState.circle( - center: GeodeticMapCoordinates, - zoomRange: IntRange = defaultZoomRange, - size: Float = 5f, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, MapCircleFeature(center, zoomRange, size, color) -) - -public fun MapFeaturesState.circle( - centerCoordinates: Pair, - zoomRange: IntRange = defaultZoomRange, - size: Float = 5f, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, MapCircleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color) -) - -public fun MapFeaturesState.rectangle( - centerCoordinates: Gmc, - zoomRange: IntRange = defaultZoomRange, - size: DpSize = DpSize(5.dp, 5.dp), - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, MapRectangleFeature(centerCoordinates, zoomRange, size, color) -) - -public fun MapFeaturesState.rectangle( - centerCoordinates: Pair, - zoomRange: IntRange = defaultZoomRange, - size: DpSize = DpSize(5.dp, 5.dp), - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, MapRectangleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color) -) - -public fun MapFeaturesState.draw( - position: Pair, - zoomRange: IntRange = defaultZoomRange, - id: String? = null, - draw: DrawScope.() -> Unit, -): FeatureId = feature(id, MapDrawFeature(position.toCoordinates(), zoomRange, drawFeature = draw)) - -public fun MapFeaturesState.line( - aCoordinates: Gmc, - bCoordinates: Gmc, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, - MapLineFeature(aCoordinates, bCoordinates, zoomRange, color) -) - -public fun MapFeaturesState.line( - curve: GmcCurve, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, - MapLineFeature(curve.forward.coordinates, curve.backward.coordinates, zoomRange, color) -) - -public fun MapFeaturesState.line( - aCoordinates: Pair, - bCoordinates: Pair, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, - MapLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), zoomRange, color) -) - -public fun MapFeaturesState.arc( - oval: GmcRectangle, - startAngle: Angle, - arcLength: Angle, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, - MapArcFeature(oval, startAngle, arcLength, zoomRange, color) -) - -public fun MapFeaturesState.arc( - center: Pair, - radius: Distance, - startAngle: Angle, - arcLength: Angle, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, - MapArcFeature( - oval = GmcRectangle.square(center.toCoordinates(), radius, radius), - startAngle = startAngle, - arcLength = arcLength, - zoomRange = zoomRange, - color = color - ) -) - -public fun MapFeaturesState.points( - points: List, - zoomRange: IntRange = defaultZoomRange, - stroke: Float = 2f, - color: Color = Color.Red, - pointMode: PointMode = PointMode.Points, - id: String? = null, -): FeatureId = feature(id, MapPointsFeature(points, zoomRange, stroke, color, pointMode)) - -@JvmName("pointsFromPairs") -public fun MapFeaturesState.points( - points: List>, - zoomRange: IntRange = defaultZoomRange, - stroke: Float = 2f, - color: Color = Color.Red, - pointMode: PointMode = PointMode.Points, - id: String? = null, -): FeatureId = - feature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode)) - -public fun MapFeaturesState.image( - position: Pair, - image: ImageVector, - size: DpSize = DpSize(20.dp, 20.dp), - zoomRange: IntRange = defaultZoomRange, - id: String? = null, -): FeatureId = - feature(id, MapVectorImageFeature(position.toCoordinates(), image, size, zoomRange)) - -public fun MapFeaturesState.group( - zoomRange: IntRange = defaultZoomRange, - id: String? = null, - builder: MapFeaturesState.() -> Unit, -): FeatureId { - val map = MapFeaturesState().apply(builder).features - val feature = MapFeatureGroup(map, zoomRange) - return feature(id, feature) -} - -public fun MapFeaturesState.text( - position: GeodeticMapCoordinates, - text: String, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - font: MapTextFeatureFont.() -> Unit = { size = 16f }, - id: String? = null, -): FeatureId = feature( - id, - MapTextFeature(position, text, zoomRange, color, fontConfig = font) -) - -public fun MapFeaturesState.text( - position: Pair, - text: String, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - font: MapTextFeatureFont.() -> Unit = { size = 16f }, - id: String? = null, -): FeatureId = feature( - id, - MapTextFeature(position.toCoordinates(), text, zoomRange, color, fontConfig = font) -) diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFont.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFont.kt deleted file mode 100644 index defe478..0000000 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFont.kt +++ /dev/null @@ -1,5 +0,0 @@ -package center.sciprog.maps.compose - -public expect class MapTextFeatureFont { - public var size: Float -} \ No newline at end of file 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 abc520d..86d73bb 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 @@ -8,7 +8,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import center.sciprog.maps.coordinates.* +import center.sciprog.maps.coordinates.Gmc +import center.sciprog.maps.features.* import kotlin.math.PI import kotlin.math.log2 import kotlin.math.min @@ -20,7 +21,7 @@ import kotlin.math.min public data class MapViewConfig( val zoomSpeed: Double = 1.0 / 3.0, val onClick: MapViewPoint.(PointerEvent) -> Unit = {}, - val dragHandle: DragHandle = DragHandle.BYPASS, + val dragHandle: DragHandle = DragHandle.bypass(), val onViewChange: MapViewPoint.() -> Unit = {}, val onSelect: (GmcRectangle) -> Unit = {}, val zoomOnSelect: Boolean = true, @@ -31,14 +32,14 @@ public data class MapViewConfig( public expect fun MapView( mapTileProvider: MapTileProvider, initialViewPoint: MapViewPoint, - featuresState: MapFeaturesState, + featuresState: FeaturesState, config: MapViewConfig = MapViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), ) internal val defaultCanvasSize = DpSize(512.dp, 512.dp) -public fun GmcRectangle.computeViewPoint( +public fun Rectangle.computeViewPoint( mapTileProvider: MapTileProvider, canvasSize: DpSize = defaultCanvasSize, ): MapViewPoint { @@ -64,7 +65,7 @@ public fun MapView( modifier: Modifier = Modifier.fillMaxSize(), ) { val featuresState = key(featureMap) { - MapFeaturesState.build { + FeaturesState.build(GmcCoordinateSpace) { featureMap.forEach { feature(it.key.id, it.value) } } } @@ -72,7 +73,7 @@ public fun MapView( val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) { initialViewPoint ?: initialRectangle?.computeViewPoint(mapTileProvider) - ?: featureMap.values.computeBoundingBox(1.0)?.computeViewPoint(mapTileProvider) + ?: featureMap.values.computeBoundingBox(GmcCoordinateSpace, 1.0)?.computeViewPoint(mapTileProvider) ?: MapViewPoint.globe } @@ -93,19 +94,20 @@ public fun MapView( initialRectangle: GmcRectangle? = null, config: MapViewConfig = MapViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), - buildFeatures: MapFeaturesState.() -> Unit = {}, + buildFeatures: FeaturesState.() -> Unit = {}, ) { - val featureState = MapFeaturesState.remember(buildFeatures) + val featureState = FeaturesState.remember(GmcCoordinateSpace, buildFeatures) val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) { initialViewPoint ?: initialRectangle?.computeViewPoint(mapTileProvider) - ?: featureState.features.values.computeBoundingBox(1.0)?.computeViewPoint(mapTileProvider) + ?: featureState.features.values.computeBoundingBox(GmcCoordinateSpace,1.0)?.computeViewPoint(mapTileProvider) ?: MapViewPoint.globe } - val featureDrag: DragHandle = DragHandle.withPrimaryButton { event, start, end -> + val featureDrag: DragHandle = DragHandle.withPrimaryButton { event, start: ViewPoint, end: ViewPoint -> featureState.forEachWithAttribute(DraggableAttribute) { _, handle -> + handle as DragHandle if (!handle.handle(event, start, end)) return@withPrimaryButton false } true diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/MapViewPoint.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewPoint.kt similarity index 85% rename from maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/MapViewPoint.kt rename to maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewPoint.kt index 923b076..2b3139e 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/MapViewPoint.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewPoint.kt @@ -1,14 +1,16 @@ -package center.sciprog.maps.coordinates +package center.sciprog.maps.compose +import center.sciprog.maps.coordinates.* +import center.sciprog.maps.features.ViewPoint import kotlin.math.pow /** * Observable position on the map. Includes observation coordinate and [zoom] factor */ public data class MapViewPoint( - val focus: GeodeticMapCoordinates, - val zoom: Double, -) { + override val focus: GeodeticMapCoordinates, + override val zoom: Double, +) : ViewPoint{ val scaleFactor: Double by lazy { WebMercatorProjection.scaleFactor(zoom) } public companion object{ diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt new file mode 100644 index 0000000..10f6964 --- /dev/null +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt @@ -0,0 +1,132 @@ +package center.sciprog.maps.compose + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PointMode +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import center.sciprog.maps.coordinates.* +import center.sciprog.maps.features.* +import center.sciprog.maps.features.Feature.Companion.defaultZoomRange + + +internal fun FeaturesState.coordinatesOf(pair: Pair) = + GeodeticMapCoordinates.ofDegrees(pair.first.toDouble(), pair.second.toDouble()) + +public typealias MapFeature = Feature + +public fun FeaturesState.circle( + centerCoordinates: Pair, + zoomRange: DoubleRange = defaultZoomRange, + size: Dp = 5.dp, + color: Color = Color.Red, + id: String? = null, +): FeatureId> = feature( + id, CircleFeature(coordinateSpace, coordinatesOf(centerCoordinates), zoomRange, size, color) +) + +public fun FeaturesState.rectangle( + centerCoordinates: Pair, + zoomRange: DoubleRange = defaultZoomRange, + size: DpSize = DpSize(5.dp, 5.dp), + color: Color = Color.Red, + id: String? = null, +): FeatureId> = feature( + id, RectangleFeature(coordinateSpace, coordinatesOf(centerCoordinates), zoomRange, size, color) +) + + +public fun FeaturesState.draw( + position: Pair, + zoomRange: DoubleRange = defaultZoomRange, + id: String? = null, + draw: DrawScope.() -> Unit, +): FeatureId> = feature( + id, + DrawFeature(coordinateSpace, coordinatesOf(position), zoomRange, drawFeature = draw) +) + + +public fun FeaturesState.line( + curve: GmcCurve, + zoomRange: DoubleRange = defaultZoomRange, + color: Color = Color.Red, + id: String? = null, +): FeatureId> = feature( + id, + LineFeature(coordinateSpace, curve.forward.coordinates, curve.backward.coordinates, zoomRange, color) +) + + +public fun FeaturesState.line( + aCoordinates: Pair, + bCoordinates: Pair, + zoomRange: DoubleRange = defaultZoomRange, + color: Color = Color.Red, + id: String? = null, +): FeatureId> = feature( + id, + LineFeature(coordinateSpace, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates), zoomRange, color) +) + + +public fun FeaturesState.arc( + center: Pair, + radius: Distance, + startAngle: Angle, + arcLength: Angle, + zoomRange: DoubleRange = defaultZoomRange, + color: Color = Color.Red, + id: String? = null, +): FeatureId> = feature( + id, + ArcFeature( + coordinateSpace, + oval = buildRectangle(coordinatesOf(center), radius, radius), + startAngle = startAngle.radians.toFloat(), + arcLength = arcLength.radians.toFloat(), + zoomRange = zoomRange, + color = color + ) +) + +public fun FeaturesState.points( + points: List>, + zoomRange: DoubleRange = defaultZoomRange, + stroke: Float = 2f, + color: Color = Color.Red, + pointMode: PointMode = PointMode.Points, + id: String? = null, +): FeatureId> = + feature(id, PointsFeature(coordinateSpace, points.map(::coordinatesOf), zoomRange, stroke, color, pointMode)) + +public fun FeaturesState.image( + position: Pair, + image: ImageVector, + size: DpSize = DpSize(20.dp, 20.dp), + zoomRange: DoubleRange = defaultZoomRange, + id: String? = null, +): FeatureId> = feature( + id, + VectorImageFeature( + coordinateSpace, + coordinatesOf(position), + size, + image, + zoomRange + ) +) + +public fun FeaturesState.text( + position: Pair, + text: String, + zoomRange: DoubleRange = defaultZoomRange, + color: Color = Color.Red, + font: FeatureFont.() -> Unit = { size = 16f }, + id: String? = null, +): FeatureId> = feature( + id, + TextFeature(coordinateSpace, coordinatesOf(position), text, zoomRange, color, fontConfig = font) +) diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFontJvm.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFontJvm.kt deleted file mode 100644 index 1a2023a..0000000 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFontJvm.kt +++ /dev/null @@ -1,5 +0,0 @@ -package center.sciprog.maps.compose - -import org.jetbrains.skia.Font - -public actual typealias MapTextFeatureFont = Font \ 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 647fb62..936d676 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 @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.input.pointer.* import androidx.compose.ui.unit.* import center.sciprog.maps.coordinates.* +import center.sciprog.maps.features.* import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import mu.KotlinLogging @@ -50,7 +51,7 @@ private val logger = KotlinLogging.logger("MapView") public actual fun MapView( mapTileProvider: MapTileProvider, initialViewPoint: MapViewPoint, - featuresState: MapFeaturesState, + featuresState: FeaturesState, config: MapViewConfig, modifier: Modifier, ): Unit = key(initialViewPoint) { @@ -214,7 +215,7 @@ public actual fun MapView( } val painterCache = key(featuresState) { - featuresState.features.values.filterIsInstance().associateWith { it.painter() } + featuresState.features.values.filterIsInstance>().associateWith { it.painter() } } Canvas(canvasModifier) { @@ -229,14 +230,14 @@ public actual fun MapView( fun DrawScope.drawFeature(zoom: Int, feature: MapFeature) { when (feature) { - is MapFeatureSelector -> drawFeature(zoom, feature.selector(zoom)) - is MapCircleFeature -> drawCircle( + is FeatureSelector -> drawFeature(zoom, feature.selector(zoom)) + is CircleFeature -> drawCircle( feature.color, - feature.size, + feature.size.toPx(), center = feature.center.toOffset() ) - is MapRectangleFeature -> drawRect( + is RectangleFeature -> drawRect( feature.color, topLeft = feature.center.toOffset() - Offset( feature.size.width.toPx() / 2, @@ -245,8 +246,8 @@ public actual fun MapView( size = feature.size.toSize() ) - is MapLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset()) - is MapArcFeature -> { + is LineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset()) + is ArcFeature -> { val topLeft = feature.oval.topLeft.toOffset() val bottomRight = feature.oval.bottomRight.toOffset() @@ -254,8 +255,8 @@ public actual fun MapView( drawArc( color = feature.color, - startAngle = feature.startAngle.degrees.toFloat(), - sweepAngle = feature.arcLength.degrees.toFloat(), + startAngle = feature.startAngle.radians.degrees.toFloat(), + sweepAngle = feature.arcLength.radians.degrees.toFloat(), useCenter = false, topLeft = topLeft, size = size, @@ -264,9 +265,9 @@ public actual fun MapView( } - is MapBitmapImageFeature -> drawImage(feature.image, feature.position.toOffset()) + is BitmapImageFeature -> drawImage(feature.image, feature.position.toOffset()) - is MapVectorImageFeature -> { + is VectorImageFeature -> { val offset = feature.position.toOffset() val size = feature.size.toSize() translate(offset.x - size.width / 2, offset.y - size.height / 2) { @@ -276,7 +277,7 @@ public actual fun MapView( } } - is MapTextFeature -> drawIntoCanvas { canvas -> + is TextFeature -> drawIntoCanvas { canvas -> val offset = feature.position.toOffset() canvas.nativeCanvas.drawString( feature.text, @@ -287,20 +288,20 @@ public actual fun MapView( ) } - is MapDrawFeature -> { + is DrawFeature -> { val offset = feature.position.toOffset() translate(offset.x, offset.y) { feature.drawFeature(this) } } - is MapFeatureGroup -> { + is FeatureGroup -> { feature.children.values.forEach { drawFeature(zoom, it) } } - is MapPathFeature -> { + is PathFeature -> { TODO("MapPathFeature not implemented") // val offset = feature.rectangle.center.toOffset() - feature.targetRect.center // translate(offset.x, offset.y) { @@ -309,7 +310,7 @@ public actual fun MapView( // } } - is MapPointsFeature -> { + is PointsFeature -> { val points = feature.points.map { it.toOffset() } drawPoints( points = points, @@ -349,7 +350,7 @@ public actual fun MapView( ) } - featuresState.features.values.filter { zoom in it.zoomRange }.forEach { feature -> + featuresState.features.values.filter { viewPoint.zoom in it.zoomRange }.forEach { feature -> drawFeature(zoom, feature) } } diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeoEllipsoid.kt b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeoEllipsoid.kt index ae59ab4..35d69b6 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeoEllipsoid.kt +++ b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeoEllipsoid.kt @@ -1,6 +1,5 @@ package center.sciprog.maps.coordinates -import kotlin.math.acos import kotlin.math.pow import kotlin.math.sqrt @@ -48,7 +47,7 @@ public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRa /** * A radius of circle normal to the axis of the ellipsoid at given latitude */ -internal fun GeoEllipsoid.reducedRadius(latitude: Angle): Distance { +public fun GeoEllipsoid.reducedRadius(latitude: Angle): Distance { val reducedLatitudeTan = (1 - f) * tan(latitude) return equatorRadius / sqrt(1.0 + reducedLatitudeTan.pow(2)) } 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 deleted file mode 100644 index d60136e..0000000 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcRectangle.kt +++ /dev/null @@ -1,118 +0,0 @@ -package center.sciprog.maps.coordinates - -/** - * A section of the map between two parallels and two meridians. The figure represents a square in a Mercator projection. - * Params are two opposing "corners" of quasi-square. - * - * Note that this is a rectangle only on a Mercator projection. - */ -public data class GmcRectangle( - public val a: GeodeticMapCoordinates, - public val b: GeodeticMapCoordinates, -) { - 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, - height: Distance, - width: Distance, - ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84, - ): GmcRectangle { - val reducedRadius = ellipsoid.reducedRadius(center.latitude) - return square(center, (height / ellipsoid.polarRadius).radians, (width / reducedRadius).radians) - } - } -} - -public fun GmcRectangle.moveTo(newCenter: Gmc): GmcRectangle = GmcRectangle.square(newCenter, height = latitudeDelta, width = longitudeDelta) - -public val GmcRectangle.center: GeodeticMapCoordinates - get() = GeodeticMapCoordinates( - (a.latitude + b.latitude) / 2, - (a.longitude + b.longitude) / 2 - ) - -/** - * Minimum longitude - */ -public val GmcRectangle.left: Angle get() = minOf(a.longitude, b.longitude) - -/** - * maximum longitude - */ -public val GmcRectangle.right: Angle get() = maxOf(a.longitude, b.longitude) - -/** - * Maximum latitude - */ -public val GmcRectangle.top: Angle get() = maxOf(a.latitude, b.latitude) - -/** - * Minimum latitude - */ -public val GmcRectangle.bottom: Angle get() = minOf(a.latitude, b.latitude) - -public val GmcRectangle.longitudeDelta: Angle get() = abs(a.longitude - b.longitude) -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 - */ -public fun Collection.wrapAll(): GmcRectangle? { - if (isEmpty()) return null - //TODO optimize computation - val minLat = minOf { it.bottom } - val maxLat = maxOf { it.top } - val minLong = minOf { it.left } - val maxLong = maxOf { it.right } - return GmcRectangle(GeodeticMapCoordinates(minLat, minLong), GeodeticMapCoordinates(maxLat, maxLong)) -} \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/AttributeMap.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/AttributeMap.kt index 0d3eae1..e158f86 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/AttributeMap.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/AttributeMap.kt @@ -4,7 +4,9 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.ui.graphics.Color public object DraggableAttribute : Feature.Attribute> + public object SelectableAttribute : Feature.Attribute<(FeatureId<*>, SelectableFeature<*>) -> Unit> + public object VisibleAttribute : Feature.Attribute public object ColorAttribute : Feature.Attribute diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt index fa802dc..6c1746e 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt @@ -3,13 +3,22 @@ package center.sciprog.maps.features import androidx.compose.ui.unit.DpSize -public interface Rectangle { - public val topLeft: T - public val bottomRight: T - +public interface Area{ public operator fun contains(point: T): Boolean } +/** + * A map coordinates rectangle. [a] and [b] represent opposing angles + * of the rectangle without specifying which ones. + */ +public interface Rectangle: Area { + public val a: T + public val b: T +} + +/** + * A context for map/scheme coordinates manipulation + */ public interface CoordinateSpace { /** @@ -21,14 +30,13 @@ public interface CoordinateSpace { * Build a rectangle of visual size [size] */ public fun buildRectangle(center: T, zoom: Double, size: DpSize): Rectangle - //GmcRectangle.square(center, (size.height.value / scale).radians, (size.width.value / scale).radians) /** * Move given rectangle to be centered at [center] */ public fun Rectangle.withCenter(center: T): Rectangle - public fun Iterable>.computeRectangle(): Rectangle? + public fun Collection>.wrapRectangles(): Rectangle? - public fun Iterable.computeRectangle(): Rectangle? + public fun Collection.wrapPoints(): Rectangle? } \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/DragHandle.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/DragHandle.kt index 3a81c1f..ff78527 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/DragHandle.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/DragHandle.kt @@ -3,7 +3,7 @@ package center.sciprog.maps.features import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.isPrimaryPressed -public fun interface DragHandle> { +public fun interface DragHandle{ /** * @param event - qualifiers of the event used for drag * @param start - is a point where drag begins, end is a point where drag ends @@ -11,17 +11,17 @@ public fun interface DragHandle> { * * @return true if default event processors should be used after this one */ - public fun handle(event: PointerEvent, start: V, end: V): Boolean + public fun handle(event: PointerEvent, start: ViewPoint, end: ViewPoint): Boolean public companion object { - public val BYPASS: DragHandle<*> = DragHandle> { _, _, _ -> true } + public fun bypass(): DragHandle = DragHandle { _, _, _ -> true } /** * Process only events with primary button pressed */ - public fun withPrimaryButton( - block: (event: PointerEvent, start: V, end: V) -> Boolean, - ): DragHandle = DragHandle { event, start, end -> + public fun withPrimaryButton( + block: (event: PointerEvent, start: ViewPoint, end: ViewPoint) -> Boolean, + ): DragHandle = DragHandle { event, start, end -> if (event.buttons.isPrimaryPressed) { block(event, start, end) } else { @@ -32,7 +32,7 @@ public fun interface DragHandle> { /** * Combine several handles into one */ - public fun combine(vararg handles: DragHandle): DragHandle = DragHandle { event, start, end -> + public fun combine(vararg handles: DragHandle): DragHandle = DragHandle { event, start, end -> handles.forEach { if (!it.handle(event, start, end)) return@DragHandle false } diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt index 2c0bd02..219ed2b 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt @@ -12,19 +12,25 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import center.sciprog.maps.features.Feature.Companion.defaultZoomRange import kotlin.math.floor +public typealias DoubleRange = ClosedFloatingPointRange public interface Feature { public interface Attribute public val space: CoordinateSpace - public val zoomRange: ClosedFloatingPointRange + public val zoomRange: DoubleRange public var attributes: AttributeMap public fun getBoundingBox(zoom: Double): Rectangle? + + public companion object { + public val defaultZoomRange: ClosedFloatingPointRange = 1.0..Double.POSITIVE_INFINITY + } } public interface SelectableFeature : Feature { @@ -41,13 +47,12 @@ public fun Iterable>.computeBoundingBox( space: CoordinateSpace, zoom: Double, ): Rectangle? = with(space) { - mapNotNull { it.getBoundingBox(zoom) }.computeRectangle() + mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() } //public fun Pair.toCoordinates(): GeodeticMapCoordinates = // GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble()) -internal val defaultZoomRange = 1.0..Double.POSITIVE_INFINITY /** * A feature that decides what to show depending on the zoom value (it could change size of shape) @@ -62,20 +67,6 @@ public class FeatureSelector( override fun getBoundingBox(zoom: Double): Rectangle? = selector(floor(zoom).toInt()).getBoundingBox(zoom) } -public class DrawFeature( - override val space: CoordinateSpace, - public val rectangle: Rectangle, - override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - override var attributes: AttributeMap = AttributeMap(), - public val drawFeature: DrawScope.() -> Unit, -) : DraggableFeature { - override fun getBoundingBox(zoom: Double): Rectangle = rectangle - - override fun withCoordinates(newCoordinates: T): Feature = with(space) { - DrawFeature(space, rectangle.withCenter(newCoordinates), zoomRange, attributes, drawFeature) - } -} - public class PathFeature( override val space: CoordinateSpace, public val rectangle: Rectangle, @@ -112,7 +103,7 @@ public class PointsFeature( override var attributes: AttributeMap = AttributeMap(), ) : Feature { override fun getBoundingBox(zoom: Double): Rectangle? = with(space) { - points.computeRectangle() + points.wrapPoints() } } @@ -182,32 +173,43 @@ public class ArcFeature( } } -public class BitmapImageFeature( + +public data class DrawFeature( override val space: CoordinateSpace, - public val rectangle: Rectangle, + public val position: T, + override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, + override var attributes: AttributeMap = AttributeMap(), + public val drawFeature: DrawScope.() -> Unit, +) : DraggableFeature { + override fun getBoundingBox(zoom: Double): Rectangle = space.buildRectangle(position, position) + + override fun withCoordinates(newCoordinates: T): Feature = copy(position = newCoordinates) +} + +public data class BitmapImageFeature( + override val space: CoordinateSpace, + public val position: T, + public val size: DpSize, public val image: ImageBitmap, override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, override var attributes: AttributeMap = AttributeMap(), ) : DraggableFeature { - override fun getBoundingBox(zoom: Double): Rectangle = rectangle + override fun getBoundingBox(zoom: Double): Rectangle = space.buildRectangle(position, zoom, size) - override fun withCoordinates(newCoordinates: T): Feature = with(space) { - BitmapImageFeature(space, rectangle.withCenter(newCoordinates), image, zoomRange, attributes) - } + override fun withCoordinates(newCoordinates: T): Feature = copy(position = newCoordinates) } -public class VectorImageFeature( +public data class VectorImageFeature( override val space: CoordinateSpace, - public val rectangle: Rectangle, + public val position: T, + public val size: DpSize, public val image: ImageVector, override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, override var attributes: AttributeMap = AttributeMap(), ) : DraggableFeature { - override fun getBoundingBox(zoom: Double): Rectangle = rectangle + override fun getBoundingBox(zoom: Double): Rectangle = space.buildRectangle(position, zoom, size) - override fun withCoordinates(newCoordinates: T): Feature = with(space) { - VectorImageFeature(space, rectangle.withCenter(newCoordinates), image, zoomRange, attributes) - } + override fun withCoordinates(newCoordinates: T): Feature = copy(position = newCoordinates) @Composable public fun painter(): VectorPainter = rememberVectorPainter(image) @@ -223,11 +225,12 @@ public class FeatureGroup( override var attributes: AttributeMap = AttributeMap(), ) : Feature { override fun getBoundingBox(zoom: Double): Rectangle? = with(space) { - children.values.mapNotNull { it.getBoundingBox(zoom) }.computeRectangle() + children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() } } public class TextFeature( + override val space: CoordinateSpace, public val position: T, public val text: String, override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, @@ -235,8 +238,8 @@ public class TextFeature( override var attributes: AttributeMap = AttributeMap(), public val fontConfig: FeatureFont.() -> Unit, ) : DraggableFeature { - override fun getBoundingBox(zoom: Double): Rectangle = GmcRectangle(position, position) + override fun getBoundingBox(zoom: Double): Rectangle = space.buildRectangle(position, position) override fun withCoordinates(newCoordinates: T): Feature = - TextFeature(newCoordinates, text, zoomRange, color, attributes, fontConfig) + TextFeature(space, newCoordinates, text, zoomRange, color, attributes, fontConfig) } diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeaturesState.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeaturesState.kt new file mode 100644 index 0000000..f1f4b18 --- /dev/null +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeaturesState.kt @@ -0,0 +1,224 @@ +package center.sciprog.maps.features + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PointMode +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import center.sciprog.maps.features.Feature.Companion.defaultZoomRange +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +@JvmInline +public value class FeatureId(public val id: String) + +public class FeaturesState(public val coordinateSpace: CoordinateSpace) : + CoordinateSpace by coordinateSpace { + + @PublishedApi + internal val featureMap: MutableMap> = mutableStateMapOf() + + public val features: Map, Feature> + get() = featureMap.mapKeys { FeatureId>(it.key) } + + @Suppress("UNCHECKED_CAST") + public fun > getFeature(id: FeatureId): F = featureMap[id.id] as F + + private fun generateID(feature: Feature): String = "@feature[${feature.hashCode().toUInt()}]" + + public fun > feature(id: String?, feature: F): FeatureId { + val safeId = id ?: generateID(feature) + featureMap[safeId] = feature + return FeatureId(safeId) + } + + public fun > feature(id: FeatureId?, feature: F): FeatureId = feature(id?.id, feature) + + + public fun , V> setAttribute(id: FeatureId, key: Feature.Attribute, value: V?) { + getFeature(id).attributes.setAttribute(key, value) + } + + //TODO use context receiver for that + public fun FeatureId>.draggable( + //TODO add constraints + callback: DragHandle = DragHandle.bypass(), + ) { + val handle = DragHandle.withPrimaryButton { event, start, end -> + val feature = featureMap[id] as? DraggableFeature ?: return@withPrimaryButton true + val boundingBox = feature.getBoundingBox(start.zoom) ?: return@withPrimaryButton true + if (start.focus in boundingBox) { + feature(id, feature.withCoordinates(end.focus)) + callback.handle(event, start, end) + false + } else { + true + } + } + setAttribute(this, DraggableAttribute, handle) + } + + /** + * Cyclic update of a feature. Called infinitely until canceled. + */ + public fun > FeatureId.updated( + scope: CoroutineScope, + update: suspend (F) -> F, + ): Job = scope.launch { + while (isActive) { + feature(this@updated, update(getFeature(this@updated))) + } + } + + @Suppress("UNCHECKED_CAST") + public fun > FeatureId.selectable( + onSelect: (FeatureId, F) -> Unit, + ) { + setAttribute(this, SelectableAttribute) { id, feature -> onSelect(id as FeatureId, feature as F) } + } + + + @Suppress("UNCHECKED_CAST") + public fun getAttribute(id: FeatureId>, key: Feature.Attribute): A? = + getFeature(id).attributes[key] + + +// @Suppress("UNCHECKED_CAST") +// public fun findAllWithAttribute(key: Attribute, condition: (T) -> Boolean): Set { +// return attributes.filterValues { +// condition(it[key] as T) +// }.keys +// } + + public inline fun forEachWithAttribute( + key: Feature.Attribute, + block: (id: FeatureId<*>, attributeValue: A) -> Unit, + ) { + featureMap.forEach { (id, feature) -> + feature.attributes[key]?.let { + block(FeatureId>(id), it) + } + } + } + + public companion object { + + /** + * Build, but do not remember map feature state + */ + public fun build( + coordinateSpace: CoordinateSpace, + builder: FeaturesState.() -> Unit = {}, + ): FeaturesState = FeaturesState(coordinateSpace).apply(builder) + + /** + * Build and remember map feature state + */ + @Composable + public fun remember( + coordinateSpace: CoordinateSpace, + builder: FeaturesState.() -> Unit = {}, + ): FeaturesState = remember(builder) { + build(coordinateSpace, builder) + } + + } +} + +public fun FeaturesState.circle( + center: T, + zoomRange: DoubleRange = defaultZoomRange, + size: Dp = 5.dp, + color: Color = Color.Red, + id: String? = null, +): FeatureId> = feature( + id, CircleFeature(coordinateSpace, center, zoomRange, size, color) +) + +public fun FeaturesState.rectangle( + centerCoordinates: T, + zoomRange: DoubleRange = defaultZoomRange, + size: DpSize = DpSize(5.dp, 5.dp), + color: Color = Color.Red, + id: String? = null, +): FeatureId> = feature( + id, RectangleFeature(coordinateSpace, centerCoordinates, zoomRange, size, color) +) + +public fun FeaturesState.line( + aCoordinates: T, + bCoordinates: T, + zoomRange: DoubleRange = defaultZoomRange, + color: Color = Color.Red, + id: String? = null, +): FeatureId> = feature( + id, + LineFeature(coordinateSpace, aCoordinates, bCoordinates, zoomRange, color) +) + +public fun FeaturesState.arc( + oval: Rectangle, + startAngle: Float, + arcLength: Float, + zoomRange: DoubleRange = defaultZoomRange, + color: Color = Color.Red, + id: String? = null, +): FeatureId> = feature( + id, + ArcFeature(coordinateSpace, oval, startAngle, arcLength, zoomRange, color) +) + +public fun FeaturesState.points( + points: List, + zoomRange: DoubleRange = defaultZoomRange, + stroke: Float = 2f, + color: Color = Color.Red, + pointMode: PointMode = PointMode.Points, + id: String? = null, +): FeatureId> = + feature(id, PointsFeature(coordinateSpace, points, zoomRange, stroke, color, pointMode)) + +public fun FeaturesState.image( + position: T, + image: ImageVector, + zoomRange: DoubleRange = defaultZoomRange, + id: String? = null, +): FeatureId> = + feature( + id, + VectorImageFeature( + coordinateSpace, + position, + DpSize(image.defaultWidth, image.defaultHeight), + image, + zoomRange + ) + ) + +public fun FeaturesState.group( + zoomRange: DoubleRange = defaultZoomRange, + id: String? = null, + builder: FeaturesState.() -> Unit, +): FeatureId> { + val map = FeaturesState(coordinateSpace).apply(builder).features + val feature = FeatureGroup(coordinateSpace, map, zoomRange) + return feature(id, feature) +} + +public fun FeaturesState.text( + position: T, + text: String, + zoomRange: DoubleRange = defaultZoomRange, + color: Color = Color.Red, + font: FeatureFont.() -> Unit = { size = 16f }, + id: String? = null, +): FeatureId> = feature( + id, + TextFeature(coordinateSpace, position, text, zoomRange, color, fontConfig = font) +) diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/MapFeaturesState.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/MapFeaturesState.kt deleted file mode 100644 index e44d9ca..0000000 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/MapFeaturesState.kt +++ /dev/null @@ -1,302 +0,0 @@ -package center.sciprog.maps.features - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.PointMode -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import center.sciprog.maps.coordinates.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch - -@JvmInline -public value class FeatureId(public val id: String) - -public class MapFeaturesState { - - @PublishedApi - internal val featureMap: MutableMap = mutableStateMapOf() - - //TODO use context receiver for that - public fun FeatureId.draggable( - //TODO add constraints - callback: DragHandle = DragHandle.BYPASS, - ) { - val handle = DragHandle.withPrimaryButton { event, start, end -> - val feature = featureMap[id] as? DraggableFeature ?: return@withPrimaryButton true - val boundingBox = feature.getBoundingBox(start.zoom) ?: return@withPrimaryButton true - if (start.focus in boundingBox) { - feature(id, feature.withCoordinates(end.focus)) - callback.handle(event, start, end) - false - } else { - true - } - } - setAttribute(this, DraggableAttribute, handle) - } - - /** - * Cyclic update of a feature. Called infinitely until canceled. - */ - public fun FeatureId.updated( - scope: CoroutineScope, - update: suspend (T) -> T, - ): Job = scope.launch { - while (isActive) { - feature(this@updated, update(getFeature(this@updated))) - } - } - - @Suppress("UNCHECKED_CAST") - public fun FeatureId.selectable( - onSelect: (FeatureId, T) -> Unit, - ) { - setAttribute(this, SelectableAttribute) { id, feature -> onSelect(id as FeatureId, feature as T) } - } - - - public val features: Map, Feature> - get() = featureMap.mapKeys { FeatureId(it.key) } - - @Suppress("UNCHECKED_CAST") - public fun getFeature(id: FeatureId): T = featureMap[id.id] as T - - - private fun generateID(feature: Feature): String = "@feature[${feature.hashCode().toUInt()}]" - - public fun feature(id: String?, feature: T): FeatureId { - val safeId = id ?: generateID(feature) - featureMap[safeId] = feature - return FeatureId(safeId) - } - - public fun feature(id: FeatureId?, feature: T): FeatureId = feature(id?.id, feature) - - public fun setAttribute(id: FeatureId, key: Feature.Attribute, value: T?) { - getFeature(id).attributes.setAttribute(key, value) - } - - @Suppress("UNCHECKED_CAST") - public fun getAttribute(id: FeatureId, key: Feature.Attribute): T? = - getFeature(id).attributes[key] - - -// @Suppress("UNCHECKED_CAST") -// public fun findAllWithAttribute(key: Attribute, condition: (T) -> Boolean): Set { -// return attributes.filterValues { -// condition(it[key] as T) -// }.keys -// } - - public inline fun forEachWithAttribute( - key: Feature.Attribute, - block: (id: FeatureId<*>, attributeValue: T) -> Unit, - ) { - featureMap.forEach { (id, feature) -> - feature.attributes[key]?.let { - block(FeatureId(id), it) - } - } - } - - public companion object { - - /** - * Build, but do not remember map feature state - */ - public fun build( - builder: MapFeaturesState.() -> Unit = {}, - ): MapFeaturesState = MapFeaturesState().apply(builder) - - /** - * Build and remember map feature state - */ - @Composable - public fun remember( - builder: MapFeaturesState.() -> Unit = {}, - ): MapFeaturesState = remember(builder) { - build(builder) - } - - } -} - -public fun MapFeaturesState.circle( - center: GeodeticMapCoordinates, - zoomRange: IntRange = defaultZoomRange, - size: Float = 5f, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, CircleFeature(center, zoomRange, size, color) -) - -public fun MapFeaturesState.circle( - centerCoordinates: Pair, - zoomRange: IntRange = defaultZoomRange, - size: Float = 5f, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, CircleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color) -) - -public fun MapFeaturesState.rectangle( - centerCoordinates: Gmc, - zoomRange: IntRange = defaultZoomRange, - size: DpSize = DpSize(5.dp, 5.dp), - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, RectangleFeature(centerCoordinates, zoomRange, size, color) -) - -public fun MapFeaturesState.rectangle( - centerCoordinates: Pair, - zoomRange: IntRange = defaultZoomRange, - size: DpSize = DpSize(5.dp, 5.dp), - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, RectangleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color) -) - -public fun MapFeaturesState.draw( - position: Pair, - zoomRange: IntRange = defaultZoomRange, - id: String? = null, - draw: DrawScope.() -> Unit, -): FeatureId = feature(id, DrawFeature(position.toCoordinates(), zoomRange, drawFeature = draw)) - -public fun MapFeaturesState.line( - aCoordinates: Gmc, - bCoordinates: Gmc, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, - LineFeature(aCoordinates, bCoordinates, zoomRange, color) -) - -public fun MapFeaturesState.line( - curve: GmcCurve, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, - LineFeature(curve.forward.coordinates, curve.backward.coordinates, zoomRange, color) -) - -public fun MapFeaturesState.line( - aCoordinates: Pair, - bCoordinates: Pair, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, - LineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), zoomRange, color) -) - -public fun MapFeaturesState.arc( - oval: GmcRectangle, - startAngle: Angle, - arcLength: Angle, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, - ArcFeature(oval, startAngle, arcLength, zoomRange, color) -) - -public fun MapFeaturesState.arc( - center: Pair, - radius: Distance, - startAngle: Angle, - arcLength: Angle, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - id: String? = null, -): FeatureId = feature( - id, - ArcFeature( - oval = GmcRectangle.square(center.toCoordinates(), radius, radius), - startAngle = startAngle, - arcLength = arcLength, - zoomRange = zoomRange, - color = color - ) -) - -public fun MapFeaturesState.points( - points: List, - zoomRange: IntRange = defaultZoomRange, - stroke: Float = 2f, - color: Color = Color.Red, - pointMode: PointMode = PointMode.Points, - id: String? = null, -): FeatureId = feature(id, PointsFeature(points, zoomRange, stroke, color, pointMode)) - -@JvmName("pointsFromPairs") -public fun MapFeaturesState.points( - points: List>, - zoomRange: IntRange = defaultZoomRange, - stroke: Float = 2f, - color: Color = Color.Red, - pointMode: PointMode = PointMode.Points, - id: String? = null, -): FeatureId = - feature(id, PointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode)) - -public fun MapFeaturesState.image( - position: Pair, - image: ImageVector, - size: DpSize = DpSize(20.dp, 20.dp), - zoomRange: IntRange = defaultZoomRange, - id: String? = null, -): FeatureId = - feature(id, VectorImageFeature(position.toCoordinates(), image, size, zoomRange)) - -public fun MapFeaturesState.group( - zoomRange: IntRange = defaultZoomRange, - id: String? = null, - builder: MapFeaturesState.() -> Unit, -): FeatureId { - val map = MapFeaturesState().apply(builder).features - val feature = FeatureGroup(map, zoomRange) - return feature(id, feature) -} - -public fun MapFeaturesState.text( - position: GeodeticMapCoordinates, - text: String, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - font: FeatureFont.() -> Unit = { size = 16f }, - id: String? = null, -): FeatureId = feature( - id, - TextFeature(position, text, zoomRange, color, fontConfig = font) -) - -public fun MapFeaturesState.text( - position: Pair, - text: String, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - font: FeatureFont.() -> Unit = { size = 16f }, - id: String? = null, -): FeatureId = feature( - id, - TextFeature(position.toCoordinates(), text, zoomRange, color, fontConfig = font) -)