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