Full refactor of map state

This commit is contained in:
Alexander Nozik 2023-09-10 13:12:45 +03:00
parent 921aff4685
commit 75b5a69a27
33 changed files with 738 additions and 570 deletions

View File

@ -10,7 +10,7 @@ val kmathVersion: String by extra("0.3.1")
allprojects { allprojects {
group = "center.sciprog" group = "center.sciprog"
version = "0.2.3-dev-1" version = "0.3.0-dev-1"
repositories { repositories {
mavenLocal() mavenLocal()

View File

@ -17,7 +17,7 @@ kotlin {
implementation(projects.mapsKtGeojson) implementation(projects.mapsKtGeojson)
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
implementation("io.ktor:ktor-client-cio") implementation("io.ktor:ktor-client-cio")
implementation("ch.qos.logback:logback-classic:1.2.11") implementation(spclibs.logback.classic)
} }
} }
val jvmTest by getting val jvmTest by getting

View File

@ -15,7 +15,9 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.attributes.Attributes import center.sciprog.attributes.Attributes
import center.sciprog.maps.compose.* import center.sciprog.maps.compose.*
import center.sciprog.maps.coordinates.* import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.kilometers
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import center.sciprog.maps.geojson.geoJson import center.sciprog.maps.geojson.geoJson
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
@ -109,8 +111,11 @@ fun App() {
) )
).pointSize(5f) ).pointSize(5f)
// geodeticLine(Gmc.ofDegrees(40.7128, -74.0060), Gmc.ofDegrees(55.742465, 37.615812)).color(Color.Blue)
// line(Gmc.ofDegrees(40.7128, -74.0060), Gmc.ofDegrees(55.742465, 37.615812))
//remember feature ID
//remember feature ref
val circleId = circle( val circleId = circle(
centerCoordinates = pointTwo, centerCoordinates = pointTwo,
) )
@ -170,6 +175,7 @@ fun App() {
} }
} }
} }
// println(toPrettyString())
} }
} }
} }

View File

@ -13,7 +13,7 @@ import center.sciprog.maps.features.*
import center.sciprog.maps.scheme.SchemeView import center.sciprog.maps.scheme.SchemeView
import center.sciprog.maps.scheme.XY import center.sciprog.maps.scheme.XY
import center.sciprog.maps.scheme.XYCoordinateSpace import center.sciprog.maps.scheme.XYCoordinateSpace
import center.sciprog.maps.scheme.XYViewScope import center.sciprog.maps.scheme.XYCanvasState
@Composable @Composable
@Preview @Preview
@ -31,7 +31,7 @@ fun App() {
) )
} }
val mapState: XYViewScope = XYViewScope.remember( val mapState: XYCanvasState = XYCanvasState.remember(
config = ViewConfig<XY>( config = ViewConfig<XY>(
onClick = { event, point -> onClick = { event, point ->
if (event.buttons.isSecondaryPressed) { if (event.buttons.isSecondaryPressed) {

View File

@ -26,6 +26,7 @@ compose{
desktop { desktop {
application { application {
mainClass = "MainKt" mainClass = "MainKt"
//mainClass = "Joker2023Kt"
nativeDistributions { nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "scheme-compose-demo" packageName = "scheme-compose-demo"

View File

@ -78,7 +78,7 @@ fun App() {
) )
} }
) { ) {
val mapState: XYViewScope = XYViewScope.remember( val mapState: XYCanvasState = XYCanvasState.remember(
ViewConfig( ViewConfig(
onClick = { _, click -> onClick = { _, click ->
println("${click.focus.x}, ${click.focus.y}") println("${click.focus.x}, ${click.focus.y}")

View File

@ -0,0 +1,76 @@
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Face
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import center.sciprog.maps.features.*
import center.sciprog.maps.scheme.*
import center.sciprog.maps.scheme.XYCoordinateSpace.Rectangle
fun main() = application {
Window(onCloseRequest = ::exitApplication, title = "Joker2023 demo", icon = painterResource("SPC-logo.png")) {
MaterialTheme {
SchemeView(
initialRectangle = Rectangle(XY(0f, 0f), XY(1734f, 724f)),
config = ViewConfig(
onClick = { _, pointer ->
println("(${pointer.focus.x}, ${pointer.focus.y})")
}
)
) {
background(1734f, 724f, id = "background") { painterResource("joker2023.png") }
group(id = "hall_1") {
polygon(
listOf(
XY(1582.0042, 210.29636),
XY(1433.7021, 127.79796),
XY(1370.7639, 127.79796),
XY(1315.293, 222.73865),
XY(1314.2262, 476.625),
XY(1364.3635, 570.4984),
XY(1434.7689, 570.4984),
XY(1579.8469, 493.69244),
)
).modifyAttributes {
ColorAttribute(Color.Blue)
AlphaAttribute(0.4f)
}.onClick {
println("hall_1")
}
}
group(id = "hall_2") {
rectanglePolygon(
left = 893, right = 1103,
bottom = 223, top = 406,
).modifyAttributes {
ColorAttribute(Color.Blue)
AlphaAttribute(0.4f)
}
}
group(id = "hall_3") {
rectanglePolygon(
Rectangle(XY(460f, 374f), width = 140f, height = 122f),
).modifyAttributes {
ColorAttribute(Color.Blue)
AlphaAttribute(0.4f)
}
}
group(id = "people") {
icon(XY(815.60535, 342.71313), Icons.Default.Face).color(Color.Red)
icon(XY(743.751, 381.09064), Icons.Default.Face).color(Color.Red)
icon(XY(1349.6648, 417.36014), Icons.Default.Face).color(Color.Red)
icon(XY (1362.4658, 287.21667), Icons.Default.Face).color(Color.Red)
icon(XY(208.24274, 317.08566), Icons.Default.Face).color(Color.Red)
icon(XY (293.5827, 319.21915), Icons.Default.Face).color(Color.Red)
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,10 +1,10 @@
kotlin.code.style=official kotlin.code.style=official
compose.version=1.4.0 compose.version=1.5.1
agp.version=7.4.2 #agp.version=7.4.2
android.useAndroidX=true #android.useAndroidX=true
org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.jscanvas.enabled=true
org.gradle.jvmargs=-Xmx4096m org.gradle.jvmargs=-Xmx4096m
toolsVersion=0.14.6-kotlin-1.8.20 toolsVersion=0.14.9-kotlin-1.9.0

View File

@ -17,7 +17,6 @@ kotlin {
api(compose.foundation) api(compose.foundation)
api(project.dependencies.platform(spclibs.ktor.bom)) api(project.dependencies.platform(spclibs.ktor.bom))
api("io.ktor:ktor-client-core") api("io.ktor:ktor-client-core")
api("io.github.microutils:kotlin-logging:2.1.23")
} }
} }
val jvmTest by getting { val jvmTest by getting {

View File

@ -14,10 +14,11 @@ import center.sciprog.maps.features.*
import space.kscience.kmath.geometry.radians import space.kscience.kmath.geometry.radians
import kotlin.math.* import kotlin.math.*
public class MapViewScope internal constructor(
public class MapCanvasState private constructor(
public val mapTileProvider: MapTileProvider, public val mapTileProvider: MapTileProvider,
config: ViewConfig<Gmc>, config: ViewConfig<Gmc>,
) : CoordinateViewScope<Gmc>(config) { ) : CanvasState<Gmc>(config) {
override val space: CoordinateSpace<Gmc> get() = WebMercatorSpace override val space: CoordinateSpace<Gmc> get() = WebMercatorSpace
private val scaleFactor: Float private val scaleFactor: Float
@ -87,12 +88,12 @@ public class MapViewScope internal constructor(
config: ViewConfig<Gmc> = ViewConfig(), config: ViewConfig<Gmc> = ViewConfig(),
initialViewPoint: ViewPoint<Gmc>? = null, initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null, initialRectangle: Rectangle<Gmc>? = null,
): MapViewScope = remember { ): MapCanvasState = remember {
MapViewScope(mapTileProvider, config).also { mapState -> MapCanvasState(mapTileProvider, config).apply {
if (initialViewPoint != null) { if (initialViewPoint != null) {
mapState.viewPoint = initialViewPoint viewPoint = initialViewPoint
} else if (initialRectangle != null) { } else if (initialRectangle != null) {
mapState.viewPoint = mapState.computeViewPoint(initialRectangle) viewPoint = computeViewPoint(initialRectangle)
} }
} }
} }

View File

@ -2,44 +2,137 @@ package center.sciprog.maps.compose
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.toComposeImageBitmap
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
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.*
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import org.jetbrains.skia.Image
import kotlin.math.ceil
import kotlin.math.pow
@Composable private fun IntRange.intersect(other: IntRange) = kotlin.math.max(first, other.first)..kotlin.math.min(last, other.last)
public expect fun MapView(
viewScope: MapViewScope, private val logger = KotlinLogging.logger("MapView")
features: FeatureGroup<Gmc>,
modifier: Modifier = Modifier.fillMaxSize(),
)
/** /**
* A builder for a Map with static features. * A component that renders map and provides basic map manipulation capabilities
*/
@Composable
public fun MapView(
mapState: MapCanvasState,
mapTileProvider: MapTileProvider,
features: FeatureGroup<Gmc>,
modifier: Modifier,
) {
val mapTiles = remember(mapTileProvider) {
mutableStateMapOf<TileId, Image>()
}
with(mapState) {
// Load tiles asynchronously
LaunchedEffect(viewPoint, canvasSize) {
with(mapTileProvider) {
val indexRange = 0 until 2.0.pow(intZoom).toInt()
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
for (j in verticalIndices) {
for (i in horizontalIndices) {
val id = TileId(intZoom, i, j)
//ensure that failed tiles do not fail the application
supervisorScope {
//start all
val deferred = loadTileAsync(id)
//wait asynchronously for it to finish
launch {
try {
val tile = deferred.await()
mapTiles[tile.id] = tile.image
} catch (ex: Exception) {
//displaying the error is maps responsibility
if(ex !is CancellationException) {
logger.error(ex) { "Failed to load tile with id=$id" }
}
}
}
}
mapTiles.keys.filter {
it.zoom != intZoom || it.j !in verticalIndices || it.i !in horizontalIndices
}.forEach {
mapTiles.remove(it)
}
}
}
}
}
}
FeatureCanvas(mapState, features, modifier = modifier.canvasControls(mapState, features)) {
val tileScale = mapState.tileScale
clipRect {
val tileSize = IntSize(
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt(),
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt()
)
mapTiles.forEach { (id, image) ->
//converting back from tile index to screen offset
val offset = IntOffset(
(mapState.canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - mapState.centerCoordinates.x.dp) * tileScale).roundToPx(),
(mapState.canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - mapState.centerCoordinates.y.dp) * tileScale).roundToPx()
)
drawImage(
image = image.toComposeImageBitmap(),
dstOffset = offset,
dstSize = tileSize
)
}
}
}
}
/**
* Create a [MapView] with given [features] group.
*/ */
@Composable @Composable
public fun MapView( public fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
config: ViewConfig<Gmc>,
features: FeatureGroup<Gmc>, features: FeatureGroup<Gmc>,
initialViewPoint: ViewPoint<Gmc>? = null, initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null, initialRectangle: Rectangle<Gmc>? = null,
config: ViewConfig<Gmc> = ViewConfig(), modifier: Modifier,
modifier: Modifier = Modifier.fillMaxSize(),
) { ) {
val mapState = MapCanvasState.remember(mapTileProvider, config, initialViewPoint, initialRectangle)
val mapState: MapViewScope = MapViewScope.remember( MapView(mapState, mapTileProvider, features, modifier)
mapTileProvider,
config,
initialViewPoint = initialViewPoint,
initialRectangle = initialRectangle ?: features.getBoundingBox(Float.MAX_VALUE),
)
MapView(mapState, features, modifier)
} }
/** /**
* Draw a map using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined, * Draw a map using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined,
* use map features to infer view region. * use map features to infer the 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.
* @param buildFeatures - a builder for features * @param buildFeatures - a builder for features
@ -47,24 +140,12 @@ public fun MapView(
@Composable @Composable
public fun MapView( public fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
config: ViewConfig<Gmc> = ViewConfig(),
initialViewPoint: ViewPoint<Gmc>? = null, initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null, initialRectangle: Rectangle<Gmc>? = null,
config: ViewConfig<Gmc> = ViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: FeatureGroup<Gmc>.() -> Unit = {}, buildFeatures: FeatureGroup<Gmc>.() -> Unit = {},
) { ) {
val featureState = FeatureGroup.remember(WebMercatorSpace, buildFeatures) val featureState = FeatureGroup.remember(WebMercatorSpace, buildFeatures)
MapView(mapTileProvider, config, featureState, initialViewPoint, initialRectangle, modifier)
val mapState: MapViewScope = MapViewScope.remember(
mapTileProvider,
config,
initialViewPoint = initialViewPoint,
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox(
WebMercatorSpace,
Float.MAX_VALUE
),
)
MapView(mapState, featureState, modifier)
} }

View File

@ -6,10 +6,7 @@ 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
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.maps.coordinates.Distance import center.sciprog.maps.coordinates.*
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.GmcCurve
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import kotlin.math.ceil import kotlin.math.ceil
@ -55,6 +52,39 @@ public fun FeatureGroup<Gmc>.line(
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates) LineFeature(space, curve.forward.coordinates, curve.backward.coordinates)
) )
/**
* A segmented geodetic curve
*/
public fun FeatureGroup<Gmc>.geodeticLine(
curve: GmcCurve,
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
maxLineDistance: Distance = 100.kilometers,
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> = if (curve.distance < maxLineDistance) {
feature(
id,
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates)
)
} else {
val segments = ceil(curve.distance / maxLineDistance).toInt()
val segmentSize = curve.distance / segments
val points = buildList<GmcPose> {
add(curve.forward)
repeat(segments) {
val segment = ellipsoid.curveInDirection(this.last(), segmentSize, 1e-2)
add(segment.backward)
}
}
multiLine(points.map { it.coordinates }, id = id)
}
public fun FeatureGroup<Gmc>.geodeticLine(
from: Gmc,
to: Gmc,
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
maxLineDistance: Distance = 100.kilometers,
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> = geodeticLine(ellipsoid.curveBetween(from, to), ellipsoid, maxLineDistance, id)
public fun FeatureGroup<Gmc>.line( public fun FeatureGroup<Gmc>.line(
aCoordinates: Pair<Double, Double>, aCoordinates: Pair<Double, Double>,
@ -65,7 +95,6 @@ public fun FeatureGroup<Gmc>.line(
LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates)) LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates))
) )
public fun FeatureGroup<Gmc>.arc( public fun FeatureGroup<Gmc>.arc(
center: Pair<Double, Double>, center: Pair<Double, Double>,
radius: Distance, radius: Distance,

View File

@ -1,15 +1,12 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.readBytes import io.ktor.client.statement.readBytes
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import mu.KotlinLogging
import org.jetbrains.skia.Image import org.jetbrains.skia.Image
import java.net.URL import java.net.URL
import java.nio.file.Path import java.nio.file.Path
@ -76,7 +73,9 @@ public class OpenStreetMapTileProvider(
//collect the result asynchronously //collect the result asynchronously
return async { return async {
val image: Image = runCatching { imageDeferred.await() }.onFailure { val image: Image = runCatching { imageDeferred.await() }.onFailure {
logger.error(it) { "Failed to load tile image with id=$tileId" } if(it !is CancellationException) {
logger.error(it) { "Failed to load tile image with id=$tileId" }
}
cache.remove(tileId) cache.remove(tileId)
}.getOrThrow() }.getOrThrow()

View File

@ -1,146 +0,0 @@
package center.sciprog.maps.compose
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
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.painter.Painter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import center.sciprog.attributes.z
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.FeatureGroup
import center.sciprog.maps.features.PainterFeature
import center.sciprog.maps.features.drawFeature
import center.sciprog.maps.features.zoomRange
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import mu.KotlinLogging
import org.jetbrains.skia.Image
import org.jetbrains.skia.Paint
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
private fun Color.toPaint(): 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("MapView")
/**
* A component that renders map and provides basic map manipulation capabilities
*/
@Composable
public actual fun MapView(
viewScope: MapViewScope,
features: FeatureGroup<Gmc>,
modifier: Modifier,
): Unit = with(viewScope) {
val mapTiles = remember(mapTileProvider) { mutableStateMapOf<TileId, Image>() }
// Load tiles asynchronously
LaunchedEffect(viewPoint, canvasSize) {
with(mapTileProvider) {
val indexRange = 0 until 2.0.pow(intZoom).toInt()
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
for (j in verticalIndices) {
for (i in horizontalIndices) {
val id = TileId(intZoom, i, j)
//ensure that failed tiles do not fail the application
supervisorScope {
//start all
val deferred = loadTileAsync(id)
//wait asynchronously for it to finish
launch {
try {
val tile = deferred.await()
mapTiles[tile.id] = tile.image
} catch (ex: Exception) {
//displaying the error is maps responsibility
logger.error(ex) { "Failed to load tile with id=$id" }
}
}
}
mapTiles.keys.filter {
it.zoom != intZoom || it.j !in verticalIndices || it.i !in horizontalIndices
}.forEach {
mapTiles.remove(it)
}
}
}
}
}
key(viewScope, features) {
val painterCache: Map<PainterFeature<Gmc>, Painter> =
features.features.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.getPainter() }
Canvas(modifier = modifier.mapControls(viewScope, features)) {
if (canvasSize != size.toDpSize()) {
logger.debug { "Recalculate canvas. Size: $size" }
config.onCanvasSizeChange(canvasSize)
canvasSize = size.toDpSize()
}
clipRect {
val tileSize = IntSize(
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt(),
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt()
)
mapTiles.forEach { (id, image) ->
//converting back from tile index to screen offset
val offset = IntOffset(
(canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale).roundToPx(),
(canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale).roundToPx()
)
drawImage(
image = image.toComposeImageBitmap(),
dstOffset = offset,
dstSize = tileSize
)
}
features.featureMap.values.sortedBy { it.z }
.filter { viewPoint.zoom in it.zoomRange }
.forEach { feature ->
drawFeature(viewScope, painterCache, feature)
}
}
selectRect?.let { dpRect ->
val rect = dpRect.toRect()
drawRect(
color = Color.Blue,
topLeft = rect.topLeft,
size = rect.size,
alpha = 0.5f,
style = Stroke(
width = 2f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
)
}
}
}
}

View File

@ -1,10 +1,8 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.*
import space.kscience.kmath.geometry.tan import kotlin.math.*
import kotlin.math.pow
import kotlin.math.sqrt
@Serializable @Serializable
public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRadius: Distance) { public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRadius: Distance) {
@ -43,12 +41,7 @@ public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRa
polarRadius = Distance(6378.137) polarRadius = Distance(6378.137)
) )
// /**
// * https://en.wikipedia.org/wiki/Great-circle_distance
// */
// public fun greatCircleAngleBetween(r1: GMC, r2: GMC): Radians = acos(
// sin(r1.latitude) * sin(r2.latitude) + cos(r1.latitude) * cos(r2.latitude) * cos(r1.longitude - r2.longitude)
// ).radians
} }
} }
@ -59,22 +52,35 @@ public fun GeoEllipsoid.reducedRadius(latitude: Angle): Distance {
val reducedLatitudeTan = (1 - f) * tan(latitude) val reducedLatitudeTan = (1 - f) * tan(latitude)
return equatorRadius / sqrt(1.0 + reducedLatitudeTan.pow(2)) return equatorRadius / sqrt(1.0 + reducedLatitudeTan.pow(2))
} }
//
//
///** /**
// * Compute distance between two map points using giv * Compute distance between two map points using giv
// * https://en.wikipedia.org/wiki/Geographical_distance#Lambert's_formula_for_long_lines * https://en.wikipedia.org/wiki/Geographical_distance#Lambert's_formula_for_long_lines
// */ */
//public fun GeoEllipsoid.lambertDistanceBetween(r1: GMC, r2: GMC): Distance { public fun GeoEllipsoid.lambertDistanceBetween(r1: Gmc, r2: Gmc): Distance {
// val s = greatCircleAngleBetween(r1, r2)
// /**
// val b1: Double = (1 - f) * tan(r1.latitude) * https://en.wikipedia.org/wiki/Great-circle_distance
// val b2: Double = (1 - f) * tan(r2.latitude) */
// val p = (b1 + b2) / 2 fun greatCircleAngleBetween(
// val q = (b2 - b1) / 2 r1: Gmc,
// r2: Gmc,
// val x = (s.value - sin(s)) * sin(p).pow(2) * cos(q).pow(2) / cos(s / 2).pow(2) ): Radians = acos(
// val y = (s.value + sin(s)) * cos(p).pow(2) * sin(q).pow(2) / sin(s / 2).pow(2) sin(r1.latitude) * sin(r2.latitude) +
// cos(r1.latitude) * cos(r2.latitude) *
// return equatorRadius * (s.value - f / 2 * (x + y)) cos(r1.longitude - r2.longitude)
//} ).radians
val s = greatCircleAngleBetween(r1, r2)
val b1: Double = (1 - f) * tan(r1.latitude)
val b2: Double = (1 - f) * tan(r2.latitude)
val p = (b1 + b2) / 2
val q = (b2 - b1) / 2
val x = (s.value - sin(s)) * sin(p).pow(2) * cos(q).pow(2) / cos(s / 2).pow(2)
val y = (s.value + sin(s)) * cos(p).pow(2) * sin(q).pow(2) / sin(s / 2).pow(2)
return equatorRadius * (s.value - f / 2 * (x + y))
}

View File

@ -5,10 +5,10 @@ import kotlin.math.*
/** /**
* A directed straight (geodetic) segment on a spheroid with given start, direction, end point and distance. * A directed straight (geodetic) segment on a spheroid with given start, direction, end point and distance.
* @param forward coordinate of a start point with forward direction * @param forward coordinate of a start point with the forward direction
* @param backward coordinate of an end point with backward direction * @param backward coordinate of an end point with the backward direction
*/ */
public class GmcCurve( public class GmcCurve internal constructor(
public val forward: GmcPose, public val forward: GmcPose,
public val backward: GmcPose, public val backward: GmcPose,
public val distance: Distance, public val distance: Distance,

View File

@ -24,6 +24,7 @@ kotlin {
dependencies { dependencies {
api(projects.trajectoryKt) api(projects.trajectoryKt)
api(compose.foundation) api(compose.foundation)
api("io.github.oshai:kotlin-logging:5.1.0")
} }
} }
} }

View File

@ -66,9 +66,3 @@ public fun <T : Any, A : Attribute<T>> Attributes(
): Attributes = Attributes(mapOf(attribute to attrValue)) ): Attributes = Attributes(mapOf(attribute to attrValue))
public operator fun Attributes.plus(other: Attributes): Attributes = Attributes(content + other.content) public operator fun Attributes.plus(other: Attributes): Attributes = Attributes(content + other.content)
public val Feature<*>.z: Float
get() = attributes[ZAttribute] ?: 0f
// set(value) {
// attributes[ZAttribute] = value
// }

View File

@ -1,16 +1,15 @@
package center.sciprog.maps.features package center.sciprog.maps.features
import androidx.compose.runtime.MutableState import androidx.compose.runtime.*
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.geometry.Offset
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
public abstract class CoordinateViewScope<T : Any>( /**
public val config: ViewConfig<T>, * A state holder for current canvas size and view point. Allows transformation from coordinates to pixels and back
) { */
public abstract class CanvasState<T: Any>(
public val viewConfig: ViewConfig<T>
){
public abstract val space: CoordinateSpace<T> public abstract val space: CoordinateSpace<T>
private var canvasSizeState: MutableState<DpSize?> = mutableStateOf(null) private var canvasSizeState: MutableState<DpSize?> = mutableStateOf(null)
@ -20,13 +19,14 @@ public abstract class CoordinateViewScope<T : Any>(
get() = canvasSizeState.value ?: DpSize(512.dp, 512.dp) get() = canvasSizeState.value ?: DpSize(512.dp, 512.dp)
set(value) { set(value) {
canvasSizeState.value = value canvasSizeState.value = value
viewConfig.onCanvasSizeChange(value)
} }
public var viewPoint: ViewPoint<T> public var viewPoint: ViewPoint<T>
get() = viewPointState.value ?: space.defaultViewPoint get() = viewPointState.value ?: space.defaultViewPoint
set(value) { set(value) {
viewPointState.value = value viewPointState.value = value
config.onViewChange(viewPoint) viewConfig.onViewChange(viewPoint)
} }
public val zoom: Float get() = viewPoint.zoom public val zoom: Float get() = viewPoint.zoom
@ -35,28 +35,28 @@ public abstract class CoordinateViewScope<T : Any>(
// Selection rectangle. If null - no selection // Selection rectangle. If null - no selection
public var selectRect: DpRect? by mutableStateOf(null) public var selectRect: DpRect? by mutableStateOf(null)
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 fun Offset.toCoordinates(density: Density): T = with(density) {
val dpOffset = DpOffset(x.toDp(), y.toDp())
dpOffset.toCoordinates()
}
public abstract fun Rectangle<T>.toDpRect(): DpRect public abstract fun Rectangle<T>.toDpRect(): DpRect
public abstract fun ViewPoint<T>.moveBy(x: Dp, y: Dp): ViewPoint<T> public abstract fun ViewPoint<T>.moveBy(x: Dp, y: Dp): ViewPoint<T>
public abstract fun computeViewPoint(rectangle: Rectangle<T>): ViewPoint<T> public abstract fun computeViewPoint(rectangle: Rectangle<T>): ViewPoint<T>
}
public abstract fun DpOffset.toCoordinates(): T
public abstract fun T.toDpOffset(): DpOffset
public fun toCoordinates(offset: Offset, density: Density): T = with(density){
val dpOffset = DpOffset(offset.x.toDp(), offset.y.toDp())
dpOffset.toCoordinates()
}
public fun toOffset(coordinates: T, density: Density): Offset = with(density){
val dpOffset = coordinates.toDpOffset()
return Offset(dpOffset.x.toPx(), dpOffset.y.toPx())
}
}
public val DpRect.topLeft: DpOffset get() = DpOffset(left, top) public val DpRect.topLeft: DpOffset get() = DpOffset(left, top)

View File

@ -0,0 +1,108 @@
package center.sciprog.maps.features
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.DrawScopeMarker
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.DpRect
import center.sciprog.attributes.Attributes
import io.github.oshai.kotlinlogging.KotlinLogging
/**
* An extension of [DrawScope] to include map-specific features
*/
@DrawScopeMarker
public abstract class FeatureDrawScope<T : Any>(
public val state: CanvasState<T>,
) : DrawScope {
public fun Offset.toCoordinates(): T = with(state) {
toCoordinates(this@toCoordinates, this@FeatureDrawScope)
}
public fun T.toOffset(): Offset = with(state) {
toOffset(this@toOffset, this@FeatureDrawScope)
}
public fun Rectangle<T>.toDpRect(): DpRect = with(state) { toDpRect() }
public abstract fun painterFor(feature: PainterFeature<T>): Painter
public abstract fun drawText(text: String, position: Offset, attributes: Attributes)
}
/**
* Default implementation of FeatureDrawScope to be used in Compose (both schemes and Maps)
*/
@DrawScopeMarker
public class ComposeFeatureDrawScope<T : Any>(
drawScope: DrawScope,
state: CanvasState<T>,
private val painterCache: Map<PainterFeature<T>, Painter>,
private val textMeasurer: TextMeasurer,
) : FeatureDrawScope<T>(state), DrawScope by drawScope {
override fun drawText(text: String, position: Offset, attributes: Attributes) {
try {
drawText(textMeasurer, text, position)
} catch (ex: Exception){
KotlinLogging.logger("features").error(ex){"Failed to measure text"}
}
}
override fun painterFor(feature: PainterFeature<T>): Painter = painterCache[feature] ?: error("Can't resolve painter for $feature")
}
/**
* Create a canvas with extended functionality (e.g., drawing text)
*/
@Composable
public fun <T : Any> FeatureCanvas(
state: CanvasState<T>,
features: FeatureGroup<T>,
modifier: Modifier = Modifier,
draw: FeatureDrawScope<T>.() -> Unit = {},
) {
val textMeasurer = rememberTextMeasurer(200)
val painterCache: Map<PainterFeature<T>, Painter> = features.features.flatMap {
if (it is FeatureGroup) it.features else listOf(it)
}.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
Canvas(modifier) {
if (state.canvasSize != size.toDpSize()) {
state.canvasSize = size.toDpSize()
}
ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply{
clipRect {
features.featureMap.values.sortedBy { it.z }
.filter { state.viewPoint.zoom in it.zoomRange }
.forEach { feature ->
this@apply.drawFeature(feature)
}
}
}
state.selectRect?.let { dpRect ->
val rect = dpRect.toRect()
drawRect(
color = Color.Blue,
topLeft = rect.topLeft,
size = rect.size,
alpha = 0.5f,
style = Stroke(
width = 2f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
)
}
}
}

View File

@ -1,8 +1,6 @@
package center.sciprog.maps.features package center.sciprog.maps.features
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateMap 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
@ -11,7 +9,8 @@ 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
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.attributes.* import center.sciprog.attributes.Attribute
import center.sciprog.attributes.Attributes
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.nd.* import space.kscience.kmath.nd.*
import space.kscience.kmath.structures.Buffer import space.kscience.kmath.structures.Buffer
@ -33,8 +32,12 @@ public val <T : Any, F : Feature<T>> FeatureRef<T, F>.attributes: Attributes get
public data class FeatureGroup<T : Any>( public data class FeatureGroup<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(), public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(),
override val attributes: Attributes = Attributes.EMPTY,
) : CoordinateSpace<T> by space, Feature<T> { ) : CoordinateSpace<T> by space, Feature<T> {
private val attributesState: MutableState<Attributes> = mutableStateOf(Attributes.EMPTY)
override val attributes: Attributes get() = attributesState.value
// //
// @Suppress("UNCHECKED_CAST") // @Suppress("UNCHECKED_CAST")
// public operator fun <F : Feature<T>> get(id: FeatureId<F>): F = // public operator fun <F : Feature<T>> get(id: FeatureId<F>): F =
@ -62,26 +65,8 @@ public data class FeatureGroup<T : Any>(
public val features: Collection<Feature<T>> get() = featureMap.values.sortedByDescending { it.z } public val features: Collection<Feature<T>> get() = featureMap.values.sortedByDescending { it.z }
public fun visit(visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Unit) {
featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
if (feature is FeatureGroup<T>) {
feature.visit(visitor)
} else {
visitor(this, key, feature)
}
}
}
public fun visitUntil(visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Boolean) { //
featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
if (feature is FeatureGroup<T>) {
feature.visitUntil(visitor)
} else {
if (!visitor(this, key, feature)) return@visitUntil
}
}
}
//
// @Suppress("UNCHECKED_CAST") // @Suppress("UNCHECKED_CAST")
// public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? = // public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
// get(id).attributes[key] // get(id).attributes[key]
@ -91,7 +76,10 @@ public data class FeatureGroup<T : Any>(
featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
} }
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes)) override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> {
attributesState.value = attributes.modify()
return this
}
public companion object { public companion object {
@ -118,6 +106,29 @@ public data class FeatureGroup<T : Any>(
} }
} }
/**
* Recursively search for feature until function returns true
*/
public fun <T : Any> FeatureGroup<T>.forEachUntil(visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Boolean) {
featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
if (feature is FeatureGroup<T>) {
feature.forEachUntil(visitor)
} else {
if (!visitor(this, key, feature)) return@forEachUntil
}
}
}
/**
* Recursively visit all features in this group
*/
public fun <T : Any> FeatureGroup<T>.forEach(
visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Unit,
): Unit = forEachUntil { id, feature ->
visitor(id, feature)
true
}
/** /**
* Process all features with a given attribute from the one with highest [z] to lowest * Process all features with a given attribute from the one with highest [z] to lowest
*/ */
@ -125,7 +136,7 @@ public fun <T : Any, A> FeatureGroup<T>.forEachWithAttribute(
key: Attribute<A>, key: Attribute<A>,
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Unit, block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Unit,
) { ) {
visit { id, feature -> forEach { id, feature ->
feature.attributes[key]?.let { feature.attributes[key]?.let {
block(id, feature, it) block(id, feature, it)
} }
@ -136,7 +147,7 @@ public fun <T : Any, A> FeatureGroup<T>.forEachWithAttributeUntil(
key: Attribute<A>, key: Attribute<A>,
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Boolean, block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Boolean,
) { ) {
visitUntil { id, feature -> forEachUntil { id, feature ->
feature.attributes[key]?.let { feature.attributes[key]?.let {
block(id, feature, it) block(id, feature, it)
} ?: true } ?: true
@ -146,7 +157,7 @@ public fun <T : Any, A> FeatureGroup<T>.forEachWithAttributeUntil(
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithType( public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithType(
crossinline block: (FeatureRef<T, F>) -> Unit, crossinline block: (FeatureRef<T, F>) -> Unit,
) { ) {
visit { id, feature -> forEach { id, feature ->
if (feature is F) block(FeatureRef(id, this)) if (feature is F) block(FeatureRef(id, this))
} }
} }
@ -154,7 +165,7 @@ public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithT
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithTypeUntil( public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithTypeUntil(
crossinline block: (FeatureRef<T, F>) -> Boolean, crossinline block: (FeatureRef<T, F>) -> Boolean,
) { ) {
visitUntil { id, feature -> forEachUntil { id, feature ->
if (feature is F) block(FeatureRef(id, this)) else true if (feature is F) block(FeatureRef(id, this)) else true
} }
} }
@ -241,25 +252,23 @@ public fun <T : Any> FeatureGroup<T>.icon(
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight), size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
id: String? = null, id: String? = null,
): FeatureRef<T, VectorIconFeature<T>> = ): FeatureRef<T, VectorIconFeature<T>> = feature(
feature( id,
id, VectorIconFeature(
VectorIconFeature( space,
space, position,
position, size,
size, image,
image, attributes
attributes
)
) )
)
public fun <T : Any> FeatureGroup<T>.group( public fun <T : Any> FeatureGroup<T>.group(
attributes: Attributes = Attributes.EMPTY,
id: String? = null, id: String? = null,
builder: FeatureGroup<T>.() -> Unit, builder: FeatureGroup<T>.() -> Unit,
): FeatureRef<T, FeatureGroup<T>> { ): FeatureRef<T, FeatureGroup<T>> {
val collection = FeatureGroup(space).apply(builder) val collection = FeatureGroup(space).apply(builder)
val feature = FeatureGroup(space, collection.featureMap, attributes) val feature = FeatureGroup(space, collection.featureMap)
return feature(id, feature) return feature(id, feature)
} }
@ -303,3 +312,22 @@ public fun <T : Any> FeatureGroup<T>.pixelMap(
id, id,
PixelMapFeature(space, rectangle, pixelMap, attributes = attributes) PixelMapFeature(space, rectangle, pixelMap, attributes = attributes)
) )
/**
* Create a pretty tree-like representation of this feature group
*/
public fun FeatureGroup<*>.toPrettyString(): String {
fun StringBuilder.printGroup(id: String, group: FeatureGroup<*>, prefix: String) {
appendLine("${prefix}* [group] $id")
group.featureMap.forEach { (id, feature) ->
if (feature is FeatureGroup<*>) {
printGroup(id, feature, " ")
} else {
appendLine("$prefix * [${feature::class.simpleName}] $id ")
}
}
}
return buildString {
printGroup("root", this@toPrettyString, "")
}
}

View File

@ -2,35 +2,31 @@ package center.sciprog.maps.features
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.Stroke 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.drawscope.translate
import androidx.compose.ui.graphics.painter.Painter
import center.sciprog.attributes.plus import center.sciprog.attributes.plus
import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint
import space.kscience.kmath.PerformancePitfall import space.kscience.kmath.PerformancePitfall
import space.kscience.kmath.geometry.degrees import space.kscience.kmath.geometry.degrees
internal fun Color.toPaint(): Paint = Paint().apply { //internal fun Color.toPaint(): Paint = Paint().apply {
isAntiAlias = true // isAntiAlias = true
color = toArgb() // color = toArgb()
} //}
public fun <T : Any> DrawScope.drawFeature(
state: CoordinateViewScope<T>, public fun <T : Any> FeatureDrawScope<T>.drawFeature(
painterCache: Map<PainterFeature<T>, Painter>,
feature: Feature<T>, feature: Feature<T>,
): Unit = with(state) { ): Unit {
val color = feature.color ?: Color.Red val color = feature.color ?: Color.Red
val alpha = feature.attributes[AlphaAttribute] ?: 1f val alpha = feature.attributes[AlphaAttribute] ?: 1f
fun T.toOffset(): Offset = toOffset(this@drawFeature)
when (feature) { when (feature) {
is FeatureSelector -> drawFeature(state, painterCache, feature.selector(state.zoom)) is FeatureSelector -> drawFeature(feature.selector(state.zoom))
is CircleFeature -> drawCircle( is CircleFeature -> drawCircle(
color, color,
feature.radius.toPx(), feature.radius.toPx(),
@ -78,22 +74,13 @@ public fun <T : Any> DrawScope.drawFeature(
val offset = feature.center.toOffset() val offset = feature.center.toOffset()
val size = feature.size.toSize() val size = feature.size.toSize()
translate(offset.x - size.width / 2, offset.y - size.height / 2) { translate(offset.x - size.width / 2, offset.y - size.height / 2) {
with(painterCache[feature]!!) { with(this@drawFeature.painterFor(feature)) {
draw(size) draw(size, colorFilter = feature.color?.let { ColorFilter.tint(it) })
} }
} }
} }
is TextFeature -> drawIntoCanvas { canvas -> is TextFeature -> drawText(feature.text, feature.position.toOffset(), feature.attributes)
val offset = feature.position.toOffset()
canvas.nativeCanvas.drawString(
feature.text,
offset.x + 5,
offset.y - 5,
Font().apply(feature.fontConfig),
(feature.color ?: Color.Black).toPaint()
)
}
is DrawFeature -> { is DrawFeature -> {
val offset = feature.position.toOffset() val offset = feature.position.toOffset()
@ -104,9 +91,7 @@ public fun <T : Any> DrawScope.drawFeature(
is FeatureGroup -> { is FeatureGroup -> {
feature.featureMap.values.forEach { feature.featureMap.values.forEach {
drawFeature(state, painterCache, it.withAttributes { drawFeature(it.withAttributes { feature.attributes + this })
feature.attributes + this
})
} }
} }
@ -163,7 +148,7 @@ public fun <T : Any> DrawScope.drawFeature(
val offset = rect.topLeft val offset = rect.topLeft
translate(offset.x, offset.y) { translate(offset.x, offset.y) {
with(painterCache[feature]!!) { with(this@drawFeature.painterFor(feature)) {
draw(rect.size) draw(rect.size)
} }
} }

View File

@ -13,6 +13,9 @@ import center.sciprog.attributes.withAttribute
public object ZAttribute : Attribute<Float> public object ZAttribute : Attribute<Float>
public val Feature<*>.z: Float
get() = attributes[ZAttribute] ?: 0f
public object DraggableAttribute : Attribute<DragHandle<Any>> public object DraggableAttribute : Attribute<DragHandle<Any>>
public object DragListenerAttribute : SetAttribute<DragListener<Any>> public object DragListenerAttribute : SetAttribute<DragListener<Any>>
@ -46,12 +49,14 @@ public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.zoomRange(range: FloatRang
public object AlphaAttribute : Attribute<Float> public object AlphaAttribute : Attribute<Float>
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes(modify: AttributesBuilder.() -> Unit): FeatureRef<T, F> { public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes(
modification: AttributesBuilder.() -> Unit
): FeatureRef<T, F> {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
parent.feature( parent.feature(
id, id,
resolve().withAttributes { resolve().withAttributes {
AttributesBuilder(this).apply(modify).build() AttributesBuilder(this).apply(modification).build()
} as F } as F
) )
return this return this

View File

@ -16,10 +16,10 @@ import kotlin.math.min
* Create a modifier for Map/Scheme canvas controls on desktop * Create a modifier for Map/Scheme canvas controls on desktop
* @param features a collection of features to be rendered in descending [ZAttribute] order * @param features a collection of features to be rendered in descending [ZAttribute] order
*/ */
public fun <T : Any> Modifier.mapControls( public fun <T : Any> Modifier.canvasControls(
state: CoordinateViewScope<T>, state: CanvasState<T>,
features: FeatureGroup<T>, features: FeatureGroup<T>,
): Modifier = with(state) { ): Modifier = with(state){
// //selecting all tapabales ahead of time // //selecting all tapabales ahead of time
// val allTapable = buildMap { // val allTapable = buildMap {
@ -32,8 +32,8 @@ public fun <T : Any> Modifier.mapControls(
awaitPointerEventScope { awaitPointerEventScope {
while (true) { while (true) {
val event = awaitPointerEvent() val event = awaitPointerEvent()
val coordinates = event.changes.first().position.toCoordinates(this) val coordinates = toCoordinates(event.changes.first().position, this)
val point = space.ViewPoint(coordinates, zoom) val point = state.space.ViewPoint(coordinates, zoom)
if (event.type == PointerEventType.Move) { if (event.type == PointerEventType.Move) {
features.forEachWithAttribute(HoverListenerAttribute) { _, feature, listeners -> features.forEachWithAttribute(HoverListenerAttribute) { _, feature, listeners ->
@ -47,9 +47,9 @@ public fun <T : Any> Modifier.mapControls(
} }
}.pointerInput(Unit) { }.pointerInput(Unit) {
detectClicks( detectClicks(
onDoubleClick = if (state.config.zoomOnDoubleClick) { onDoubleClick = if (viewConfig.zoomOnDoubleClick) {
{ event -> { event ->
val invariant = event.position.toCoordinates(this) val invariant = toCoordinates(event.position, this)
viewPoint = with(space) { viewPoint = with(space) {
viewPoint.zoomBy( viewPoint.zoomBy(
if (event.buttons.isPrimaryPressed) 1f else if (event.buttons.isSecondaryPressed) -1f else 0f, if (event.buttons.isPrimaryPressed) 1f else if (event.buttons.isSecondaryPressed) -1f else 0f,
@ -59,10 +59,10 @@ public fun <T : Any> Modifier.mapControls(
} }
} else null, } else null,
onClick = { event -> onClick = { event ->
val coordinates = event.position.toCoordinates(this) val coordinates = toCoordinates(event.position, this)
val point = space.ViewPoint(coordinates, zoom) val point = space.ViewPoint(coordinates, zoom)
config.onClick?.handle( viewConfig.onClick?.handle(
event, event,
point point
) )
@ -88,7 +88,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 * config.zoomSpeed, invariant) viewPoint.zoomBy(-change.scrollDelta.y * viewConfig.zoomSpeed, invariant)
} }
change.consume() change.consume()
} }
@ -110,14 +110,14 @@ public fun <T : Any> Modifier.mapControls(
//apply drag handle and check if it prohibits the drag even propagation //apply drag handle and check if it prohibits the drag even propagation
if (selectionStart == null) { if (selectionStart == null) {
val dragStart = space.ViewPoint( val dragStart = space.ViewPoint(
dragChange.previousPosition.toCoordinates(this), toCoordinates(dragChange.previousPosition, this),
zoom zoom
) )
val dragEnd = space.ViewPoint( val dragEnd = space.ViewPoint(
dragChange.position.toCoordinates(this), toCoordinates(dragChange.position, this),
zoom zoom
) )
val dragResult = config.dragHandle?.handle(event, dragStart, dragEnd) val dragResult = viewConfig.dragHandle?.handle(event, dragStart, dragEnd)
if (dragResult?.handleNext == false) return@drag if (dragResult?.handleNext == false) return@drag
var continueAfter = true var continueAfter = true
@ -132,7 +132,7 @@ public fun <T : Any> Modifier.mapControls(
} }
if (event.buttons.isPrimaryPressed) { if (event.buttons.isPrimaryPressed) {
//If selection process is started, modify the frame //If the selection process is started, modify the frame
selectionStart?.let { start -> selectionStart?.let { start ->
val offset = dragChange.position val offset = dragChange.position
selectRect = DpRect( selectRect = DpRect(
@ -161,8 +161,8 @@ public fun <T : Any> Modifier.mapControls(
rect.topLeft.toCoordinates(), rect.topLeft.toCoordinates(),
rect.bottomRight.toCoordinates() rect.bottomRight.toCoordinates()
) )
config.onSelect(coordinateRect) viewConfig.onSelect(coordinateRect)
if (config.zoomOnSelect) { if (viewConfig.zoomOnSelect) {
viewPoint = computeViewPoint(coordinateRect) viewPoint = computeViewPoint(coordinateRect)
} }
selectRect = null selectRect = null

View File

@ -1,18 +1,14 @@
package center.sciprog.maps.scheme package center.sciprog.maps.scheme
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.unit.DpSize import androidx.compose.ui.unit.DpSize
import center.sciprog.attributes.z import center.sciprog.maps.compose.canvasControls
import center.sciprog.maps.compose.mapControls
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import mu.KotlinLogging import mu.KotlinLogging
import kotlin.math.min import kotlin.math.min
@ -22,48 +18,14 @@ private val logger = KotlinLogging.logger("SchemeView")
@Composable @Composable
public fun SchemeView( public fun SchemeView(
state: XYViewScope, state: XYCanvasState,
features: FeatureGroup<XY>, features: FeatureGroup<XY>,
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
): Unit = key(state, features) { ): Unit {
with(state) { FeatureCanvas(state, features, modifier = modifier.canvasControls(state, features))
//Can't do that inside canvas
val painterCache: Map<PainterFeature<XY>, Painter> =
features.features.filterIsInstance<PainterFeature<XY>>().associateWith { it.getPainter() }
Canvas(modifier = modifier.mapControls(state, features)) {
if (canvasSize != size.toDpSize()) {
canvasSize = size.toDpSize()
logger.debug { "Recalculate canvas. Size: $size" }
}
clipRect {
features.featureMap.values.sortedBy { it.z }
.filter { viewPoint.zoom in it.zoomRange }
.forEach { feature ->
drawFeature(state, painterCache, feature)
}
}
selectRect?.let { dpRect ->
val rect = dpRect.toRect()
drawRect(
color = Color.Blue,
topLeft = rect.topLeft,
size = rect.size,
alpha = 0.5f,
style = Stroke(
width = 2f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
)
}
}
}
} }
public fun Rectangle<XY>.computeViewPoint( public fun Rectangle<XY>.computeViewPoint(
canvasSize: DpSize = defaultCanvasSize, canvasSize: DpSize = defaultCanvasSize,
): ViewPoint<XY> { ): ViewPoint<XY> {
@ -87,7 +49,7 @@ public fun SchemeView(
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
) { ) {
val state = XYViewScope.remember( val state = XYCanvasState.remember(
config, config,
initialViewPoint = initialViewPoint, initialViewPoint = initialViewPoint,
initialRectangle = initialRectangle ?: features.getBoundingBox(Float.MAX_VALUE), initialRectangle = initialRectangle ?: features.getBoundingBox(Float.MAX_VALUE),
@ -112,7 +74,7 @@ public fun SchemeView(
buildFeatures: FeatureGroup<XY>.() -> Unit = {}, buildFeatures: FeatureGroup<XY>.() -> Unit = {},
) { ) {
val featureState = FeatureGroup.remember(XYCoordinateSpace, buildFeatures) val featureState = FeatureGroup.remember(XYCoordinateSpace, buildFeatures)
val mapState: XYViewScope = XYViewScope.remember( val mapState: XYCanvasState = XYCanvasState.remember(
config, config,
initialViewPoint = initialViewPoint, initialViewPoint = initialViewPoint,
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox( initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox(

View File

@ -12,6 +12,8 @@ import kotlin.math.min
public data class XY(override val x: Float, override val y: Float): Vector2D<Float> public data class XY(override val x: Float, override val y: Float): Vector2D<Float>
public fun XY(x: Number, y: Number): XY = XY(x.toFloat(), y.toFloat())
internal data class XYRectangle( internal data class XYRectangle(
override val a: XY, override val a: XY,
override val b: XY, override val b: XY,

View File

@ -9,9 +9,9 @@ import androidx.compose.ui.unit.dp
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import kotlin.math.min import kotlin.math.min
public class XYViewScope( public class XYCanvasState(
config: ViewConfig<XY>, config: ViewConfig<XY>,
) : CoordinateViewScope<XY>(config) { ) : CanvasState<XY>(config) {
override val space: CoordinateSpace<XY> override val space: CoordinateSpace<XY>
get() = XYCoordinateSpace get() = XYCoordinateSpace
@ -54,12 +54,12 @@ public class XYViewScope(
config: ViewConfig<XY> = ViewConfig(), config: ViewConfig<XY> = ViewConfig(),
initialViewPoint: ViewPoint<XY>? = null, initialViewPoint: ViewPoint<XY>? = null,
initialRectangle: Rectangle<XY>? = null, initialRectangle: Rectangle<XY>? = null,
): XYViewScope = remember { ): XYCanvasState = remember {
XYViewScope(config).also { mapState-> XYCanvasState(config).apply {
if (initialViewPoint != null) { if (initialViewPoint != null) {
mapState.viewPoint = initialViewPoint viewPoint = initialViewPoint
} else if (initialRectangle != null) { } else if (initialRectangle != null) {
mapState.viewPoint = mapState.computeViewPoint(initialRectangle) viewPoint = computeViewPoint(initialRectangle)
} }
} }
} }

View File

@ -41,7 +41,7 @@ public fun FeatureGroup<XY>.circle(
centerCoordinates: Pair<Number, Number>, centerCoordinates: Pair<Number, Number>,
size: Dp = 5.dp, size: Dp = 5.dp,
id: String? = null, id: String? = null,
): FeatureRef<XY, CircleFeature<XY>> = circle(centerCoordinates.toCoordinates(), size, id = id) ): FeatureRef<XY, CircleFeature<XY>> = circle(centerCoordinates.toCoordinates(), size, id = id)
public fun FeatureGroup<XY>.draw( public fun FeatureGroup<XY>.draw(
position: Pair<Number, Number>, position: Pair<Number, Number>,
@ -63,7 +63,7 @@ public fun FeatureGroup<XY>.arc(
arcLength: Angle, arcLength: Angle,
id: String? = null, id: String? = null,
): FeatureRef<XY, ArcFeature<XY>> = arc( ): FeatureRef<XY, ArcFeature<XY>> = arc(
oval = XYCoordinateSpace.Rectangle(center.toCoordinates(), 2*radius, 2*radius), oval = XYCoordinateSpace.Rectangle(center.toCoordinates(), 2 * radius, 2 * radius),
startAngle = startAngle, startAngle = startAngle,
arcLength = arcLength, arcLength = arcLength,
id = id id = id
@ -108,4 +108,33 @@ public fun FeatureGroup<XY>.pixelMap(
) )
) )
public fun FeatureGroup<XY>.rectanglePolygon(
left: Number, right: Number,
bottom: Number, top: Number,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<XY, PolygonFeature<XY>> = polygon(
listOf(
XY(left.toFloat(), top.toFloat()),
XY(right.toFloat(), top.toFloat()),
XY(right.toFloat(), bottom.toFloat()),
XY(left.toFloat(), bottom.toFloat())
),
attributes, id
)
public fun FeatureGroup<XY>.rectanglePolygon(
rectangle: Rectangle<XY>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<XY, PolygonFeature<XY>> = polygon(
listOf(
XY(rectangle.left, rectangle.top),
XY(rectangle.right, rectangle.top),
XY(rectangle.right, rectangle.bottom),
XY(rectangle.left, rectangle.bottom)
),
attributes, id
)

View File

@ -5,21 +5,26 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.graphics.painter.Painter
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.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import center.sciprog.attributes.Attributes
import center.sciprog.maps.features.*
import center.sciprog.maps.scheme.XY
import org.jfree.svg.SVGGraphics2D import org.jfree.svg.SVGGraphics2D
import java.awt.BasicStroke import java.awt.BasicStroke
import java.awt.Font
import java.awt.geom.* import java.awt.geom.*
import java.awt.image.AffineTransformOp import java.awt.image.AffineTransformOp
import java.awt.Color as AWTColor import java.awt.Color as AWTColor
public class SvgDrawScope( public class SvgDrawScope(
state: CanvasState<XY>,
private val graphics: SVGGraphics2D, private val graphics: SVGGraphics2D,
size: Size, size: Size,
private val defaultStrokeWidth: Float = 1f, private val painterCache: Map<PainterFeature<XY>, Painter>,
) : DrawScope { private val defaultStrokeWidth: Float = 1f
) : FeatureDrawScope<XY>(state) {
override val layoutDirection: LayoutDirection override val layoutDirection: LayoutDirection
get() = LayoutDirection.Ltr get() = LayoutDirection.Ltr
@ -459,16 +464,20 @@ public class SvgDrawScope(
} }
} }
public fun drawText( public fun renderText(
text: String, textFeature: TextFeature<XY>,
x: Float,
y: Float,
font: Font,
color: Color,
) { ) {
setupColor(color) textFeature.color?.let { setupColor(it)}
graphics.font = font graphics.drawString(textFeature.text, textFeature.position.x, textFeature.position.y)
graphics.drawString(text, x, y) }
override fun painterFor(feature: PainterFeature<XY>): Painter {
return painterCache[feature]!!
}
override fun drawText(text: String, position: Offset, attributes: Attributes) {
attributes[ColorAttribute]?.let { setupColor(it)}
graphics.drawString(text, position.x, position.y)
} }
override val drawContext: DrawContext = SvgDrawContext(graphics, size) override val drawContext: DrawContext = SvgDrawContext(graphics, size)

View File

@ -1,21 +1,13 @@
package center.sciprog.maps.svg package center.sciprog.maps.svg
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode
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.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import center.sciprog.maps.scheme.* import center.sciprog.maps.scheme.XY
import center.sciprog.maps.scheme.XYCanvasState
import org.jfree.svg.SVGGraphics2D import org.jfree.svg.SVGGraphics2D
import org.jfree.svg.SVGUtils import org.jfree.svg.SVGUtils
import space.kscience.kmath.geometry.degrees
import java.awt.Font.PLAIN
import kotlin.math.abs
public class FeatureStateSnapshot<T : Any>( public class FeatureStateSnapshot<T : Any>(
@ -26,9 +18,12 @@ public class FeatureStateSnapshot<T : Any>(
@Composable @Composable
public fun <T : Any> FeatureGroup<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot( public fun <T : Any> FeatureGroup<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
featureMap, featureMap,
features.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() } features.flatMap {
if (it is FeatureGroup) it.features else listOf(it)
}.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
) )
public fun FeatureStateSnapshot<XY>.generateSvg( public fun FeatureStateSnapshot<XY>.generateSvg(
viewPoint: ViewPoint<XY>, viewPoint: ViewPoint<XY>,
width: Double, width: Double,
@ -36,139 +31,137 @@ public fun FeatureStateSnapshot<XY>.generateSvg(
id: String? = null, id: String? = null,
): String { ): String {
fun XY.toOffset(): Offset = Offset( // fun XY.toOffset(): Offset = Offset(
(width / 2 + (x - viewPoint.focus.x) * viewPoint.zoom).toFloat(), // (width / 2 + (x - viewPoint.focus.x) * viewPoint.zoom).toFloat(),
(height / 2 + (viewPoint.focus.y - y) * viewPoint.zoom).toFloat() // (height / 2 + (viewPoint.focus.y - y) * viewPoint.zoom).toFloat()
) // )
//
//
fun SvgDrawScope.drawFeature(scale: Float, feature: Feature<XY>) { // fun SvgDrawScope.drawFeature(scale: Float, feature: Feature<XY>) {
//
val color = feature.color ?: Color.Red // val color = feature.color ?: Color.Red
val alpha = feature.attributes[AlphaAttribute] ?: 1f // val alpha = feature.attributes[AlphaAttribute] ?: 1f
//
when (feature) { // when (feature) {
is ScalableImageFeature -> { // is ScalableImageFeature -> {
val offset = XY(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)
) // )
//
translate(offset.x, offset.y) { // translate(offset.x, offset.y) {
with(painterCache[feature]!!) { // with(painterCache[feature]!!) {
draw(backgroundSize) // draw(backgroundSize)
} // }
} // }
} // }
//
is FeatureSelector -> drawFeature(scale, feature.selector(scale)) // is FeatureSelector -> drawFeature(scale, feature.selector(scale))
//
is CircleFeature -> drawCircle( // is CircleFeature -> drawCircle(
color, // color,
feature.radius.toPx(), // feature.radius.toPx(),
center = feature.center.toOffset(), // center = feature.center.toOffset(),
alpha = alpha // alpha = alpha
) // )
//
is LineFeature -> drawLine( // is LineFeature -> drawLine(
color, // color,
feature.a.toOffset(), // feature.a.toOffset(),
feature.b.toOffset(), // feature.b.toOffset(),
alpha = alpha // alpha = alpha
) // )
//
is PointsFeature -> { // is PointsFeature -> {
val points = feature.points.map { it.toOffset() } // val points = feature.points.map { it.toOffset() }
drawPoints( // drawPoints(
points = points, // points = points,
color = color, // color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, // strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Points, // pointMode = PointMode.Points,
pathEffect = feature.attributes[PathEffectAttribute], // pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha // alpha = alpha
) // )
} // }
//
is MultiLineFeature -> { // is MultiLineFeature -> {
val points = feature.points.map { it.toOffset() } // val points = feature.points.map { it.toOffset() }
drawPoints( // drawPoints(
points = points, // points = points,
color = color, // color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, // strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Polygon, // pointMode = PointMode.Polygon,
pathEffect = feature.attributes[PathEffectAttribute], // pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha // alpha = alpha
) // )
} // }
//
is ArcFeature -> { // 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()
//
val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y)) // val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
//
drawArc( // drawArc(
color = color, // color = color,
startAngle = feature.startAngle.degrees.toFloat(), // startAngle = feature.startAngle.degrees.toFloat(),
sweepAngle = feature.arcLength.degrees.toFloat(), // sweepAngle = feature.arcLength.degrees.toFloat(),
useCenter = false, // useCenter = false,
topLeft = topLeft, // topLeft = topLeft,
size = size, // size = size,
style = Stroke(), // style = Stroke(),
alpha = alpha // alpha = alpha
) // )
} // }
//
is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset()) // is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset())
//
is VectorIconFeature -> { // is VectorIconFeature -> {
val offset = feature.center.toOffset() // val offset = feature.center.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(painterCache[feature]!!) { // with(painterCache[feature]!!) {
draw(imageSize, alpha = alpha) // draw(imageSize, alpha = alpha)
} // }
} // }
} // }
//
is TextFeature -> drawIntoCanvas { _ -> // is TextFeature -> drawIntoCanvas { _ ->
val offset = feature.position.toOffset() // val offset = feature.position.toOffset()
drawText( // drawText(
feature.text, // feature.text,
offset.x + 5, // offset.x + 5,
offset.y - 5, // offset.y - 5,
java.awt.Font(null, PLAIN, 16), // java.awt.Font(null, PLAIN, 16),
color // color
) // )
} // }
//
is DrawFeature -> { // 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 FeatureGroup -> { // is FeatureGroup -> {
feature.featureMap.values.forEach { // feature.featureMap.values.forEach {
drawFeature(scale, it) // drawFeature(scale, it)
} // }
} // }
} // }
} // }
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(width, height) val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(width, height)
val svgScope = SvgDrawScope(svgGraphics2D, Size(width.toFloat(), height.toFloat())) val svgCanvasState: XYCanvasState = XYCanvasState(ViewConfig())
val svgScope = SvgDrawScope(svgCanvasState, svgGraphics2D, Size(width.toFloat(), height.toFloat()), painterCache)
svgScope.apply { svgScope.apply {
features.values.filterIsInstance<ScalableImageFeature<XY>>().forEach { background ->
drawFeature(viewPoint.zoom, background)
}
features.values.filter { features.values.filter {
it !is ScalableImageFeature && viewPoint.zoom in it.zoomRange viewPoint.zoom in it.zoomRange
}.forEach { feature -> }.forEach { feature ->
drawFeature(viewPoint.zoom, feature) drawFeature(feature)
} }
} }
return svgGraphics2D.getSVGElement(id) return svgGraphics2D.getSVGElement(id)

View File

@ -16,8 +16,8 @@ pluginManagement {
} }
plugins { plugins {
id("com.android.application").version(extra["agp.version"] as String) // id("com.android.application").version(extra["agp.version"] as String)
id("com.android.library").version(extra["agp.version"] as String) // id("com.android.library").version(extra["agp.version"] as String)
id("org.jetbrains.compose").version(extra["compose.version"] as String) id("org.jetbrains.compose").version(extra["compose.version"] as String)
id("space.kscience.gradle.project") version toolsVersion id("space.kscience.gradle.project") version toolsVersion
id("space.kscience.gradle.mpp") version toolsVersion id("space.kscience.gradle.mpp") version toolsVersion

View File

@ -10,7 +10,7 @@ val kmathVersion: String by rootProject.extra
kscience{ kscience{
jvm() jvm()
js() js()
native() // native()
useContextReceivers() useContextReceivers()
useSerialization() useSerialization()