Drag done
This commit is contained in:
parent
5a1d3d701f
commit
491a4e6000
@ -7,7 +7,8 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.PointMode
|
import androidx.compose.ui.graphics.PointMode
|
||||||
import androidx.compose.ui.input.pointer.isPrimaryPressed
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Window
|
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.*
|
||||||
@ -51,32 +52,24 @@ fun App() {
|
|||||||
|
|
||||||
|
|
||||||
val pointOne = 55.568548 to 37.568604
|
val pointOne = 55.568548 to 37.568604
|
||||||
var pointTwo: Pair<Double, Double> by remember { mutableStateOf(55.929444 to 37.518434) }
|
val pointTwo = 55.929444 to 37.518434
|
||||||
val pointThree = 60.929444 to 37.518434
|
val pointThree = 60.929444 to 37.518434
|
||||||
|
|
||||||
|
val dragPoint = 55.744 to 37.614
|
||||||
|
|
||||||
MapView(
|
MapView(
|
||||||
mapTileProvider = mapTileProvider,
|
mapTileProvider = mapTileProvider,
|
||||||
initialViewPoint = viewPoint,
|
initialViewPoint = viewPoint,
|
||||||
config = MapViewConfig(
|
config = MapViewConfig(
|
||||||
inferViewBoxFromFeatures = true,
|
inferViewBoxFromFeatures = true,
|
||||||
onViewChange = { centerCoordinates = focus },
|
onViewChange = { centerCoordinates = focus },
|
||||||
dragHandle = { event, start, end ->
|
|
||||||
if (!event.buttons.isPrimaryPressed) {
|
|
||||||
true
|
|
||||||
} else if (start.focus.latitude.degrees.value in (pointTwo.first - 0.05)..(pointTwo.first + 0.05) &&
|
|
||||||
start.focus.longitude.degrees.value in (pointTwo.second - 0.05)..(pointTwo.second + 0.05)
|
|
||||||
) {
|
|
||||||
pointTwo = pointTwo.first + (end.focus.latitude - start.focus.latitude).degrees.value to
|
|
||||||
pointTwo.second + (end.focus.longitude - start.focus.longitude).degrees.value
|
|
||||||
false// returning false, because when we are dragging circle we don't want to drag map
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
image(pointOne, Icons.Filled.Home)
|
image(pointOne, Icons.Filled.Home)
|
||||||
|
|
||||||
|
rectangle(dragPoint, id = "dragMe", size = DpSize(10.dp, 10.dp)).draggable()
|
||||||
|
|
||||||
points(
|
points(
|
||||||
points = listOf(
|
points = listOf(
|
||||||
55.742465 to 37.615812,
|
55.742465 to 37.615812,
|
||||||
@ -89,7 +82,7 @@ fun App() {
|
|||||||
pointMode = PointMode.Polygon
|
pointMode = PointMode.Polygon
|
||||||
)
|
)
|
||||||
|
|
||||||
//remember feature Id
|
//remember feature ID
|
||||||
val circleId: FeatureId = circle(
|
val circleId: FeatureId = circle(
|
||||||
centerCoordinates = pointTwo,
|
centerCoordinates = pointTwo,
|
||||||
)
|
)
|
||||||
|
@ -11,16 +11,20 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
|
import center.sciprog.maps.coordinates.*
|
||||||
import center.sciprog.maps.coordinates.GmcRectangle
|
import kotlin.math.floor
|
||||||
import center.sciprog.maps.coordinates.wrapAll
|
|
||||||
|
|
||||||
public interface MapFeature {
|
public interface MapFeature {
|
||||||
public val zoomRange: IntRange
|
public val zoomRange: IntRange
|
||||||
public fun getBoundingBox(zoom: Int): GmcRectangle?
|
public fun getBoundingBox(zoom: Double): GmcRectangle?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun Iterable<MapFeature>.computeBoundingBox(zoom: Int): GmcRectangle? =
|
public interface DraggableMapFeature : MapFeature {
|
||||||
|
public fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun Iterable<MapFeature>.computeBoundingBox(zoom: Double): GmcRectangle? =
|
||||||
mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
|
mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
|
||||||
|
|
||||||
internal fun Pair<Double, Double>.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second)
|
internal fun Pair<Double, Double>.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second)
|
||||||
@ -35,18 +39,21 @@ public class MapFeatureSelector(
|
|||||||
) : MapFeature {
|
) : MapFeature {
|
||||||
override val zoomRange: IntRange get() = defaultZoomRange
|
override val zoomRange: IntRange get() = defaultZoomRange
|
||||||
|
|
||||||
override fun getBoundingBox(zoom: Int): GmcRectangle? = selector(zoom).getBoundingBox(zoom)
|
override fun getBoundingBox(zoom: Double): GmcRectangle? = selector(floor(zoom).toInt()).getBoundingBox(zoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MapDrawFeature(
|
public class MapDrawFeature(
|
||||||
public val position: GeodeticMapCoordinates,
|
public val position: GeodeticMapCoordinates,
|
||||||
override val zoomRange: IntRange = defaultZoomRange,
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
public val drawFeature: DrawScope.() -> Unit,
|
public val drawFeature: DrawScope.() -> Unit,
|
||||||
) : MapFeature {
|
) : DraggableMapFeature {
|
||||||
override fun getBoundingBox(zoom: Int): GmcRectangle {
|
override fun getBoundingBox(zoom: Double): GmcRectangle {
|
||||||
//TODO add box computation
|
//TODO add box computation
|
||||||
return GmcRectangle(position, position)
|
return GmcRectangle(position, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
|
||||||
|
MapDrawFeature(newCoordinates, zoomRange, drawFeature)
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MapPointsFeature(
|
public class MapPointsFeature(
|
||||||
@ -54,9 +61,9 @@ public class MapPointsFeature(
|
|||||||
override val zoomRange: IntRange = defaultZoomRange,
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
public val stroke: Float = 2f,
|
public val stroke: Float = 2f,
|
||||||
public val color: Color = Color.Red,
|
public val color: Color = Color.Red,
|
||||||
public val pointMode: PointMode = PointMode.Points
|
public val pointMode: PointMode = PointMode.Points,
|
||||||
) : MapFeature {
|
) : MapFeature {
|
||||||
override fun getBoundingBox(zoom: Int): GmcRectangle {
|
override fun getBoundingBox(zoom: Double): GmcRectangle {
|
||||||
return GmcRectangle(points.first(), points.last())
|
return GmcRectangle(points.first(), points.last())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,8 +73,14 @@ public class MapCircleFeature(
|
|||||||
override val zoomRange: IntRange = defaultZoomRange,
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
public val size: Float = 5f,
|
public val size: Float = 5f,
|
||||||
public val color: Color = Color.Red,
|
public val color: Color = Color.Red,
|
||||||
) : MapFeature {
|
) : DraggableMapFeature {
|
||||||
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(center, center)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MapRectangleFeature(
|
public class MapRectangleFeature(
|
||||||
@ -75,8 +88,14 @@ public class MapRectangleFeature(
|
|||||||
override val zoomRange: IntRange = defaultZoomRange,
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
public val size: DpSize = DpSize(5.dp, 5.dp),
|
public val size: DpSize = DpSize(5.dp, 5.dp),
|
||||||
public val color: Color = Color.Red,
|
public val color: Color = Color.Red,
|
||||||
) : MapFeature {
|
) : DraggableMapFeature {
|
||||||
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(center, center)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MapLineFeature(
|
public class MapLineFeature(
|
||||||
@ -85,7 +104,7 @@ public class MapLineFeature(
|
|||||||
override val zoomRange: IntRange = defaultZoomRange,
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
public val color: Color = Color.Red,
|
public val color: Color = Color.Red,
|
||||||
) : MapFeature {
|
) : MapFeature {
|
||||||
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(a, b)
|
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(a, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MapArcFeature(
|
public class MapArcFeature(
|
||||||
@ -95,7 +114,7 @@ public class MapArcFeature(
|
|||||||
override val zoomRange: IntRange = defaultZoomRange,
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
public val color: Color = Color.Red,
|
public val color: Color = Color.Red,
|
||||||
) : MapFeature {
|
) : MapFeature {
|
||||||
override fun getBoundingBox(zoom: Int): GmcRectangle = oval
|
override fun getBoundingBox(zoom: Double): GmcRectangle = oval
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MapBitmapImageFeature(
|
public class MapBitmapImageFeature(
|
||||||
@ -103,8 +122,11 @@ public class MapBitmapImageFeature(
|
|||||||
public val image: ImageBitmap,
|
public val image: ImageBitmap,
|
||||||
public val size: IntSize = IntSize(15, 15),
|
public val size: IntSize = IntSize(15, 15),
|
||||||
override val zoomRange: IntRange = defaultZoomRange,
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
) : MapFeature {
|
) : DraggableMapFeature {
|
||||||
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(position, position)
|
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
|
||||||
|
|
||||||
|
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
|
||||||
|
MapBitmapImageFeature(newCoordinates, image, size, zoomRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MapVectorImageFeature(
|
public class MapVectorImageFeature(
|
||||||
@ -112,8 +134,11 @@ public class MapVectorImageFeature(
|
|||||||
public val painter: Painter,
|
public val painter: Painter,
|
||||||
public val size: DpSize,
|
public val size: DpSize,
|
||||||
override val zoomRange: IntRange = defaultZoomRange,
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
) : MapFeature {
|
) : DraggableMapFeature{
|
||||||
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(position, position)
|
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
|
||||||
|
|
||||||
|
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
|
||||||
|
MapVectorImageFeature(newCoordinates,painter, size, zoomRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -131,7 +156,8 @@ public class MapFeatureGroup(
|
|||||||
public val children: Map<FeatureId, MapFeature>,
|
public val children: Map<FeatureId, MapFeature>,
|
||||||
override val zoomRange: IntRange = defaultZoomRange,
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
) : MapFeature {
|
) : MapFeature {
|
||||||
override fun getBoundingBox(zoom: Int): GmcRectangle? = children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
|
override fun getBoundingBox(zoom: Double): GmcRectangle? =
|
||||||
|
children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MapTextFeature(
|
public class MapTextFeature(
|
||||||
@ -140,6 +166,9 @@ public class MapTextFeature(
|
|||||||
override val zoomRange: IntRange = defaultZoomRange,
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
public val color: Color,
|
public val color: Color,
|
||||||
public val fontConfig: MapTextFeatureFont.() -> Unit,
|
public val fontConfig: MapTextFeatureFont.() -> Unit,
|
||||||
) : MapFeature {
|
) : DraggableMapFeature{
|
||||||
override fun getBoundingBox(zoom: Int): GmcRectangle = GmcRectangle(position, position)
|
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
|
||||||
|
|
||||||
|
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
|
||||||
|
MapTextFeature(newCoordinates, text, zoomRange, color, fontConfig)
|
||||||
}
|
}
|
||||||
|
@ -15,27 +15,53 @@ import center.sciprog.maps.coordinates.GmcRectangle
|
|||||||
|
|
||||||
public typealias FeatureId = String
|
public typealias FeatureId = String
|
||||||
|
|
||||||
|
public interface MapFeatureAttributeKey<T>
|
||||||
|
|
||||||
|
|
||||||
|
public class MapFeatureAttributeSet(private val map: Map<MapFeatureAttributeKey<*>, *>) {
|
||||||
|
public operator fun <T> get(key: MapFeatureAttributeKey<*>): T? = map[key]?.let {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
it as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public interface MapFeatureBuilder {
|
public interface MapFeatureBuilder {
|
||||||
public fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId
|
public fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId
|
||||||
|
|
||||||
public fun build(): SnapshotStateMap<FeatureId, MapFeature>
|
public fun <T> setAttribute(id: FeatureId, key: MapFeatureAttributeKey<T>, value: T)
|
||||||
|
|
||||||
|
public val features: MutableMap<FeatureId, MapFeature>
|
||||||
|
|
||||||
|
public fun attributes(): Map<FeatureId, MapFeatureAttributeSet>
|
||||||
|
|
||||||
|
//TODO use context receiver for that
|
||||||
|
public fun FeatureId.draggable(enabled: Boolean = true) {
|
||||||
|
setAttribute(this, DraggableAttribute, enabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MapFeatureBuilderImpl(initialFeatures: Map<FeatureId, MapFeature>) : MapFeatureBuilder {
|
internal class MapFeatureBuilderImpl(
|
||||||
|
override val features: SnapshotStateMap<FeatureId, MapFeature>,
|
||||||
|
) : MapFeatureBuilder {
|
||||||
|
|
||||||
|
private val attributes = SnapshotStateMap<FeatureId, SnapshotStateMap<MapFeatureAttributeKey<out Any?>, in Any?>>()
|
||||||
|
|
||||||
private val content: SnapshotStateMap<FeatureId, MapFeature> = mutableStateMapOf<FeatureId, MapFeature>().apply {
|
|
||||||
putAll(initialFeatures)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]"
|
private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]"
|
||||||
|
|
||||||
override fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId {
|
override fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId {
|
||||||
val safeId = id ?: generateID(feature)
|
val safeId = id ?: generateID(feature)
|
||||||
content[id ?: generateID(feature)] = feature
|
features[id ?: generateID(feature)] = feature
|
||||||
return safeId
|
return safeId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun build(): SnapshotStateMap<FeatureId, MapFeature> = content
|
override fun <T> setAttribute(id: FeatureId, key: MapFeatureAttributeKey<T>, value: T) {
|
||||||
|
attributes.getOrPut(id) { SnapshotStateMap() }[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attributes(): Map<FeatureId, MapFeatureAttributeSet> =
|
||||||
|
attributes.mapValues { MapFeatureAttributeSet(it.value) }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun MapFeatureBuilder.circle(
|
public fun MapFeatureBuilder.circle(
|
||||||
@ -123,7 +149,7 @@ public fun MapFeatureBuilder.points(
|
|||||||
stroke: Float = 2f,
|
stroke: Float = 2f,
|
||||||
color: Color = Color.Red,
|
color: Color = Color.Red,
|
||||||
pointMode: PointMode = PointMode.Points,
|
pointMode: PointMode = PointMode.Points,
|
||||||
id: FeatureId? = null
|
id: FeatureId? = null,
|
||||||
): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode))
|
): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode))
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -140,7 +166,7 @@ public fun MapFeatureBuilder.group(
|
|||||||
id: FeatureId? = null,
|
id: FeatureId? = null,
|
||||||
builder: MapFeatureBuilder.() -> Unit,
|
builder: MapFeatureBuilder.() -> Unit,
|
||||||
): FeatureId {
|
): FeatureId {
|
||||||
val map = MapFeatureBuilderImpl(emptyMap()).apply(builder).build()
|
val map = MapFeatureBuilderImpl(mutableStateMapOf()).apply(builder).features
|
||||||
val feature = MapFeatureGroup(map, zoomRange)
|
val feature = MapFeatureGroup(map, zoomRange)
|
||||||
return addFeature(id, feature)
|
return addFeature(id, feature)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
package center.sciprog.maps.compose
|
package center.sciprog.maps.compose
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
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.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import center.sciprog.maps.coordinates.*
|
import center.sciprog.maps.coordinates.*
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
@ -19,10 +20,33 @@ public fun interface DragHandle {
|
|||||||
*
|
*
|
||||||
* @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 drag(event: PointerEvent, start: MapViewPoint, end: MapViewPoint): Boolean
|
public fun handle(event: PointerEvent, start: MapViewPoint, end: MapViewPoint): Boolean
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public val BYPASS: DragHandle = DragHandle { _, _, _ -> true }
|
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 {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,53 +73,83 @@ public expect fun MapView(
|
|||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun prepareConfig(initialConfig: MapViewConfig, featureBuilder: MapFeatureBuilder): MapViewConfig {
|
||||||
|
val draggableFeatureIds: Set<FeatureId> = featureBuilder.attributes().filterValues {
|
||||||
|
it[DraggableAttribute] ?: false
|
||||||
|
}.keys
|
||||||
|
|
||||||
|
val features = featureBuilder.features
|
||||||
|
|
||||||
|
val featureDrag = DragHandle.withPrimaryButton { _, start, end ->
|
||||||
|
val zoom = start.zoom
|
||||||
|
draggableFeatureIds.forEach { id ->
|
||||||
|
val feature = features[id] as? DraggableMapFeature ?: return@forEach
|
||||||
|
//val border = WebMercatorProjection.scaleFactor(zoom)
|
||||||
|
val boundingBox = feature.getBoundingBox(zoom) ?: return@forEach
|
||||||
|
if (start.focus in boundingBox) {
|
||||||
|
features[id] = feature.withCoordinates(end.focus)
|
||||||
|
return@withPrimaryButton false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@withPrimaryButton true
|
||||||
|
}
|
||||||
|
return initialConfig.copy(
|
||||||
|
dragHandle = DragHandle.combine(featureDrag, initialConfig.dragHandle),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
public fun MapView(
|
public fun MapView(
|
||||||
mapTileProvider: MapTileProvider,
|
mapTileProvider: MapTileProvider,
|
||||||
initialViewPoint: MapViewPoint,
|
initialViewPoint: MapViewPoint,
|
||||||
features: Map<FeatureId, MapFeature> = emptyMap(),
|
|
||||||
config: MapViewConfig = MapViewConfig(),
|
config: MapViewConfig = MapViewConfig(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
|
buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
|
||||||
) {
|
) {
|
||||||
val featuresBuilder = MapFeatureBuilderImpl(features)
|
val featuresBuilder = MapFeatureBuilderImpl(mutableStateMapOf())
|
||||||
featuresBuilder.buildFeatures()
|
featuresBuilder.buildFeatures()
|
||||||
|
val features = remember { featuresBuilder.features }
|
||||||
|
|
||||||
|
val newConfig = remember(features) {
|
||||||
|
prepareConfig(config, featuresBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
MapView(
|
MapView(
|
||||||
mapTileProvider,
|
mapTileProvider,
|
||||||
{ initialViewPoint },
|
{ initialViewPoint },
|
||||||
featuresBuilder.build(),
|
features,
|
||||||
config,
|
newConfig,
|
||||||
modifier
|
modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun GmcRectangle.computeViewPoint(mapTileProvider: MapTileProvider): (canvasSize: DpSize) -> MapViewPoint =
|
internal fun GmcRectangle.computeViewPoint(
|
||||||
{ canvasSize ->
|
|
||||||
val zoom = log2(
|
|
||||||
min(
|
|
||||||
canvasSize.width.value / longitudeDelta.radians.value,
|
|
||||||
canvasSize.height.value / latitudeDelta.radians.value
|
|
||||||
) * PI / mapTileProvider.tileSize
|
|
||||||
)
|
|
||||||
MapViewPoint(center, zoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
public fun MapView(
|
|
||||||
mapTileProvider: MapTileProvider,
|
mapTileProvider: MapTileProvider,
|
||||||
box: GmcRectangle,
|
): (canvasSize: DpSize) -> MapViewPoint = { canvasSize ->
|
||||||
features: Map<FeatureId, MapFeature> = emptyMap(),
|
val zoom = log2(
|
||||||
config: MapViewConfig = MapViewConfig(),
|
min(
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
canvasSize.width.value / longitudeDelta.radians.value,
|
||||||
buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
|
canvasSize.height.value / latitudeDelta.radians.value
|
||||||
) {
|
) * PI / mapTileProvider.tileSize
|
||||||
val featuresBuilder = MapFeatureBuilderImpl(features)
|
|
||||||
featuresBuilder.buildFeatures()
|
|
||||||
MapView(
|
|
||||||
mapTileProvider,
|
|
||||||
box.computeViewPoint(mapTileProvider),
|
|
||||||
featuresBuilder.build(),
|
|
||||||
config,
|
|
||||||
modifier
|
|
||||||
)
|
)
|
||||||
|
MapViewPoint(center, zoom)
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
//@Composable
|
||||||
|
//public fun MapView(
|
||||||
|
// mapTileProvider: MapTileProvider,
|
||||||
|
// box: GmcRectangle,
|
||||||
|
// config: MapViewConfig = MapViewConfig(),
|
||||||
|
// modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
|
// buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
|
||||||
|
//) {
|
||||||
|
// val builder by derivedStateOf { MapFeatureBuilderImpl().apply(buildFeatures) }
|
||||||
|
//
|
||||||
|
// MapView(
|
||||||
|
// mapTileProvider,
|
||||||
|
// box.computeViewPoint(mapTileProvider),
|
||||||
|
// builder.features,
|
||||||
|
// prepareConfig(config, builder),
|
||||||
|
// modifier
|
||||||
|
// )
|
||||||
|
//}
|
@ -0,0 +1,3 @@
|
|||||||
|
package center.sciprog.maps.compose
|
||||||
|
|
||||||
|
public object DraggableAttribute: MapFeatureAttributeKey<Boolean>
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.gestures.drag
|
|||||||
import androidx.compose.foundation.gestures.forEachGesture
|
import androidx.compose.foundation.gestures.forEachGesture
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@ -66,7 +67,7 @@ public actual fun MapView(
|
|||||||
|
|
||||||
val viewPoint: MapViewPoint by derivedStateOf {
|
val viewPoint: MapViewPoint by derivedStateOf {
|
||||||
viewPointInternal ?: if (config.inferViewBoxFromFeatures) {
|
viewPointInternal ?: if (config.inferViewBoxFromFeatures) {
|
||||||
features.values.computeBoundingBox(1)?.let { box ->
|
features.values.computeBoundingBox(1.0)?.let { box ->
|
||||||
val zoom = log2(
|
val zoom = log2(
|
||||||
min(
|
min(
|
||||||
canvasSize.width.value / box.longitudeDelta.radians.value,
|
canvasSize.width.value / box.longitudeDelta.radians.value,
|
||||||
@ -132,8 +133,9 @@ public actual fun MapView(
|
|||||||
)
|
)
|
||||||
val dpEnd = DpOffset(dragChange.position.x.toDp(), dragChange.position.y.toDp())
|
val dpEnd = DpOffset(dragChange.position.x.toDp(), dragChange.position.y.toDp())
|
||||||
|
|
||||||
|
//apply drag handle and check if it prohibits the drag even propagation
|
||||||
if (
|
if (
|
||||||
!config.dragHandle.drag(
|
!config.dragHandle.handle(
|
||||||
event,
|
event,
|
||||||
MapViewPoint(dpStart.toGeodetic(), viewPoint.zoom),
|
MapViewPoint(dpStart.toGeodetic(), viewPoint.zoom),
|
||||||
MapViewPoint(dpEnd.toGeodetic(), viewPoint.zoom)
|
MapViewPoint(dpEnd.toGeodetic(), viewPoint.zoom)
|
||||||
|
@ -50,7 +50,7 @@ public class GeodeticMapCoordinates(
|
|||||||
/**
|
/**
|
||||||
* Short name for GeodeticMapCoordinates
|
* Short name for GeodeticMapCoordinates
|
||||||
*/
|
*/
|
||||||
public typealias GMC = GeodeticMapCoordinates
|
public typealias Gmc = GeodeticMapCoordinates
|
||||||
|
|
||||||
//public interface GeoToScreenConversion {
|
//public interface GeoToScreenConversion {
|
||||||
// public fun getScreenX(gmc: GeodeticMapCoordinates): Double
|
// public fun getScreenX(gmc: GeodeticMapCoordinates): Double
|
||||||
|
@ -57,8 +57,8 @@ public fun GeoEllipsoid.meridianCurve(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return GmcCurve(
|
return GmcCurve(
|
||||||
forward = GmcPose(GMC(fromLatitude, longitude), if (up) zero else pi),
|
forward = GmcPose(Gmc(fromLatitude, longitude), if (up) zero else pi),
|
||||||
backward = GmcPose(GMC(toLatitude, longitude), if (up) pi else zero),
|
backward = GmcPose(Gmc(toLatitude, longitude), if (up) pi else zero),
|
||||||
distance = s
|
distance = s
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -70,8 +70,8 @@ public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toL
|
|||||||
require(latitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
|
require(latitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
|
||||||
val right = toLongitude > fromLongitude
|
val right = toLongitude > fromLongitude
|
||||||
return GmcCurve(
|
return GmcCurve(
|
||||||
forward = GmcPose(GMC(latitude, fromLongitude), if (right) piDiv2.radians else -piDiv2.radians),
|
forward = GmcPose(Gmc(latitude, fromLongitude), if (right) piDiv2.radians else -piDiv2.radians),
|
||||||
backward = GmcPose(GMC(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians),
|
backward = GmcPose(Gmc(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians),
|
||||||
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians.value)
|
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians.value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -186,7 +186,7 @@ public fun GeoEllipsoid.curveInDirection(
|
|||||||
val L = lambda - (1 - C) * f * sinAlpha *
|
val L = lambda - (1 - C) * f * sinAlpha *
|
||||||
(sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)))
|
(sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)))
|
||||||
|
|
||||||
val endPoint = GMC(phi2, L.radians)
|
val endPoint = Gmc(phi2, L.radians)
|
||||||
|
|
||||||
// eq. 12
|
// eq. 12
|
||||||
|
|
||||||
@ -212,7 +212,7 @@ public fun GeoEllipsoid.curveInDirection(
|
|||||||
* @return solution to the inverse geodetic problem
|
* @return solution to the inverse geodetic problem
|
||||||
*/
|
*/
|
||||||
@Suppress("SpellCheckingInspection", "LocalVariableName")
|
@Suppress("SpellCheckingInspection", "LocalVariableName")
|
||||||
public fun GeoEllipsoid.curveBetween(start: GMC, end: GMC, precision: Double = 1e-6): GmcCurve {
|
public fun GeoEllipsoid.curveBetween(start: Gmc, end: Gmc, precision: Double = 1e-6): GmcCurve {
|
||||||
//
|
//
|
||||||
// All equation numbers refer back to Vincenty's publication:
|
// All equation numbers refer back to Vincenty's publication:
|
||||||
// See http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf
|
// See http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf
|
||||||
|
@ -9,29 +9,39 @@ package center.sciprog.maps.coordinates
|
|||||||
public data class GmcRectangle(
|
public data class GmcRectangle(
|
||||||
public val a: GeodeticMapCoordinates,
|
public val a: GeodeticMapCoordinates,
|
||||||
public val b: GeodeticMapCoordinates,
|
public val b: GeodeticMapCoordinates,
|
||||||
public val ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
|
|
||||||
) {
|
) {
|
||||||
public companion object {
|
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
|
* A quasi-square section. Note that latitudinal distance could be imprecise for large distances
|
||||||
*/
|
*/
|
||||||
public fun square(
|
public fun square(
|
||||||
center: GeodeticMapCoordinates,
|
center: GeodeticMapCoordinates,
|
||||||
width: Distance,
|
|
||||||
height: Distance,
|
height: Distance,
|
||||||
|
width: Distance,
|
||||||
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
|
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
|
||||||
): GmcRectangle {
|
): GmcRectangle {
|
||||||
val reducedRadius = ellipsoid.reducedRadius(center.latitude)
|
val reducedRadius = ellipsoid.reducedRadius(center.latitude)
|
||||||
val a = GeodeticMapCoordinates(
|
return square(center, (height / ellipsoid.polarRadius).radians, (width / reducedRadius).radians)
|
||||||
center.latitude - (height / ellipsoid.polarRadius / 2).radians,
|
|
||||||
center.longitude - (width / reducedRadius / 2).radians
|
|
||||||
)
|
|
||||||
val b = GeodeticMapCoordinates(
|
|
||||||
center.latitude + (height / ellipsoid.polarRadius / 2).radians,
|
|
||||||
center.longitude + (width / reducedRadius / 2).radians
|
|
||||||
)
|
|
||||||
return GmcRectangle(a, b, ellipsoid)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,6 +78,30 @@ public val GmcRectangle.latitudeDelta: Angle get() = abs(a.latitude - b.latitude
|
|||||||
public val GmcRectangle.topLeft: GeodeticMapCoordinates get() = GeodeticMapCoordinates(top, left)
|
public val GmcRectangle.topLeft: GeodeticMapCoordinates get() = GeodeticMapCoordinates(top, left)
|
||||||
public val GmcRectangle.bottomRight: GeodeticMapCoordinates get() = GeodeticMapCoordinates(bottom, right)
|
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
|
* Compute a minimal bounding box including all given boxes. Return null if collection is empty
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user