[WIP] Generic features
This commit is contained in:
parent
0d9efadcb8
commit
7735d667bc
@ -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
|
||||
|
@ -1,5 +1,3 @@
|
||||
import org.jetbrains.compose.compose
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("org.jetbrains.compose")
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
/**
|
||||
*/
|
||||
|
36
maps-kt-features/build.gradle.kts
Normal file
36
maps-kt-features/build.gradle.kts
Normal 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()
|
||||
}
|
@ -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()
|
@ -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})"
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
)
|
@ -46,6 +46,7 @@ dependencyResolutionManagement {
|
||||
|
||||
include(
|
||||
":maps-kt-core",
|
||||
":maps-kt-features",
|
||||
":maps-kt-compose",
|
||||
":demo:maps",
|
||||
":maps-kt-scheme",
|
||||
|
Loading…
Reference in New Issue
Block a user