Generalize feature draw logic
This commit is contained in:
parent
fb13fa1431
commit
5b95adc649
@ -2,10 +2,7 @@ package center.sciprog.maps.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.*
|
||||
import center.sciprog.maps.coordinates.*
|
||||
import center.sciprog.maps.features.*
|
||||
import kotlin.math.*
|
||||
@ -18,9 +15,6 @@ internal class MapState internal constructor(
|
||||
) : CoordinateViewState<Gmc>(config, canvasSize, viewPoint) {
|
||||
override val space: CoordinateSpace<Gmc> get() = GmcCoordinateSpace
|
||||
|
||||
public val zoom: Int
|
||||
get() = floor(viewPoint.zoom).toInt()
|
||||
|
||||
public val scaleFactor: Double
|
||||
get() = WebMercatorProjection.scaleFactor(viewPoint.zoom)
|
||||
|
||||
@ -31,7 +25,7 @@ internal class MapState internal constructor(
|
||||
get() = 2.0.pow(viewPoint.zoom - zoom)
|
||||
|
||||
private fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates(
|
||||
zoom,
|
||||
floor(zoom).toInt(),
|
||||
(x - canvasSize.width / 2).value / tileScale + centerCoordinates.x,
|
||||
(y - canvasSize.height / 2).value / tileScale + centerCoordinates.y,
|
||||
)
|
||||
@ -50,6 +44,12 @@ internal class MapState internal constructor(
|
||||
override fun Gmc.toDpOffset(): DpOffset =
|
||||
WebMercatorProjection.toMercator(this, zoom).toOffset()
|
||||
|
||||
override fun Rectangle<Gmc>.toDpRect(): DpRect {
|
||||
val topLeft = topLeft.toDpOffset()
|
||||
val bottomRight = bottomRight.toDpOffset()
|
||||
return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
|
||||
}
|
||||
|
||||
override fun viewPointFor(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> {
|
||||
val zoom = log2(
|
||||
min(
|
||||
|
@ -4,22 +4,21 @@ import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import androidx.compose.ui.graphics.vector.VectorPainter
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.coordinates.radians
|
||||
import center.sciprog.maps.coordinates.toFloat
|
||||
import center.sciprog.maps.features.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import mu.KotlinLogging
|
||||
import org.jetbrains.skia.Font
|
||||
import org.jetbrains.skia.Paint
|
||||
import kotlin.math.*
|
||||
|
||||
@ -51,8 +50,6 @@ public actual fun MapView(
|
||||
initialViewPoint,
|
||||
mapTileProvider.tileSize
|
||||
)
|
||||
val canvasModifier = modifier.mapControls(state).fillMaxSize()
|
||||
|
||||
with(state) {
|
||||
|
||||
val mapTiles = remember(mapTileProvider) { mutableStateListOf<MapTile>() }
|
||||
@ -74,7 +71,7 @@ public actual fun MapView(
|
||||
|
||||
for (j in verticalIndices) {
|
||||
for (i in horizontalIndices) {
|
||||
val id = TileId(zoom, i, j)
|
||||
val id = TileId(floor(zoom).toInt(), i, j)
|
||||
//ensure that failed tiles do not fail the application
|
||||
supervisorScope {
|
||||
//start all
|
||||
@ -96,110 +93,11 @@ public actual fun MapView(
|
||||
}
|
||||
}
|
||||
|
||||
val painterCache = key(featuresState) {
|
||||
val painterCache: Map<VectorImageFeature<Gmc>, VectorPainter> = key(featuresState) {
|
||||
featuresState.features.values.filterIsInstance<VectorImageFeature<Gmc>>().associateWith { it.painter() }
|
||||
}
|
||||
|
||||
Canvas(canvasModifier) {
|
||||
fun GeodeticMapCoordinates.toOffset(): Offset = toOffset(this@Canvas)
|
||||
|
||||
fun DrawScope.drawFeature(zoom: Int, feature: MapFeature) {
|
||||
when (feature) {
|
||||
is FeatureSelector -> drawFeature(zoom, feature.selector(zoom))
|
||||
is CircleFeature -> drawCircle(
|
||||
feature.color,
|
||||
feature.size.toPx(),
|
||||
center = feature.center.toOffset()
|
||||
)
|
||||
|
||||
is RectangleFeature -> drawRect(
|
||||
feature.color,
|
||||
topLeft = feature.center.toOffset() - Offset(
|
||||
feature.size.width.toPx() / 2,
|
||||
feature.size.height.toPx() / 2
|
||||
),
|
||||
size = feature.size.toSize()
|
||||
)
|
||||
|
||||
is LineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
|
||||
is ArcFeature -> {
|
||||
val topLeft = feature.oval.topLeft.toOffset()
|
||||
val bottomRight = feature.oval.bottomRight.toOffset()
|
||||
|
||||
val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
|
||||
|
||||
drawArc(
|
||||
color = feature.color,
|
||||
startAngle = feature.startAngle.radians.degrees.toFloat(),
|
||||
sweepAngle = feature.arcLength.radians.degrees.toFloat(),
|
||||
useCenter = false,
|
||||
topLeft = topLeft,
|
||||
size = size,
|
||||
style = Stroke()
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
is BitmapImageFeature -> drawImage(feature.image, feature.position.toOffset())
|
||||
|
||||
is VectorImageFeature -> {
|
||||
val offset = feature.position.toOffset()
|
||||
val size = feature.size.toSize()
|
||||
translate(offset.x - size.width / 2, offset.y - size.height / 2) {
|
||||
with(painterCache[feature]!!) {
|
||||
draw(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TextFeature -> drawIntoCanvas { canvas ->
|
||||
val offset = feature.position.toOffset()
|
||||
canvas.nativeCanvas.drawString(
|
||||
feature.text,
|
||||
offset.x + 5,
|
||||
offset.y - 5,
|
||||
Font().apply(feature.fontConfig),
|
||||
feature.color.toPaint()
|
||||
)
|
||||
}
|
||||
|
||||
is DrawFeature -> {
|
||||
val offset = feature.position.toOffset()
|
||||
translate(offset.x, offset.y) {
|
||||
feature.drawFeature(this)
|
||||
}
|
||||
}
|
||||
|
||||
is FeatureGroup -> {
|
||||
feature.children.values.forEach {
|
||||
drawFeature(zoom, it)
|
||||
}
|
||||
}
|
||||
|
||||
is PathFeature -> {
|
||||
TODO("MapPathFeature not implemented")
|
||||
// val offset = feature.rectangle.center.toOffset() - feature.targetRect.center
|
||||
// translate(offset.x, offset.y) {
|
||||
// sca
|
||||
// drawPath(feature.path, brush = feature.brush, style = feature.style)
|
||||
// }
|
||||
}
|
||||
|
||||
is PointsFeature -> {
|
||||
val points = feature.points.map { it.toOffset() }
|
||||
drawPoints(
|
||||
points = points,
|
||||
color = feature.color,
|
||||
strokeWidth = feature.stroke,
|
||||
pointMode = feature.pointMode
|
||||
)
|
||||
}
|
||||
|
||||
// else -> {
|
||||
// logger.error { "Unrecognized feature type: ${feature::class}" }
|
||||
// }
|
||||
}
|
||||
}
|
||||
Canvas(modifier = modifier.mapControls(state).fillMaxSize()) {
|
||||
|
||||
if (canvasSize != size.toDpSize()) {
|
||||
logger.debug { "Recalculate canvas. Size: $size" }
|
||||
@ -226,7 +124,7 @@ public actual fun MapView(
|
||||
}
|
||||
|
||||
featuresState.features.values.filter { viewPoint.zoom in it.zoomRange }.forEach { feature ->
|
||||
drawFeature(zoom, feature)
|
||||
drawFeature(state, painterCache, feature)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,12 +26,12 @@ public object WebMercatorProjection {
|
||||
/**
|
||||
* https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas
|
||||
*/
|
||||
public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Int): WebMercatorCoordinates {
|
||||
public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Double): WebMercatorCoordinates {
|
||||
require(abs(gmc.latitude) <= MercatorProjection.MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" }
|
||||
|
||||
val scaleFactor = scaleFactor(zoom.toDouble())
|
||||
val scaleFactor = scaleFactor(zoom)
|
||||
return WebMercatorCoordinates(
|
||||
zoom = zoom,
|
||||
zoom = floor(zoom).toInt(),
|
||||
x = scaleFactor * (gmc.longitude.radians.value + PI),
|
||||
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians.value / 2)))
|
||||
)
|
||||
|
@ -0,0 +1,52 @@
|
||||
package center.sciprog.maps.features
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.unit.*
|
||||
|
||||
public abstract class CoordinateViewState<T : Any>(
|
||||
public val config: ViewConfig<T>,
|
||||
canvasSize: DpSize,
|
||||
viewPoint: ViewPoint<T>,
|
||||
) {
|
||||
|
||||
public abstract val space: CoordinateSpace<T>
|
||||
|
||||
public var canvasSize: DpSize by mutableStateOf(canvasSize)
|
||||
protected var viewPointState: MutableState<ViewPoint<T>> = mutableStateOf(viewPoint)
|
||||
|
||||
public var viewPoint: ViewPoint<T>
|
||||
get() = viewPointState.value
|
||||
set(value) {
|
||||
config.onViewChange(value)
|
||||
viewPointState.value = value
|
||||
}
|
||||
|
||||
public val zoom: Double get() = viewPoint.zoom
|
||||
|
||||
public abstract fun DpOffset.toCoordinates(): T
|
||||
|
||||
public abstract fun T.toDpOffset(): DpOffset
|
||||
|
||||
public fun T.toOffset(density: Density): Offset = with(density){
|
||||
val dpOffset = this@toOffset.toDpOffset()
|
||||
Offset(dpOffset.x.toPx(), dpOffset.y.toPx())
|
||||
}
|
||||
|
||||
public abstract fun Rectangle<T>.toDpRect(): DpRect
|
||||
|
||||
public abstract fun ViewPoint<T>.moveBy(x: Dp, y: Dp): ViewPoint<T>
|
||||
|
||||
public abstract fun viewPointFor(rectangle: Rectangle<T>): ViewPoint<T>
|
||||
|
||||
// Selection rectangle. If null - no selection
|
||||
public var selectRect: DpRect? by mutableStateOf(null)
|
||||
}
|
||||
|
||||
|
||||
public val DpRect.topLeft: DpOffset get() = DpOffset(left, top)
|
||||
|
||||
public val DpRect.bottomRight: DpOffset get() = DpOffset(right, bottom)
|
@ -13,7 +13,6 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.features.Feature.Companion.defaultZoomRange
|
||||
import kotlin.math.floor
|
||||
|
||||
public typealias DoubleRange = ClosedFloatingPointRange<Double>
|
||||
|
||||
@ -60,11 +59,11 @@ public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
|
||||
public class FeatureSelector<T : Any>(
|
||||
override val space: CoordinateSpace<T>,
|
||||
override var attributes: AttributeMap = AttributeMap(),
|
||||
public val selector: (zoom: Int) -> Feature<T>,
|
||||
public val selector: (zoom: Double) -> Feature<T>,
|
||||
) : Feature<T> {
|
||||
override val zoomRange: ClosedFloatingPointRange<Double> get() = defaultZoomRange
|
||||
|
||||
override fun getBoundingBox(zoom: Double): Rectangle<T>? = selector(floor(zoom).toInt()).getBoundingBox(zoom)
|
||||
override fun getBoundingBox(zoom: Double): Rectangle<T>? = selector(zoom).getBoundingBox(zoom)
|
||||
}
|
||||
|
||||
public class PathFeature<T : Any>(
|
||||
|
@ -0,0 +1,124 @@
|
||||
package center.sciprog.maps.features
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.graphics.vector.VectorPainter
|
||||
import org.jetbrains.skia.Font
|
||||
import org.jetbrains.skia.Paint
|
||||
import kotlin.math.PI
|
||||
|
||||
|
||||
internal fun Color.toPaint(): Paint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = toArgb()
|
||||
}
|
||||
|
||||
public fun <T : Any> DrawScope.drawFeature(
|
||||
state: CoordinateViewState<T>,
|
||||
painterCache: Map<VectorImageFeature<T>, VectorPainter>,
|
||||
feature: Feature<T>,
|
||||
): Unit = with(state) {
|
||||
fun T.toOffset(): Offset = toOffset(this@drawFeature)
|
||||
|
||||
when (feature) {
|
||||
is FeatureSelector -> drawFeature(state, painterCache, feature.selector(state.zoom))
|
||||
is CircleFeature -> drawCircle(
|
||||
feature.color,
|
||||
feature.size.toPx(),
|
||||
center = feature.center.toOffset()
|
||||
)
|
||||
|
||||
is RectangleFeature -> drawRect(
|
||||
feature.color,
|
||||
topLeft = feature.center.toOffset() - Offset(
|
||||
feature.size.width.toPx() / 2,
|
||||
feature.size.height.toPx() / 2
|
||||
),
|
||||
size = feature.size.toSize()
|
||||
)
|
||||
|
||||
is LineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
|
||||
is ArcFeature -> {
|
||||
val dpRect = feature.oval.toDpRect().toRect()
|
||||
|
||||
val size = Size(dpRect.width, dpRect.height)
|
||||
|
||||
drawArc(
|
||||
color = feature.color,
|
||||
startAngle = feature.startAngle / PI.toFloat() * 180f,
|
||||
sweepAngle = feature.arcLength / PI.toFloat() * 180f,
|
||||
useCenter = false,
|
||||
topLeft = dpRect.topLeft,
|
||||
size = size,
|
||||
style = Stroke()
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
is BitmapImageFeature -> drawImage(feature.image, feature.position.toOffset())
|
||||
|
||||
is VectorImageFeature -> {
|
||||
val offset = feature.position.toOffset()
|
||||
val size = feature.size.toSize()
|
||||
translate(offset.x - size.width / 2, offset.y - size.height / 2) {
|
||||
with(painterCache[feature]!!) {
|
||||
draw(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TextFeature -> drawIntoCanvas { canvas ->
|
||||
val offset = feature.position.toOffset()
|
||||
canvas.nativeCanvas.drawString(
|
||||
feature.text,
|
||||
offset.x + 5,
|
||||
offset.y - 5,
|
||||
Font().apply(feature.fontConfig),
|
||||
feature.color.toPaint()
|
||||
)
|
||||
}
|
||||
|
||||
is DrawFeature -> {
|
||||
val offset = feature.position.toOffset()
|
||||
translate(offset.x, offset.y) {
|
||||
feature.drawFeature(this)
|
||||
}
|
||||
}
|
||||
|
||||
is FeatureGroup -> {
|
||||
feature.children.values.forEach {
|
||||
drawFeature(state, painterCache, it)
|
||||
}
|
||||
}
|
||||
|
||||
is PathFeature -> {
|
||||
TODO("MapPathFeature not implemented")
|
||||
// val offset = feature.rectangle.center.toOffset() - feature.targetRect.center
|
||||
// translate(offset.x, offset.y) {
|
||||
// sca
|
||||
// drawPath(feature.path, brush = feature.brush, style = feature.style)
|
||||
// }
|
||||
}
|
||||
|
||||
is PointsFeature -> {
|
||||
val points = feature.points.map { it.toOffset() }
|
||||
drawPoints(
|
||||
points = points,
|
||||
color = feature.color,
|
||||
strokeWidth = feature.stroke,
|
||||
pointMode = feature.pointMode
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
//logger.error { "Unrecognized feature type: ${feature::class}" }
|
||||
}
|
||||
}
|
||||
}
|
@ -12,45 +12,6 @@ import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
public abstract class CoordinateViewState<T : Any>(
|
||||
public val config: ViewConfig<T>,
|
||||
canvasSize: DpSize,
|
||||
viewPoint: ViewPoint<T>,
|
||||
) {
|
||||
|
||||
public abstract val space: CoordinateSpace<T>
|
||||
|
||||
public var canvasSize: DpSize by mutableStateOf(canvasSize)
|
||||
protected var viewPointState: MutableState<ViewPoint<T>> = mutableStateOf(viewPoint)
|
||||
|
||||
public var viewPoint: ViewPoint<T>
|
||||
get() = viewPointState.value
|
||||
set(value) {
|
||||
config.onViewChange(value)
|
||||
viewPointState.value = value
|
||||
}
|
||||
|
||||
public abstract fun DpOffset.toCoordinates(): T
|
||||
|
||||
public abstract fun T.toDpOffset(): DpOffset
|
||||
|
||||
public fun T.toOffset(density: Density): Offset = with(density){
|
||||
val dpOffset = this@toOffset.toDpOffset()
|
||||
Offset(dpOffset.x.toPx(), dpOffset.y.toPx())
|
||||
}
|
||||
|
||||
public abstract fun ViewPoint<T>.moveBy(x: Dp, y: Dp): ViewPoint<T>
|
||||
|
||||
public abstract fun viewPointFor(rectangle: Rectangle<T>): ViewPoint<T>
|
||||
|
||||
// Selection rectangle. If null - no selection
|
||||
public var selectRect: DpRect? by mutableStateOf<DpRect?>(null)
|
||||
}
|
||||
|
||||
public val DpRect.topLeft: DpOffset get() = DpOffset(left, top)
|
||||
public val DpRect.bottomRight: DpOffset get() = DpOffset(right, bottom)
|
||||
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
public fun <T : Any> Modifier.mapControls(
|
||||
state: CoordinateViewState<T>,
|
||||
@ -64,7 +25,7 @@ public fun <T : Any> Modifier.mapControls(
|
||||
|
||||
event.changes.forEach { change ->
|
||||
val dragStart = change.position
|
||||
val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
|
||||
//val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
|
||||
|
||||
//start selection
|
||||
val selectionStart: Offset? =
|
||||
|
Loading…
Reference in New Issue
Block a user