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 androidx.compose.ui.window.application
|
||||||
import center.sciprog.maps.compose.*
|
import center.sciprog.maps.compose.*
|
||||||
import center.sciprog.maps.coordinates.*
|
import center.sciprog.maps.coordinates.*
|
||||||
|
import center.sciprog.maps.features.*
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -129,7 +130,7 @@ fun App() {
|
|||||||
|
|
||||||
centerCoordinates?.let {
|
centerCoordinates?.let {
|
||||||
group(id = "center") {
|
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)
|
text(position = it, it.toShortString(), id = "text", color = Color.Blue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api(projects.mapsKtCore)
|
api(projects.mapsKtCore)
|
||||||
|
api(projects.mapsKtFeatures)
|
||||||
api(compose.foundation)
|
api(compose.foundation)
|
||||||
api(project.dependencies.platform(spclibs.ktor.bom))
|
api(project.dependencies.platform(spclibs.ktor.bom))
|
||||||
api("io.ktor:ktor-client-core")
|
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.input.pointer.PointerEvent
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.coordinates.*
|
import center.sciprog.maps.coordinates.Gmc
|
||||||
|
import center.sciprog.maps.features.*
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
import kotlin.math.log2
|
import kotlin.math.log2
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@ -20,7 +21,7 @@ import kotlin.math.min
|
|||||||
public data class MapViewConfig(
|
public data class MapViewConfig(
|
||||||
val zoomSpeed: Double = 1.0 / 3.0,
|
val zoomSpeed: Double = 1.0 / 3.0,
|
||||||
val onClick: MapViewPoint.(PointerEvent) -> Unit = {},
|
val onClick: MapViewPoint.(PointerEvent) -> Unit = {},
|
||||||
val dragHandle: DragHandle = DragHandle.BYPASS,
|
val dragHandle: DragHandle<Gmc> = DragHandle.bypass(),
|
||||||
val onViewChange: MapViewPoint.() -> Unit = {},
|
val onViewChange: MapViewPoint.() -> Unit = {},
|
||||||
val onSelect: (GmcRectangle) -> Unit = {},
|
val onSelect: (GmcRectangle) -> Unit = {},
|
||||||
val zoomOnSelect: Boolean = true,
|
val zoomOnSelect: Boolean = true,
|
||||||
@ -31,14 +32,14 @@ public data class MapViewConfig(
|
|||||||
public expect fun MapView(
|
public expect fun MapView(
|
||||||
mapTileProvider: MapTileProvider,
|
mapTileProvider: MapTileProvider,
|
||||||
initialViewPoint: MapViewPoint,
|
initialViewPoint: MapViewPoint,
|
||||||
featuresState: MapFeaturesState,
|
featuresState: FeaturesState<Gmc>,
|
||||||
config: MapViewConfig = MapViewConfig(),
|
config: MapViewConfig = MapViewConfig(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
|
|
||||||
internal val defaultCanvasSize = DpSize(512.dp, 512.dp)
|
internal val defaultCanvasSize = DpSize(512.dp, 512.dp)
|
||||||
|
|
||||||
public fun GmcRectangle.computeViewPoint(
|
public fun Rectangle<Gmc>.computeViewPoint(
|
||||||
mapTileProvider: MapTileProvider,
|
mapTileProvider: MapTileProvider,
|
||||||
canvasSize: DpSize = defaultCanvasSize,
|
canvasSize: DpSize = defaultCanvasSize,
|
||||||
): MapViewPoint {
|
): MapViewPoint {
|
||||||
@ -64,7 +65,7 @@ public fun MapView(
|
|||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
val featuresState = key(featureMap) {
|
val featuresState = key(featureMap) {
|
||||||
MapFeaturesState.build {
|
FeaturesState.build(GmcCoordinateSpace) {
|
||||||
featureMap.forEach { feature(it.key.id, it.value) }
|
featureMap.forEach { feature(it.key.id, it.value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,7 +73,7 @@ public fun MapView(
|
|||||||
val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) {
|
val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) {
|
||||||
initialViewPoint
|
initialViewPoint
|
||||||
?: initialRectangle?.computeViewPoint(mapTileProvider)
|
?: initialRectangle?.computeViewPoint(mapTileProvider)
|
||||||
?: featureMap.values.computeBoundingBox(1.0)?.computeViewPoint(mapTileProvider)
|
?: featureMap.values.computeBoundingBox(GmcCoordinateSpace, 1.0)?.computeViewPoint(mapTileProvider)
|
||||||
?: MapViewPoint.globe
|
?: MapViewPoint.globe
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,19 +94,20 @@ public fun MapView(
|
|||||||
initialRectangle: GmcRectangle? = null,
|
initialRectangle: GmcRectangle? = null,
|
||||||
config: MapViewConfig = MapViewConfig(),
|
config: MapViewConfig = MapViewConfig(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
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) {
|
val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) {
|
||||||
initialViewPoint
|
initialViewPoint
|
||||||
?: initialRectangle?.computeViewPoint(mapTileProvider)
|
?: initialRectangle?.computeViewPoint(mapTileProvider)
|
||||||
?: featureState.features.values.computeBoundingBox(1.0)?.computeViewPoint(mapTileProvider)
|
?: featureState.features.values.computeBoundingBox(GmcCoordinateSpace,1.0)?.computeViewPoint(mapTileProvider)
|
||||||
?: MapViewPoint.globe
|
?: 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 ->
|
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
|
||||||
|
handle as DragHandle<Gmc>
|
||||||
if (!handle.handle(event, start, end)) return@withPrimaryButton false
|
if (!handle.handle(event, start, end)) return@withPrimaryButton false
|
||||||
}
|
}
|
||||||
true
|
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
|
import kotlin.math.pow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable position on the map. Includes observation coordinate and [zoom] factor
|
* Observable position on the map. Includes observation coordinate and [zoom] factor
|
||||||
*/
|
*/
|
||||||
public data class MapViewPoint(
|
public data class MapViewPoint(
|
||||||
val focus: GeodeticMapCoordinates,
|
override val focus: GeodeticMapCoordinates,
|
||||||
val zoom: Double,
|
override val zoom: Double,
|
||||||
) {
|
) : ViewPoint<Gmc>{
|
||||||
val scaleFactor: Double by lazy { WebMercatorProjection.scaleFactor(zoom) }
|
val scaleFactor: Double by lazy { WebMercatorProjection.scaleFactor(zoom) }
|
||||||
|
|
||||||
public companion object{
|
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.input.pointer.*
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import center.sciprog.maps.coordinates.*
|
import center.sciprog.maps.coordinates.*
|
||||||
|
import center.sciprog.maps.features.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
@ -50,7 +51,7 @@ private val logger = KotlinLogging.logger("MapView")
|
|||||||
public actual fun MapView(
|
public actual fun MapView(
|
||||||
mapTileProvider: MapTileProvider,
|
mapTileProvider: MapTileProvider,
|
||||||
initialViewPoint: MapViewPoint,
|
initialViewPoint: MapViewPoint,
|
||||||
featuresState: MapFeaturesState,
|
featuresState: FeaturesState<Gmc>,
|
||||||
config: MapViewConfig,
|
config: MapViewConfig,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
): Unit = key(initialViewPoint) {
|
): Unit = key(initialViewPoint) {
|
||||||
@ -214,7 +215,7 @@ public actual fun MapView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val painterCache = key(featuresState) {
|
val painterCache = key(featuresState) {
|
||||||
featuresState.features.values.filterIsInstance<MapVectorImageFeature>().associateWith { it.painter() }
|
featuresState.features.values.filterIsInstance<VectorImageFeature<Gmc>>().associateWith { it.painter() }
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas(canvasModifier) {
|
Canvas(canvasModifier) {
|
||||||
@ -229,14 +230,14 @@ public actual fun MapView(
|
|||||||
|
|
||||||
fun DrawScope.drawFeature(zoom: Int, feature: MapFeature) {
|
fun DrawScope.drawFeature(zoom: Int, feature: MapFeature) {
|
||||||
when (feature) {
|
when (feature) {
|
||||||
is MapFeatureSelector -> drawFeature(zoom, feature.selector(zoom))
|
is FeatureSelector -> drawFeature(zoom, feature.selector(zoom))
|
||||||
is MapCircleFeature -> drawCircle(
|
is CircleFeature -> drawCircle(
|
||||||
feature.color,
|
feature.color,
|
||||||
feature.size,
|
feature.size.toPx(),
|
||||||
center = feature.center.toOffset()
|
center = feature.center.toOffset()
|
||||||
)
|
)
|
||||||
|
|
||||||
is MapRectangleFeature -> drawRect(
|
is RectangleFeature -> drawRect(
|
||||||
feature.color,
|
feature.color,
|
||||||
topLeft = feature.center.toOffset() - Offset(
|
topLeft = feature.center.toOffset() - Offset(
|
||||||
feature.size.width.toPx() / 2,
|
feature.size.width.toPx() / 2,
|
||||||
@ -245,8 +246,8 @@ public actual fun MapView(
|
|||||||
size = feature.size.toSize()
|
size = feature.size.toSize()
|
||||||
)
|
)
|
||||||
|
|
||||||
is MapLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
|
is LineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
|
||||||
is MapArcFeature -> {
|
is ArcFeature -> {
|
||||||
val topLeft = feature.oval.topLeft.toOffset()
|
val topLeft = feature.oval.topLeft.toOffset()
|
||||||
val bottomRight = feature.oval.bottomRight.toOffset()
|
val bottomRight = feature.oval.bottomRight.toOffset()
|
||||||
|
|
||||||
@ -254,8 +255,8 @@ public actual fun MapView(
|
|||||||
|
|
||||||
drawArc(
|
drawArc(
|
||||||
color = feature.color,
|
color = feature.color,
|
||||||
startAngle = feature.startAngle.degrees.toFloat(),
|
startAngle = feature.startAngle.radians.degrees.toFloat(),
|
||||||
sweepAngle = feature.arcLength.degrees.toFloat(),
|
sweepAngle = feature.arcLength.radians.degrees.toFloat(),
|
||||||
useCenter = false,
|
useCenter = false,
|
||||||
topLeft = topLeft,
|
topLeft = topLeft,
|
||||||
size = size,
|
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 offset = feature.position.toOffset()
|
||||||
val size = feature.size.toSize()
|
val size = feature.size.toSize()
|
||||||
translate(offset.x - size.width / 2, offset.y - size.height / 2) {
|
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()
|
val offset = feature.position.toOffset()
|
||||||
canvas.nativeCanvas.drawString(
|
canvas.nativeCanvas.drawString(
|
||||||
feature.text,
|
feature.text,
|
||||||
@ -287,20 +288,20 @@ public actual fun MapView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is MapDrawFeature -> {
|
is DrawFeature -> {
|
||||||
val offset = feature.position.toOffset()
|
val offset = feature.position.toOffset()
|
||||||
translate(offset.x, offset.y) {
|
translate(offset.x, offset.y) {
|
||||||
feature.drawFeature(this)
|
feature.drawFeature(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is MapFeatureGroup -> {
|
is FeatureGroup -> {
|
||||||
feature.children.values.forEach {
|
feature.children.values.forEach {
|
||||||
drawFeature(zoom, it)
|
drawFeature(zoom, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is MapPathFeature -> {
|
is PathFeature -> {
|
||||||
TODO("MapPathFeature not implemented")
|
TODO("MapPathFeature not implemented")
|
||||||
// val offset = feature.rectangle.center.toOffset() - feature.targetRect.center
|
// val offset = feature.rectangle.center.toOffset() - feature.targetRect.center
|
||||||
// translate(offset.x, offset.y) {
|
// translate(offset.x, offset.y) {
|
||||||
@ -309,7 +310,7 @@ public actual fun MapView(
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
is MapPointsFeature -> {
|
is PointsFeature -> {
|
||||||
val points = feature.points.map { it.toOffset() }
|
val points = feature.points.map { it.toOffset() }
|
||||||
drawPoints(
|
drawPoints(
|
||||||
points = points,
|
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)
|
drawFeature(zoom, feature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package center.sciprog.maps.coordinates
|
||||||
|
|
||||||
import kotlin.math.acos
|
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.sqrt
|
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
|
* 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)
|
val reducedLatitudeTan = (1 - f) * tan(latitude)
|
||||||
return equatorRadius / sqrt(1.0 + reducedLatitudeTan.pow(2))
|
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
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
public object DraggableAttribute : Feature.Attribute<DragHandle<*>>
|
public object DraggableAttribute : Feature.Attribute<DragHandle<*>>
|
||||||
|
|
||||||
public object SelectableAttribute : Feature.Attribute<(FeatureId<*>, SelectableFeature<*>) -> Unit>
|
public object SelectableAttribute : Feature.Attribute<(FeatureId<*>, SelectableFeature<*>) -> Unit>
|
||||||
|
|
||||||
public object VisibleAttribute : Feature.Attribute<Boolean>
|
public object VisibleAttribute : Feature.Attribute<Boolean>
|
||||||
|
|
||||||
public object ColorAttribute : Feature.Attribute<Color>
|
public object ColorAttribute : Feature.Attribute<Color>
|
||||||
|
@ -3,13 +3,22 @@ package center.sciprog.maps.features
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
|
||||||
|
|
||||||
public interface Rectangle<T : Any> {
|
public interface Area<T: Any>{
|
||||||
public val topLeft: T
|
|
||||||
public val bottomRight: T
|
|
||||||
|
|
||||||
public operator fun contains(point: T): Boolean
|
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> {
|
public interface CoordinateSpace<T : Any> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,14 +30,13 @@ public interface CoordinateSpace<T : Any> {
|
|||||||
* Build a rectangle of visual size [size]
|
* Build a rectangle of visual size [size]
|
||||||
*/
|
*/
|
||||||
public fun buildRectangle(center: T, zoom: Double, size: DpSize): Rectangle<T>
|
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]
|
* Move given rectangle to be centered at [center]
|
||||||
*/
|
*/
|
||||||
public fun Rectangle<T>.withCenter(center: T): Rectangle<T>
|
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.PointerEvent
|
||||||
import androidx.compose.ui.input.pointer.isPrimaryPressed
|
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 event - qualifiers of the event used for drag
|
||||||
* @param start - is a point where drag begins, end is a point where drag ends
|
* @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
|
* @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 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
|
* Process only events with primary button pressed
|
||||||
*/
|
*/
|
||||||
public fun <V> withPrimaryButton(
|
public fun <T: Any> withPrimaryButton(
|
||||||
block: (event: PointerEvent, start: V, end: V) -> Boolean,
|
block: (event: PointerEvent, start: ViewPoint<T>, end: ViewPoint<T>) -> Boolean,
|
||||||
): DragHandle<V> = DragHandle { event, start, end ->
|
): DragHandle<T> = DragHandle { event, start, end ->
|
||||||
if (event.buttons.isPrimaryPressed) {
|
if (event.buttons.isPrimaryPressed) {
|
||||||
block(event, start, end)
|
block(event, start, end)
|
||||||
} else {
|
} else {
|
||||||
@ -32,7 +32,7 @@ public fun interface DragHandle<in V: ViewPoint<*>> {
|
|||||||
/**
|
/**
|
||||||
* Combine several handles into one
|
* 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 {
|
handles.forEach {
|
||||||
if (!it.handle(event, start, end)) return@DragHandle false
|
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.Dp
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import center.sciprog.maps.features.Feature.Companion.defaultZoomRange
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
|
public typealias DoubleRange = ClosedFloatingPointRange<Double>
|
||||||
|
|
||||||
public interface Feature<T : Any> {
|
public interface Feature<T : Any> {
|
||||||
public interface Attribute<T>
|
public interface Attribute<T>
|
||||||
|
|
||||||
public val space: CoordinateSpace<T>
|
public val space: CoordinateSpace<T>
|
||||||
|
|
||||||
public val zoomRange: ClosedFloatingPointRange<Double>
|
public val zoomRange: DoubleRange
|
||||||
|
|
||||||
public var attributes: AttributeMap
|
public var attributes: AttributeMap
|
||||||
|
|
||||||
public fun getBoundingBox(zoom: Double): Rectangle<T>?
|
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> {
|
public interface SelectableFeature<T : Any> : Feature<T> {
|
||||||
@ -41,13 +47,12 @@ public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
|
|||||||
space: CoordinateSpace<T>,
|
space: CoordinateSpace<T>,
|
||||||
zoom: Double,
|
zoom: Double,
|
||||||
): Rectangle<T>? = with(space) {
|
): Rectangle<T>? = with(space) {
|
||||||
mapNotNull { it.getBoundingBox(zoom) }.computeRectangle()
|
mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
|
||||||
}
|
}
|
||||||
|
|
||||||
//public fun Pair<Number, Number>.toCoordinates(): GeodeticMapCoordinates =
|
//public fun Pair<Number, Number>.toCoordinates(): GeodeticMapCoordinates =
|
||||||
// GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble())
|
// 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)
|
* 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)
|
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>(
|
public class PathFeature<T : Any>(
|
||||||
override val space: CoordinateSpace<T>,
|
override val space: CoordinateSpace<T>,
|
||||||
public val rectangle: Rectangle<T>,
|
public val rectangle: Rectangle<T>,
|
||||||
@ -112,7 +103,7 @@ public class PointsFeature<T : Any>(
|
|||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
) : Feature<T> {
|
) : Feature<T> {
|
||||||
override fun getBoundingBox(zoom: Double): Rectangle<T>? = with(space) {
|
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>,
|
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,
|
public val image: ImageBitmap,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
) : DraggableFeature<T> {
|
) : 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) {
|
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
|
||||||
BitmapImageFeature(space, rectangle.withCenter(newCoordinates), image, zoomRange, attributes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class VectorImageFeature<T : Any>(
|
public data class VectorImageFeature<T : Any>(
|
||||||
override val space: CoordinateSpace<T>,
|
override val space: CoordinateSpace<T>,
|
||||||
public val rectangle: Rectangle<T>,
|
public val position: T,
|
||||||
|
public val size: DpSize,
|
||||||
public val image: ImageVector,
|
public val image: ImageVector,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
) : DraggableFeature<T> {
|
) : 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) {
|
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
|
||||||
VectorImageFeature(space, rectangle.withCenter(newCoordinates), image, zoomRange, attributes)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
public fun painter(): VectorPainter = rememberVectorPainter(image)
|
public fun painter(): VectorPainter = rememberVectorPainter(image)
|
||||||
@ -223,11 +225,12 @@ public class FeatureGroup<T : Any>(
|
|||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
) : Feature<T> {
|
) : Feature<T> {
|
||||||
override fun getBoundingBox(zoom: Double): Rectangle<T>? = with(space) {
|
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>(
|
public class TextFeature<T : Any>(
|
||||||
|
override val space: CoordinateSpace<T>,
|
||||||
public val position: T,
|
public val position: T,
|
||||||
public val text: String,
|
public val text: String,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
||||||
@ -235,8 +238,8 @@ public class TextFeature<T : Any>(
|
|||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
public val fontConfig: FeatureFont.() -> Unit,
|
public val fontConfig: FeatureFont.() -> Unit,
|
||||||
) : DraggableFeature<T> {
|
) : 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> =
|
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