Typed features for Map

This commit is contained in:
Alexander Nozik 2022-12-09 22:21:24 +03:00
parent 5448929d31
commit edeb422335
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
5 changed files with 123 additions and 85 deletions

View File

@ -15,8 +15,6 @@ import center.sciprog.maps.coordinates.*
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.nio.file.Path import java.nio.file.Path
import kotlin.math.PI import kotlin.math.PI
import kotlin.random.Random import kotlin.random.Random
@ -45,6 +43,8 @@ fun App() {
val pointTwo = 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 = MapViewPoint( // initialViewPoint = MapViewPoint(
@ -67,8 +67,12 @@ fun App() {
var drag2 = Gmc.ofDegrees(55.8, 37.5) var drag2 = Gmc.ofDegrees(55.8, 37.5)
var drag3 = Gmc.ofDegrees(56.0, 37.5)
fun updateLine() { fun updateLine() {
line(drag1, drag2, id = "connection", color = Color.Magenta) line(drag1, drag2, id = "connection1", color = Color.Magenta)
line(drag2, drag3, id = "connection2", color = Color.Magenta)
line(drag3, drag1, id = "connection3", color = Color.Magenta)
} }
rectangle(drag1, size = DpSize(10.dp, 10.dp)).draggable { _, _, end -> rectangle(drag1, size = DpSize(10.dp, 10.dp)).draggable { _, _, end ->
@ -83,6 +87,13 @@ fun App() {
true true
} }
rectangle(drag3, size = DpSize(10.dp, 10.dp)).draggable { _, _, end ->
drag3 = end.focus
updateLine()
true
}
updateLine() updateLine()
points( points(
@ -98,9 +109,13 @@ fun App() {
) )
//remember feature ID //remember feature ID
val circleId: FeatureId = circle( circle(
centerCoordinates = pointTwo, centerCoordinates = pointTwo,
) ).updates(scope) {
delay(200)
//Overwrite a feature with new color
it.copy(color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))
}
draw(position = pointThree) { draw(position = pointThree) {
drawLine(start = Offset(-10f, -10f), end = Offset(10f, 10f), color = Color.Red) drawLine(start = Offset(-10f, -10f), end = Offset(10f, 10f), color = Color.Red)
@ -118,18 +133,6 @@ fun App() {
text(position = it, it.toShortString(), id = "text", color = Color.Blue) text(position = it, it.toShortString(), id = "text", color = Color.Blue)
} }
} }
scope.launch {
while (isActive) {
delay(200)
//Overwrite a feature with new color
circle(
pointTwo,
id = circleId,
color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat())
)
}
}
} }
} }
} }

View File

@ -33,7 +33,7 @@ public interface DraggableMapFeature : SelectableMapFeature {
public fun Iterable<MapFeature>.computeBoundingBox(zoom: Double): GmcRectangle? = public fun Iterable<MapFeature>.computeBoundingBox(zoom: Double): GmcRectangle? =
mapNotNull { it.getBoundingBox(zoom) }.wrapAll() mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
internal fun Pair<Number, Number>.toCoordinates() = public fun Pair<Number, Number>.toCoordinates() =
GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble()) GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble())
internal val defaultZoomRange = 1..18 internal val defaultZoomRange = 1..18
@ -88,7 +88,7 @@ public class MapPointsFeature(
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(points.first(), points.last()) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(points.first(), points.last())
} }
public class MapCircleFeature( public data class MapCircleFeature(
public val center: GeodeticMapCoordinates, public val center: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val size: Float = 5f, public val size: Float = 5f,
@ -179,7 +179,7 @@ public class MapVectorImageFeature(
* A group of other features * A group of other features
*/ */
public class MapFeatureGroup( 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: Double): GmcRectangle? = override fun getBoundingBox(zoom: Double): GmcRectangle? =

View File

@ -11,27 +11,33 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.maps.coordinates.* import center.sciprog.maps.coordinates.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
public typealias FeatureId = String @JvmInline
public value class FeatureId<out MapFeature>(public val id: String)
public object DraggableAttribute : MapFeaturesState.Attribute<DragHandle> public object DraggableAttribute : MapFeaturesState.Attribute<DragHandle>
public object SelectableAttribute : MapFeaturesState.Attribute<(FeatureId<*>, SelectableMapFeature) -> Unit>
public class MapFeaturesState internal constructor( public class MapFeaturesState internal constructor(
private val features: MutableMap<FeatureId, MapFeature>, private val featureMap: MutableMap<String, MapFeature>,
@PublishedApi internal val attributes: MutableMap<FeatureId, SnapshotStateMap<Attribute<out Any?>, in Any?>>, @PublishedApi internal val attributeMap: MutableMap<String, SnapshotStateMap<Attribute<out Any?>, in Any?>>,
) { ) {
public interface Attribute<T> public interface Attribute<T>
//TODO use context receiver for that //TODO use context receiver for that
public fun FeatureId.draggable( public fun FeatureId<DraggableMapFeature>.draggable(
//TODO add constraints //TODO add constraints
callback: DragHandle = DragHandle.BYPASS, callback: DragHandle = DragHandle.BYPASS,
) { ) {
val handle = DragHandle.withPrimaryButton { event, start, end -> val handle = DragHandle.withPrimaryButton { event, start, end ->
val feature = features[this] as? DraggableMapFeature ?: return@withPrimaryButton true val feature = featureMap[id] as? DraggableMapFeature ?: return@withPrimaryButton true
val boundingBox = feature.getBoundingBox(start.zoom) ?: return@withPrimaryButton true val boundingBox = feature.getBoundingBox(start.zoom) ?: return@withPrimaryButton true
if (start.focus in boundingBox) { if (start.focus in boundingBox) {
addFeature(this, feature.withCoordinates(end.focus)) feature(id, feature.withCoordinates(end.focus))
callback.handle(event, start, end) callback.handle(event, start, end)
false false
} else { } else {
@ -41,29 +47,53 @@ public class MapFeaturesState internal constructor(
setAttribute(this, DraggableAttribute, handle) setAttribute(this, DraggableAttribute, handle)
} }
/**
public fun features(): Map<FeatureId, MapFeature> = features * Cyclic update of a feature. Called infinitely until canceled.
*/
public fun <T : MapFeature> FeatureId<T>.updates(
private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]" scope: CoroutineScope,
update: suspend (T) -> T,
public fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId { ): Job = scope.launch {
val safeId = id ?: generateID(feature) while (isActive) {
features[id ?: generateID(feature)] = feature feature(this@updates, update(getFeature(this@updates)))
return safeId }
}
public fun <T> setAttribute(id: FeatureId, key: Attribute<T>, value: T) {
attributes.getOrPut(id) { mutableStateMapOf() }[key] = value
}
public fun removeAttribute(id: FeatureId, key: Attribute<*>) {
attributes[id]?.remove(key)
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun <T> getAttribute(id: FeatureId, key: Attribute<T>): T? = public fun <T : SelectableMapFeature> FeatureId<T>.selectable(
attributes[id]?.get(key)?.let { it as T } onSelect: (FeatureId<T>, T) -> Unit,
) {
setAttribute(this, SelectableAttribute) { id, feature -> onSelect(id as FeatureId<T>, feature as T) }
}
public fun features(): Map<FeatureId<*>, MapFeature> = featureMap.mapKeys { FeatureId<MapFeature>(it.key) }
@Suppress("UNCHECKED_CAST")
public fun <T : MapFeature> getFeature(id: FeatureId<T>): T = featureMap[id.id] as T
private fun generateID(feature: MapFeature): String = "@feature[${feature.hashCode().toUInt()}]"
public fun <T : MapFeature> feature(id: String?, feature: T): FeatureId<T> {
val safeId = id ?: generateID(feature)
featureMap[safeId] = feature
return FeatureId(safeId)
}
public fun <T : MapFeature> feature(id: FeatureId<T>?, feature: T): FeatureId<T> = feature(id?.id, feature)
public fun <T> setAttribute(id: FeatureId<*>, key: Attribute<T>, value: T) {
attributeMap.getOrPut(id.id) { mutableStateMapOf() }[key] = value
}
public fun removeAttribute(id: FeatureId<*>, key: Attribute<*>) {
attributeMap[id.id]?.remove(key)
}
@Suppress("UNCHECKED_CAST")
public fun <T> getAttribute(id: FeatureId<*>, key: Attribute<T>): T? =
attributeMap[id.id]?.get(key)?.let { it as T }
// @Suppress("UNCHECKED_CAST") // @Suppress("UNCHECKED_CAST")
// public fun <T> findAllWithAttribute(key: Attribute<T>, condition: (T) -> Boolean): Set<FeatureId> { // public fun <T> findAllWithAttribute(key: Attribute<T>, condition: (T) -> Boolean): Set<FeatureId> {
@ -72,11 +102,14 @@ public class MapFeaturesState internal constructor(
// }.keys // }.keys
// } // }
public inline fun <T> forEachWithAttribute(key: Attribute<T>, block: (id: FeatureId, attributeValue: T) -> Unit) { public inline fun <T> forEachWithAttribute(
attributes.forEach { (id, attributeMap) -> key: Attribute<T>,
block: (id: FeatureId<*>, attributeValue: T) -> Unit,
) {
attributeMap.forEach { (id, attributeMap) ->
attributeMap[key]?.let { attributeMap[key]?.let {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
block(id, it as T) block(FeatureId<MapFeature>(id), it as T)
} }
} }
} }
@ -111,8 +144,8 @@ public fun MapFeaturesState.circle(
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
size: Float = 5f, size: Float = 5f,
color: Color = Color.Red, color: Color = Color.Red,
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature( ): FeatureId<MapCircleFeature> = feature(
id, MapCircleFeature(center, zoomRange, size, color) id, MapCircleFeature(center, zoomRange, size, color)
) )
@ -121,8 +154,8 @@ public fun MapFeaturesState.circle(
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
size: Float = 5f, size: Float = 5f,
color: Color = Color.Red, color: Color = Color.Red,
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature( ): FeatureId<MapCircleFeature> = feature(
id, MapCircleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color) id, MapCircleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color)
) )
@ -131,8 +164,8 @@ public fun MapFeaturesState.rectangle(
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp), size: DpSize = DpSize(5.dp, 5.dp),
color: Color = Color.Red, color: Color = Color.Red,
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature( ): FeatureId<MapRectangleFeature> = feature(
id, MapRectangleFeature(centerCoordinates, zoomRange, size, color) id, MapRectangleFeature(centerCoordinates, zoomRange, size, color)
) )
@ -141,25 +174,25 @@ public fun MapFeaturesState.rectangle(
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp), size: DpSize = DpSize(5.dp, 5.dp),
color: Color = Color.Red, color: Color = Color.Red,
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature( ): FeatureId<MapRectangleFeature> = feature(
id, MapRectangleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color) id, MapRectangleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color)
) )
public fun MapFeaturesState.draw( public fun MapFeaturesState.draw(
position: Pair<Double, Double>, position: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
id: FeatureId? = null, id: String? = null,
drawFeature: DrawScope.() -> Unit, drawFeature: DrawScope.() -> Unit,
): FeatureId = addFeature(id, MapDrawFeature(position.toCoordinates(), zoomRange, drawFeature)) ): FeatureId<MapDrawFeature> = feature(id, MapDrawFeature(position.toCoordinates(), zoomRange, drawFeature))
public fun MapFeaturesState.line( public fun MapFeaturesState.line(
aCoordinates: Gmc, aCoordinates: Gmc,
bCoordinates: Gmc, bCoordinates: Gmc,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red, color: Color = Color.Red,
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature( ): FeatureId<MapLineFeature> = feature(
id, id,
MapLineFeature(aCoordinates, bCoordinates, zoomRange, color) MapLineFeature(aCoordinates, bCoordinates, zoomRange, color)
) )
@ -168,8 +201,8 @@ public fun MapFeaturesState.line(
curve: GmcCurve, curve: GmcCurve,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red, color: Color = Color.Red,
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature( ): FeatureId<MapLineFeature> = feature(
id, id,
MapLineFeature(curve.forward.coordinates, curve.backward.coordinates, zoomRange, color) MapLineFeature(curve.forward.coordinates, curve.backward.coordinates, zoomRange, color)
) )
@ -179,8 +212,8 @@ public fun MapFeaturesState.line(
bCoordinates: Pair<Double, Double>, bCoordinates: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red, color: Color = Color.Red,
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature( ): FeatureId<MapLineFeature> = feature(
id, id,
MapLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), zoomRange, color) MapLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), zoomRange, color)
) )
@ -191,8 +224,8 @@ public fun MapFeaturesState.arc(
arcLength: Angle, arcLength: Angle,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red, color: Color = Color.Red,
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature( ): FeatureId<MapArcFeature> = feature(
id, id,
MapArcFeature(oval, startAngle, arcLength, zoomRange, color) MapArcFeature(oval, startAngle, arcLength, zoomRange, color)
) )
@ -204,8 +237,8 @@ public fun MapFeaturesState.arc(
arcLength: Angle, arcLength: Angle,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red, color: Color = Color.Red,
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature( ): FeatureId<MapArcFeature> = feature(
id, id,
MapArcFeature( MapArcFeature(
oval = GmcRectangle.square(center.toCoordinates(), radius, radius), oval = GmcRectangle.square(center.toCoordinates(), radius, radius),
@ -222,8 +255,8 @@ public fun MapFeaturesState.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: String? = null,
): FeatureId = addFeature(id, MapPointsFeature(points, zoomRange, stroke, color, pointMode)) ): FeatureId<MapPointsFeature> = feature(id, MapPointsFeature(points, zoomRange, stroke, color, pointMode))
@JvmName("pointsFromPairs") @JvmName("pointsFromPairs")
public fun MapFeaturesState.points( public fun MapFeaturesState.points(
@ -232,28 +265,30 @@ public fun MapFeaturesState.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: String? = null,
): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode)) ): FeatureId<MapPointsFeature> =
feature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode))
public fun MapFeaturesState.image( public fun MapFeaturesState.image(
position: Pair<Double, Double>, position: Pair<Double, Double>,
image: ImageVector, image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp), size: DpSize = DpSize(20.dp, 20.dp),
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature(id, MapVectorImageFeature(position.toCoordinates(), image, size, zoomRange)) ): FeatureId<MapVectorImageFeature> =
feature(id, MapVectorImageFeature(position.toCoordinates(), image, size, zoomRange))
public fun MapFeaturesState.group( public fun MapFeaturesState.group(
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
id: FeatureId? = null, id: String? = null,
builder: MapFeaturesState.() -> Unit, builder: MapFeaturesState.() -> Unit,
): FeatureId { ): FeatureId<MapFeatureGroup> {
val map = MapFeaturesState( val map = MapFeaturesState(
mutableStateMapOf(), mutableStateMapOf(),
mutableStateMapOf() mutableStateMapOf()
).apply(builder).features() ).apply(builder).features()
val feature = MapFeatureGroup(map, zoomRange) val feature = MapFeatureGroup(map, zoomRange)
return addFeature(id, feature) return feature(id, feature)
} }
public fun MapFeaturesState.text( public fun MapFeaturesState.text(
@ -262,8 +297,8 @@ public fun MapFeaturesState.text(
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red, color: Color = Color.Red,
font: MapTextFeatureFont.() -> Unit = { size = 16f }, font: MapTextFeatureFont.() -> Unit = { size = 16f },
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature(id, MapTextFeature(position, text, zoomRange, color, font)) ): FeatureId<MapTextFeature> = feature(id, MapTextFeature(position, text, zoomRange, color, font))
public fun MapFeaturesState.text( public fun MapFeaturesState.text(
position: Pair<Double, Double>, position: Pair<Double, Double>,
@ -271,5 +306,5 @@ public fun MapFeaturesState.text(
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red, color: Color = Color.Red,
font: MapTextFeatureFont.() -> Unit = { size = 16f }, font: MapTextFeatureFont.() -> Unit = { size = 16f },
id: FeatureId? = null, id: String? = null,
): FeatureId = addFeature(id, MapTextFeature(position.toCoordinates(), text, zoomRange, color, font)) ): FeatureId<MapTextFeature> = feature(id, MapTextFeature(position.toCoordinates(), text, zoomRange, color, font))

View File

@ -98,13 +98,13 @@ public fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
initialViewPoint: MapViewPoint? = null, initialViewPoint: MapViewPoint? = null,
initialRectangle: GmcRectangle? = null, initialRectangle: GmcRectangle? = null,
featureMap: Map<FeatureId, MapFeature>, featureMap: Map<FeatureId<*>, MapFeature>,
config: MapViewConfig = MapViewConfig(), config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
) { ) {
val featuresState = key(featureMap) { val featuresState = key(featureMap) {
MapFeaturesState.build { MapFeaturesState.build {
featureMap.forEach(::addFeature) featureMap.forEach { feature(it.key.id, it.value) }
} }
} }
@ -147,7 +147,7 @@ public fun MapView(
val featureDrag: DragHandle = DragHandle.withPrimaryButton { event, start, end -> val featureDrag: DragHandle = DragHandle.withPrimaryButton { event, start, end ->
featureState.forEachWithAttribute(DraggableAttribute) { _, handle -> featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
if(!handle.handle(event, start, end)) return@withPrimaryButton false if (!handle.handle(event, start, end)) return@withPrimaryButton false
} }
true true
} }