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.engine.cio.CIO
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.nio.file.Path
import kotlin.math.PI
import kotlin.random.Random
@ -45,6 +43,8 @@ fun App() {
val pointTwo = 55.929444 to 37.518434
val pointThree = 60.929444 to 37.518434
val dragPoint = 55.744 to 37.614
MapView(
mapTileProvider = mapTileProvider,
// initialViewPoint = MapViewPoint(
@ -67,8 +67,12 @@ fun App() {
var drag2 = Gmc.ofDegrees(55.8, 37.5)
var drag3 = Gmc.ofDegrees(56.0, 37.5)
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 ->
@ -83,6 +87,13 @@ fun App() {
true
}
rectangle(drag3, size = DpSize(10.dp, 10.dp)).draggable { _, _, end ->
drag3 = end.focus
updateLine()
true
}
updateLine()
points(
@ -98,9 +109,13 @@ fun App() {
)
//remember feature ID
val circleId: FeatureId = circle(
circle(
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) {
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)
}
}
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? =
mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
internal fun Pair<Number, Number>.toCoordinates() =
public fun Pair<Number, Number>.toCoordinates() =
GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble())
internal val defaultZoomRange = 1..18
@ -88,7 +88,7 @@ public class MapPointsFeature(
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(points.first(), points.last())
}
public class MapCircleFeature(
public data class MapCircleFeature(
public val center: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange,
public val size: Float = 5f,
@ -179,7 +179,7 @@ public class MapVectorImageFeature(
* A group of other features
*/
public class MapFeatureGroup(
public val children: Map<FeatureId, MapFeature>,
public val children: Map<FeatureId<*>, MapFeature>,
override val zoomRange: IntRange = defaultZoomRange,
) : MapFeature {
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.dp
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 SelectableAttribute : MapFeaturesState.Attribute<(FeatureId<*>, SelectableMapFeature) -> Unit>
public class MapFeaturesState internal constructor(
private val features: MutableMap<FeatureId, MapFeature>,
@PublishedApi internal val attributes: MutableMap<FeatureId, SnapshotStateMap<Attribute<out Any?>, in Any?>>,
private val featureMap: MutableMap<String, MapFeature>,
@PublishedApi internal val attributeMap: MutableMap<String, SnapshotStateMap<Attribute<out Any?>, in Any?>>,
) {
public interface Attribute<T>
//TODO use context receiver for that
public fun FeatureId.draggable(
public fun FeatureId<DraggableMapFeature>.draggable(
//TODO add constraints
callback: DragHandle = DragHandle.BYPASS,
) {
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
if (start.focus in boundingBox) {
addFeature(this, feature.withCoordinates(end.focus))
feature(id, feature.withCoordinates(end.focus))
callback.handle(event, start, end)
false
} else {
@ -41,29 +47,53 @@ public class MapFeaturesState internal constructor(
setAttribute(this, DraggableAttribute, handle)
}
public fun features(): Map<FeatureId, MapFeature> = features
private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]"
public fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId {
val safeId = id ?: generateID(feature)
features[id ?: generateID(feature)] = feature
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)
/**
* Cyclic update of a feature. Called infinitely until canceled.
*/
public fun <T : MapFeature> FeatureId<T>.updates(
scope: CoroutineScope,
update: suspend (T) -> T,
): Job = scope.launch {
while (isActive) {
feature(this@updates, update(getFeature(this@updates)))
}
}
@Suppress("UNCHECKED_CAST")
public fun <T> getAttribute(id: FeatureId, key: Attribute<T>): T? =
attributes[id]?.get(key)?.let { it as T }
public fun <T : SelectableMapFeature> FeatureId<T>.selectable(
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")
// public fun <T> findAllWithAttribute(key: Attribute<T>, condition: (T) -> Boolean): Set<FeatureId> {
@ -72,11 +102,14 @@ public class MapFeaturesState internal constructor(
// }.keys
// }
public inline fun <T> forEachWithAttribute(key: Attribute<T>, block: (id: FeatureId, attributeValue: T) -> Unit) {
attributes.forEach { (id, attributeMap) ->
public inline fun <T> forEachWithAttribute(
key: Attribute<T>,
block: (id: FeatureId<*>, attributeValue: T) -> Unit,
) {
attributeMap.forEach { (id, attributeMap) ->
attributeMap[key]?.let {
@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,
size: Float = 5f,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id: String? = null,
): FeatureId<MapCircleFeature> = feature(
id, MapCircleFeature(center, zoomRange, size, color)
)
@ -121,8 +154,8 @@ public fun MapFeaturesState.circle(
zoomRange: IntRange = defaultZoomRange,
size: Float = 5f,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id: String? = null,
): FeatureId<MapCircleFeature> = feature(
id, MapCircleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color)
)
@ -131,8 +164,8 @@ public fun MapFeaturesState.rectangle(
zoomRange: IntRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp),
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id: String? = null,
): FeatureId<MapRectangleFeature> = feature(
id, MapRectangleFeature(centerCoordinates, zoomRange, size, color)
)
@ -141,25 +174,25 @@ public fun MapFeaturesState.rectangle(
zoomRange: IntRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp),
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id: String? = null,
): FeatureId<MapRectangleFeature> = feature(
id, MapRectangleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color)
)
public fun MapFeaturesState.draw(
position: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange,
id: FeatureId? = null,
id: String? = null,
drawFeature: DrawScope.() -> Unit,
): FeatureId = addFeature(id, MapDrawFeature(position.toCoordinates(), zoomRange, drawFeature))
): FeatureId<MapDrawFeature> = feature(id, MapDrawFeature(position.toCoordinates(), zoomRange, drawFeature))
public fun MapFeaturesState.line(
aCoordinates: Gmc,
bCoordinates: Gmc,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id: String? = null,
): FeatureId<MapLineFeature> = feature(
id,
MapLineFeature(aCoordinates, bCoordinates, zoomRange, color)
)
@ -168,8 +201,8 @@ public fun MapFeaturesState.line(
curve: GmcCurve,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id: String? = null,
): FeatureId<MapLineFeature> = feature(
id,
MapLineFeature(curve.forward.coordinates, curve.backward.coordinates, zoomRange, color)
)
@ -179,8 +212,8 @@ public fun MapFeaturesState.line(
bCoordinates: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id: String? = null,
): FeatureId<MapLineFeature> = feature(
id,
MapLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), zoomRange, color)
)
@ -191,8 +224,8 @@ public fun MapFeaturesState.arc(
arcLength: Angle,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id: String? = null,
): FeatureId<MapArcFeature> = feature(
id,
MapArcFeature(oval, startAngle, arcLength, zoomRange, color)
)
@ -204,8 +237,8 @@ public fun MapFeaturesState.arc(
arcLength: Angle,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id: String? = null,
): FeatureId<MapArcFeature> = feature(
id,
MapArcFeature(
oval = GmcRectangle.square(center.toCoordinates(), radius, radius),
@ -222,8 +255,8 @@ public fun MapFeaturesState.points(
stroke: Float = 2f,
color: Color = Color.Red,
pointMode: PointMode = PointMode.Points,
id: FeatureId? = null,
): FeatureId = addFeature(id, MapPointsFeature(points, zoomRange, stroke, color, pointMode))
id: String? = null,
): FeatureId<MapPointsFeature> = feature(id, MapPointsFeature(points, zoomRange, stroke, color, pointMode))
@JvmName("pointsFromPairs")
public fun MapFeaturesState.points(
@ -232,28 +265,30 @@ public fun MapFeaturesState.points(
stroke: Float = 2f,
color: Color = Color.Red,
pointMode: PointMode = PointMode.Points,
id: FeatureId? = null,
): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode))
id: String? = null,
): FeatureId<MapPointsFeature> =
feature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode))
public fun MapFeaturesState.image(
position: Pair<Double, Double>,
image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp),
zoomRange: IntRange = defaultZoomRange,
id: FeatureId? = null,
): FeatureId = addFeature(id, MapVectorImageFeature(position.toCoordinates(), image, size, zoomRange))
id: String? = null,
): FeatureId<MapVectorImageFeature> =
feature(id, MapVectorImageFeature(position.toCoordinates(), image, size, zoomRange))
public fun MapFeaturesState.group(
zoomRange: IntRange = defaultZoomRange,
id: FeatureId? = null,
id: String? = null,
builder: MapFeaturesState.() -> Unit,
): FeatureId {
): FeatureId<MapFeatureGroup> {
val map = MapFeaturesState(
mutableStateMapOf(),
mutableStateMapOf()
).apply(builder).features()
val feature = MapFeatureGroup(map, zoomRange)
return addFeature(id, feature)
return feature(id, feature)
}
public fun MapFeaturesState.text(
@ -262,8 +297,8 @@ public fun MapFeaturesState.text(
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
font: MapTextFeatureFont.() -> Unit = { size = 16f },
id: FeatureId? = null,
): FeatureId = addFeature(id, MapTextFeature(position, text, zoomRange, color, font))
id: String? = null,
): FeatureId<MapTextFeature> = feature(id, MapTextFeature(position, text, zoomRange, color, font))
public fun MapFeaturesState.text(
position: Pair<Double, Double>,
@ -271,5 +306,5 @@ public fun MapFeaturesState.text(
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
font: MapTextFeatureFont.() -> Unit = { size = 16f },
id: FeatureId? = null,
): FeatureId = addFeature(id, MapTextFeature(position.toCoordinates(), text, zoomRange, color, font))
id: String? = null,
): FeatureId<MapTextFeature> = feature(id, MapTextFeature(position.toCoordinates(), text, zoomRange, color, font))

View File

@ -98,13 +98,13 @@ public fun MapView(
mapTileProvider: MapTileProvider,
initialViewPoint: MapViewPoint? = null,
initialRectangle: GmcRectangle? = null,
featureMap: Map<FeatureId, MapFeature>,
featureMap: Map<FeatureId<*>, MapFeature>,
config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(),
) {
val featuresState = key(featureMap) {
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 ->
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
if(!handle.handle(event, start, end)) return@withPrimaryButton false
if (!handle.handle(event, start, end)) return@withPrimaryButton false
}
true
}