Update schemes to match Maps features

This commit is contained in:
Alexander Nozik 2022-09-17 10:32:28 +03:00
parent 6decba8e83
commit 409d749054
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
7 changed files with 256 additions and 130 deletions

View File

@ -10,7 +10,7 @@ val ktorVersion by extra("2.0.3")
allprojects { allprojects {
group = "center.sciprog" group = "center.sciprog"
version = "0.1.0-dev-7" version = "0.1.0-dev-8"
} }
ksciencePublish{ ksciencePublish{

View File

@ -2,7 +2,6 @@
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@ -17,26 +16,17 @@ import kotlinx.coroutines.launch
@Preview @Preview
fun App() { fun App() {
MaterialTheme { MaterialTheme {
//create a view point
val viewPoint = remember {
SchemeViewPoint(
SchemeCoordinates(0f, 0f),
1f
)
}
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
SchemeView( SchemeView(
viewPoint,
config = SchemeViewConfig( config = SchemeViewConfig(
inferViewBoxFromFeatures = true,
onClick = { onClick = {
println("${focus.x}, ${focus.y}") println("${focus.x}, ${focus.y}")
} }
) )
) { ) {
background(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)
circle(1132.0881 to 394.99127, color = Color.Red) circle(1132.0881 to 394.99127, color = Color.Red)

View File

@ -216,8 +216,9 @@ public actual fun MapView(
} }
} }
val painterCache = featuresState.features().values.filterIsInstance<MapVectorImageFeature>() val painterCache = key(featuresState) {
.associateWith { it.painter() } featuresState.features().values.filterIsInstance<MapVectorImageFeature>().associateWith { it.painter() }
}
Canvas(canvasModifier) { Canvas(canvasModifier) {
fun WebMercatorCoordinates.toOffset(): Offset = Offset( fun WebMercatorCoordinates.toOffset(): Offset = Offset(

View File

@ -1,37 +1,54 @@
package center.sciprog.maps.scheme package center.sciprog.maps.scheme
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
data class SchemeCoordinates(val x: Float, val y: Float) data class SchemeCoordinates(val x: Float, val y: Float)
data class SchemeCoordinateBox( data class SchemeRectangle(
val a: SchemeCoordinates, val a: SchemeCoordinates,
val b: SchemeCoordinates, val b: SchemeCoordinates,
) )
val SchemeCoordinateBox.top get() = max(a.y, b.y) val SchemeRectangle.top get() = max(a.y, b.y)
val SchemeCoordinateBox.bottom get() = min(a.y, b.y) val SchemeRectangle.bottom get() = min(a.y, b.y)
val SchemeCoordinateBox.right get() = max(a.x, b.x) val SchemeRectangle.right get() = max(a.x, b.x)
val SchemeCoordinateBox.left get() = min(a.x, b.x) val SchemeRectangle.left get() = min(a.x, b.x)
val SchemeCoordinateBox.width get() = abs(a.x - b.x) val SchemeRectangle.width get() = abs(a.x - b.x)
val SchemeCoordinateBox.height get() = abs(a.y - b.y) val SchemeRectangle.height get() = abs(a.y - b.y)
val SchemeCoordinateBox.center get() = SchemeCoordinates((a.x + b.x) / 2, (a.y + b.y) / 2) val SchemeRectangle.center get() = SchemeCoordinates((a.x + b.x) / 2, (a.y + b.y) / 2)
public val SchemeRectangle.topLeft: SchemeCoordinates get() = SchemeCoordinates(top, left)
public val SchemeRectangle.bottomRight: SchemeCoordinates get() = SchemeCoordinates(bottom, right)
fun Collection<SchemeCoordinateBox>.wrapAll(): SchemeCoordinateBox? { fun Collection<SchemeRectangle>.wrapAll(): SchemeRectangle? {
if (isEmpty()) return null if (isEmpty()) return null
val minX = minOf { it.left } val minX = minOf { it.left }
val maxX = maxOf { it.right } val maxX = maxOf { it.right }
val minY = minOf { it.bottom } val minY = minOf { it.bottom }
val maxY = maxOf { it.top } val maxY = maxOf { it.top }
return SchemeCoordinateBox( return SchemeRectangle(
SchemeCoordinates(minX, minY), SchemeCoordinates(minX, minY),
SchemeCoordinates(maxX, maxY) 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)
} }

View File

@ -15,7 +15,7 @@ import center.sciprog.maps.scheme.SchemeFeature.Companion.defaultScaleRange
internal typealias FloatRange = ClosedFloatingPointRange<Float> internal typealias FloatRange = ClosedFloatingPointRange<Float>
sealed class SchemeFeature(val scaleRange: FloatRange) { sealed class SchemeFeature(val scaleRange: FloatRange) {
abstract fun getBoundingBox(scale: Float): SchemeCoordinateBox? abstract fun getBoundingBox(scale: Float): SchemeRectangle?
companion object { companion object {
val defaultScaleRange = 0f..Float.MAX_VALUE val defaultScaleRange = 0f..Float.MAX_VALUE
@ -23,27 +23,31 @@ sealed class SchemeFeature(val scaleRange: FloatRange) {
} }
fun Iterable<SchemeFeature>.computeBoundingBox(scale: Float): SchemeCoordinateBox? = fun Iterable<SchemeFeature>.computeBoundingBox(scale: Float): SchemeRectangle? =
mapNotNull { it.getBoundingBox(scale) }.wrapAll() mapNotNull { it.getBoundingBox(scale) }.wrapAll()
internal fun Pair<Number, Number>.toCoordinates() = SchemeCoordinates(first.toFloat(), second.toFloat()) 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 * A background image that is bound to scheme coordinates and is scaled together with them
* *
* @param position the size of background in scheme size units. The screen units to scheme units ratio equals scale. * @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale.
*/ */
class SchemeBackgroundFeature( class SchemeBackgroundFeature(
val position: SchemeCoordinateBox, val rectangle: SchemeRectangle,
val painter: Painter,
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
) : SchemeFeature(scaleRange) { override val painter: @Composable () -> Painter,
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = position ) : SchemeFeature(scaleRange), PainterFeature {
override fun getBoundingBox(scale: Float): SchemeRectangle = rectangle
} }
class SchemeFeatureSelector(val selector: (scale: Float) -> SchemeFeature) : SchemeFeature(defaultScaleRange) { class SchemeFeatureSelector(val selector: (scale: Float) -> SchemeFeature) : SchemeFeature(defaultScaleRange) {
override fun getBoundingBox(scale: Float): SchemeCoordinateBox? = selector(scale).getBoundingBox(scale) override fun getBoundingBox(scale: Float): SchemeRectangle? = selector(scale).getBoundingBox(scale)
} }
class SchemeDrawFeature( class SchemeDrawFeature(
@ -51,7 +55,7 @@ class SchemeDrawFeature(
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
val drawFeature: DrawScope.() -> Unit, val drawFeature: DrawScope.() -> Unit,
) : SchemeFeature(scaleRange) { ) : SchemeFeature(scaleRange) {
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position) override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position)
} }
class SchemeCircleFeature( class SchemeCircleFeature(
@ -60,7 +64,7 @@ class SchemeCircleFeature(
val size: Float = 5f, val size: Float = 5f,
val color: Color = Color.Red, val color: Color = Color.Red,
) : SchemeFeature(scaleRange) { ) : SchemeFeature(scaleRange) {
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(center, center) override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(center, center)
} }
class SchemeLineFeature( class SchemeLineFeature(
@ -69,7 +73,21 @@ class SchemeLineFeature(
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
val color: Color = Color.Red, val color: Color = Color.Red,
) : SchemeFeature(scaleRange) { ) : SchemeFeature(scaleRange) {
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(a, b) 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( class SchemeTextFeature(
@ -78,7 +96,7 @@ class SchemeTextFeature(
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
val color: Color = Color.Red, val color: Color = Color.Red,
) : SchemeFeature(scaleRange) { ) : SchemeFeature(scaleRange) {
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position) override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position)
} }
class SchemeBitmapFeature( class SchemeBitmapFeature(
@ -87,25 +105,24 @@ class SchemeBitmapFeature(
val size: IntSize = IntSize(15, 15), val size: IntSize = IntSize(15, 15),
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
) : SchemeFeature(scaleRange) { ) : SchemeFeature(scaleRange) {
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position) override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position)
} }
class SchemeImageFeature( class SchemeImageFeature(
val position: SchemeCoordinates, val position: SchemeCoordinates,
val painter: Painter,
val size: DpSize, val size: DpSize,
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
) : SchemeFeature(scaleRange) { override val painter: @Composable () -> Painter,
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position) ) : SchemeFeature(scaleRange), PainterFeature {
override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position)
} }
@Composable fun SchemeImageFeature(
fun SchemeVectorImageFeature(
position: SchemeCoordinates, position: SchemeCoordinates,
image: ImageVector, image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp), size: DpSize = DpSize(20.dp, 20.dp),
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
): SchemeImageFeature = SchemeImageFeature(position, rememberVectorPainter(image), size, scaleRange) ): SchemeImageFeature = SchemeImageFeature(position, size, scaleRange) { rememberVectorPainter(image) }
/** /**
* A group of other features * A group of other features
@ -114,6 +131,6 @@ class SchemeFeatureGroup(
val children: Map<FeatureId, SchemeFeature>, val children: Map<FeatureId, SchemeFeature>,
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
) : SchemeFeature(scaleRange) { ) : SchemeFeature(scaleRange) {
override fun getBoundingBox(scale: Float): SchemeCoordinateBox? = override fun getBoundingBox(scale: Float): SchemeRectangle? =
children.values.mapNotNull { it.getBoundingBox(scale) }.wrapAll() children.values.mapNotNull { it.getBoundingBox(scale) }.wrapAll()
} }

View File

@ -3,7 +3,6 @@ package center.sciprog.maps.scheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.geometry.Size
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
@ -14,55 +13,88 @@ import center.sciprog.maps.scheme.SchemeFeature.Companion.defaultScaleRange
typealias FeatureId = String typealias FeatureId = String
interface SchemeFeatureBuilder {
fun addFeature(id: FeatureId?, feature: SchemeFeature): FeatureId
fun build(): SnapshotStateMap<FeatureId, SchemeFeature> 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>
internal class SchemeFeatureBuilderImpl( public fun features(): Map<FeatureId, SchemeFeature> = features
initialFeatures: Map<FeatureId, SchemeFeature>,
) : SchemeFeatureBuilder {
private val content: SnapshotStateMap<FeatureId, SchemeFeature> =
mutableStateMapOf<FeatureId, SchemeFeature>().apply {
putAll(initialFeatures)
}
private fun generateID(feature: SchemeFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]" private fun generateID(feature: SchemeFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]"
override fun addFeature(id: FeatureId?, feature: SchemeFeature): FeatureId { public fun addFeature(id: FeatureId?, feature: SchemeFeature): FeatureId {
val safeId = id ?: generateID(feature) val safeId = id ?: generateID(feature)
content[id ?: generateID(feature)] = feature features[id ?: generateID(feature)] = feature
return safeId return safeId
} }
override fun build(): SnapshotStateMap<FeatureId, SchemeFeature> = content 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 SchemeFeatureBuilder.background( fun SchemeFeaturesState.background(
painter: Painter, box: SchemeRectangle,
box: SchemeCoordinateBox,
id: FeatureId? = null, id: FeatureId? = null,
painter: @Composable () -> Painter,
): FeatureId = addFeature( ): FeatureId = addFeature(
id, id,
SchemeBackgroundFeature(box, painter) SchemeBackgroundFeature(box, painter = painter)
) )
fun SchemeFeatureBuilder.background( fun SchemeFeaturesState.background(
painter: Painter, width: Float,
size: Size = painter.intrinsicSize, height: Float,
offset: SchemeCoordinates = SchemeCoordinates(0f, 0f), offset: SchemeCoordinates = SchemeCoordinates(0f, 0f),
id: FeatureId? = null, id: FeatureId? = null,
painter: @Composable () -> Painter,
): FeatureId { ): FeatureId {
val box = SchemeCoordinateBox( val box = SchemeRectangle(
offset, offset,
SchemeCoordinates(size.width + offset.x, size.height + offset.y) SchemeCoordinates(width + offset.x, height + offset.y)
) )
return background(painter, box, id) return background(box, id, painter = painter)
} }
fun SchemeFeatureBuilder.circle( fun SchemeFeaturesState.circle(
center: SchemeCoordinates, center: SchemeCoordinates,
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
size: Float = 5f, size: Float = 5f,
@ -72,7 +104,7 @@ fun SchemeFeatureBuilder.circle(
id, SchemeCircleFeature(center, scaleRange, size, color) id, SchemeCircleFeature(center, scaleRange, size, color)
) )
fun SchemeFeatureBuilder.circle( fun SchemeFeaturesState.circle(
centerCoordinates: Pair<Number, Number>, centerCoordinates: Pair<Number, Number>,
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
size: Float = 5f, size: Float = 5f,
@ -82,14 +114,14 @@ fun SchemeFeatureBuilder.circle(
id, SchemeCircleFeature(centerCoordinates.toCoordinates(), scaleRange, size, color) id, SchemeCircleFeature(centerCoordinates.toCoordinates(), scaleRange, size, color)
) )
fun SchemeFeatureBuilder.draw( fun SchemeFeaturesState.draw(
position: Pair<Number, Number>, position: Pair<Number, Number>,
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
id: FeatureId? = null, id: FeatureId? = null,
drawFeature: DrawScope.() -> Unit, drawFeature: DrawScope.() -> Unit,
) = addFeature(id, SchemeDrawFeature(position.toCoordinates(), scaleRange, drawFeature)) ) = addFeature(id, SchemeDrawFeature(position.toCoordinates(), scaleRange, drawFeature))
fun SchemeFeatureBuilder.line( fun SchemeFeaturesState.line(
aCoordinates: Pair<Number, Number>, aCoordinates: Pair<Number, Number>,
bCoordinates: Pair<Number, Number>, bCoordinates: Pair<Number, Number>,
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
@ -97,7 +129,7 @@ fun SchemeFeatureBuilder.line(
id: FeatureId? = null, id: FeatureId? = null,
) = addFeature(id, SchemeLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), scaleRange, color)) ) = addFeature(id, SchemeLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), scaleRange, color))
fun SchemeFeatureBuilder.text( fun SchemeFeaturesState.text(
position: SchemeCoordinates, position: SchemeCoordinates,
text: String, text: String,
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
@ -105,7 +137,7 @@ fun SchemeFeatureBuilder.text(
id: FeatureId? = null, id: FeatureId? = null,
) = addFeature(id, SchemeTextFeature(position, text, scaleRange, color)) ) = addFeature(id, SchemeTextFeature(position, text, scaleRange, color))
fun SchemeFeatureBuilder.text( fun SchemeFeaturesState.text(
position: Pair<Number, Number>, position: Pair<Number, Number>,
text: String, text: String,
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
@ -113,21 +145,20 @@ fun SchemeFeatureBuilder.text(
id: FeatureId? = null, id: FeatureId? = null,
) = addFeature(id, SchemeTextFeature(position.toCoordinates(), text, scaleRange, color)) ) = addFeature(id, SchemeTextFeature(position.toCoordinates(), text, scaleRange, color))
@Composable fun SchemeFeaturesState.image(
fun SchemeFeatureBuilder.image(
position: Pair<Number, Number>, position: Pair<Number, Number>,
image: ImageVector, image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp), size: DpSize = DpSize(20.dp, 20.dp),
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
id: FeatureId? = null, id: FeatureId? = null,
) = addFeature(id, SchemeVectorImageFeature(position.toCoordinates(), image, size, scaleRange)) ) = addFeature(id, SchemeImageFeature(position.toCoordinates(), image, size, scaleRange))
fun SchemeFeatureBuilder.group( fun SchemeFeaturesState.group(
scaleRange: FloatRange = defaultScaleRange, scaleRange: FloatRange = defaultScaleRange,
id: FeatureId? = null, id: FeatureId? = null,
builder: SchemeFeatureBuilder.() -> Unit, builder: SchemeFeaturesState.() -> Unit,
): FeatureId { ): FeatureId {
val map = SchemeFeatureBuilderImpl(emptyMap()).apply(builder).build() val groupBuilder = SchemeFeaturesState.build(builder)
val feature = SchemeFeatureGroup(map, scaleRange) val feature = SchemeFeatureGroup(groupBuilder.features(), scaleRange)
return addFeature(id, feature) return addFeature(id, feature)
} }

View File

@ -9,11 +9,8 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.* import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
@ -36,40 +33,29 @@ private val logger = KotlinLogging.logger("SchemeView")
data class SchemeViewConfig( data class SchemeViewConfig(
val zoomSpeed: Float = 1f / 3f, val zoomSpeed: Float = 1f / 3f,
val inferViewBoxFromFeatures: Boolean = false,
val onClick: SchemeViewPoint.() -> Unit = {}, val onClick: SchemeViewPoint.() -> Unit = {},
val onViewChange: SchemeViewPoint.() -> Unit = {}, val onViewChange: SchemeViewPoint.() -> Unit = {},
val onSelect: (SchemeCoordinateBox) -> Unit = {}, val onSelect: (SchemeRectangle) -> Unit = {},
val zoomOnSelect: Boolean = true, val zoomOnSelect: Boolean = true,
) )
@Composable @Composable
public fun SchemeView( public fun SchemeView(
computeViewPoint: (canvasSize: DpSize) -> SchemeViewPoint, initialViewPoint: SchemeViewPoint,
features: Map<FeatureId, SchemeFeature>, featuresState: SchemeFeaturesState,
config: SchemeViewConfig = SchemeViewConfig(), config: SchemeViewConfig = SchemeViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
) { ) {
var canvasSize by remember { mutableStateOf(defaultCanvasSize) }
var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) } var viewPoint by remember { mutableStateOf(initialViewPoint) }
var viewPointInternal: SchemeViewPoint? by remember {
mutableStateOf(null) fun setViewPoint(newViewPoint: SchemeViewPoint) {
config.onViewChange(newViewPoint)
viewPoint = newViewPoint
} }
val viewPoint: SchemeViewPoint by derivedStateOf {
viewPointInternal ?: if (config.inferViewBoxFromFeatures) {
features.values.computeBoundingBox(1f)?.let { box ->
val scale = min(
canvasSize.width.value / box.width,
canvasSize.height.value / box.height
)
SchemeViewPoint(box.center, scale)
} ?: computeViewPoint(canvasSize)
} else {
computeViewPoint(canvasSize)
}
}
fun DpOffset.toCoordinates(): SchemeCoordinates = SchemeCoordinates( fun DpOffset.toCoordinates(): SchemeCoordinates = SchemeCoordinates(
(x - canvasSize.width / 2).value / viewPoint.scale + viewPoint.focus.x, (x - canvasSize.width / 2).value / viewPoint.scale + viewPoint.focus.x,
@ -104,7 +90,7 @@ public fun SchemeView(
} }
selectRect?.let { rect -> selectRect?.let { rect ->
//Use selection override if it is defined //Use selection override if it is defined
val box = SchemeCoordinateBox( val box = SchemeRectangle(
rect.topLeft.toDpOffset().toCoordinates(), rect.topLeft.toDpOffset().toCoordinates(),
rect.bottomRight.toDpOffset().toCoordinates() rect.bottomRight.toDpOffset().toCoordinates()
) )
@ -117,8 +103,7 @@ public fun SchemeView(
val newViewPoint = SchemeViewPoint(box.center, newScale) val newViewPoint = SchemeViewPoint(box.center, newScale)
config.onViewChange(newViewPoint) setViewPoint(newViewPoint)
viewPointInternal = newViewPoint
} }
selectRect = null selectRect = null
} }
@ -132,8 +117,7 @@ public fun SchemeView(
-dragAmount.x.toDp().value / viewPoint.scale, -dragAmount.x.toDp().value / viewPoint.scale,
dragAmount.y.toDp().value / viewPoint.scale dragAmount.y.toDp().value / viewPoint.scale
) )
config.onViewChange(newViewPoint) setViewPoint(newViewPoint)
viewPointInternal = newViewPoint
} }
} }
} }
@ -146,10 +130,13 @@ public fun SchemeView(
//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()
val newViewPoint = viewPoint.zoom(-change.scrollDelta.y * config.zoomSpeed, invariant) val newViewPoint = viewPoint.zoom(-change.scrollDelta.y * config.zoomSpeed, invariant)
config.onViewChange(newViewPoint) setViewPoint(newViewPoint)
viewPointInternal = newViewPoint
}.fillMaxSize() }.fillMaxSize()
val painterCache = key(featuresState){
featuresState.features().values.filterIsInstance<PainterFeature>().associateWith { it.painter() }
}
Canvas(canvasModifier) { Canvas(canvasModifier) {
fun SchemeCoordinates.toOffset(): Offset = Offset( fun SchemeCoordinates.toOffset(): Offset = Offset(
(canvasSize.width / 2 + (x.dp - viewPoint.focus.x.dp) * viewPoint.scale).toPx(), (canvasSize.width / 2 + (x.dp - viewPoint.focus.x.dp) * viewPoint.scale).toPx(),
@ -160,36 +147,55 @@ public fun SchemeView(
fun DrawScope.drawFeature(scale: Float, feature: SchemeFeature) { fun DrawScope.drawFeature(scale: Float, feature: SchemeFeature) {
when (feature) { when (feature) {
is SchemeBackgroundFeature -> { is SchemeBackgroundFeature -> {
val offset = SchemeCoordinates(feature.position.left, feature.position.top).toOffset() val offset = SchemeCoordinates(feature.rectangle.left, feature.rectangle.top).toOffset()
val backgroundSize = DpSize( val backgroundSize = DpSize(
(feature.position.width * scale).dp, (feature.rectangle.width * scale).dp,
(feature.position.height * scale).dp (feature.rectangle.height * scale).dp
).toSize() ).toSize()
translate(offset.x, offset.y) { translate(offset.x, offset.y) {
with(feature.painter) { with(painterCache[feature]!!) {
draw(backgroundSize) draw(backgroundSize)
} }
} }
} }
is SchemeFeatureSelector -> drawFeature(scale, feature.selector(scale)) is SchemeFeatureSelector -> drawFeature(scale, feature.selector(scale))
is SchemeCircleFeature -> drawCircle( is SchemeCircleFeature -> drawCircle(
feature.color, feature.color,
feature.size, feature.size,
center = feature.center.toOffset() center = feature.center.toOffset()
) )
is SchemeLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset()) is SchemeLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
is SchemeArcFeature -> {
val topLeft = feature.oval.topLeft.toOffset()
val bottomRight = feature.oval.bottomRight.toOffset()
val path = Path().apply {
addArcRad(
Rect(topLeft, bottomRight),
feature.startAngle,
feature.arcLength
)
}
drawPath(path, color = feature.color, style = Stroke())
}
is SchemeBitmapFeature -> drawImage(feature.image, feature.position.toOffset()) is SchemeBitmapFeature -> drawImage(feature.image, feature.position.toOffset())
is SchemeImageFeature -> { is SchemeImageFeature -> {
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) {
with(feature.painter) { with(painterCache[feature]!!) {
draw(imageSize) draw(imageSize)
} }
} }
} }
is SchemeTextFeature -> drawIntoCanvas { canvas -> is SchemeTextFeature -> drawIntoCanvas { canvas ->
val offset = feature.position.toOffset() val offset = feature.position.toOffset()
canvas.nativeCanvas.drawString( canvas.nativeCanvas.drawString(
@ -200,12 +206,14 @@ public fun SchemeView(
feature.color.toPaint() feature.color.toPaint()
) )
} }
is SchemeDrawFeature -> { is SchemeDrawFeature -> {
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 SchemeFeatureGroup -> {
feature.children.values.forEach { feature.children.values.forEach {
drawFeature(scale, it) drawFeature(scale, it)
@ -219,10 +227,10 @@ public fun SchemeView(
logger.debug { "Recalculate canvas. Size: $size" } logger.debug { "Recalculate canvas. Size: $size" }
} }
clipRect { clipRect {
features.values.filterIsInstance<SchemeBackgroundFeature>().forEach { background -> featuresState.features().values.filterIsInstance<SchemeBackgroundFeature>().forEach { background ->
drawFeature(viewPoint.scale, background) drawFeature(viewPoint.scale, background)
} }
features.values.filter { featuresState.features().values.filter {
it !is SchemeBackgroundFeature && viewPoint.scale in it.scaleRange it !is SchemeBackgroundFeature && viewPoint.scale in it.scaleRange
}.forEach { feature -> }.forEach { feature ->
drawFeature(viewPoint.scale, feature) drawFeature(viewPoint.scale, feature)
@ -243,20 +251,82 @@ public fun SchemeView(
} }
} }
/**
* A builder for a Scheme with static features.
*/
@Composable @Composable
fun SchemeView( public fun SchemeView(
initialViewPoint: SchemeViewPoint, initialViewPoint: SchemeViewPoint? = null,
features: Map<FeatureId, SchemeFeature> = emptyMap(), initialRectangle: SchemeRectangle? = null,
featureMap: Map<FeatureId, SchemeFeature>,
config: SchemeViewConfig = SchemeViewConfig(), config: SchemeViewConfig = SchemeViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: @Composable (SchemeFeatureBuilder.() -> Unit) = {},
) { ) {
val featuresBuilder = SchemeFeatureBuilderImpl(features) val featuresState = key(featureMap) {
featuresBuilder.buildFeatures() SchemeFeaturesState.build {
featureMap.forEach(::addFeature)
}
}
val viewPointOverride: SchemeViewPoint = remember(initialViewPoint, initialRectangle) {
initialViewPoint
?: initialRectangle?.computeViewPoint()
?: featureMap.values.computeBoundingBox(1f)?.computeViewPoint()
?: SchemeViewPoint(SchemeCoordinates(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: SchemeViewPoint? = null,
initialRectangle: SchemeRectangle? = null,
config: SchemeViewConfig = SchemeViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: SchemeFeaturesState.() -> Unit = {},
) {
val featureState = SchemeFeaturesState.remember(buildFeatures)
val features = featureState.features()
val viewPointOverride: SchemeViewPoint = remember(initialViewPoint, initialRectangle) {
initialViewPoint
?: initialRectangle?.computeViewPoint()
?: features.values.computeBoundingBox(1f)?.computeViewPoint()
?: SchemeViewPoint(SchemeCoordinates(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( SchemeView(
{ initialViewPoint }, initialViewPoint = viewPointOverride,
featuresBuilder.build(), featuresState = featureState,
config, config = config,
modifier modifier = modifier,
) )
} }