[WIP] Generic features

This commit is contained in:
Alexander Nozik 2022-12-17 23:12:00 +03:00
parent 0d9efadcb8
commit 7735d667bc
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
13 changed files with 808 additions and 56 deletions

View File

@ -1,6 +1,6 @@
kotlin.code.style=official
compose.version=1.2.1
compose.version=1.2.2
agp.version=4.2.2
android.useAndroidX=true

View File

@ -1,5 +1,3 @@
import org.jetbrains.compose.compose
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")

View File

@ -0,0 +1,43 @@
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
}
}
}

View File

@ -18,7 +18,7 @@ import kotlin.math.floor
public interface MapFeature {
public interface Attribute<T>
public val zoomRange: IntRange
public val zoomRange: ClosedFloatingPointRange<Double>
public var attributes: AttributeMap
@ -41,7 +41,7 @@ public fun Iterable<MapFeature>.computeBoundingBox(zoom: Double): GmcRectangle?
public fun Pair<Number, Number>.toCoordinates(): GeodeticMapCoordinates =
GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble())
internal val defaultZoomRange = 1..18
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)
@ -50,14 +50,14 @@ public class MapFeatureSelector(
override var attributes: AttributeMap = AttributeMap(),
public val selector: (zoom: Int) -> MapFeature,
) : MapFeature {
override val zoomRange: IntRange get() = defaultZoomRange
override val zoomRange: ClosedFloatingPointRange<Double> get() = defaultZoomRange
override fun getBoundingBox(zoom: Double): GmcRectangle? = selector(floor(zoom).toInt()).getBoundingBox(zoom)
}
public class MapDrawFeature(
public val position: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
public val drawFeature: DrawScope.() -> Unit,
) : DraggableMapFeature {
@ -76,7 +76,7 @@ public class MapPathFeature(
public val brush: Brush,
public val style: DrawStyle = Fill,
public val targetRect: Rect = path.getBounds(),
override val zoomRange: IntRange = defaultZoomRange,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableMapFeature {
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
@ -88,7 +88,7 @@ public class MapPathFeature(
public class MapPointsFeature(
public val points: List<GeodeticMapCoordinates>,
override val zoomRange: IntRange = defaultZoomRange,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val stroke: Float = 2f,
public val color: Color = Color.Red,
public val pointMode: PointMode = PointMode.Points,
@ -99,7 +99,7 @@ public class MapPointsFeature(
public data class MapCircleFeature(
public val center: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val size: Float = 5f,
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
@ -115,7 +115,7 @@ public data class MapCircleFeature(
public class MapRectangleFeature(
public val center: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val size: DpSize = DpSize(5.dp, 5.dp),
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
@ -132,7 +132,7 @@ public class MapRectangleFeature(
public class MapLineFeature(
public val a: GeodeticMapCoordinates,
public val b: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
) : SelectableMapFeature {
@ -151,7 +151,7 @@ public class MapArcFeature(
public val oval: GmcRectangle,
public val startAngle: Angle,
public val arcLength: Angle,
override val zoomRange: IntRange = defaultZoomRange,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableMapFeature {
@ -165,7 +165,7 @@ public class MapBitmapImageFeature(
public val position: GeodeticMapCoordinates,
public val image: ImageBitmap,
public val size: IntSize = IntSize(15, 15),
override val zoomRange: IntRange = defaultZoomRange,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
@ -178,7 +178,7 @@ public class MapVectorImageFeature(
public val position: GeodeticMapCoordinates,
public val image: ImageVector,
public val size: DpSize = DpSize(20.dp, 20.dp),
override val zoomRange: IntRange = defaultZoomRange,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
@ -195,7 +195,7 @@ public class MapVectorImageFeature(
*/
public class MapFeatureGroup(
public val children: Map<FeatureId<*>, MapFeature>,
override val zoomRange: IntRange = defaultZoomRange,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : MapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle? =
@ -205,7 +205,7 @@ public class MapFeatureGroup(
public class MapTextFeature(
public val position: GeodeticMapCoordinates,
public val text: String,
override val zoomRange: IntRange = defaultZoomRange,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Black,
override var attributes: AttributeMap = AttributeMap(),
public val fontConfig: MapTextFeatureFont.() -> Unit,

View File

@ -6,7 +6,6 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.isPrimaryPressed
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import center.sciprog.maps.coordinates.*
@ -15,44 +14,6 @@ import kotlin.math.log2
import kotlin.math.min
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
}
}
}
//TODO consider replacing by modifier
/**
*/

View File

@ -0,0 +1,36 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
`maven-publish`
}
kotlin {
explicitApi = org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode.Warning
jvm {
compilations.all {
kotlinOptions.jvmTarget = space.kscience.gradle.KScienceVersions.JVM_TARGET.toString()
}
}
sourceSets {
commonMain {
dependencies {
api(compose.foundation)
}
}
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit5"))
implementation("org.junit.jupiter:junit-jupiter:5.8.2")
}
}
}
}
java {
targetCompatibility = space.kscience.gradle.KScienceVersions.JVM_TARGET
}
tasks.withType<Test> {
useJUnitPlatform()
}

View File

@ -0,0 +1,94 @@
/*
* Copyright 2018-2021 KMath contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package center.sciprog.maps.features
import kotlin.math.PI
import kotlin.math.floor
// Taken from KMath dev version, to be used directly in the future
public sealed interface Angle : Comparable<Angle> {
public val radians: Radians
public val degrees: Degrees
public operator fun plus(other: Angle): Angle
public operator fun minus(other: Angle): Angle
public operator fun times(other: Number): Angle
public operator fun div(other: Number): Angle
public operator fun div(other: Angle): Double
public operator fun unaryMinus(): Angle
public companion object {
public val zero: Angle = 0.radians
public val pi: Angle = PI.radians
public val piTimes2: Angle = (2 * PI).radians
public val piDiv2: Angle = (PI / 2).radians
}
}
/**
* Type safe radians
*/
@JvmInline
public value class Radians(public val value: Double) : Angle {
override val radians: Radians
get() = this
override val degrees: Degrees
get() = Degrees(value * 180 / PI)
public override fun plus(other: Angle): Radians = Radians(value + other.radians.value)
public override fun minus(other: Angle): Radians = Radians(value - other.radians.value)
public override fun times(other: Number): Radians = Radians(value * other.toDouble())
public override fun div(other: Number): Radians = Radians(value / other.toDouble())
override fun div(other: Angle): Double = value / other.radians.value
public override fun unaryMinus(): Radians = Radians(-value)
override fun compareTo(other: Angle): Int = value.compareTo(other.radians.value)
}
public fun sin(angle: Angle): Double = kotlin.math.sin(angle.radians.value)
public fun cos(angle: Angle): Double = kotlin.math.cos(angle.radians.value)
public fun tan(angle: Angle): Double = kotlin.math.tan(angle.radians.value)
public val Number.radians: Radians get() = Radians(toDouble())
/**
* Type safe degrees
*/
@JvmInline
public value class Degrees(public val value: Double) : Angle {
override val radians: Radians
get() = Radians(value * PI / 180)
override val degrees: Degrees
get() = this
public override fun plus(other: Angle): Degrees = Degrees(value + other.degrees.value)
public override fun minus(other: Angle): Degrees = Degrees(value - other.degrees.value)
public override fun times(other: Number): Degrees = Degrees(value * other.toDouble())
public override fun div(other: Number): Degrees = Degrees(value / other.toDouble())
override fun div(other: Angle): Double = value / other.degrees.value
public override fun unaryMinus(): Degrees = Degrees(-value)
override fun compareTo(other: Angle): Int = value.compareTo(other.degrees.value)
}
public val Number.degrees: Degrees get() = Degrees(toDouble())
/**
* Normalized angle 2 PI range symmetric around [center]. By default, uses (0, 2PI) range.
*/
public fun Angle.normalized(center: Angle = Angle.pi): Angle =
this - Angle.piTimes2 * floor((radians.value + PI - center.radians.value) / PI/2)
public fun abs(angle: Angle): Angle = if (angle < Angle.zero) -angle else angle
public fun Radians.toFloat(): Float = value.toFloat()
public fun Degrees.toFloat(): Float = value.toFloat()

View File

@ -0,0 +1,43 @@
package center.sciprog.maps.features
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.ui.graphics.Color
public object DraggableAttribute : Feature.Attribute<DragHandle<*>>
public object SelectableAttribute : Feature.Attribute<(FeatureId<*>, SelectableFeature<*>) -> Unit>
public object VisibleAttribute : Feature.Attribute<Boolean>
public object ColorAttribute : Feature.Attribute<Color>
public class AttributeMap {
public val map: MutableMap<Feature.Attribute<*>, Any> = mutableStateMapOf()
public fun <T, A : Feature.Attribute<T>> setAttribute(
attribute: A,
attrValue: T?,
) {
if (attrValue == null) {
map.remove(attribute)
} else {
map[attribute] = attrValue
}
}
@Suppress("UNCHECKED_CAST")
public operator fun <T> get(attribute: Feature.Attribute<T>): 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})"
}

View File

@ -0,0 +1,42 @@
package center.sciprog.maps.features
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.isPrimaryPressed
public fun interface DragHandle<in V: ViewPoint<*>> {
/**
* @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: V, end: V): Boolean
public companion object {
public val BYPASS: DragHandle<*> = DragHandle<ViewPoint<*>> { _, _, _ -> true }
/**
* Process only events with primary button pressed
*/
public fun <V> withPrimaryButton(
block: (event: PointerEvent, start: V, end: V) -> Boolean,
): DragHandle<V> = DragHandle { event, start, end ->
if (event.buttons.isPrimaryPressed) {
block(event, start, end)
} else {
true
}
}
/**
* Combine several handles into one
*/
public fun <V> combine(vararg handles: DragHandle<V>): DragHandle<V> = DragHandle { event, start, end ->
handles.forEach {
if (!it.handle(event, start, end)) return@DragHandle false
}
return@DragHandle true
}
}
}

View File

@ -0,0 +1,232 @@
package center.sciprog.maps.features
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 kotlin.math.floor
/**
* @param T type of coordinates used for the view point
*/
public interface ViewPoint<T: Any> {
public val focus: T
public val zoom: Double
}
public interface Rectangle<T: Any>{
public val topLeft: T
public val bottomRight: T
public operator fun contains(point: T): Boolean
}
public interface Feature<T: Any> {
public interface Attribute<T>
public val zoomRange: ClosedFloatingPointRange<Double>
public var attributes: AttributeMap
public fun getBoundingBox(zoom: Double): Rectangle<T>?
}
public interface SelectableFeature<T: Any> : Feature<T> {
public operator fun contains(point: ViewPoint<T>): Boolean = getBoundingBox(point.zoom)?.let {
point.focus in it
} ?: false
}
public interface DraggableFeature<T: Any> : SelectableFeature<T> {
public fun withCoordinates(newCoordinates: T): Feature<T>
}
public fun <T: Any> Iterable<Feature<T>>.computeBoundingBox(zoom: Double): Rectangle<T>? =
mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
//public fun Pair<Number, Number>.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 FeatureSelector<T: Any>(
override var attributes: AttributeMap = AttributeMap(),
public val selector: (zoom: Int) -> Feature<T>,
) : Feature<T> {
override val zoomRange: ClosedFloatingPointRange<Double> get() = defaultZoomRange
override fun getBoundingBox(zoom: Double): Rectangle<T>? = selector(floor(zoom).toInt()).getBoundingBox(zoom)
}
public class DrawFeature<T: Any>(
public val position: T,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
public val drawFeature: DrawScope.() -> Unit,
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> {
//TODO add box computation
return GmcRectangle(position, position)
}
override fun withCoordinates(newCoordinates: T): Feature<T> =
DrawFeature(newCoordinates, zoomRange, attributes, drawFeature)
}
public class PathFeature<T: Any>(
public val rectangle: Rectangle<T>,
public val path: Path,
public val brush: Brush,
public val style: DrawStyle = Fill,
public val targetRect: Rect = path.getBounds(),
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun withCoordinates(newCoordinates: T): Feature<T> =
PathFeature(rectangle.moveTo(newCoordinates), path, brush, style, targetRect, zoomRange)
override fun getBoundingBox(zoom: Double): Rectangle<T> = rectangle
}
public class PointsFeature<T: Any>(
public val points: List<T>,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val stroke: Float = 2f,
public val color: Color = Color.Red,
public val pointMode: PointMode = PointMode.Points,
override var attributes: AttributeMap = AttributeMap(),
) : Feature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = GmcRectangle(points.first(), points.last())
}
public data class CircleFeature<T: Any>(
public val center: T,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val size: Float = 5f,
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> {
val scale = WebMercatorProjection.scaleFactor(zoom)
return GmcRectangle.square(center, (size / scale).radians, (size / scale).radians)
}
override fun withCoordinates(newCoordinates: T): Feature<T> =
CircleFeature(newCoordinates, zoomRange, size, color, attributes)
}
public class RectangleFeature<T: Any>(
public val center: T,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val size: DpSize = DpSize(5.dp, 5.dp),
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> {
val scale = WebMercatorProjection.scaleFactor(zoom)
return GmcRectangle.square(center, (size.height.value / scale).radians, (size.width.value / scale).radians)
}
override fun withCoordinates(newCoordinates: T): Feature<T> =
RectangleFeature(newCoordinates, zoomRange, size, color, attributes)
}
public class LineFeature<T: Any>(
public val a: T,
public val b: T,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
) : SelectableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = GmcRectangle(a, b)
override fun contains(point: ViewPoint<T>): Boolean {
return super.contains(point)
}
}
/**
* @param startAngle the angle from parallel downwards for the start of the arc
* @param arcLength arc length
*/
public class ArcFeature<T:Any>(
public val oval: Rectangle<T>,
public val startAngle: Angle,
public val arcLength: Angle,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = oval
override fun withCoordinates(newCoordinates: T): Feature<T> =
ArcFeature(oval.moveTo(newCoordinates), startAngle, arcLength, zoomRange, color, attributes)
}
public class BitmapImageFeature<T: Any>(
public val position: T,
public val image: ImageBitmap,
public val size: IntSize = IntSize(15, 15),
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: T): Feature<T> =
BitmapImageFeature(newCoordinates, image, size, zoomRange, attributes)
}
public class VectorImageFeature<T: Any>(
public val position: T,
public val image: ImageVector,
public val size: DpSize = DpSize(20.dp, 20.dp),
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: T): Feature<T> =
VectorImageFeature(newCoordinates, image, size, zoomRange, attributes)
@Composable
public fun painter(): VectorPainter = rememberVectorPainter(image)
}
/**
* A group of other features
*/
public class FeatureGroup<T: Any>(
public val children: Map<FeatureId<*>, Feature<T>>,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : Feature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T>? =
children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
}
public class TextFeature<T: Any>(
public val position: T,
public val text: String,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Black,
override var attributes: AttributeMap = AttributeMap(),
public val fontConfig: MapTextFeatureFont.() -> Unit,
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: T): Feature<T> =
TextFeature(newCoordinates, text, zoomRange, color, attributes, fontConfig)
}

View File

@ -0,0 +1,302 @@
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<out MapFeature>(public val id: String)
public class MapFeaturesState {
@PublishedApi
internal val featureMap: MutableMap<String, Feature> = mutableStateMapOf()
//TODO use context receiver for that
public fun FeatureId<DraggableFeature>.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 <T : Feature> FeatureId<T>.updated(
scope: CoroutineScope,
update: suspend (T) -> T,
): Job = scope.launch {
while (isActive) {
feature(this@updated, update(getFeature(this@updated)))
}
}
@Suppress("UNCHECKED_CAST")
public fun <T : SelectableFeature> FeatureId<T>.selectable(
onSelect: (FeatureId<T>, T) -> Unit,
) {
setAttribute(this, SelectableAttribute) { id, feature -> onSelect(id as FeatureId<T>, feature as T) }
}
public val features: Map<FeatureId<*>, Feature>
get() = featureMap.mapKeys { FeatureId<Feature>(it.key) }
@Suppress("UNCHECKED_CAST")
public fun <T : Feature> getFeature(id: FeatureId<T>): T = featureMap[id.id] as T
private fun generateID(feature: Feature): String = "@feature[${feature.hashCode().toUInt()}]"
public fun <T : Feature> feature(id: String?, feature: T): FeatureId<T> {
val safeId = id ?: generateID(feature)
featureMap[safeId] = feature
return FeatureId(safeId)
}
public fun <T : Feature> feature(id: FeatureId<T>?, feature: T): FeatureId<T> = feature(id?.id, feature)
public fun <T> setAttribute(id: FeatureId<Feature>, key: Feature.Attribute<T>, value: T?) {
getFeature(id).attributes.setAttribute(key, value)
}
@Suppress("UNCHECKED_CAST")
public fun <T> getAttribute(id: FeatureId<Feature>, key: Feature.Attribute<T>): T? =
getFeature(id).attributes[key]
// @Suppress("UNCHECKED_CAST")
// public fun <T> findAllWithAttribute(key: Attribute<T>, condition: (T) -> Boolean): Set<FeatureId> {
// return attributes.filterValues {
// condition(it[key] as T)
// }.keys
// }
public inline fun <T> forEachWithAttribute(
key: Feature.Attribute<T>,
block: (id: FeatureId<*>, attributeValue: T) -> Unit,
) {
featureMap.forEach { (id, feature) ->
feature.attributes[key]?.let {
block(FeatureId<Feature>(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<CircleFeature> = feature(
id, CircleFeature(center, zoomRange, size, color)
)
public fun MapFeaturesState.circle(
centerCoordinates: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange,
size: Float = 5f,
color: Color = Color.Red,
id: String? = null,
): FeatureId<CircleFeature> = 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<RectangleFeature> = feature(
id, RectangleFeature(centerCoordinates, zoomRange, size, color)
)
public fun MapFeaturesState.rectangle(
centerCoordinates: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp),
color: Color = Color.Red,
id: String? = null,
): FeatureId<RectangleFeature> = feature(
id, RectangleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color)
)
public fun MapFeaturesState.draw(
position: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange,
id: String? = null,
draw: DrawScope.() -> Unit,
): FeatureId<DrawFeature> = 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<LineFeature> = feature(
id,
LineFeature(aCoordinates, bCoordinates, zoomRange, color)
)
public fun MapFeaturesState.line(
curve: GmcCurve,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: String? = null,
): FeatureId<LineFeature> = feature(
id,
LineFeature(curve.forward.coordinates, curve.backward.coordinates, zoomRange, color)
)
public fun MapFeaturesState.line(
aCoordinates: Pair<Double, Double>,
bCoordinates: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: String? = null,
): FeatureId<LineFeature> = 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<ArcFeature> = feature(
id,
ArcFeature(oval, startAngle, arcLength, zoomRange, color)
)
public fun MapFeaturesState.arc(
center: Pair<Double, Double>,
radius: Distance,
startAngle: Angle,
arcLength: Angle,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: String? = null,
): FeatureId<ArcFeature> = feature(
id,
ArcFeature(
oval = GmcRectangle.square(center.toCoordinates(), radius, radius),
startAngle = startAngle,
arcLength = arcLength,
zoomRange = zoomRange,
color = color
)
)
public fun MapFeaturesState.points(
points: List<Gmc>,
zoomRange: IntRange = defaultZoomRange,
stroke: Float = 2f,
color: Color = Color.Red,
pointMode: PointMode = PointMode.Points,
id: String? = null,
): FeatureId<PointsFeature> = feature(id, PointsFeature(points, zoomRange, stroke, color, pointMode))
@JvmName("pointsFromPairs")
public fun MapFeaturesState.points(
points: List<Pair<Double, Double>>,
zoomRange: IntRange = defaultZoomRange,
stroke: Float = 2f,
color: Color = Color.Red,
pointMode: PointMode = PointMode.Points,
id: String? = null,
): FeatureId<PointsFeature> =
feature(id, PointsFeature(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: String? = null,
): FeatureId<VectorImageFeature> =
feature(id, VectorImageFeature(position.toCoordinates(), image, size, zoomRange))
public fun MapFeaturesState.group(
zoomRange: IntRange = defaultZoomRange,
id: String? = null,
builder: MapFeaturesState.() -> Unit,
): FeatureId<FeatureGroup> {
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: MapTextFeatureFont.() -> Unit = { size = 16f },
id: String? = null,
): FeatureId<TextFeature> = feature(
id,
TextFeature(position, text, zoomRange, color, fontConfig = font)
)
public fun MapFeaturesState.text(
position: Pair<Double, Double>,
text: String,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
font: MapTextFeatureFont.() -> Unit = { size = 16f },
id: String? = null,
): FeatureId<TextFeature> = feature(
id,
TextFeature(position.toCoordinates(), text, zoomRange, color, fontConfig = font)
)

View File

@ -46,6 +46,7 @@ dependencyResolutionManagement {
include(
":maps-kt-core",
":maps-kt-features",
":maps-kt-compose",
":demo:maps",
":maps-kt-scheme",