Generalize feature draw logic for schemes
This commit is contained in:
parent
8a94438dfd
commit
5ed46b278c
@ -8,7 +8,7 @@ plugins {
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "center.sciprog"
|
group = "center.sciprog"
|
||||||
version = "0.1.1"
|
version = "0.2.0-dev-1"
|
||||||
}
|
}
|
||||||
|
|
||||||
apiValidation{
|
apiValidation{
|
||||||
|
@ -8,6 +8,10 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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.features.FeaturesState
|
||||||
|
import center.sciprog.maps.features.ViewConfig
|
||||||
|
import center.sciprog.maps.features.ViewPoint
|
||||||
|
import center.sciprog.maps.features.computeBoundingBox
|
||||||
import center.sciprog.maps.scheme.*
|
import center.sciprog.maps.scheme.*
|
||||||
import center.sciprog.maps.svg.FeatureStateSnapshot
|
import center.sciprog.maps.svg.FeatureStateSnapshot
|
||||||
import center.sciprog.maps.svg.exportToSvg
|
import center.sciprog.maps.svg.exportToSvg
|
||||||
@ -25,7 +29,7 @@ fun App() {
|
|||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val schemeFeaturesState = SchemeFeaturesState.remember {
|
val schemeFeaturesState = FeaturesState.remember(XYCoordinateSpace) {
|
||||||
background(1600f, 1200f) { painterResource("middle-earth.jpg") }
|
background(1600f, 1200f) { painterResource("middle-earth.jpg") }
|
||||||
circle(410.52737 to 868.7676, color = Color.Blue)
|
circle(410.52737 to 868.7676, color = Color.Blue)
|
||||||
text(410.52737 to 868.7676, "Shire", color = Color.Blue)
|
text(410.52737 to 868.7676, "Shire", color = Color.Blue)
|
||||||
@ -33,14 +37,14 @@ fun App() {
|
|||||||
text(1132.0881 to 394.99127, "Ordruin", color = Color.Red)
|
text(1132.0881 to 394.99127, "Ordruin", color = Color.Red)
|
||||||
arc(center = 1132.0881 to 394.99127, radius = 20f, startAngle = 0f, 2 * PI.toFloat())
|
arc(center = 1132.0881 to 394.99127, radius = 20f, startAngle = 0f, 2 * PI.toFloat())
|
||||||
|
|
||||||
val hobbitId = circle(410.52737 to 868.7676)
|
circle(410.52737 to 868.7676, id = "hobbit")
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
var t = 0.0
|
var t = 0.0
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
val x = 410.52737 + t * (1132.0881 - 410.52737)
|
val x = 410.52737 + t * (1132.0881 - 410.52737)
|
||||||
val y = 868.7676 + t * (394.99127 - 868.7676)
|
val y = 868.7676 + t * (394.99127 - 868.7676)
|
||||||
circle(x to y, color = Color.Green, id = hobbitId)
|
circle(x to y, color = Color.Green, id = "hobbit")
|
||||||
delay(100)
|
delay(100)
|
||||||
t += 0.005
|
t += 0.005
|
||||||
if (t >= 1.0) t = 0.0
|
if (t >= 1.0) t = 0.0
|
||||||
@ -48,14 +52,14 @@ fun App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val initialViewPoint: SchemeViewPoint = remember {
|
val initialViewPoint: ViewPoint<XY> = remember {
|
||||||
schemeFeaturesState.features().values.computeBoundingBox(1f)?.computeViewPoint()
|
schemeFeaturesState.features.values.computeBoundingBox(XYCoordinateSpace, 1f)?.computeViewPoint()
|
||||||
?: SchemeViewPoint(SchemeCoordinates(0f, 0f))
|
?: XYViewPoint(XY(0f, 0f))
|
||||||
}
|
}
|
||||||
|
|
||||||
var viewPoint by remember { mutableStateOf<SchemeViewPoint>(initialViewPoint) }
|
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
|
||||||
|
|
||||||
var snapshot: FeatureStateSnapshot? by remember { mutableStateOf(null) }
|
var snapshot: FeatureStateSnapshot<XY>? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
if (snapshot == null) {
|
if (snapshot == null) {
|
||||||
snapshot = schemeFeaturesState.snapshot()
|
snapshot = schemeFeaturesState.snapshot()
|
||||||
@ -78,7 +82,7 @@ fun App() {
|
|||||||
SchemeView(
|
SchemeView(
|
||||||
initialViewPoint = initialViewPoint,
|
initialViewPoint = initialViewPoint,
|
||||||
featuresState = schemeFeaturesState,
|
featuresState = schemeFeaturesState,
|
||||||
config = SchemeViewConfig(
|
config = ViewConfig(
|
||||||
onClick = {
|
onClick = {
|
||||||
println("${focus.x}, ${focus.y}")
|
println("${focus.x}, ${focus.y}")
|
||||||
},
|
},
|
||||||
|
@ -8,14 +8,14 @@ import center.sciprog.maps.features.ViewPoint
|
|||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
|
||||||
public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
||||||
override fun Rectangle(first: Gmc, second: Gmc): GmcRectangle = GmcRectangle(first, second)
|
override fun Rectangle(first: Gmc, second: Gmc): Rectangle<Gmc> = GmcRectangle(first, second)
|
||||||
|
|
||||||
override fun Rectangle(center: Gmc, zoom: Double, size: DpSize): GmcRectangle {
|
override fun Rectangle(center: Gmc, zoom: Float, size: DpSize): Rectangle<Gmc> {
|
||||||
val scale = WebMercatorProjection.scaleFactor(zoom)
|
val scale = WebMercatorProjection.scaleFactor(zoom)
|
||||||
return buildRectangle(center, (size.width.value / scale).radians, (size.height.value / scale).radians)
|
return Rectangle(center, (size.width.value / scale).radians, (size.height.value / scale).radians)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun ViewPoint(center: Gmc, zoom: Double): ViewPoint<Gmc> = MapViewPoint(center, zoom)
|
override fun ViewPoint(center: Gmc, zoom: Float): ViewPoint<Gmc> = MapViewPoint(center, zoom)
|
||||||
|
|
||||||
override fun ViewPoint<Gmc>.moveBy(delta: Gmc): ViewPoint<Gmc> {
|
override fun ViewPoint<Gmc>.moveBy(delta: Gmc): ViewPoint<Gmc> {
|
||||||
val newCoordinates = GeodeticMapCoordinates(
|
val newCoordinates = GeodeticMapCoordinates(
|
||||||
@ -28,21 +28,19 @@ public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
|||||||
return MapViewPoint(newCoordinates, zoom)
|
return MapViewPoint(newCoordinates, zoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun ViewPoint<Gmc>.zoomBy(zoomDelta: Double, invariant: Gmc): ViewPoint<Gmc> = if (invariant == focus) {
|
override fun ViewPoint<Gmc>.zoomBy(zoomDelta: Float, invariant: Gmc): ViewPoint<Gmc> = if (invariant == focus) {
|
||||||
ViewPoint(focus, (zoom + zoomDelta).coerceIn(2.0, 18.0) )
|
ViewPoint(focus, (zoom + zoomDelta).coerceIn(2f, 18f) )
|
||||||
} else {
|
} else {
|
||||||
val difScale = (1 - 2.0.pow(-zoomDelta))
|
val difScale = (1 - 2f.pow(-zoomDelta))
|
||||||
val newCenter = GeodeticMapCoordinates(
|
val newCenter = GeodeticMapCoordinates(
|
||||||
focus.latitude + (invariant.latitude - focus.latitude) * difScale,
|
focus.latitude + (invariant.latitude - focus.latitude) * difScale,
|
||||||
focus.longitude + (invariant.longitude - focus.longitude) * difScale
|
focus.longitude + (invariant.longitude - focus.longitude) * difScale
|
||||||
)
|
)
|
||||||
MapViewPoint(newCenter, (zoom + zoomDelta).coerceIn(2.0, 18.0))
|
MapViewPoint(newCenter, (zoom + zoomDelta).coerceIn(2f, 18f))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun Rectangle<Gmc>.withCenter(center: Gmc): Rectangle<Gmc> =
|
||||||
override fun Rectangle<Gmc>.withCenter(center: Gmc): GmcRectangle {
|
Rectangle(center, height = latitudeDelta, width = longitudeDelta)
|
||||||
return buildRectangle(center, height = latitudeDelta, width = longitudeDelta)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun Collection<Rectangle<Gmc>>.wrapRectangles(): Rectangle<Gmc>? {
|
override fun Collection<Rectangle<Gmc>>.wrapRectangles(): Rectangle<Gmc>? {
|
||||||
if (isEmpty()) return null
|
if (isEmpty()) return null
|
||||||
@ -68,24 +66,24 @@ public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
|||||||
/**
|
/**
|
||||||
* 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 CoordinateSpace<Gmc>.buildRectangle(
|
public fun CoordinateSpace<Gmc>.Rectangle(
|
||||||
center: Gmc,
|
center: Gmc,
|
||||||
height: Distance,
|
height: Distance,
|
||||||
width: Distance,
|
width: Distance,
|
||||||
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
|
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
|
||||||
): GmcRectangle {
|
): Rectangle<Gmc> {
|
||||||
val reducedRadius = ellipsoid.reducedRadius(center.latitude)
|
val reducedRadius = ellipsoid.reducedRadius(center.latitude)
|
||||||
return buildRectangle(center, (height / ellipsoid.polarRadius).radians, (width / reducedRadius).radians)
|
return Rectangle(center, (height / ellipsoid.polarRadius).radians, (width / reducedRadius).radians)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A quasi-square section.
|
* A quasi-square section.
|
||||||
*/
|
*/
|
||||||
public fun CoordinateSpace<Gmc>.buildRectangle(
|
public fun CoordinateSpace<Gmc>.Rectangle(
|
||||||
center: GeodeticMapCoordinates,
|
center: GeodeticMapCoordinates,
|
||||||
height: Angle,
|
height: Angle,
|
||||||
width: Angle,
|
width: Angle,
|
||||||
): GmcRectangle {
|
): Rectangle<Gmc> {
|
||||||
val a = GeodeticMapCoordinates(
|
val a = GeodeticMapCoordinates(
|
||||||
center.latitude - (height / 2),
|
center.latitude - (height / 2),
|
||||||
center.longitude - (width / 2)
|
center.longitude - (width / 2)
|
||||||
|
@ -12,7 +12,7 @@ import center.sciprog.maps.features.Rectangle
|
|||||||
*
|
*
|
||||||
* Note that this is a rectangle only on a Mercator projection.
|
* Note that this is a rectangle only on a Mercator projection.
|
||||||
*/
|
*/
|
||||||
public data class GmcRectangle(
|
internal data class GmcRectangle(
|
||||||
override val a: GeodeticMapCoordinates,
|
override val a: GeodeticMapCoordinates,
|
||||||
override val b: GeodeticMapCoordinates,
|
override val b: GeodeticMapCoordinates,
|
||||||
) : Rectangle<Gmc> {
|
) : Rectangle<Gmc> {
|
||||||
|
@ -35,7 +35,7 @@ public fun Rectangle<Gmc>.computeViewPoint(
|
|||||||
canvasSize.height.value / latitudeDelta.radians.value
|
canvasSize.height.value / latitudeDelta.radians.value
|
||||||
) * PI / mapTileProvider.tileSize
|
) * PI / mapTileProvider.tileSize
|
||||||
)
|
)
|
||||||
return MapViewPoint(center, zoom)
|
return MapViewPoint(center, zoom.toFloat())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,7 +45,7 @@ public fun Rectangle<Gmc>.computeViewPoint(
|
|||||||
public fun MapView(
|
public fun MapView(
|
||||||
mapTileProvider: MapTileProvider,
|
mapTileProvider: MapTileProvider,
|
||||||
initialViewPoint: MapViewPoint? = null,
|
initialViewPoint: MapViewPoint? = null,
|
||||||
initialRectangle: GmcRectangle? = null,
|
initialRectangle: Rectangle<Gmc>? = null,
|
||||||
featureMap: Map<FeatureId<*>, MapFeature>,
|
featureMap: Map<FeatureId<*>, MapFeature>,
|
||||||
config: ViewConfig<Gmc> = ViewConfig(),
|
config: ViewConfig<Gmc> = ViewConfig(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
@ -59,7 +59,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(GmcCoordinateSpace, 1.0)?.computeViewPoint(mapTileProvider)
|
?: featureMap.values.computeBoundingBox(GmcCoordinateSpace, 1f)?.computeViewPoint(mapTileProvider)
|
||||||
?: MapViewPoint.globe
|
?: MapViewPoint.globe
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ public fun MapView(
|
|||||||
public fun MapView(
|
public fun MapView(
|
||||||
mapTileProvider: MapTileProvider,
|
mapTileProvider: MapTileProvider,
|
||||||
initialViewPoint: MapViewPoint? = null,
|
initialViewPoint: MapViewPoint? = null,
|
||||||
initialRectangle: GmcRectangle? = null,
|
initialRectangle: Rectangle<Gmc>? = null,
|
||||||
config: ViewConfig<Gmc> = ViewConfig(),
|
config: ViewConfig<Gmc> = ViewConfig(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
buildFeatures: FeaturesState<Gmc>.() -> Unit = {},
|
buildFeatures: FeaturesState<Gmc>.() -> Unit = {},
|
||||||
@ -87,12 +87,13 @@ public fun MapView(
|
|||||||
val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) {
|
val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) {
|
||||||
initialViewPoint
|
initialViewPoint
|
||||||
?: initialRectangle?.computeViewPoint(mapTileProvider)
|
?: initialRectangle?.computeViewPoint(mapTileProvider)
|
||||||
?: featureState.features.values.computeBoundingBox(GmcCoordinateSpace,1.0)?.computeViewPoint(mapTileProvider)
|
?: featureState.features.values.computeBoundingBox(GmcCoordinateSpace,1f)?.computeViewPoint(mapTileProvider)
|
||||||
?: MapViewPoint.globe
|
?: MapViewPoint.globe
|
||||||
}
|
}
|
||||||
|
|
||||||
val featureDrag: DragHandle<Gmc> = DragHandle.withPrimaryButton { event, start: ViewPoint<Gmc>, end: ViewPoint<Gmc> ->
|
val featureDrag: DragHandle<Gmc> = DragHandle.withPrimaryButton { event, start: ViewPoint<Gmc>, end: ViewPoint<Gmc> ->
|
||||||
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
|
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
|
||||||
|
//TODO add safety
|
||||||
handle as DragHandle<Gmc>
|
handle as DragHandle<Gmc>
|
||||||
if (!handle.handle(event, start, end)) return@withPrimaryButton false
|
if (!handle.handle(event, start, end)) return@withPrimaryButton false
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,12 @@ import kotlin.math.pow
|
|||||||
*/
|
*/
|
||||||
public data class MapViewPoint(
|
public data class MapViewPoint(
|
||||||
override val focus: GeodeticMapCoordinates,
|
override val focus: GeodeticMapCoordinates,
|
||||||
override val zoom: Double,
|
override val zoom: Float,
|
||||||
) : ViewPoint<Gmc>{
|
) : ViewPoint<Gmc>{
|
||||||
val scaleFactor: Double by lazy { WebMercatorProjection.scaleFactor(zoom) }
|
val scaleFactor: Double by lazy { WebMercatorProjection.scaleFactor(zoom) }
|
||||||
|
|
||||||
public companion object{
|
public companion object{
|
||||||
public val globe: MapViewPoint = MapViewPoint(GeodeticMapCoordinates(0.0.radians, 0.0.radians), 1.0)
|
public val globe: MapViewPoint = MapViewPoint(GeodeticMapCoordinates(0.0.radians, 0.0.radians), 1f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,15 +30,15 @@ public fun MapViewPoint.move(delta: GeodeticMapCoordinates): MapViewPoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fun MapViewPoint.zoom(
|
public fun MapViewPoint.zoom(
|
||||||
zoomDelta: Double,
|
zoomDelta: Float,
|
||||||
invariant: GeodeticMapCoordinates = focus,
|
invariant: GeodeticMapCoordinates = focus,
|
||||||
): MapViewPoint = if (invariant == focus) {
|
): MapViewPoint = if (invariant == focus) {
|
||||||
copy(zoom = (zoom + zoomDelta).coerceIn(2.0, 18.0))
|
copy(zoom = (zoom + zoomDelta).coerceIn(2f, 18f))
|
||||||
} else {
|
} else {
|
||||||
val difScale = (1 - 2.0.pow(-zoomDelta))
|
val difScale = (1 - 2f.pow(-zoomDelta))
|
||||||
val newCenter = GeodeticMapCoordinates(
|
val newCenter = GeodeticMapCoordinates(
|
||||||
focus.latitude + (invariant.latitude - focus.latitude) * difScale,
|
focus.latitude + (invariant.latitude - focus.latitude) * difScale,
|
||||||
focus.longitude + (invariant.longitude - focus.longitude) * difScale
|
focus.longitude + (invariant.longitude - focus.longitude) * difScale
|
||||||
)
|
)
|
||||||
MapViewPoint(newCenter, (zoom + zoomDelta).coerceIn(2.0, 18.0))
|
MapViewPoint(newCenter, (zoom + zoomDelta).coerceIn(2f, 18f))
|
||||||
}
|
}
|
@ -7,7 +7,7 @@ import center.sciprog.maps.coordinates.*
|
|||||||
import center.sciprog.maps.features.*
|
import center.sciprog.maps.features.*
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
internal class MapState internal constructor(
|
internal class MapViewState internal constructor(
|
||||||
config: ViewConfig<Gmc>,
|
config: ViewConfig<Gmc>,
|
||||||
canvasSize: DpSize,
|
canvasSize: DpSize,
|
||||||
viewPoint: ViewPoint<Gmc>,
|
viewPoint: ViewPoint<Gmc>,
|
||||||
@ -15,36 +15,36 @@ internal class MapState internal constructor(
|
|||||||
) : CoordinateViewState<Gmc>(config, canvasSize, viewPoint) {
|
) : CoordinateViewState<Gmc>(config, canvasSize, viewPoint) {
|
||||||
override val space: CoordinateSpace<Gmc> get() = GmcCoordinateSpace
|
override val space: CoordinateSpace<Gmc> get() = GmcCoordinateSpace
|
||||||
|
|
||||||
public val scaleFactor: Double
|
val scaleFactor: Double
|
||||||
get() = WebMercatorProjection.scaleFactor(viewPoint.zoom)
|
get() = WebMercatorProjection.scaleFactor(viewPoint.zoom)
|
||||||
|
|
||||||
val intZoom: Int get() = floor(zoom).toInt()
|
val intZoom: Int get() = floor(zoom).toInt()
|
||||||
|
|
||||||
public val centerCoordinates: WebMercatorCoordinates
|
val centerCoordinates: WebMercatorCoordinates
|
||||||
get() = WebMercatorProjection.toMercator(viewPoint.focus, intZoom)
|
get() = WebMercatorProjection.toMercator(viewPoint.focus, intZoom)
|
||||||
|
|
||||||
internal val tileScale: Double
|
val tileScale: Float
|
||||||
get() = 2.0.pow(viewPoint.zoom - floor(viewPoint.zoom))
|
get() = 2f.pow(viewPoint.zoom - floor(viewPoint.zoom))
|
||||||
|
|
||||||
private fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates(
|
|
||||||
intZoom,
|
|
||||||
(x - canvasSize.width / 2).value / tileScale + centerCoordinates.x,
|
|
||||||
(y - canvasSize.height / 2).value / tileScale + centerCoordinates.y,
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convert screen independent offset to GMC, adjusting for fractional zoom
|
* Convert screen independent offset to GMC, adjusting for fractional zoom
|
||||||
*/
|
*/
|
||||||
override fun DpOffset.toCoordinates(): Gmc =
|
override fun DpOffset.toCoordinates(): Gmc {
|
||||||
WebMercatorProjection.toGeodetic(toMercator())
|
val mercator = WebMercatorCoordinates(
|
||||||
|
intZoom,
|
||||||
internal fun WebMercatorCoordinates.toOffset(): DpOffset = DpOffset(
|
(x - canvasSize.width / 2).value / tileScale + centerCoordinates.x,
|
||||||
(canvasSize.width / 2 + (x.dp - centerCoordinates.x.dp) * tileScale.toFloat()),
|
(y - canvasSize.height / 2).value / tileScale + centerCoordinates.y,
|
||||||
(canvasSize.height / 2 + (y.dp - centerCoordinates.y.dp) * tileScale.toFloat())
|
|
||||||
)
|
)
|
||||||
|
return WebMercatorProjection.toGeodetic(mercator)
|
||||||
|
}
|
||||||
|
|
||||||
override fun Gmc.toDpOffset(): DpOffset =
|
override fun Gmc.toDpOffset(): DpOffset {
|
||||||
WebMercatorProjection.toMercator(this, intZoom).toOffset()
|
val mercator = WebMercatorProjection.toMercator(this, intZoom)
|
||||||
|
return DpOffset(
|
||||||
|
(canvasSize.width / 2 + (mercator.x.dp - centerCoordinates.x.dp) * tileScale.toFloat()),
|
||||||
|
(canvasSize.height / 2 + (mercator.y.dp - centerCoordinates.y.dp) * tileScale.toFloat())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun Rectangle<Gmc>.toDpRect(): DpRect {
|
override fun Rectangle<Gmc>.toDpRect(): DpRect {
|
||||||
val topLeft = topLeft.toDpOffset()
|
val topLeft = topLeft.toDpOffset()
|
||||||
@ -59,7 +59,7 @@ internal class MapState internal constructor(
|
|||||||
canvasSize.height.value / rectangle.latitudeDelta.radians.value
|
canvasSize.height.value / rectangle.latitudeDelta.radians.value
|
||||||
) * PI / tileSize
|
) * PI / tileSize
|
||||||
)
|
)
|
||||||
return MapViewPoint(rectangle.center, zoom)
|
return MapViewPoint(rectangle.center, zoom.toFloat())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun ViewPoint<Gmc>.moveBy(x: Dp, y: Dp): ViewPoint<Gmc> {
|
override fun ViewPoint<Gmc>.moveBy(x: Dp, y: Dp): ViewPoint<Gmc> {
|
||||||
@ -82,6 +82,6 @@ internal fun rememberMapState(
|
|||||||
canvasSize: DpSize,
|
canvasSize: DpSize,
|
||||||
viewPoint: ViewPoint<Gmc>,
|
viewPoint: ViewPoint<Gmc>,
|
||||||
tileSize: Int,
|
tileSize: Int,
|
||||||
): MapState = remember {
|
): MapViewState = remember {
|
||||||
MapState(config, canvasSize, viewPoint, tileSize)
|
MapViewState(config, canvasSize, viewPoint, tileSize)
|
||||||
}
|
}
|
@ -84,7 +84,7 @@ public fun FeaturesState<Gmc>.arc(
|
|||||||
id,
|
id,
|
||||||
ArcFeature(
|
ArcFeature(
|
||||||
coordinateSpace,
|
coordinateSpace,
|
||||||
oval = buildRectangle(coordinatesOf(center), radius, radius),
|
oval = Rectangle(coordinatesOf(center), radius, radius),
|
||||||
startAngle = startAngle.radians.toFloat(),
|
startAngle = startAngle.radians.toFloat(),
|
||||||
arcLength = arcLength.radians.toFloat(),
|
arcLength = arcLength.radians.toFloat(),
|
||||||
zoomRange = zoomRange,
|
zoomRange = zoomRange,
|
||||||
|
@ -8,14 +8,17 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.PathEffect
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||||
import androidx.compose.ui.graphics.vector.VectorPainter
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
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.Gmc
|
import center.sciprog.maps.coordinates.Gmc
|
||||||
import center.sciprog.maps.features.*
|
import center.sciprog.maps.features.FeaturesState
|
||||||
|
import center.sciprog.maps.features.PainterFeature
|
||||||
|
import center.sciprog.maps.features.ViewConfig
|
||||||
|
import center.sciprog.maps.features.drawFeature
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
@ -53,6 +56,7 @@ public actual fun MapView(
|
|||||||
initialViewPoint,
|
initialViewPoint,
|
||||||
mapTileProvider.tileSize
|
mapTileProvider.tileSize
|
||||||
)
|
)
|
||||||
|
|
||||||
with(state) {
|
with(state) {
|
||||||
|
|
||||||
val mapTiles = remember(mapTileProvider) { mutableStateListOf<MapTile>() }
|
val mapTiles = remember(mapTileProvider) { mutableStateListOf<MapTile>() }
|
||||||
@ -96,8 +100,8 @@ public actual fun MapView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val painterCache: Map<VectorImageFeature<Gmc>, VectorPainter> = key(featuresState) {
|
val painterCache: Map<PainterFeature<Gmc>, Painter> = key(featuresState) {
|
||||||
featuresState.features.values.filterIsInstance<VectorImageFeature<Gmc>>().associateWith { it.painter() }
|
featuresState.features.values.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.painter() }
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas(modifier = modifier.mapControls(state).fillMaxSize()) {
|
Canvas(modifier = modifier.mapControls(state).fillMaxSize()) {
|
||||||
|
@ -14,10 +14,10 @@ public object WebMercatorProjection {
|
|||||||
/**
|
/**
|
||||||
* Compute radians to projection coordinates ratio for given [zoom] factor
|
* Compute radians to projection coordinates ratio for given [zoom] factor
|
||||||
*/
|
*/
|
||||||
public fun scaleFactor(zoom: Double): Double = 256.0 / 2 / PI * 2.0.pow(zoom)
|
public fun scaleFactor(zoom: Float): Double = 256.0 / 2 / PI * 2f.pow(zoom)
|
||||||
|
|
||||||
public fun toGeodetic(mercator: WebMercatorCoordinates): GeodeticMapCoordinates {
|
public fun toGeodetic(mercator: WebMercatorCoordinates): GeodeticMapCoordinates {
|
||||||
val scaleFactor = scaleFactor(mercator.zoom.toDouble())
|
val scaleFactor = scaleFactor(mercator.zoom.toFloat())
|
||||||
val longitude = mercator.x / scaleFactor - PI
|
val longitude = mercator.x / scaleFactor - PI
|
||||||
val latitude = (atan(exp(PI - mercator.y / scaleFactor)) - PI / 4) * 2
|
val latitude = (atan(exp(PI - mercator.y / scaleFactor)) - PI / 4) * 2
|
||||||
return GeodeticMapCoordinates.ofRadians(latitude, longitude)
|
return GeodeticMapCoordinates.ofRadians(latitude, longitude)
|
||||||
@ -29,7 +29,7 @@ public object WebMercatorProjection {
|
|||||||
public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Int): WebMercatorCoordinates {
|
public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Int): WebMercatorCoordinates {
|
||||||
require(abs(gmc.latitude) <= MercatorProjection.MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" }
|
require(abs(gmc.latitude) <= MercatorProjection.MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" }
|
||||||
|
|
||||||
val scaleFactor = scaleFactor(zoom.toDouble())
|
val scaleFactor = scaleFactor(zoom.toFloat())
|
||||||
return WebMercatorCoordinates(
|
return WebMercatorCoordinates(
|
||||||
zoom = zoom,
|
zoom = zoom,
|
||||||
x = scaleFactor * (gmc.longitude.radians.value + PI),
|
x = scaleFactor * (gmc.longitude.radians.value + PI),
|
||||||
|
@ -29,17 +29,17 @@ public interface CoordinateSpace<T : Any> {
|
|||||||
/**
|
/**
|
||||||
* Build a rectangle of visual size [size]
|
* Build a rectangle of visual size [size]
|
||||||
*/
|
*/
|
||||||
public fun Rectangle(center: T, zoom: Double, size: DpSize): Rectangle<T>
|
public fun Rectangle(center: T, zoom: Float, size: DpSize): Rectangle<T>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a [ViewPoint] associated with this coordinate space.
|
* Create a [ViewPoint] associated with this coordinate space.
|
||||||
*/
|
*/
|
||||||
public fun ViewPoint(center: T, zoom: Double): ViewPoint<T>
|
public fun ViewPoint(center: T, zoom: Float): ViewPoint<T>
|
||||||
|
|
||||||
public fun ViewPoint<T>.moveBy(delta: T): ViewPoint<T>
|
public fun ViewPoint<T>.moveBy(delta: T): ViewPoint<T>
|
||||||
|
|
||||||
public fun ViewPoint<T>.zoomBy(
|
public fun ViewPoint<T>.zoomBy(
|
||||||
zoomDelta: Double,
|
zoomDelta: Float,
|
||||||
invariant: T = focus,
|
invariant: T = focus,
|
||||||
): ViewPoint<T>
|
): ViewPoint<T>
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ public abstract class CoordinateViewState<T : Any>(
|
|||||||
viewPointState.value = value
|
viewPointState.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
public val zoom: Double get() = viewPoint.zoom
|
public val zoom: Float get() = viewPoint.zoom
|
||||||
|
|
||||||
public abstract fun DpOffset.toCoordinates(): T
|
public abstract fun DpOffset.toCoordinates(): T
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.*
|
|||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||||
import androidx.compose.ui.graphics.drawscope.Fill
|
import androidx.compose.ui.graphics.drawscope.Fill
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.graphics.vector.VectorPainter
|
import androidx.compose.ui.graphics.vector.VectorPainter
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
@ -14,24 +15,30 @@ 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 center.sciprog.maps.features.Feature.Companion.defaultZoomRange
|
||||||
|
|
||||||
public typealias DoubleRange = ClosedFloatingPointRange<Double>
|
public typealias DoubleRange = FloatRange
|
||||||
|
public typealias FloatRange = ClosedFloatingPointRange<Float>
|
||||||
|
|
||||||
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: DoubleRange
|
public val zoomRange: FloatRange
|
||||||
|
|
||||||
public var attributes: AttributeMap
|
public var attributes: AttributeMap
|
||||||
|
|
||||||
public fun getBoundingBox(zoom: Double): Rectangle<T>?
|
public fun getBoundingBox(zoom: Float): Rectangle<T>?
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public val defaultZoomRange: ClosedFloatingPointRange<Double> = 1.0..Double.POSITIVE_INFINITY
|
public val defaultZoomRange: FloatRange = 0f..Float.POSITIVE_INFINITY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface PainterFeature<T:Any>: Feature<T> {
|
||||||
|
@Composable
|
||||||
|
public fun painter(): Painter
|
||||||
|
}
|
||||||
|
|
||||||
public interface SelectableFeature<T : Any> : Feature<T> {
|
public interface SelectableFeature<T : Any> : Feature<T> {
|
||||||
public operator fun contains(point: ViewPoint<T>): Boolean = getBoundingBox(point.zoom)?.let {
|
public operator fun contains(point: ViewPoint<T>): Boolean = getBoundingBox(point.zoom)?.let {
|
||||||
point.focus in it
|
point.focus in it
|
||||||
@ -44,7 +51,7 @@ public interface DraggableFeature<T : Any> : SelectableFeature<T> {
|
|||||||
|
|
||||||
public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
|
public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
|
||||||
space: CoordinateSpace<T>,
|
space: CoordinateSpace<T>,
|
||||||
zoom: Double,
|
zoom: Float,
|
||||||
): Rectangle<T>? = with(space) {
|
): Rectangle<T>? = with(space) {
|
||||||
mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
|
mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
|
||||||
}
|
}
|
||||||
@ -59,11 +66,11 @@ public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
|
|||||||
public class FeatureSelector<T : Any>(
|
public class FeatureSelector<T : Any>(
|
||||||
override val space: CoordinateSpace<T>,
|
override val space: CoordinateSpace<T>,
|
||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
public val selector: (zoom: Double) -> Feature<T>,
|
public val selector: (zoom: Float) -> Feature<T>,
|
||||||
) : Feature<T> {
|
) : Feature<T> {
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> get() = defaultZoomRange
|
override val zoomRange: FloatRange get() = defaultZoomRange
|
||||||
|
|
||||||
override fun getBoundingBox(zoom: Double): Rectangle<T>? = selector(zoom).getBoundingBox(zoom)
|
override fun getBoundingBox(zoom: Float): Rectangle<T>? = selector(zoom).getBoundingBox(zoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PathFeature<T : Any>(
|
public class PathFeature<T : Any>(
|
||||||
@ -73,7 +80,7 @@ public class PathFeature<T : Any>(
|
|||||||
public val brush: Brush,
|
public val brush: Brush,
|
||||||
public val style: DrawStyle = Fill,
|
public val style: DrawStyle = Fill,
|
||||||
public val targetRect: Rect = path.getBounds(),
|
public val targetRect: Rect = path.getBounds(),
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: FloatRange = defaultZoomRange,
|
||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
) : DraggableFeature<T> {
|
) : DraggableFeature<T> {
|
||||||
override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
|
override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
|
||||||
@ -88,20 +95,20 @@ public class PathFeature<T : Any>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBoundingBox(zoom: Double): Rectangle<T> = rectangle
|
override fun getBoundingBox(zoom: Float): Rectangle<T> = rectangle
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PointsFeature<T : Any>(
|
public class PointsFeature<T : Any>(
|
||||||
override val space: CoordinateSpace<T>,
|
override val space: CoordinateSpace<T>,
|
||||||
public val points: List<T>,
|
public val points: List<T>,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: FloatRange = 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,
|
||||||
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: Float): Rectangle<T>? = with(space) {
|
||||||
points.wrapPoints()
|
points.wrapPoints()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,12 +116,12 @@ public class PointsFeature<T : Any>(
|
|||||||
public data class CircleFeature<T : Any>(
|
public data class CircleFeature<T : Any>(
|
||||||
override val space: CoordinateSpace<T>,
|
override val space: CoordinateSpace<T>,
|
||||||
public val center: T,
|
public val center: T,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: FloatRange = defaultZoomRange,
|
||||||
public val size: Dp = 5.dp,
|
public val size: Dp = 5.dp,
|
||||||
public val color: Color = Color.Red,
|
public val color: Color = Color.Red,
|
||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
) : DraggableFeature<T> {
|
) : DraggableFeature<T> {
|
||||||
override fun getBoundingBox(zoom: Double): Rectangle<T> =
|
override fun getBoundingBox(zoom: Float): Rectangle<T> =
|
||||||
space.Rectangle(center, zoom, DpSize(size, size))
|
space.Rectangle(center, zoom, DpSize(size, size))
|
||||||
|
|
||||||
override fun withCoordinates(newCoordinates: T): Feature<T> =
|
override fun withCoordinates(newCoordinates: T): Feature<T> =
|
||||||
@ -124,12 +131,12 @@ public data class CircleFeature<T : Any>(
|
|||||||
public class RectangleFeature<T : Any>(
|
public class RectangleFeature<T : Any>(
|
||||||
override val space: CoordinateSpace<T>,
|
override val space: CoordinateSpace<T>,
|
||||||
public val center: T,
|
public val center: T,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: FloatRange = 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,
|
||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
) : DraggableFeature<T> {
|
) : DraggableFeature<T> {
|
||||||
override fun getBoundingBox(zoom: Double): Rectangle<T> =
|
override fun getBoundingBox(zoom: Float): Rectangle<T> =
|
||||||
space.Rectangle(center, zoom, size)
|
space.Rectangle(center, zoom, size)
|
||||||
|
|
||||||
override fun withCoordinates(newCoordinates: T): Feature<T> =
|
override fun withCoordinates(newCoordinates: T): Feature<T> =
|
||||||
@ -140,11 +147,11 @@ public class LineFeature<T : Any>(
|
|||||||
override val space: CoordinateSpace<T>,
|
override val space: CoordinateSpace<T>,
|
||||||
public val a: T,
|
public val a: T,
|
||||||
public val b: T,
|
public val b: T,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: FloatRange = defaultZoomRange,
|
||||||
public val color: Color = Color.Red,
|
public val color: Color = Color.Red,
|
||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
) : SelectableFeature<T> {
|
) : SelectableFeature<T> {
|
||||||
override fun getBoundingBox(zoom: Double): Rectangle<T> =
|
override fun getBoundingBox(zoom: Float): Rectangle<T> =
|
||||||
space.Rectangle(a, b)
|
space.Rectangle(a, b)
|
||||||
|
|
||||||
override fun contains(point: ViewPoint<T>): Boolean {
|
override fun contains(point: ViewPoint<T>): Boolean {
|
||||||
@ -161,26 +168,25 @@ public class ArcFeature<T : Any>(
|
|||||||
public val oval: Rectangle<T>,
|
public val oval: Rectangle<T>,
|
||||||
public val startAngle: Float,
|
public val startAngle: Float,
|
||||||
public val arcLength: Float,
|
public val arcLength: Float,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: FloatRange = defaultZoomRange,
|
||||||
public val color: Color = Color.Red,
|
public val color: Color = Color.Red,
|
||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
) : DraggableFeature<T> {
|
) : DraggableFeature<T> {
|
||||||
override fun getBoundingBox(zoom: Double): Rectangle<T> = oval
|
override fun getBoundingBox(zoom: Float): Rectangle<T> = oval
|
||||||
|
|
||||||
override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
|
override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
|
||||||
ArcFeature(space, oval.withCenter(newCoordinates), startAngle, arcLength, zoomRange, color, attributes)
|
ArcFeature(space, oval.withCenter(newCoordinates), startAngle, arcLength, zoomRange, color, attributes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public data class DrawFeature<T : Any>(
|
public data class DrawFeature<T : Any>(
|
||||||
override val space: CoordinateSpace<T>,
|
override val space: CoordinateSpace<T>,
|
||||||
public val position: T,
|
public val position: T,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: FloatRange = defaultZoomRange,
|
||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
public val drawFeature: DrawScope.() -> Unit,
|
public val drawFeature: DrawScope.() -> Unit,
|
||||||
) : DraggableFeature<T> {
|
) : DraggableFeature<T> {
|
||||||
override fun getBoundingBox(zoom: Double): Rectangle<T> = space.Rectangle(position, position)
|
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position)
|
||||||
|
|
||||||
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
|
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
|
||||||
}
|
}
|
||||||
@ -190,10 +196,10 @@ public data class BitmapImageFeature<T : Any>(
|
|||||||
public val position: T,
|
public val position: T,
|
||||||
public val size: DpSize,
|
public val size: DpSize,
|
||||||
public val image: ImageBitmap,
|
public val image: ImageBitmap,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: FloatRange = defaultZoomRange,
|
||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
) : DraggableFeature<T> {
|
) : DraggableFeature<T> {
|
||||||
override fun getBoundingBox(zoom: Double): Rectangle<T> = space.Rectangle(position, zoom, size)
|
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, zoom, size)
|
||||||
|
|
||||||
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
|
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
|
||||||
}
|
}
|
||||||
@ -203,27 +209,46 @@ public data class VectorImageFeature<T : Any>(
|
|||||||
public val position: T,
|
public val position: T,
|
||||||
public val size: DpSize,
|
public val size: DpSize,
|
||||||
public val image: ImageVector,
|
public val image: ImageVector,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: FloatRange = defaultZoomRange,
|
||||||
override var attributes: AttributeMap = AttributeMap(),
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
) : DraggableFeature<T> {
|
) : DraggableFeature<T>, PainterFeature<T> {
|
||||||
override fun getBoundingBox(zoom: Double): Rectangle<T> = space.Rectangle(position, zoom, size)
|
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, zoom, size)
|
||||||
|
|
||||||
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
|
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
public fun painter(): VectorPainter = rememberVectorPainter(image)
|
override fun painter(): VectorPainter = rememberVectorPainter(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A background image that is bound to coordinates and is scaled together with them
|
||||||
|
*
|
||||||
|
* @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale.
|
||||||
|
*/
|
||||||
|
public class ScalableImageFeature<T: Any>(
|
||||||
|
override val space: CoordinateSpace<T>,
|
||||||
|
public val rectangle: Rectangle<T>,
|
||||||
|
override val zoomRange: FloatRange = defaultZoomRange,
|
||||||
|
override var attributes: AttributeMap = AttributeMap(),
|
||||||
|
public val painter: @Composable () -> Painter,
|
||||||
|
) : Feature<T>, PainterFeature<T>{
|
||||||
|
@Composable
|
||||||
|
override fun painter(): Painter = painter.invoke()
|
||||||
|
|
||||||
|
override fun getBoundingBox(zoom: Float): Rectangle<T> =rectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A group of other features
|
* A group of other features
|
||||||
*/
|
*/
|
||||||
public class FeatureGroup<T : Any>(
|
public class FeatureGroup<T : Any>(
|
||||||
override val space: CoordinateSpace<T>,
|
override val space: CoordinateSpace<T>,
|
||||||
public val children: Map<FeatureId<*>, Feature<T>>,
|
public val children: Map<FeatureId<*>, Feature<T>>,
|
||||||
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
|
override val zoomRange: FloatRange = defaultZoomRange,
|
||||||
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: Float): Rectangle<T>? = with(space) {
|
||||||
children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
|
children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -232,12 +257,12 @@ public class TextFeature<T : Any>(
|
|||||||
override val space: CoordinateSpace<T>,
|
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: FloatRange = defaultZoomRange,
|
||||||
public val color: Color = Color.Black,
|
public val color: Color = Color.Black,
|
||||||
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> = space.Rectangle(position, position)
|
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position)
|
||||||
|
|
||||||
override fun withCoordinates(newCoordinates: T): Feature<T> =
|
override fun withCoordinates(newCoordinates: T): Feature<T> =
|
||||||
TextFeature(space, newCoordinates, text, zoomRange, color, attributes, fontConfig)
|
TextFeature(space, newCoordinates, text, zoomRange, color, attributes, fontConfig)
|
||||||
|
@ -5,6 +5,8 @@ import androidx.compose.runtime.mutableStateMapOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
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.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
@ -133,7 +135,7 @@ public class FeaturesState<T : Any>(public val coordinateSpace: CoordinateSpace<
|
|||||||
|
|
||||||
public fun <T : Any> FeaturesState<T>.circle(
|
public fun <T : Any> FeaturesState<T>.circle(
|
||||||
center: T,
|
center: T,
|
||||||
zoomRange: DoubleRange = defaultZoomRange,
|
zoomRange: FloatRange = defaultZoomRange,
|
||||||
size: Dp = 5.dp,
|
size: Dp = 5.dp,
|
||||||
color: Color = Color.Red,
|
color: Color = Color.Red,
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
@ -143,7 +145,7 @@ public fun <T : Any> FeaturesState<T>.circle(
|
|||||||
|
|
||||||
public fun <T : Any> FeaturesState<T>.rectangle(
|
public fun <T : Any> FeaturesState<T>.rectangle(
|
||||||
centerCoordinates: T,
|
centerCoordinates: T,
|
||||||
zoomRange: DoubleRange = defaultZoomRange,
|
zoomRange: FloatRange = defaultZoomRange,
|
||||||
size: DpSize = DpSize(5.dp, 5.dp),
|
size: DpSize = DpSize(5.dp, 5.dp),
|
||||||
color: Color = Color.Red,
|
color: Color = Color.Red,
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
@ -151,10 +153,20 @@ public fun <T : Any> FeaturesState<T>.rectangle(
|
|||||||
id, RectangleFeature(coordinateSpace, centerCoordinates, zoomRange, size, color)
|
id, RectangleFeature(coordinateSpace, centerCoordinates, zoomRange, size, color)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
public fun <T : Any> FeaturesState<T>.draw(
|
||||||
|
position: T,
|
||||||
|
zoomRange: FloatRange = defaultZoomRange,
|
||||||
|
id: String? = null,
|
||||||
|
draw: DrawScope.() -> Unit,
|
||||||
|
): FeatureId<DrawFeature<T>> = feature(
|
||||||
|
id,
|
||||||
|
DrawFeature(coordinateSpace, position, zoomRange, drawFeature = draw)
|
||||||
|
)
|
||||||
|
|
||||||
public fun <T : Any> FeaturesState<T>.line(
|
public fun <T : Any> FeaturesState<T>.line(
|
||||||
aCoordinates: T,
|
aCoordinates: T,
|
||||||
bCoordinates: T,
|
bCoordinates: T,
|
||||||
zoomRange: DoubleRange = defaultZoomRange,
|
zoomRange: FloatRange = defaultZoomRange,
|
||||||
color: Color = Color.Red,
|
color: Color = Color.Red,
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
): FeatureId<LineFeature<T>> = feature(
|
): FeatureId<LineFeature<T>> = feature(
|
||||||
@ -166,7 +178,7 @@ public fun <T : Any> FeaturesState<T>.arc(
|
|||||||
oval: Rectangle<T>,
|
oval: Rectangle<T>,
|
||||||
startAngle: Float,
|
startAngle: Float,
|
||||||
arcLength: Float,
|
arcLength: Float,
|
||||||
zoomRange: DoubleRange = defaultZoomRange,
|
zoomRange: FloatRange = defaultZoomRange,
|
||||||
color: Color = Color.Red,
|
color: Color = Color.Red,
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
): FeatureId<ArcFeature<T>> = feature(
|
): FeatureId<ArcFeature<T>> = feature(
|
||||||
@ -176,7 +188,7 @@ public fun <T : Any> FeaturesState<T>.arc(
|
|||||||
|
|
||||||
public fun <T : Any> FeaturesState<T>.points(
|
public fun <T : Any> FeaturesState<T>.points(
|
||||||
points: List<T>,
|
points: List<T>,
|
||||||
zoomRange: DoubleRange = defaultZoomRange,
|
zoomRange: FloatRange = defaultZoomRange,
|
||||||
stroke: Float = 2f,
|
stroke: Float = 2f,
|
||||||
color: Color = Color.Red,
|
color: Color = Color.Red,
|
||||||
pointMode: PointMode = PointMode.Points,
|
pointMode: PointMode = PointMode.Points,
|
||||||
@ -187,7 +199,8 @@ public fun <T : Any> FeaturesState<T>.points(
|
|||||||
public fun <T : Any> FeaturesState<T>.image(
|
public fun <T : Any> FeaturesState<T>.image(
|
||||||
position: T,
|
position: T,
|
||||||
image: ImageVector,
|
image: ImageVector,
|
||||||
zoomRange: DoubleRange = defaultZoomRange,
|
zoomRange: FloatRange = defaultZoomRange,
|
||||||
|
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
): FeatureId<VectorImageFeature<T>> =
|
): FeatureId<VectorImageFeature<T>> =
|
||||||
feature(
|
feature(
|
||||||
@ -195,14 +208,14 @@ public fun <T : Any> FeaturesState<T>.image(
|
|||||||
VectorImageFeature(
|
VectorImageFeature(
|
||||||
coordinateSpace,
|
coordinateSpace,
|
||||||
position,
|
position,
|
||||||
DpSize(image.defaultWidth, image.defaultHeight),
|
size,
|
||||||
image,
|
image,
|
||||||
zoomRange
|
zoomRange
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun <T : Any> FeaturesState<T>.group(
|
public fun <T : Any> FeaturesState<T>.group(
|
||||||
zoomRange: DoubleRange = defaultZoomRange,
|
zoomRange: FloatRange = defaultZoomRange,
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
builder: FeaturesState<T>.() -> Unit,
|
builder: FeaturesState<T>.() -> Unit,
|
||||||
): FeatureId<FeatureGroup<T>> {
|
): FeatureId<FeatureGroup<T>> {
|
||||||
@ -211,10 +224,20 @@ public fun <T : Any> FeaturesState<T>.group(
|
|||||||
return feature(id, feature)
|
return feature(id, feature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun <T : Any> FeaturesState<T>.scalableImage(
|
||||||
|
box: Rectangle<T>,
|
||||||
|
zoomRange: FloatRange = defaultZoomRange,
|
||||||
|
id: String? = null,
|
||||||
|
painter: @Composable () -> Painter,
|
||||||
|
): FeatureId<ScalableImageFeature<T>> = feature(
|
||||||
|
id,
|
||||||
|
ScalableImageFeature<T>(coordinateSpace, box, zoomRange, painter = painter)
|
||||||
|
)
|
||||||
|
|
||||||
public fun <T : Any> FeaturesState<T>.text(
|
public fun <T : Any> FeaturesState<T>.text(
|
||||||
position: T,
|
position: T,
|
||||||
text: String,
|
text: String,
|
||||||
zoomRange: DoubleRange = defaultZoomRange,
|
zoomRange: FloatRange = defaultZoomRange,
|
||||||
color: Color = Color.Red,
|
color: Color = Color.Red,
|
||||||
font: FeatureFont.() -> Unit = { size = 16f },
|
font: FeatureFont.() -> Unit = { size = 16f },
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
|
@ -4,7 +4,7 @@ import androidx.compose.ui.input.pointer.PointerEvent
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
|
||||||
public data class ViewConfig<T : Any>(
|
public data class ViewConfig<T : Any>(
|
||||||
val zoomSpeed: Double = 1.0 / 3.0,
|
val zoomSpeed: Float = 1f / 3f,
|
||||||
val onClick: ViewPoint<T>.(PointerEvent) -> Unit = {},
|
val onClick: ViewPoint<T>.(PointerEvent) -> Unit = {},
|
||||||
val dragHandle: DragHandle<T> = DragHandle.bypass(),
|
val dragHandle: DragHandle<T> = DragHandle.bypass(),
|
||||||
val onViewChange: ViewPoint<T>.() -> Unit = {},
|
val onViewChange: ViewPoint<T>.() -> Unit = {},
|
||||||
|
@ -5,5 +5,5 @@ package center.sciprog.maps.features
|
|||||||
*/
|
*/
|
||||||
public interface ViewPoint<T: Any> {
|
public interface ViewPoint<T: Any> {
|
||||||
public val focus: T
|
public val focus: T
|
||||||
public val zoom: Double
|
public val zoom: Float
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.features
|
package center.sciprog.maps.compose
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.drag
|
import androidx.compose.foundation.gestures.drag
|
||||||
import androidx.compose.foundation.gestures.forEachGesture
|
import androidx.compose.foundation.gestures.forEachGesture
|
||||||
@ -8,6 +8,9 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
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.features.CoordinateViewState
|
||||||
|
import center.sciprog.maps.features.bottomRight
|
||||||
|
import center.sciprog.maps.features.topLeft
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@ -24,7 +27,7 @@ public fun <T : Any> Modifier.mapControls(
|
|||||||
val event: PointerEvent = awaitPointerEvent()
|
val event: PointerEvent = awaitPointerEvent()
|
||||||
|
|
||||||
event.changes.forEach { change ->
|
event.changes.forEach { change ->
|
||||||
val dragStart = change.position
|
//val dragStart = change.position
|
||||||
//val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
|
//val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
|
||||||
|
|
||||||
//start selection
|
//start selection
|
||||||
@ -95,7 +98,7 @@ public fun <T : Any> Modifier.mapControls(
|
|||||||
//compute invariant point of translation
|
//compute invariant point of translation
|
||||||
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
|
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
|
||||||
viewPoint = with(space) {
|
viewPoint = with(space) {
|
||||||
viewPoint.zoomBy(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant)
|
viewPoint.zoomBy(-change.scrollDelta.y * config.zoomSpeed, invariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,8 +8,8 @@ import androidx.compose.ui.graphics.drawscope.Stroke
|
|||||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
import androidx.compose.ui.graphics.drawscope.translate
|
import androidx.compose.ui.graphics.drawscope.translate
|
||||||
import androidx.compose.ui.graphics.nativeCanvas
|
import androidx.compose.ui.graphics.nativeCanvas
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.graphics.vector.VectorPainter
|
|
||||||
import org.jetbrains.skia.Font
|
import org.jetbrains.skia.Font
|
||||||
import org.jetbrains.skia.Paint
|
import org.jetbrains.skia.Paint
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
@ -22,7 +22,7 @@ internal fun Color.toPaint(): Paint = Paint().apply {
|
|||||||
|
|
||||||
public fun <T : Any> DrawScope.drawFeature(
|
public fun <T : Any> DrawScope.drawFeature(
|
||||||
state: CoordinateViewState<T>,
|
state: CoordinateViewState<T>,
|
||||||
painterCache: Map<VectorImageFeature<T>, VectorPainter>,
|
painterCache: Map<PainterFeature<T>, Painter>,
|
||||||
feature: Feature<T>,
|
feature: Feature<T>,
|
||||||
): Unit = with(state) {
|
): Unit = with(state) {
|
||||||
fun T.toOffset(): Offset = toOffset(this@drawFeature)
|
fun T.toOffset(): Offset = toOffset(this@drawFeature)
|
||||||
@ -117,6 +117,17 @@ public fun <T : Any> DrawScope.drawFeature(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is ScalableImageFeature -> {
|
||||||
|
val rect = feature.rectangle.toDpRect().toRect()
|
||||||
|
val offset = rect.topLeft
|
||||||
|
|
||||||
|
translate(offset.x, offset.y) {
|
||||||
|
with(painterCache[feature]!!) {
|
||||||
|
draw(rect.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
//logger.error { "Unrecognized feature type: ${feature::class}" }
|
//logger.error { "Unrecognized feature type: ${feature::class}" }
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
api(projects.mapsKtFeatures)
|
||||||
api("io.github.microutils:kotlin-logging:2.1.23")
|
api("io.github.microutils:kotlin-logging:2.1.23")
|
||||||
api(compose.foundation)
|
api(compose.foundation)
|
||||||
}
|
}
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
package center.sciprog.maps.scheme
|
|
||||||
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
data class SchemeCoordinates(val x: Float, val y: Float)
|
|
||||||
|
|
||||||
data class SchemeRectangle(
|
|
||||||
val a: SchemeCoordinates,
|
|
||||||
val b: SchemeCoordinates,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun square(center: SchemeCoordinates, height: Float, width: Float): SchemeRectangle = SchemeRectangle(
|
|
||||||
SchemeCoordinates(center.x - width / 2, center.y + height / 2),
|
|
||||||
SchemeCoordinates(center.x + width / 2, center.y - height / 2),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val SchemeRectangle.top get() = max(a.y, b.y)
|
|
||||||
val SchemeRectangle.bottom get() = min(a.y, b.y)
|
|
||||||
|
|
||||||
val SchemeRectangle.right get() = max(a.x, b.x)
|
|
||||||
val SchemeRectangle.left get() = min(a.x, b.x)
|
|
||||||
|
|
||||||
val SchemeRectangle.width get() = abs(a.x - b.x)
|
|
||||||
val SchemeRectangle.height get() = abs(a.y - b.y)
|
|
||||||
|
|
||||||
val SchemeRectangle.center get() = SchemeCoordinates((a.x + b.x) / 2, (a.y + b.y) / 2)
|
|
||||||
|
|
||||||
public val SchemeRectangle.leftTop: SchemeCoordinates get() = SchemeCoordinates(left, top)
|
|
||||||
public val SchemeRectangle.rightBottom: SchemeCoordinates get() = SchemeCoordinates(right, bottom)
|
|
||||||
|
|
||||||
fun Collection<SchemeRectangle>.wrapAll(): SchemeRectangle? {
|
|
||||||
if (isEmpty()) return null
|
|
||||||
val minX = minOf { it.left }
|
|
||||||
val maxX = maxOf { it.right }
|
|
||||||
|
|
||||||
val minY = minOf { it.bottom }
|
|
||||||
val maxY = maxOf { it.top }
|
|
||||||
return SchemeRectangle(
|
|
||||||
SchemeCoordinates(minX, minY),
|
|
||||||
SchemeCoordinates(maxX, maxY)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val defaultCanvasSize = DpSize(512.dp, 512.dp)
|
|
||||||
|
|
||||||
public fun SchemeRectangle.computeViewPoint(
|
|
||||||
canvasSize: DpSize = defaultCanvasSize,
|
|
||||||
): SchemeViewPoint {
|
|
||||||
val scale = min(
|
|
||||||
canvasSize.width.value / width,
|
|
||||||
canvasSize.height.value / height
|
|
||||||
)
|
|
||||||
|
|
||||||
return SchemeViewPoint(center, scale)
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
package center.sciprog.maps.scheme
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
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.scheme.SchemeFeature.Companion.defaultScaleRange
|
|
||||||
|
|
||||||
internal typealias FloatRange = ClosedFloatingPointRange<Float>
|
|
||||||
|
|
||||||
sealed class SchemeFeature(val scaleRange: FloatRange) {
|
|
||||||
abstract fun getBoundingBox(scale: Float): SchemeRectangle?
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val defaultScaleRange = 0f..Float.MAX_VALUE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun Iterable<SchemeFeature>.computeBoundingBox(scale: Float): SchemeRectangle? =
|
|
||||||
mapNotNull { it.getBoundingBox(scale) }.wrapAll()
|
|
||||||
|
|
||||||
|
|
||||||
internal fun Pair<Number, Number>.toCoordinates() = SchemeCoordinates(first.toFloat(), second.toFloat())
|
|
||||||
|
|
||||||
interface PainterFeature {
|
|
||||||
val painter: @Composable () -> Painter
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A background image that is bound to scheme coordinates and is scaled together with them
|
|
||||||
*
|
|
||||||
* @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale.
|
|
||||||
*/
|
|
||||||
class SchemeBackgroundFeature(
|
|
||||||
val rectangle: SchemeRectangle,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
override val painter: @Composable () -> Painter,
|
|
||||||
) : SchemeFeature(scaleRange), PainterFeature {
|
|
||||||
override fun getBoundingBox(scale: Float): SchemeRectangle = rectangle
|
|
||||||
}
|
|
||||||
|
|
||||||
class SchemeFeatureSelector(val selector: (scale: Float) -> SchemeFeature) : SchemeFeature(defaultScaleRange) {
|
|
||||||
override fun getBoundingBox(scale: Float): SchemeRectangle? = selector(scale).getBoundingBox(scale)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SchemeDrawFeature(
|
|
||||||
val position: SchemeCoordinates,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
val drawFeature: DrawScope.() -> Unit,
|
|
||||||
) : SchemeFeature(scaleRange) {
|
|
||||||
override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SchemeCircleFeature(
|
|
||||||
val center: SchemeCoordinates,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
val size: Float = 5f,
|
|
||||||
val color: Color = Color.Red,
|
|
||||||
) : SchemeFeature(scaleRange) {
|
|
||||||
override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(center, center)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SchemeLineFeature(
|
|
||||||
val a: SchemeCoordinates,
|
|
||||||
val b: SchemeCoordinates,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
val color: Color = Color.Red,
|
|
||||||
) : SchemeFeature(scaleRange) {
|
|
||||||
override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(a, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param startAngle the angle in radians from parallel downwards for the start of the arc
|
|
||||||
* @param arcLength arc length in radians
|
|
||||||
*/
|
|
||||||
public class SchemeArcFeature(
|
|
||||||
public val oval: SchemeRectangle,
|
|
||||||
public val startAngle: Float,
|
|
||||||
public val arcLength: Float,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
public val color: Color = Color.Red,
|
|
||||||
) : SchemeFeature(scaleRange) {
|
|
||||||
override fun getBoundingBox(scale: Float): SchemeRectangle = oval
|
|
||||||
}
|
|
||||||
|
|
||||||
class SchemeTextFeature(
|
|
||||||
val position: SchemeCoordinates,
|
|
||||||
val text: String,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
val color: Color = Color.Red,
|
|
||||||
) : SchemeFeature(scaleRange) {
|
|
||||||
override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SchemeBitmapFeature(
|
|
||||||
val position: SchemeCoordinates,
|
|
||||||
val image: ImageBitmap,
|
|
||||||
val size: IntSize = IntSize(15, 15),
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
) : SchemeFeature(scaleRange) {
|
|
||||||
override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SchemeImageFeature(
|
|
||||||
val position: SchemeCoordinates,
|
|
||||||
val size: DpSize,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
override val painter: @Composable () -> Painter,
|
|
||||||
) : SchemeFeature(scaleRange), PainterFeature {
|
|
||||||
override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SchemeImageFeature(
|
|
||||||
position: SchemeCoordinates,
|
|
||||||
image: ImageVector,
|
|
||||||
size: DpSize = DpSize(20.dp, 20.dp),
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
): SchemeImageFeature = SchemeImageFeature(position, size, scaleRange) { rememberVectorPainter(image) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A group of other features
|
|
||||||
*/
|
|
||||||
class SchemeFeatureGroup(
|
|
||||||
val children: Map<FeatureId, SchemeFeature>,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
) : SchemeFeature(scaleRange) {
|
|
||||||
override fun getBoundingBox(scale: Float): SchemeRectangle? =
|
|
||||||
children.values.mapNotNull { it.getBoundingBox(scale) }.wrapAll()
|
|
||||||
}
|
|
@ -1,206 +1,85 @@
|
|||||||
package center.sciprog.maps.scheme
|
package center.sciprog.maps.scheme
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
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.scheme.SchemeFeature.Companion.defaultScaleRange
|
import center.sciprog.maps.features.*
|
||||||
|
|
||||||
typealias FeatureId = String
|
internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat())
|
||||||
|
|
||||||
|
fun FeaturesState<XY>.background(
|
||||||
public class SchemeFeaturesState internal constructor(
|
|
||||||
private val features: MutableMap<FeatureId, SchemeFeature>,
|
|
||||||
private val attributes: MutableMap<FeatureId, SnapshotStateMap<Attribute<out Any?>, in Any?>>,
|
|
||||||
) {
|
|
||||||
public interface Attribute<T>
|
|
||||||
|
|
||||||
public fun features(): Map<FeatureId, SchemeFeature> = features
|
|
||||||
|
|
||||||
|
|
||||||
private fun generateID(feature: SchemeFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]"
|
|
||||||
|
|
||||||
public fun addFeature(id: FeatureId?, feature: SchemeFeature): FeatureId {
|
|
||||||
val safeId = id ?: generateID(feature)
|
|
||||||
features[id ?: generateID(feature)] = feature
|
|
||||||
return safeId
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <T> setAttribute(id: FeatureId, key: Attribute<T>, value: T) {
|
|
||||||
attributes.getOrPut(id) { mutableStateMapOf() }[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
public fun <T> getAttribute(id: FeatureId, key: Attribute<T>): T? =
|
|
||||||
attributes[id]?.get(key)?.let { it as T }
|
|
||||||
|
|
||||||
@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 companion object {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build, but do not remember map feature state
|
|
||||||
*/
|
|
||||||
public fun build(
|
|
||||||
builder: SchemeFeaturesState.() -> Unit = {},
|
|
||||||
): SchemeFeaturesState = SchemeFeaturesState(
|
|
||||||
mutableStateMapOf(),
|
|
||||||
mutableStateMapOf()
|
|
||||||
).apply(builder)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build and remember map feature state
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
public fun remember(
|
|
||||||
builder: SchemeFeaturesState.() -> Unit = {},
|
|
||||||
): SchemeFeaturesState = androidx.compose.runtime.remember(builder) {
|
|
||||||
build(builder)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SchemeFeaturesState.background(
|
|
||||||
box: SchemeRectangle,
|
|
||||||
id: FeatureId? = null,
|
|
||||||
painter: @Composable () -> Painter,
|
|
||||||
): FeatureId = addFeature(
|
|
||||||
id,
|
|
||||||
SchemeBackgroundFeature(box, painter = painter)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun SchemeFeaturesState.background(
|
|
||||||
width: Float,
|
width: Float,
|
||||||
height: Float,
|
height: Float,
|
||||||
offset: SchemeCoordinates = SchemeCoordinates(0f, 0f),
|
offset: XY = XY(0f, 0f),
|
||||||
id: FeatureId? = null,
|
id: String? = null,
|
||||||
painter: @Composable () -> Painter,
|
painter: @Composable () -> Painter,
|
||||||
): FeatureId {
|
): FeatureId<ScalableImageFeature<XY>> {
|
||||||
val box = SchemeRectangle(
|
val box = XYRectangle(
|
||||||
offset,
|
offset,
|
||||||
SchemeCoordinates(width + offset.x, height + offset.y)
|
XY(width + offset.x, height + offset.y)
|
||||||
)
|
)
|
||||||
return background(box, id, painter = painter)
|
return scalableImage(box, id = id, painter = painter)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SchemeFeaturesState.circle(
|
fun FeaturesState<XY>.circle(
|
||||||
center: SchemeCoordinates,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
size: Float = 5f,
|
|
||||||
color: Color = Color.Red,
|
|
||||||
id: FeatureId? = null,
|
|
||||||
) = addFeature(
|
|
||||||
id, SchemeCircleFeature(center, scaleRange, size, color)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun SchemeFeaturesState.circle(
|
|
||||||
centerCoordinates: Pair<Number, Number>,
|
centerCoordinates: Pair<Number, Number>,
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
zoomRange: FloatRange = Feature.defaultZoomRange,
|
||||||
size: Float = 5f,
|
size: Dp = 5.dp,
|
||||||
color: Color = Color.Red,
|
color: Color = Color.Red,
|
||||||
id: FeatureId? = null,
|
id: String? = null,
|
||||||
) = addFeature(
|
): FeatureId<CircleFeature<XY>> = circle(centerCoordinates.toCoordinates(), zoomRange, size, color, id = id)
|
||||||
id, SchemeCircleFeature(centerCoordinates.toCoordinates(), scaleRange, size, color)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun SchemeFeaturesState.draw(
|
fun FeaturesState<XY>.draw(
|
||||||
position: Pair<Number, Number>,
|
position: Pair<Number, Number>,
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
zoomRange: FloatRange = Feature.defaultZoomRange,
|
||||||
id: FeatureId? = null,
|
id: String? = null,
|
||||||
drawFeature: DrawScope.() -> Unit,
|
draw: DrawScope.() -> Unit,
|
||||||
) = addFeature(id, SchemeDrawFeature(position.toCoordinates(), scaleRange, drawFeature))
|
): FeatureId<DrawFeature<XY>> = draw(position.toCoordinates(), zoomRange = zoomRange, id = id, draw = draw)
|
||||||
|
|
||||||
fun SchemeFeaturesState.line(
|
fun FeaturesState<XY>.line(
|
||||||
aCoordinates: SchemeCoordinates,
|
|
||||||
bCoordinates: SchemeCoordinates,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
color: Color = Color.Red,
|
|
||||||
id: FeatureId? = null,
|
|
||||||
): FeatureId = addFeature(
|
|
||||||
id,
|
|
||||||
SchemeLineFeature(aCoordinates, bCoordinates, scaleRange, color)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun SchemeFeaturesState.line(
|
|
||||||
aCoordinates: Pair<Number, Number>,
|
aCoordinates: Pair<Number, Number>,
|
||||||
bCoordinates: Pair<Number, Number>,
|
bCoordinates: Pair<Number, Number>,
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
scaleRange: FloatRange = Feature.defaultZoomRange,
|
||||||
color: Color = Color.Red,
|
color: Color = Color.Red,
|
||||||
id: FeatureId? = null,
|
id: String? = null,
|
||||||
) = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), scaleRange, color, id)
|
): FeatureId<LineFeature<XY>> = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), scaleRange, color, id)
|
||||||
|
|
||||||
public fun SchemeFeaturesState.arc(
|
|
||||||
oval: SchemeRectangle,
|
|
||||||
startAngle: Float,
|
|
||||||
arcLength: Float,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
color: Color = Color.Red,
|
|
||||||
id: FeatureId? = null,
|
|
||||||
): FeatureId = addFeature(
|
|
||||||
id,
|
|
||||||
SchemeArcFeature(oval, startAngle, arcLength, scaleRange, color)
|
|
||||||
)
|
|
||||||
|
|
||||||
public fun SchemeFeaturesState.arc(
|
public fun FeaturesState<XY>.arc(
|
||||||
center: Pair<Double, Double>,
|
center: Pair<Double, Double>,
|
||||||
radius: Float,
|
radius: Float,
|
||||||
startAngle: Float,
|
startAngle: Float,
|
||||||
arcLength: Float,
|
arcLength: Float,
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
zoomRange: FloatRange = Feature.defaultZoomRange,
|
||||||
color: Color = Color.Red,
|
color: Color = Color.Red,
|
||||||
id: FeatureId? = null,
|
id: String? = null,
|
||||||
): FeatureId = addFeature(
|
): FeatureId<ArcFeature<XY>> = arc(
|
||||||
id,
|
oval = XYCoordinateSpace.Rectangle(center.toCoordinates(), radius, radius),
|
||||||
SchemeArcFeature(
|
|
||||||
oval = SchemeRectangle.square(center.toCoordinates(), radius, radius),
|
|
||||||
startAngle = startAngle,
|
startAngle = startAngle,
|
||||||
arcLength = arcLength,
|
arcLength = arcLength,
|
||||||
scaleRange = scaleRange,
|
zoomRange = zoomRange,
|
||||||
color = color
|
color = color
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
fun SchemeFeaturesState.text(
|
fun FeaturesState<XY>.image(
|
||||||
position: SchemeCoordinates,
|
|
||||||
text: String,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
color: Color = Color.Red,
|
|
||||||
id: FeatureId? = null,
|
|
||||||
) = addFeature(id, SchemeTextFeature(position, text, scaleRange, color))
|
|
||||||
|
|
||||||
fun SchemeFeaturesState.text(
|
|
||||||
position: Pair<Number, Number>,
|
|
||||||
text: String,
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
color: Color = Color.Red,
|
|
||||||
id: FeatureId? = null,
|
|
||||||
) = addFeature(id, SchemeTextFeature(position.toCoordinates(), text, scaleRange, color))
|
|
||||||
|
|
||||||
fun SchemeFeaturesState.image(
|
|
||||||
position: Pair<Number, Number>,
|
position: Pair<Number, Number>,
|
||||||
image: ImageVector,
|
image: ImageVector,
|
||||||
size: DpSize = DpSize(20.dp, 20.dp),
|
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
zoomRange: FloatRange = Feature.defaultZoomRange,
|
||||||
id: FeatureId? = null,
|
id: String? = null,
|
||||||
) = addFeature(id, SchemeImageFeature(position.toCoordinates(), image, size, scaleRange))
|
): FeatureId<VectorImageFeature<XY>> =
|
||||||
|
image(position.toCoordinates(), image, size = size, zoomRange = zoomRange, id = id)
|
||||||
|
|
||||||
|
fun FeaturesState<XY>.text(
|
||||||
|
position: Pair<Number, Number>,
|
||||||
|
text: String,
|
||||||
|
zoomRange: FloatRange = Feature.defaultZoomRange,
|
||||||
|
color: Color = Color.Red,
|
||||||
|
id: String? = null,
|
||||||
|
): FeatureId<TextFeature<XY>> = text(position.toCoordinates(), text, zoomRange, color, id = id)
|
||||||
|
|
||||||
fun SchemeFeaturesState.group(
|
|
||||||
scaleRange: FloatRange = defaultScaleRange,
|
|
||||||
id: FeatureId? = null,
|
|
||||||
builder: SchemeFeaturesState.() -> Unit,
|
|
||||||
): FeatureId {
|
|
||||||
val groupBuilder = SchemeFeaturesState.build(builder)
|
|
||||||
val feature = SchemeFeatureGroup(groupBuilder.features(), scaleRange)
|
|
||||||
return addFeature(id, feature)
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package center.sciprog.maps.scheme
|
|
||||||
|
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
data class SchemeViewPoint(val focus: SchemeCoordinates, val scale: Float = 1f)
|
|
||||||
|
|
||||||
fun SchemeViewPoint.move(deltaX: Float, deltaY: Float): SchemeViewPoint {
|
|
||||||
return copy(focus = SchemeCoordinates(focus.x + deltaX, focus.y + deltaY))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SchemeViewPoint.zoom(
|
|
||||||
zoom: Float,
|
|
||||||
invariant: SchemeCoordinates = focus,
|
|
||||||
): SchemeViewPoint = if (invariant == focus) {
|
|
||||||
copy(scale = scale * 2f.pow(zoom))
|
|
||||||
} else {
|
|
||||||
val difScale = (1 - 2f.pow(-zoom))
|
|
||||||
val newCenter = SchemeCoordinates(
|
|
||||||
focus.x + (invariant.x - focus.x) * difScale,
|
|
||||||
focus.y + (invariant.y - focus.y) * difScale
|
|
||||||
)
|
|
||||||
SchemeViewPoint(newCenter, scale * 2f.pow(zoom))
|
|
||||||
}
|
|
@ -0,0 +1,64 @@
|
|||||||
|
package center.sciprog.maps.scheme
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import center.sciprog.maps.features.CoordinateSpace
|
||||||
|
import center.sciprog.maps.features.Rectangle
|
||||||
|
import center.sciprog.maps.features.ViewPoint
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
data class XY(val x: Float, val y: Float)
|
||||||
|
|
||||||
|
internal data class XYRectangle(
|
||||||
|
override val a: XY,
|
||||||
|
override val b: XY,
|
||||||
|
) : Rectangle<XY> {
|
||||||
|
|
||||||
|
override fun contains(point: XY): Boolean = point.x in a.x..b.x && point.y in a.y..b.y
|
||||||
|
|
||||||
|
// companion object {
|
||||||
|
// fun square(center: XY, height: Float, width: Float): XYRectangle = XYRectangle(
|
||||||
|
// XY(center.x - width / 2, center.y + height / 2),
|
||||||
|
// XY(center.x + width / 2, center.y - height / 2),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
val Rectangle<XY>.top get() = max(a.y, b.y)
|
||||||
|
val Rectangle<XY>.bottom get() = min(a.y, b.y)
|
||||||
|
|
||||||
|
val Rectangle<XY>.right get() = max(a.x, b.x)
|
||||||
|
val Rectangle<XY>.left get() = min(a.x, b.x)
|
||||||
|
|
||||||
|
val Rectangle<XY>.width: Float get() = abs(a.x - b.x)
|
||||||
|
val Rectangle<XY>.height: Float get() = abs(a.y - b.y)
|
||||||
|
|
||||||
|
val Rectangle<XY>.center get() = XY((a.x + b.x) / 2, (a.y + b.y) / 2)
|
||||||
|
|
||||||
|
public val Rectangle<XY>.leftTop: XY get() = XY(left, top)
|
||||||
|
public val Rectangle<XY>.rightBottom: XY get() = XY(right, bottom)
|
||||||
|
|
||||||
|
internal val defaultCanvasSize = DpSize(512.dp, 512.dp)
|
||||||
|
|
||||||
|
data class XYViewPoint(
|
||||||
|
override val focus: XY,
|
||||||
|
override val zoom: Float = 1f,
|
||||||
|
) : ViewPoint<XY>
|
||||||
|
|
||||||
|
public fun CoordinateSpace<XY>.Rectangle(
|
||||||
|
center: XY,
|
||||||
|
height: Float,
|
||||||
|
width: Float,
|
||||||
|
): Rectangle<XY> {
|
||||||
|
val a = XY(
|
||||||
|
center.x - (width / 2),
|
||||||
|
center.y - (height / 2)
|
||||||
|
)
|
||||||
|
val b = XY(
|
||||||
|
center.x + (width / 2),
|
||||||
|
center.y + (height / 2)
|
||||||
|
)
|
||||||
|
return XYRectangle(a, b)
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package center.sciprog.maps.scheme
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import center.sciprog.maps.features.CoordinateSpace
|
||||||
|
import center.sciprog.maps.features.Rectangle
|
||||||
|
import center.sciprog.maps.features.ViewPoint
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
object XYCoordinateSpace : CoordinateSpace<XY> {
|
||||||
|
override fun Rectangle(first: XY, second: XY): Rectangle<XY> =
|
||||||
|
XYRectangle(first, second)
|
||||||
|
|
||||||
|
override fun Rectangle(center: XY, zoom: Float, size: DpSize): Rectangle<XY> =
|
||||||
|
Rectangle(center, (size.width.value / zoom), (size.height.value / zoom))
|
||||||
|
|
||||||
|
override fun ViewPoint(center: XY, zoom: Float): ViewPoint<XY> =
|
||||||
|
XYViewPoint(center, zoom)
|
||||||
|
|
||||||
|
override fun ViewPoint<XY>.moveBy(delta: XY): ViewPoint<XY> =
|
||||||
|
XYViewPoint(XY(focus.x + delta.x, focus.y + delta.y))
|
||||||
|
|
||||||
|
override fun ViewPoint<XY>.zoomBy(
|
||||||
|
zoomDelta: Float,
|
||||||
|
invariant: XY,
|
||||||
|
): ViewPoint<XY> = if (invariant == focus) {
|
||||||
|
XYViewPoint(focus, zoom = zoom * 2f.pow(zoomDelta))
|
||||||
|
} else {
|
||||||
|
val difScale = (1 - 2f.pow(-zoomDelta))
|
||||||
|
val newCenter = XY(
|
||||||
|
focus.x + (invariant.x - focus.x) * difScale,
|
||||||
|
focus.y + (invariant.y - focus.y) * difScale
|
||||||
|
)
|
||||||
|
XYViewPoint(newCenter, zoom * 2f.pow(zoomDelta))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun Rectangle<XY>.withCenter(center: XY): Rectangle<XY> =
|
||||||
|
Rectangle(center, width, height)
|
||||||
|
|
||||||
|
override fun Collection<Rectangle<XY>>.wrapRectangles(): Rectangle<XY>? {
|
||||||
|
if (isEmpty()) return null
|
||||||
|
val minX = minOf { it.left }
|
||||||
|
val maxX = maxOf { it.right }
|
||||||
|
|
||||||
|
val minY = minOf { it.bottom }
|
||||||
|
val maxY = maxOf { it.top }
|
||||||
|
return XYRectangle(
|
||||||
|
XY(minX, minY),
|
||||||
|
XY(maxX, maxY)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun Collection<XY>.wrapPoints(): Rectangle<XY>? {
|
||||||
|
if (isEmpty()) return null
|
||||||
|
val minX = minOf { it.x }
|
||||||
|
val maxX = maxOf { it.x }
|
||||||
|
|
||||||
|
val minY = minOf { it.y }
|
||||||
|
val maxY = maxOf { it.y }
|
||||||
|
return XYRectangle(
|
||||||
|
XY(minX, minY),
|
||||||
|
XY(maxX, maxY)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package center.sciprog.maps.scheme
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
import center.sciprog.maps.features.*
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class XYViewState(
|
||||||
|
config: ViewConfig<XY>,
|
||||||
|
canvasSize: DpSize,
|
||||||
|
viewPoint: ViewPoint<XY>,
|
||||||
|
) : CoordinateViewState<XY>(config, canvasSize, viewPoint) {
|
||||||
|
override val space: CoordinateSpace<XY>
|
||||||
|
get() = XYCoordinateSpace
|
||||||
|
|
||||||
|
override fun DpOffset.toCoordinates(): XY = XY(
|
||||||
|
(x - canvasSize.width / 2).value / viewPoint.zoom + viewPoint.focus.x,
|
||||||
|
(canvasSize.height / 2 - y).value / viewPoint.zoom + viewPoint.focus.y
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun XY.toDpOffset(): DpOffset = DpOffset(
|
||||||
|
(canvasSize.width / 2 + (x.dp - viewPoint.focus.x.dp) * viewPoint.zoom),
|
||||||
|
(canvasSize.height / 2 + (viewPoint.focus.y.dp - y.dp) * viewPoint.zoom)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun viewPointFor(rectangle: Rectangle<XY>): ViewPoint<XY> {
|
||||||
|
val scale = min(
|
||||||
|
canvasSize.width.value / rectangle.width,
|
||||||
|
canvasSize.height.value / rectangle.height
|
||||||
|
)
|
||||||
|
|
||||||
|
return XYViewPoint(rectangle.center, scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ViewPoint<XY>.moveBy(x: Dp, y: Dp): ViewPoint<XY> {
|
||||||
|
val newCoordinates = XY(focus.x + x.value / zoom, focus.y + y.value / zoom)
|
||||||
|
return XYViewPoint(newCoordinates, zoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun Rectangle<XY>.toDpRect(): DpRect {
|
||||||
|
val topLeft = leftTop.toDpOffset()
|
||||||
|
val bottomRight = rightBottom.toDpOffset()
|
||||||
|
return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,252 +1,64 @@
|
|||||||
package center.sciprog.maps.scheme
|
package center.sciprog.maps.scheme
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.gestures.drag
|
|
||||||
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.Composable
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
import androidx.compose.ui.graphics.drawscope.*
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.input.pointer.*
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import center.sciprog.maps.compose.mapControls
|
||||||
|
import center.sciprog.maps.features.*
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.jetbrains.skia.Font
|
|
||||||
import org.jetbrains.skia.Paint
|
|
||||||
import kotlin.math.PI
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
internal fun Color.toSkiaPaint(): Paint = Paint().apply {
|
|
||||||
isAntiAlias = true
|
|
||||||
color = toArgb()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun IntRange.intersect(other: IntRange) = max(first, other.first)..min(last, other.last)
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger("SchemeView")
|
private val logger = KotlinLogging.logger("SchemeView")
|
||||||
|
|
||||||
data class SchemeViewConfig(
|
|
||||||
val zoomSpeed: Float = 1f / 3f,
|
|
||||||
val onClick: SchemeViewPoint.() -> Unit = {},
|
|
||||||
val onViewChange: SchemeViewPoint.() -> Unit = {},
|
|
||||||
val onSelect: (SchemeRectangle) -> Unit = {},
|
|
||||||
val zoomOnSelect: Boolean = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
public fun SchemeView(
|
public fun SchemeView(
|
||||||
initialViewPoint: SchemeViewPoint,
|
initialViewPoint: ViewPoint<XY>,
|
||||||
featuresState: SchemeFeaturesState,
|
featuresState: FeaturesState<XY>,
|
||||||
config: SchemeViewConfig = SchemeViewConfig(),
|
config: ViewConfig<XY>,
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
) = key(initialViewPoint) {
|
) = key(initialViewPoint) {
|
||||||
|
|
||||||
var canvasSize by remember { mutableStateOf(defaultCanvasSize) }
|
val state = remember {
|
||||||
|
XYViewState(
|
||||||
var viewPoint by remember { mutableStateOf(initialViewPoint) }
|
config,
|
||||||
|
defaultCanvasSize,
|
||||||
|
initialViewPoint,
|
||||||
fun setViewPoint(newViewPoint: SchemeViewPoint) {
|
|
||||||
config.onViewChange(newViewPoint)
|
|
||||||
viewPoint = newViewPoint
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun DpOffset.toCoordinates(): SchemeCoordinates = SchemeCoordinates(
|
|
||||||
(x - canvasSize.width / 2).value / viewPoint.scale + viewPoint.focus.x,
|
|
||||||
(canvasSize.height / 2 - y).value / viewPoint.scale + viewPoint.focus.y
|
|
||||||
)
|
|
||||||
|
|
||||||
// Selection rectangle. If null - no selection
|
|
||||||
var selectRect by remember { mutableStateOf<Rect?>(null) }
|
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
val canvasModifier = modifier.pointerInput(Unit) {
|
|
||||||
forEachGesture {
|
|
||||||
awaitPointerEventScope {
|
|
||||||
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
|
|
||||||
|
|
||||||
val event: PointerEvent = awaitPointerEvent()
|
|
||||||
event.changes.forEach { change ->
|
|
||||||
if (event.buttons.isPrimaryPressed) {
|
|
||||||
//Evaluating selection frame
|
|
||||||
if (event.keyboardModifiers.isShiftPressed) {
|
|
||||||
selectRect = Rect(change.position, change.position)
|
|
||||||
drag(change.id) { dragChange ->
|
|
||||||
selectRect?.let { rect ->
|
|
||||||
val offset = dragChange.position
|
|
||||||
selectRect = Rect(
|
|
||||||
min(offset.x, rect.left),
|
|
||||||
min(offset.y, rect.top),
|
|
||||||
max(offset.x, rect.right),
|
|
||||||
max(offset.y, rect.bottom)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
with(state) {
|
||||||
selectRect?.let { rect ->
|
val painterCache: Map<PainterFeature<XY>, Painter> = key(featuresState) {
|
||||||
//Use selection override if it is defined
|
featuresState.features.values.filterIsInstance<PainterFeature<XY>>().associateWith { it.painter() }
|
||||||
val box = SchemeRectangle(
|
|
||||||
rect.topLeft.toDpOffset().toCoordinates(),
|
|
||||||
rect.bottomRight.toDpOffset().toCoordinates()
|
|
||||||
)
|
|
||||||
config.onSelect(box)
|
|
||||||
if (config.zoomOnSelect) {
|
|
||||||
val newScale = min(
|
|
||||||
canvasSize.width.value / box.width,
|
|
||||||
canvasSize.height.value / box.height
|
|
||||||
)
|
|
||||||
|
|
||||||
val newViewPoint = SchemeViewPoint(box.center, newScale)
|
|
||||||
|
|
||||||
setViewPoint(newViewPoint)
|
|
||||||
}
|
|
||||||
selectRect = null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val dragStart = change.position
|
|
||||||
val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
|
|
||||||
config.onClick(SchemeViewPoint(dpPos.toCoordinates(), viewPoint.scale))
|
|
||||||
drag(change.id) { dragChange ->
|
|
||||||
val dragAmount = dragChange.position - dragChange.previousPosition
|
|
||||||
val newViewPoint = viewPoint.move(
|
|
||||||
-dragAmount.x.toDp().value / viewPoint.scale,
|
|
||||||
dragAmount.y.toDp().value / viewPoint.scale
|
|
||||||
)
|
|
||||||
setViewPoint(newViewPoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onPointerEvent(PointerEventType.Scroll) {
|
|
||||||
val change = it.changes.first()
|
|
||||||
val (xPos, yPos) = change.position
|
|
||||||
//compute invariant point of translation
|
|
||||||
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
|
|
||||||
val newViewPoint = viewPoint.zoom(-change.scrollDelta.y * config.zoomSpeed, invariant)
|
|
||||||
setViewPoint(newViewPoint)
|
|
||||||
}.fillMaxSize()
|
|
||||||
|
|
||||||
val painterCache = key(featuresState) {
|
|
||||||
featuresState.features().values.filterIsInstance<PainterFeature>().associateWith { it.painter() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas(canvasModifier) {
|
Canvas(modifier = modifier.mapControls(state).fillMaxSize()) {
|
||||||
fun SchemeCoordinates.toOffset(): Offset = Offset(
|
|
||||||
(canvasSize.width / 2 + (x.dp - viewPoint.focus.x.dp) * viewPoint.scale).toPx(),
|
|
||||||
(canvasSize.height / 2 + (viewPoint.focus.y.dp - y.dp) * viewPoint.scale).toPx()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
fun DrawScope.drawFeature(scale: Float, feature: SchemeFeature) {
|
|
||||||
when (feature) {
|
|
||||||
is SchemeBackgroundFeature -> {
|
|
||||||
val offset = SchemeCoordinates(feature.rectangle.left, feature.rectangle.top).toOffset()
|
|
||||||
|
|
||||||
val backgroundSize = DpSize(
|
|
||||||
(feature.rectangle.width * scale).dp,
|
|
||||||
(feature.rectangle.height * scale).dp
|
|
||||||
).toSize()
|
|
||||||
|
|
||||||
translate(offset.x, offset.y) {
|
|
||||||
with(painterCache[feature]!!) {
|
|
||||||
draw(backgroundSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is SchemeFeatureSelector -> drawFeature(scale, feature.selector(scale))
|
|
||||||
is SchemeCircleFeature -> drawCircle(
|
|
||||||
feature.color,
|
|
||||||
feature.size,
|
|
||||||
center = feature.center.toOffset()
|
|
||||||
)
|
|
||||||
|
|
||||||
is SchemeLineFeature -> drawLine(
|
|
||||||
feature.color,
|
|
||||||
feature.a.toOffset(),
|
|
||||||
feature.b.toOffset(),
|
|
||||||
)
|
|
||||||
|
|
||||||
is SchemeArcFeature -> {
|
|
||||||
val topLeft = feature.oval.leftTop.toOffset()
|
|
||||||
val bottomRight = feature.oval.rightBottom.toOffset()
|
|
||||||
|
|
||||||
val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
|
|
||||||
|
|
||||||
drawArc(
|
|
||||||
color = feature.color,
|
|
||||||
startAngle = (feature.startAngle * 180 / PI).toFloat(),
|
|
||||||
sweepAngle = (feature.arcLength * 180 / PI).toFloat(),
|
|
||||||
useCenter = false,
|
|
||||||
topLeft = topLeft,
|
|
||||||
size = size,
|
|
||||||
style = Stroke()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is SchemeBitmapFeature -> drawImage(feature.image, feature.position.toOffset())
|
|
||||||
is SchemeImageFeature -> {
|
|
||||||
val offset = feature.position.toOffset()
|
|
||||||
val imageSize = feature.size.toSize()
|
|
||||||
translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
|
|
||||||
with(painterCache[feature]!!) {
|
|
||||||
draw(imageSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is SchemeTextFeature -> drawIntoCanvas { canvas ->
|
|
||||||
val offset = feature.position.toOffset()
|
|
||||||
canvas.nativeCanvas.drawString(
|
|
||||||
feature.text,
|
|
||||||
offset.x + 5,
|
|
||||||
offset.y - 5,
|
|
||||||
Font().apply { size = 16f },
|
|
||||||
feature.color.toSkiaPaint()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is SchemeDrawFeature -> {
|
|
||||||
val offset = feature.position.toOffset()
|
|
||||||
translate(offset.x, offset.y) {
|
|
||||||
feature.drawFeature(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is SchemeFeatureGroup -> {
|
|
||||||
feature.children.values.forEach {
|
|
||||||
drawFeature(scale, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canvasSize != size.toDpSize()) {
|
if (canvasSize != size.toDpSize()) {
|
||||||
canvasSize = size.toDpSize()
|
canvasSize = size.toDpSize()
|
||||||
logger.debug { "Recalculate canvas. Size: $size" }
|
logger.debug { "Recalculate canvas. Size: $size" }
|
||||||
}
|
}
|
||||||
|
|
||||||
clipRect {
|
clipRect {
|
||||||
featuresState.features().values.filterIsInstance<SchemeBackgroundFeature>().forEach { background ->
|
featuresState.features.values.filterIsInstance<ScalableImageFeature<XY>>().forEach { background ->
|
||||||
drawFeature(viewPoint.scale, background)
|
drawFeature(state, painterCache, background)
|
||||||
}
|
}
|
||||||
featuresState.features().values.filter {
|
featuresState.features.values.filter {
|
||||||
it !is SchemeBackgroundFeature && viewPoint.scale in it.scaleRange
|
it !is ScalableImageFeature && viewPoint.zoom in it.zoomRange
|
||||||
}.forEach { feature ->
|
}.forEach { feature ->
|
||||||
drawFeature(viewPoint.scale, feature)
|
drawFeature(state, painterCache, feature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectRect?.let { rect ->
|
selectRect?.let { dpRect ->
|
||||||
|
val rect = dpRect.toRect()
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color.Blue,
|
color = Color.Blue,
|
||||||
topLeft = rect.topLeft,
|
topLeft = rect.topLeft,
|
||||||
@ -261,36 +73,48 @@ public fun SchemeView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun Rectangle<XY>.computeViewPoint(
|
||||||
|
canvasSize: DpSize = defaultCanvasSize,
|
||||||
|
): ViewPoint<XY> {
|
||||||
|
val zoom = min(
|
||||||
|
canvasSize.width.value / width,
|
||||||
|
canvasSize.height.value / height
|
||||||
|
)
|
||||||
|
|
||||||
|
return XYViewPoint(center, zoom.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder for a Scheme with static features.
|
* A builder for a Scheme with static features.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
public fun SchemeView(
|
public fun SchemeView(
|
||||||
initialViewPoint: SchemeViewPoint? = null,
|
initialViewPoint: ViewPoint<XY>? = null,
|
||||||
initialRectangle: SchemeRectangle? = null,
|
initialRectangle: Rectangle<XY>? = null,
|
||||||
featureMap: Map<FeatureId, SchemeFeature>,
|
featureMap: Map<FeatureId<*>, Feature<XY>>,
|
||||||
config: SchemeViewConfig = SchemeViewConfig(),
|
config: ViewConfig<XY> = ViewConfig(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
val featuresState = key(featureMap) {
|
val featuresState = key(featureMap) {
|
||||||
SchemeFeaturesState.build {
|
FeaturesState.build(XYCoordinateSpace) {
|
||||||
featureMap.forEach(::addFeature)
|
featureMap.forEach { feature(it.key.id, it.value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val viewPointOverride: SchemeViewPoint = remember(initialViewPoint, initialRectangle) {
|
val viewPointOverride: ViewPoint<XY> = remember(initialViewPoint, initialRectangle) {
|
||||||
initialViewPoint
|
initialViewPoint
|
||||||
?: initialRectangle?.computeViewPoint()
|
?: initialRectangle?.computeViewPoint()
|
||||||
?: featureMap.values.computeBoundingBox(1f)?.computeViewPoint()
|
?: featureMap.values.computeBoundingBox(XYCoordinateSpace, 1f)?.computeViewPoint()
|
||||||
?: SchemeViewPoint(SchemeCoordinates(0f, 0f))
|
?: XYViewPoint(XY(0f, 0f), 1f)
|
||||||
}
|
}
|
||||||
|
|
||||||
SchemeView(viewPointOverride, featuresState, config, modifier)
|
SchemeView(viewPointOverride, featuresState, config, modifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw a map using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined,
|
* Draw a scheme using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined,
|
||||||
* use map features to infer view region.
|
* use map features to infer view region.
|
||||||
* @param initialViewPoint The view point of the map using center and zoom. Is used if provided
|
* @param initialViewPoint The view point of the map using center and zoom. Is used if provided
|
||||||
* @param initialRectangle The rectangle to be used for view point computation. Used if [initialViewPoint] is not defined.
|
* @param initialRectangle The rectangle to be used for view point computation. Used if [initialViewPoint] is not defined.
|
||||||
@ -298,45 +122,119 @@ public fun SchemeView(
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
public fun SchemeView(
|
public fun SchemeView(
|
||||||
initialViewPoint: SchemeViewPoint? = null,
|
initialViewPoint: ViewPoint<XY>? = null,
|
||||||
initialRectangle: SchemeRectangle? = null,
|
initialRectangle: Rectangle<XY>? = null,
|
||||||
config: SchemeViewConfig = SchemeViewConfig(),
|
config: ViewConfig<XY> = ViewConfig(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
buildFeatures: SchemeFeaturesState.() -> Unit = {},
|
buildFeatures: FeaturesState<XY>.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val featureState = SchemeFeaturesState.remember(buildFeatures)
|
val featureState = FeaturesState.remember(XYCoordinateSpace, buildFeatures)
|
||||||
|
|
||||||
val features = featureState.features()
|
val viewPointOverride: ViewPoint<XY> = remember(initialViewPoint, initialRectangle) {
|
||||||
|
|
||||||
val viewPointOverride: SchemeViewPoint = remember(initialViewPoint, initialRectangle) {
|
|
||||||
initialViewPoint
|
initialViewPoint
|
||||||
?: initialRectangle?.computeViewPoint()
|
?: initialRectangle?.computeViewPoint()
|
||||||
?: features.values.computeBoundingBox(1f)?.computeViewPoint()
|
?: featureState.features.values.computeBoundingBox(XYCoordinateSpace, 1f)?.computeViewPoint()
|
||||||
?: SchemeViewPoint(SchemeCoordinates(0f, 0f))
|
?: XYViewPoint(XY(0f, 0f), 1f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// val featureDrag = DragHandle.withPrimaryButton { _, start, end ->
|
val featureDrag: DragHandle<XY> =
|
||||||
// val zoom = start.zoom
|
DragHandle.withPrimaryButton { event, start: ViewPoint<XY>, end: ViewPoint<XY> ->
|
||||||
// featureState.findAllWithAttribute(DraggableAttribute) { it }.forEach { id ->
|
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
|
||||||
// val feature = features[id] as? DraggableMapFeature ?: return@forEach
|
//TODO add safety
|
||||||
// val boundingBox = feature.getBoundingBox(zoom) ?: return@forEach
|
handle as DragHandle<XY>
|
||||||
// if (start.focus in boundingBox) {
|
if (!handle.handle(event, start, end)) return@withPrimaryButton false
|
||||||
// featureState.addFeature(id, feature.withCoordinates(end.focus))
|
}
|
||||||
// return@withPrimaryButton false
|
true
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
// return@withPrimaryButton true
|
|
||||||
// }
|
val newConfig = config.copy(
|
||||||
//
|
dragHandle = DragHandle.combine(featureDrag, config.dragHandle)
|
||||||
//
|
)
|
||||||
// val newConfig = config.copy(
|
|
||||||
// dragHandle = DragHandle.combine(featureDrag, config.dragHandle)
|
|
||||||
// )
|
|
||||||
|
|
||||||
SchemeView(
|
SchemeView(
|
||||||
initialViewPoint = viewPointOverride,
|
initialViewPoint = viewPointOverride,
|
||||||
featuresState = featureState,
|
featuresState = featureState,
|
||||||
config = config,
|
config = newConfig,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///**
|
||||||
|
// * A builder for a Scheme with static features.
|
||||||
|
// */
|
||||||
|
//@Composable
|
||||||
|
//public fun SchemeView(
|
||||||
|
// initialViewPoint: XYViewPoint? = null,
|
||||||
|
// initialRectangle: XYRectangle? = null,
|
||||||
|
// featureMap: Map<FeatureId<*>,>,
|
||||||
|
// config: SchemeViewConfig = SchemeViewConfig(),
|
||||||
|
// modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
|
//) {
|
||||||
|
// val featuresState = key(featureMap) {
|
||||||
|
// SchemeFeaturesState.build {
|
||||||
|
// featureMap.forEach(::addFeature)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// val viewPointOverride: XYViewPoint = remember(initialViewPoint, initialRectangle) {
|
||||||
|
// initialViewPoint
|
||||||
|
// ?: initialRectangle?.computeViewPoint()
|
||||||
|
// ?: featureMap.values.computeBoundingBox(1f)?.computeViewPoint()
|
||||||
|
// ?: XYViewPoint(XY(0f, 0f))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// SchemeView(viewPointOverride, featuresState, config, modifier)
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * Draw a map using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined,
|
||||||
|
// * use map features to infer view region.
|
||||||
|
// * @param initialViewPoint The view point of the map using center and zoom. Is used if provided
|
||||||
|
// * @param initialRectangle The rectangle to be used for view point computation. Used if [initialViewPoint] is not defined.
|
||||||
|
// * @param buildFeatures - a builder for features
|
||||||
|
// */
|
||||||
|
//@Composable
|
||||||
|
//public fun SchemeView(
|
||||||
|
// initialViewPoint: XYViewPoint? = null,
|
||||||
|
// initialRectangle: Rectangle<XY>? = null,
|
||||||
|
// config: ViewConfig<XY> = ViewConfig(),
|
||||||
|
// modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
|
// buildFeatures: FeaturesState<XY>.() -> Unit = {},
|
||||||
|
//) {
|
||||||
|
// val featureState = FeaturesState.remember(XYCoordinateSpace, buildFeatures)
|
||||||
|
//
|
||||||
|
// val features = featureState.features
|
||||||
|
//
|
||||||
|
// val viewPointOverride: XYViewPoint = remember(initialViewPoint, initialRectangle) {
|
||||||
|
// initialViewPoint
|
||||||
|
// ?: initialRectangle?.computeViewPoint()
|
||||||
|
// ?: features.values.computeBoundingBox(1f)?.computeViewPoint()
|
||||||
|
// ?: XYViewPoint(XY(0f, 0f))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//// val featureDrag = DragHandle.withPrimaryButton { _, start, end ->
|
||||||
|
//// val zoom = start.zoom
|
||||||
|
//// featureState.findAllWithAttribute(DraggableAttribute) { it }.forEach { id ->
|
||||||
|
//// val feature = features[id] as? DraggableMapFeature ?: return@forEach
|
||||||
|
//// val boundingBox = feature.getBoundingBox(zoom) ?: return@forEach
|
||||||
|
//// if (start.focus in boundingBox) {
|
||||||
|
//// featureState.addFeature(id, feature.withCoordinates(end.focus))
|
||||||
|
//// return@withPrimaryButton false
|
||||||
|
//// }
|
||||||
|
//// }
|
||||||
|
//// return@withPrimaryButton true
|
||||||
|
//// }
|
||||||
|
////
|
||||||
|
////
|
||||||
|
//// val newConfig = config.copy(
|
||||||
|
//// dragHandle = DragHandle.combine(featureDrag, config.dragHandle)
|
||||||
|
//// )
|
||||||
|
//
|
||||||
|
// SchemeView(
|
||||||
|
// initialViewPoint = viewPointOverride,
|
||||||
|
// featuresState = featureState,
|
||||||
|
// config = config,
|
||||||
|
// modifier = modifier,
|
||||||
|
// )
|
||||||
|
//}
|
||||||
|
@ -7,6 +7,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke
|
|||||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
import androidx.compose.ui.graphics.drawscope.translate
|
import androidx.compose.ui.graphics.drawscope.translate
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import center.sciprog.maps.features.*
|
||||||
import center.sciprog.maps.scheme.*
|
import center.sciprog.maps.scheme.*
|
||||||
import org.jfree.svg.SVGGraphics2D
|
import org.jfree.svg.SVGGraphics2D
|
||||||
import org.jfree.svg.SVGUtils
|
import org.jfree.svg.SVGUtils
|
||||||
@ -15,34 +16,34 @@ import kotlin.math.PI
|
|||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
|
||||||
class FeatureStateSnapshot(
|
class FeatureStateSnapshot<T : Any>(
|
||||||
val features: Map<FeatureId, SchemeFeature>,
|
val features: Map<FeatureId<*>, Feature<T>>,
|
||||||
val painterCache: Map<PainterFeature, Painter>,
|
val painterCache: Map<PainterFeature<T>, Painter>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SchemeFeaturesState.snapshot(): FeatureStateSnapshot =
|
fun <T: Any> FeaturesState<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
|
||||||
FeatureStateSnapshot(
|
features,
|
||||||
features(),
|
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.painter() }
|
||||||
features().values.filterIsInstance<PainterFeature>().associateWith { it.painter() })
|
)
|
||||||
|
|
||||||
fun FeatureStateSnapshot.generateSvg(
|
fun FeatureStateSnapshot<XY>.generateSvg(
|
||||||
viewPoint: SchemeViewPoint,
|
viewPoint: ViewPoint<XY>,
|
||||||
width: Double,
|
width: Double,
|
||||||
height: Double,
|
height: Double,
|
||||||
id: String? = null
|
id: String? = null,
|
||||||
): String {
|
): String {
|
||||||
|
|
||||||
fun SchemeCoordinates.toOffset(): Offset = Offset(
|
fun XY.toOffset(): Offset = Offset(
|
||||||
(width / 2 + (x - viewPoint.focus.x) * viewPoint.scale).toFloat(),
|
(width / 2 + (x - viewPoint.focus.x) * viewPoint.zoom).toFloat(),
|
||||||
(height / 2 + (viewPoint.focus.y - y) * viewPoint.scale).toFloat()
|
(height / 2 + (viewPoint.focus.y - y) * viewPoint.zoom).toFloat()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
fun SvgDrawScope.drawFeature(scale: Float, feature: SchemeFeature) {
|
fun SvgDrawScope.drawFeature(scale: Float, feature: Feature<XY>) {
|
||||||
when (feature) {
|
when (feature) {
|
||||||
is SchemeBackgroundFeature -> {
|
is ScalableImageFeature -> {
|
||||||
val offset = SchemeCoordinates(feature.rectangle.left, feature.rectangle.top).toOffset()
|
val offset = XY(feature.rectangle.left, feature.rectangle.top).toOffset()
|
||||||
val backgroundSize = Size(
|
val backgroundSize = Size(
|
||||||
(feature.rectangle.width * scale),
|
(feature.rectangle.width * scale),
|
||||||
(feature.rectangle.height * scale)
|
(feature.rectangle.height * scale)
|
||||||
@ -55,15 +56,17 @@ fun FeatureStateSnapshot.generateSvg(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is SchemeFeatureSelector -> drawFeature(scale, feature.selector(scale))
|
is FeatureSelector -> drawFeature(scale, feature.selector(scale))
|
||||||
is SchemeCircleFeature -> drawCircle(
|
|
||||||
|
is CircleFeature -> drawCircle(
|
||||||
feature.color,
|
feature.color,
|
||||||
feature.size,
|
feature.size.toPx(),
|
||||||
center = feature.center.toOffset()
|
center = feature.center.toOffset()
|
||||||
)
|
)
|
||||||
|
|
||||||
is SchemeLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
|
is LineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
|
||||||
is SchemeArcFeature -> {
|
|
||||||
|
is ArcFeature -> {
|
||||||
val topLeft = feature.oval.leftTop.toOffset()
|
val topLeft = feature.oval.leftTop.toOffset()
|
||||||
val bottomRight = feature.oval.rightBottom.toOffset()
|
val bottomRight = feature.oval.rightBottom.toOffset()
|
||||||
|
|
||||||
@ -80,9 +83,9 @@ fun FeatureStateSnapshot.generateSvg(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is SchemeBitmapFeature -> drawImage(feature.image, feature.position.toOffset())
|
is BitmapImageFeature -> drawImage(feature.image, feature.position.toOffset())
|
||||||
|
|
||||||
is SchemeImageFeature -> {
|
is VectorImageFeature -> {
|
||||||
val offset = feature.position.toOffset()
|
val offset = feature.position.toOffset()
|
||||||
val imageSize = feature.size.toSize()
|
val imageSize = feature.size.toSize()
|
||||||
translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
|
translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
|
||||||
@ -92,7 +95,7 @@ fun FeatureStateSnapshot.generateSvg(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is SchemeTextFeature -> drawIntoCanvas { canvas ->
|
is TextFeature -> drawIntoCanvas { canvas ->
|
||||||
val offset = feature.position.toOffset()
|
val offset = feature.position.toOffset()
|
||||||
drawText(
|
drawText(
|
||||||
feature.text,
|
feature.text,
|
||||||
@ -103,14 +106,14 @@ fun FeatureStateSnapshot.generateSvg(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is SchemeDrawFeature -> {
|
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 SchemeFeatureGroup -> {
|
is FeatureGroup -> {
|
||||||
feature.children.values.forEach {
|
feature.children.values.forEach {
|
||||||
drawFeature(scale, it)
|
drawFeature(scale, it)
|
||||||
}
|
}
|
||||||
@ -122,20 +125,20 @@ fun FeatureStateSnapshot.generateSvg(
|
|||||||
val svgScope = SvgDrawScope(svgGraphics2D, Size(width.toFloat(), height.toFloat()))
|
val svgScope = SvgDrawScope(svgGraphics2D, Size(width.toFloat(), height.toFloat()))
|
||||||
|
|
||||||
svgScope.apply {
|
svgScope.apply {
|
||||||
features.values.filterIsInstance<SchemeBackgroundFeature>().forEach { background ->
|
features.values.filterIsInstance<ScalableImageFeature<XY>>().forEach { background ->
|
||||||
drawFeature(viewPoint.scale, background)
|
drawFeature(viewPoint.zoom, background)
|
||||||
}
|
}
|
||||||
features.values.filter {
|
features.values.filter {
|
||||||
it !is SchemeBackgroundFeature && viewPoint.scale in it.scaleRange
|
it !is ScalableImageFeature && viewPoint.zoom in it.zoomRange
|
||||||
}.forEach { feature ->
|
}.forEach { feature ->
|
||||||
drawFeature(viewPoint.scale, feature)
|
drawFeature(viewPoint.zoom, feature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return svgGraphics2D.getSVGElement(id)
|
return svgGraphics2D.getSVGElement(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun FeatureStateSnapshot.exportToSvg(
|
fun FeatureStateSnapshot<XY>.exportToSvg(
|
||||||
viewPoint: SchemeViewPoint,
|
viewPoint: ViewPoint<XY>,
|
||||||
width: Double,
|
width: Double,
|
||||||
height: Double,
|
height: Double,
|
||||||
path: java.nio.file.Path,
|
path: java.nio.file.Path,
|
||||||
|
Loading…
Reference in New Issue
Block a user