0.3.0 #23

Open
altavir wants to merge 40 commits from dev into main
114 changed files with 1773 additions and 1429 deletions
Showing only changes of commit bd6d8e2f8e - Show all commits

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
build/ build/
.gradle/ .gradle/
.idea/ .idea/
.kotlin
/*.iml /*.iml
mapCache/ mapCache/

View File

@ -9,17 +9,17 @@ job("Publish") {
gitPush { enabled = false } gitPush { enabled = false }
} }
container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") { container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") {
env["SPACE_USER"] = Secrets("space_user") env["SPACE_USER"] = "{{ project:space_user }}"
env["SPACE_TOKEN"] = Secrets("space_token") env["SPACE_TOKEN"] = "{{ project:space_token }}"
kotlinScript { api -> kotlinScript { api ->
val spaceUser = System.getenv("SPACE_USER") val spaceUser = System.getenv("SPACE_USER")
val spaceToken = System.getenv("SPACE_TOKEN") val spaceToken = System.getenv("SPACE_TOKEN")
// write version to the build directory // write the version to the build directory
api.gradlew("version") api.gradlew("version")
//read version from build file //read the version from build file
val version = java.nio.file.Path.of("build/project-version.txt").readText() val version = java.nio.file.Path.of("build/project-version.txt").readText()
val revisionSuffix = if (version.endsWith("SNAPSHOT")) { val revisionSuffix = if (version.endsWith("SNAPSHOT")) {
@ -32,7 +32,7 @@ job("Publish") {
project = api.projectIdentifier(), project = api.projectIdentifier(),
targetIdentifier = TargetIdentifier.Key("maps-kt"), targetIdentifier = TargetIdentifier.Key("maps-kt"),
version = version+revisionSuffix, version = version+revisionSuffix,
// automatically update deployment status based on a status of a job // automatically update deployment status based on the status of a job
syncWithAutomationJob = true syncWithAutomationJob = true
) )
api.gradlew( api.gradlew(

View File

@ -3,13 +3,27 @@
## Unreleased ## Unreleased
### Added ### Added
- `alpha` extension for feature attribute builder
### Changed ### Changed
- avoid drawing features with VisibleAttribute false
### Deprecated ### Deprecated
### Removed ### Removed
### Fixed ### Fixed
- Add alpha attribute comprehension for all standard features.
### Security ### Security
## 0.3.0 - 2024-06-04
### Changed
- Package changed to `space.kscience`
- Kotlin 2.0
### Fixed
- Use of generated resources for Wasm

View File

@ -9,7 +9,6 @@ This repository is a work-in-progress implementation of Map-with-markers compone
### [demo](demo) ### [demo](demo)
> >
>
> **Maturity**: EXPERIMENTAL > **Maturity**: EXPERIMENTAL
### [maps-kt-compose](maps-kt-compose) ### [maps-kt-compose](maps-kt-compose)
@ -34,17 +33,14 @@ This repository is a work-in-progress implementation of Map-with-markers compone
### [maps-kt-features](maps-kt-features) ### [maps-kt-features](maps-kt-features)
> >
>
> **Maturity**: EXPERIMENTAL > **Maturity**: EXPERIMENTAL
### [maps-kt-geojson](maps-kt-geojson) ### [maps-kt-geojson](maps-kt-geojson)
> >
>
> **Maturity**: EXPERIMENTAL > **Maturity**: EXPERIMENTAL
### [maps-kt-scheme](maps-kt-scheme) ### [maps-kt-scheme](maps-kt-scheme)
> >
>
> **Maturity**: EXPERIMENTAL > **Maturity**: EXPERIMENTAL
### [trajectory-kt](trajectory-kt) ### [trajectory-kt](trajectory-kt)
@ -54,20 +50,20 @@ This repository is a work-in-progress implementation of Map-with-markers compone
### [demo/maps](demo/maps) ### [demo/maps](demo/maps)
> >
> **Maturity**: EXPERIMENTAL
### [demo/maps-wasm](demo/maps-wasm)
> >
> **Maturity**: EXPERIMENTAL > **Maturity**: EXPERIMENTAL
### [demo/polygon-editor](demo/polygon-editor) ### [demo/polygon-editor](demo/polygon-editor)
> >
>
> **Maturity**: EXPERIMENTAL > **Maturity**: EXPERIMENTAL
### [demo/scheme](demo/scheme) ### [demo/scheme](demo/scheme)
> >
>
> **Maturity**: EXPERIMENTAL > **Maturity**: EXPERIMENTAL
### [demo/trajectory-playground](demo/trajectory-playground) ### [demo/trajectory-playground](demo/trajectory-playground)
> >
>
> **Maturity**: EXPERIMENTAL > **Maturity**: EXPERIMENTAL

View File

@ -1,4 +1,3 @@
import space.kscience.gradle.isInDevelopment
import space.kscience.gradle.useApache2Licence import space.kscience.gradle.useApache2Licence
import space.kscience.gradle.useSPCTeam import space.kscience.gradle.useSPCTeam
@ -6,16 +5,15 @@ plugins {
id("space.kscience.gradle.project") id("space.kscience.gradle.project")
} }
val kmathVersion: String by extra("0.3.1-dev-RC") val kmathVersion: String by extra("0.4.0")
allprojects { allprojects {
group = "center.sciprog" group = "space.kscience"
version = "0.2.2" version = "0.3.1-dev"
repositories { repositories {
mavenLocal() mavenLocal()
maven("https://repo.kotlin.link") maven("https://repo.kotlin.link")
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
} }
} }
@ -24,20 +22,12 @@ ksciencePublish {
useApache2Licence() useApache2Licence()
useSPCTeam() useSPCTeam()
} }
github("SciProgCentre", "maps-kt") repository("spc","https://maven.sciprog.center/kscience")
space( sonatype("https://oss.sonatype.org")
if (isInDevelopment) {
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
} else {
"https://maven.pkg.jetbrains.space/spc/p/sci/maven"
}
)
sonatype()
} }
subprojects { subprojects {
repositories { repositories {
maven("https://maven.pkg.jetbrains.space/mipt-npm/p/sci/dev")
google() google()
mavenCentral() mavenCentral()
maven("https://repo.kotlin.link") maven("https://repo.kotlin.link")
@ -48,4 +38,3 @@ subprojects {
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md") readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")

4
demo/maps-wasm/README.md Normal file
View File

@ -0,0 +1,4 @@
# Module maps-wasm

View File

@ -0,0 +1,37 @@
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
plugins {
kotlin("multiplatform")
alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
}
//val ktorVersion: String by rootProject.extra
kotlin {
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
}
sourceSets {
commonMain {
dependencies {
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
api(compose.components.resources)
}
}
wasmJsMain {
dependencies {
implementation(projects.mapsKtScheme)
}
}
}
}
compose {
web {
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

View File

@ -0,0 +1,82 @@
@file:OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class)
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.window.CanvasBasedWindow
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
import space.kscience.kmath.geometry.Angle
import space.kscience.maps.features.FeatureGroup
import space.kscience.maps.features.ViewConfig
import space.kscience.maps.features.ViewPoint
import space.kscience.maps.features.color
import space.kscience.maps.scheme.*
import space.kscience.maps_wasm.generated.resources.Res
import space.kscience.maps_wasm.generated.resources.middle_earth
@Composable
fun App() {
val scope = rememberCoroutineScope()
val features: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) {
background(1600f, 1200f) {
painterResource(Res.drawable.middle_earth)
}
circle(410.52737 to 868.7676).color(Color.Blue)
text(410.52737 to 868.7676, "Shire").color(Color.Blue)
circle(1132.0881 to 394.99127).color(Color.Red)
text(1132.0881 to 394.99127, "Ordruin").color(Color.Red)
arc(center = 1132.0881 to 394.99127, radius = 20f, startAngle = Angle.zero, Angle.piTimes2)
//circle(410.52737 to 868.7676, id = "hobbit")
scope.launch {
var t = 0.0
while (isActive) {
val x = 410.52737 + t * (1132.0881 - 410.52737)
val y = 868.7676 + t * (394.99127 - 868.7676)
circle(x to y, id = "hobbit").color(Color.Green)
delay(100)
t += 0.005
if (t >= 1.0) t = 0.0
}
}
}
val initialViewPoint: ViewPoint<XY> = remember {
features.getBoundingBox(1f)?.computeViewPoint() ?: XYViewPoint(XY(0f, 0f))
}
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
val mapState: XYCanvasState = XYCanvasState.remember(
ViewConfig(
onClick = { _, click ->
println("${click.focus.x}, ${click.focus.y}")
},
onViewChange = { viewPoint = this }
),
initialViewPoint = initialViewPoint,
)
SchemeView(
mapState,
features,
)
}
fun main() {
// renderComposable(rootElementId = "root") {
CanvasBasedWindow("Maps demo", canvasElementId = "ComposeTarget") {
App()
}
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Compose App</title>
<script type="application/javascript" src="skiko.js"></script>
<script type="application/javascript" src="maps-wasm.js"></script>
</head>
<body>
<canvas id="ComposeTarget"></canvas>
</body>
</html>

View File

@ -2,7 +2,8 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins { plugins {
kotlin("multiplatform") kotlin("multiplatform")
id("org.jetbrains.compose") alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
} }
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra
@ -17,7 +18,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

@ -13,11 +13,6 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.attributes.Attributes
import center.sciprog.maps.compose.*
import center.sciprog.maps.coordinates.*
import center.sciprog.maps.features.*
import center.sciprog.maps.geojson.geoJson
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -27,15 +22,22 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.attributes.Attributes
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.degrees import space.kscience.kmath.geometry.degrees
import space.kscience.kmath.geometry.radians import space.kscience.kmath.geometry.radians
import space.kscience.maps.compose.*
import space.kscience.maps.coordinates.GeodeticMapCoordinates
import space.kscience.maps.coordinates.Gmc
import space.kscience.maps.coordinates.kilometers
import space.kscience.maps.features.*
import space.kscience.maps.geojson.geoJson
import java.nio.file.Path import java.nio.file.Path
import kotlin.math.PI import kotlin.math.PI
import kotlin.random.Random import kotlin.random.Random
public fun GeodeticMapCoordinates.toShortString(): String = public fun GeodeticMapCoordinates.toShortString(): String =
"${(latitude.degrees).toString().take(6)}:${(longitude.degrees).toString().take(6)}" "${(latitude.toDegrees().value).toString().take(6)}:${(longitude.toDegrees().value).toString().take(6)}"
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -55,7 +57,6 @@ fun App() {
val centerCoordinates = MutableStateFlow<Gmc?>(null) val centerCoordinates = MutableStateFlow<Gmc?>(null)
val pointOne = 55.568548 to 37.568604 val pointOne = 55.568548 to 37.568604
val pointTwo = 55.929444 to 37.518434 val pointTwo = 55.929444 to 37.518434
// val pointThree = 60.929444 to 37.518434 // val pointThree = 60.929444 to 37.518434
@ -71,14 +72,17 @@ fun App() {
) { ) {
geoJson(javaClass.getResource("/moscow.geo.json")!!) geoJson(javaClass.getResource("/moscow.geo.json")!!)
.modifyAttribute(ColorAttribute, Color.Blue) .color(Color.Blue)
.modifyAttribute(AlphaAttribute, 0.4f) .modifyAttribute(AlphaAttribute, 0.4f)
icon(pointOne, Icons.Filled.Home) icon(pointOne, Icons.Filled.Home)
val marker1 = rectangle(55.744 to 38.614, size = DpSize(10.dp, 10.dp)).color(Color.Magenta) val marker1 = rectangle(55.744 to 38.614, size = DpSize(10.dp, 10.dp))
val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta) .color(Color.Magenta)
val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta) val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp))
.color(Color.Magenta)
val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp))
.color(Color.Magenta)
draggableLine(marker1, marker2, id = "line 1").color(Color.Red).onClick { draggableLine(marker1, marker2, id = "line 1").color(Color.Red).onClick {
println("line 1 clicked") println("line 1 clicked")
@ -90,6 +94,7 @@ fun App() {
println("line 3 clicked") println("line 3 clicked")
} }
multiLine( multiLine(
points = listOf( points = listOf(
55.742465 to 37.615812, 55.742465 to 37.615812,
@ -101,7 +106,19 @@ fun App() {
), ),
) )
//remember feature ID // points(
// points = listOf(
// 55.744 to 38.614,
// 55.8 to 38.5,
// 56.0 to 38.5,
// )
// ).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 ref
val circleId = circle( val circleId = circle(
centerCoordinates = pointTwo, centerCoordinates = pointTwo,
) )
@ -120,9 +137,11 @@ fun App() {
arc(pointOne, 10.0.kilometers, (PI / 4).radians, -Angle.pi / 2) arc(pointOne, 10.0.kilometers, (PI / 4).radians, -Angle.pi / 2)
line(pointOne, pointTwo, id = "line") line(pointOne, pointTwo, id = "line")
text(pointOne, "Home", font = { size = 32f }) text(pointOne, "Home", font = { size = 32f })
pixelMap( pixelMap(
space.Rectangle( space.Rectangle(
Gmc(latitude = 55.58461879539754.degrees, longitude = 37.8746197303493.degrees), Gmc(latitude = 55.58461879539754.degrees, longitude = 37.8746197303493.degrees),
@ -132,10 +151,11 @@ fun App() {
0.005.degrees 0.005.degrees
) { gmc -> ) { gmc ->
Color( Color(
red = ((gmc.latitude + Angle.piDiv2).degrees*10 % 1f).toFloat(), red = ((gmc.latitude + Angle.piDiv2).toDegrees().value * 10 % 1f).toFloat(),
green = ((gmc.longitude + Angle.pi).degrees*10 % 1f).toFloat(), green = ((gmc.longitude + Angle.pi).toDegrees().value * 10 % 1f).toFloat(),
blue = 0f blue = 0f,
).copy(alpha = 0.3f) alpha = 0.3f
)
} }
centerCoordinates.filterNotNull().onEach { centerCoordinates.filterNotNull().onEach {
@ -159,6 +179,7 @@ fun App() {
} }
} }
} }
// println(toPrettyString())
} }
} }
} }

View File

@ -2,7 +2,8 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins { plugins {
kotlin("multiplatform") kotlin("multiplatform")
id("org.jetbrains.compose") alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
} }
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra

View File

@ -9,11 +9,11 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.input.pointer.isSecondaryPressed import androidx.compose.ui.input.pointer.isSecondaryPressed
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.maps.features.* import space.kscience.maps.features.*
import center.sciprog.maps.scheme.SchemeView import space.kscience.maps.scheme.SchemeView
import center.sciprog.maps.scheme.XY import space.kscience.maps.scheme.XY
import center.sciprog.maps.scheme.XYCoordinateSpace import space.kscience.maps.scheme.XYCanvasState
import center.sciprog.maps.scheme.XYViewScope import space.kscience.maps.scheme.XYCoordinateSpace
@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

@ -2,7 +2,8 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins { plugins {
kotlin("multiplatform") kotlin("multiplatform")
id("org.jetbrains.compose") alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
} }
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra
@ -26,6 +27,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

@ -8,18 +8,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.maps.features.FeatureGroup
import center.sciprog.maps.features.ViewConfig
import center.sciprog.maps.features.ViewPoint
import center.sciprog.maps.features.color
import center.sciprog.maps.scheme.*
import center.sciprog.maps.svg.FeatureStateSnapshot
import center.sciprog.maps.svg.exportToSvg
import center.sciprog.maps.svg.snapshot
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.maps.features.FeatureGroup
import space.kscience.maps.features.ViewConfig
import space.kscience.maps.features.ViewPoint
import space.kscience.maps.features.color
import space.kscience.maps.scheme.*
import space.kscience.maps.svg.FeatureStateSnapshot
import space.kscience.maps.svg.exportToSvg
import space.kscience.maps.svg.snapshot
import java.awt.Desktop import java.awt.Desktop
import java.nio.file.Files import java.nio.file.Files
@ -29,7 +29,7 @@ fun App() {
MaterialTheme { MaterialTheme {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val schemeFeaturesState: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) { val features: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) {
background(1600f, 1200f) { painterResource("middle-earth.jpg") } background(1600f, 1200f) { painterResource("middle-earth.jpg") }
circle(410.52737 to 868.7676).color(Color.Blue) circle(410.52737 to 868.7676).color(Color.Blue)
text(410.52737 to 868.7676, "Shire").color(Color.Blue) text(410.52737 to 868.7676, "Shire").color(Color.Blue)
@ -53,7 +53,7 @@ fun App() {
} }
val initialViewPoint: ViewPoint<XY> = remember { val initialViewPoint: ViewPoint<XY> = remember {
schemeFeaturesState.getBoundingBox(1f)?.computeViewPoint() ?: XYViewPoint(XY(0f, 0f)) features.getBoundingBox(1f)?.computeViewPoint() ?: XYViewPoint(XY(0f, 0f))
} }
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) } var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
@ -61,7 +61,7 @@ fun App() {
var snapshot: FeatureStateSnapshot<XY>? by remember { mutableStateOf(null) } var snapshot: FeatureStateSnapshot<XY>? by remember { mutableStateOf(null) }
if (snapshot == null) { if (snapshot == null) {
snapshot = schemeFeaturesState.snapshot() snapshot = features.snapshot()
} }
ContextMenuArea( ContextMenuArea(
@ -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}")
@ -90,7 +90,7 @@ fun App() {
SchemeView( SchemeView(
mapState, mapState,
schemeFeaturesState, features,
) )
} }

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 space.kscience.maps.features.*
import space.kscience.maps.scheme.*
import space.kscience.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 KiB

View File

@ -1,6 +1,7 @@
plugins { plugins {
kotlin("multiplatform") kotlin("multiplatform")
id("org.jetbrains.compose") alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
} }
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra

View File

@ -8,17 +8,17 @@ import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.maps.features.*
import center.sciprog.maps.scheme.SchemeView
import center.sciprog.maps.scheme.XY
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.Circle2D import space.kscience.kmath.geometry.Vector2D
import space.kscience.kmath.geometry.DoubleVector2D import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.Euclidean2DSpace import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.maps.features.*
import space.kscience.maps.scheme.SchemeView
import space.kscience.maps.scheme.XY
import space.kscience.trajectory.* import space.kscience.trajectory.*
import kotlin.random.Random import kotlin.random.Random
private fun DoubleVector2D.toXY() = XY(x.toFloat(), y.toFloat()) private fun Vector2D<out Number>.toXY() = XY(x.toFloat(), y.toFloat())
private val random = Random(123) private val random = Random(123)
@ -32,7 +32,7 @@ fun FeatureGroup<XY>.trajectory(
bCoordinates = trajectory.end.toXY(), bCoordinates = trajectory.end.toXY(),
).color(colorPicker(trajectory)) ).color(colorPicker(trajectory))
is CircleTrajectory2D -> with(Euclidean2DSpace) { is CircleTrajectory2D -> with(Float64Space2D) {
val topLeft = trajectory.circle.center + vector(-trajectory.circle.radius, trajectory.circle.radius) val topLeft = trajectory.circle.center + vector(-trajectory.circle.radius, trajectory.circle.radius)
val bottomRight = trajectory.circle.center + vector(trajectory.circle.radius, -trajectory.circle.radius) val bottomRight = trajectory.circle.center + vector(trajectory.circle.radius, -trajectory.circle.radius)
@ -59,20 +59,20 @@ fun FeatureGroup<XY>.obstacle(obstacle: Obstacle, colorPicker: (Trajectory2D) ->
polygon(obstacle.arcs.map { it.center.toXY() }).color(Color.Gray) polygon(obstacle.arcs.map { it.center.toXY() }).color(Color.Gray)
} }
fun FeatureGroup<XY>.pose(pose2D: Pose2D) = with(Euclidean2DSpace) { fun FeatureGroup<XY>.pose(pose2D: Pose2D) = with(Float64Space2D) {
line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY()) line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY())
} }
@Composable @Composable
@Preview @Preview
fun closePoints() { fun closePoints() = with(Float64Space2D){
SchemeView { SchemeView {
val obstacle = Obstacle( val obstacle = Obstacle(
Circle2D(Euclidean2DSpace.vector(0.0, 0.0), 1.0), Circle2D(vector(0.0, 0.0), 1.0),
Circle2D(Euclidean2DSpace.vector(0.0, 1.0), 1.0), Circle2D(vector(0.0, 1.0), 1.0),
Circle2D(Euclidean2DSpace.vector(1.0, 1.0), 1.0), Circle2D(vector(1.0, 1.0), 1.0),
Circle2D(Euclidean2DSpace.vector(1.0, 0.0), 1.0) Circle2D(vector(1.0, 0.0), 1.0)
) )
val enter = Pose2D(-0.8, -0.8, Angle.pi) val enter = Pose2D(-0.8, -0.8, Angle.pi)
val exit = Pose2D(-0.8, -0.8, Angle.piDiv2) val exit = Pose2D(-0.8, -0.8, Angle.piDiv2)
@ -101,7 +101,7 @@ fun closePoints() {
@Preview @Preview
fun singleObstacle() { fun singleObstacle() {
SchemeView { SchemeView {
val obstacle = Obstacle(Circle2D(Euclidean2DSpace.vector(7.0, 1.0), 5.0)) val obstacle = Obstacle(Circle2D(Float64Space2D.vector(7.0, 1.0), 5.0))
val enter = Pose2D(-5, -1, Angle.pi / 4) val enter = Pose2D(-5, -1, Angle.pi / 4)
val exit = Pose2D(20, 4, Angle.pi * 3 / 4) val exit = Pose2D(20, 4, Angle.pi * 3 / 4)
@ -123,19 +123,19 @@ fun singleObstacle() {
@Composable @Composable
@Preview @Preview
fun doubleObstacle() { fun doubleObstacle() = with(Float64Space2D){
SchemeView { SchemeView {
val obstacles = arrayOf( val obstacles = arrayOf(
Obstacle( Obstacle(
Circle2D(Euclidean2DSpace.vector(1.0, 6.5), 0.5), Circle2D(vector(1.0, 6.5), 0.5),
Circle2D(Euclidean2DSpace.vector(2.0, 1.0), 0.5), Circle2D(vector(2.0, 1.0), 0.5),
Circle2D(Euclidean2DSpace.vector(6.0, 0.0), 0.5), Circle2D(vector(6.0, 0.0), 0.5),
Circle2D(Euclidean2DSpace.vector(5.0, 5.0), 0.5) Circle2D(vector(5.0, 5.0), 0.5)
), Obstacle( ), Obstacle(
Circle2D(Euclidean2DSpace.vector(10.0, 1.0), 0.5), Circle2D(vector(10.0, 1.0), 0.5),
Circle2D(Euclidean2DSpace.vector(16.0, 0.0), 0.5), Circle2D(vector(16.0, 0.0), 0.5),
Circle2D(Euclidean2DSpace.vector(14.0, 6.0), 0.5), Circle2D(vector(14.0, 6.0), 0.5),
Circle2D(Euclidean2DSpace.vector(9.0, 4.0), 0.5) Circle2D(vector(9.0, 4.0), 0.5)
) )
) )
@ -157,6 +157,14 @@ fun doubleObstacle() {
} }
} }
@Composable
@Preview
fun singleElement() {
SchemeView {
points(listOf(XY(1f,1f)))
}
}
@Composable @Composable
@Preview @Preview
@ -165,6 +173,7 @@ fun playground() {
"Close starting points", "Close starting points",
"Single obstacle", "Single obstacle",
"Two obstacles", "Two obstacles",
"Single element"
) )
var currentExample by remember { mutableStateOf(examples.first()) } var currentExample by remember { mutableStateOf(examples.first()) }
@ -182,6 +191,7 @@ fun playground() {
examples[0] -> closePoints() examples[0] -> closePoints()
examples[1] -> singleObstacle() examples[1] -> singleObstacle()
examples[2] -> doubleObstacle() examples[2] -> doubleObstacle()
examples[3] -> singleElement()
} }
} }
} }

View File

@ -1,10 +1,5 @@
kotlin.code.style=official kotlin.code.style=official
compose.version=1.4.0
agp.version=7.4.2
android.useAndroidX=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.15.4-kotlin-2.0.0

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -7,19 +7,8 @@ The core interfaces of KMath.
## Artifact: ## Artifact:
The Maven coordinates of this project are `center.sciprog:maps-kt-compose:0.2.2`. The Maven coordinates of this project are `space.kscience:maps-kt-compose:0.3.0`.
**Gradle Groovy:**
```groovy
repositories {
maven { url 'https://repo.kotlin.link' }
mavenCentral()
}
dependencies {
implementation 'center.sciprog:maps-kt-compose:0.2.2'
}
```
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
repositories { repositories {
@ -28,6 +17,6 @@ repositories {
} }
dependencies { dependencies {
implementation("center.sciprog:maps-kt-compose:0.2.2") implementation("space.kscience:maps-kt-compose:0.3.0")
} }
``` ```

View File

@ -1,35 +1,33 @@
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.mpp")
id("org.jetbrains.compose") alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
// id("com.android.library")
`maven-publish` `maven-publish`
} }
kscience{ kscience {
jvm() jvm()
} wasm()
kotlin { useCoroutines()
sourceSets {
commonMain { commonMain{
dependencies {
api(projects.mapsKtCore) api(projects.mapsKtCore)
api(projects.mapsKtFeatures) api(projects.mapsKtFeatures)
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.github.microutils:kotlin-logging:2.1.23")
} }
jvmMain{
api("io.ktor:ktor-client-cio")
} }
val jvmTest by getting { jvmTest{
dependencies {
implementation("io.ktor:ktor-client-cio") implementation("io.ktor:ktor-client-cio")
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
implementation(spclibs.kotlinx.coroutines.test) implementation(spclibs.kotlinx.coroutines.test)
implementation(spclibs.logback.classic) implementation(spclibs.logback.classic)
} }
}
}
} }
readme { readme {
@ -41,3 +39,7 @@ readme {
id = "osm", id = "osm",
) { "OpenStreetMap tile provider." } ) { "OpenStreetMap tile provider." }
} }
//tasks.getByName<Copy>("downloadWix"){
// duplicatesStrategy = DuplicatesStrategy.WARN
//}

View File

@ -1,70 +0,0 @@
package center.sciprog.maps.compose
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.*
@Composable
public expect fun MapView(
viewScope: MapViewScope,
features: FeatureGroup<Gmc>,
modifier: Modifier = Modifier.fillMaxSize(),
)
/**
* A builder for a Map with static features.
*/
@Composable
public fun MapView(
mapTileProvider: MapTileProvider,
features: FeatureGroup<Gmc>,
initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null,
config: ViewConfig<Gmc> = ViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(),
) {
val mapState: MapViewScope = MapViewScope.remember(
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,
* 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 MapView(
mapTileProvider: MapTileProvider,
initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null,
config: ViewConfig<Gmc> = ViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: FeatureGroup<Gmc>.() -> Unit = {},
) {
val featureState = FeatureGroup.remember(WebMercatorSpace, buildFeatures)
val mapState: MapViewScope = MapViewScope.remember(
mapTileProvider,
config,
initialViewPoint = initialViewPoint,
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox(
WebMercatorSpace,
Float.MAX_VALUE
),
)
MapView(mapState, featureState, modifier)
}

View File

@ -1,10 +1,10 @@
package center.sciprog.maps.compose package space.kscience.maps.compose
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.Rectangle
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.abs import space.kscience.kmath.geometry.abs
import space.kscience.maps.coordinates.GeodeticMapCoordinates
import space.kscience.maps.coordinates.Gmc
import space.kscience.maps.features.Rectangle
internal fun Angle.isBetween(a: Angle, b: Angle) = this in a..b || this in b..a internal fun Angle.isBetween(a: Angle, b: Angle) = this in a..b || this in b..a

View File

@ -1,4 +1,6 @@
package center.sciprog.maps.compose @file:Suppress("DEPRECATION")
package space.kscience.maps.compose
import kotlin.jvm.Synchronized import kotlin.jvm.Synchronized

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.compose package space.kscience.maps.compose
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -6,18 +6,19 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.MercatorProjection
import center.sciprog.maps.coordinates.WebMercatorCoordinates
import center.sciprog.maps.coordinates.WebMercatorProjection
import center.sciprog.maps.features.*
import space.kscience.kmath.geometry.radians import space.kscience.kmath.geometry.radians
import space.kscience.maps.coordinates.Gmc
import space.kscience.maps.coordinates.MercatorProjection
import space.kscience.maps.coordinates.WebMercatorCoordinates
import space.kscience.maps.coordinates.WebMercatorProjection
import space.kscience.maps.features.*
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
@ -60,10 +61,10 @@ public class MapViewScope internal constructor(
override fun computeViewPoint(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> { override fun computeViewPoint(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> {
val zoom = log2( val zoom = log2(
min( min(
canvasSize.width.value / rectangle.longitudeDelta.radians, canvasSize.width.value / rectangle.longitudeDelta.toRadians().value,
canvasSize.height.value / rectangle.latitudeDelta.radians canvasSize.height.value / rectangle.latitudeDelta.toRadians().value
) * 2 * PI / mapTileProvider.tileSize ) * 2 * PI / mapTileProvider.tileSize
) ).coerceIn(0.0..22.0)
return space.ViewPoint(rectangle.center, zoom.toFloat()) return space.ViewPoint(rectangle.center, zoom.toFloat())
} }
@ -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

@ -1,4 +1,4 @@
package center.sciprog.maps.compose package space.kscience.maps.compose
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred

View File

@ -0,0 +1,149 @@
package space.kscience.maps.compose
import androidx.compose.foundation.layout.fillMaxSize
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.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 io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import org.jetbrains.skia.Image
import space.kscience.maps.coordinates.Gmc
import space.kscience.maps.features.*
import kotlin.math.ceil
import kotlin.math.pow
private fun IntRange.intersect(other: IntRange) = kotlin.math.max(first, other.first)..kotlin.math.min(last, other.last)
private val logger = KotlinLogging.logger("MapView")
/**
* 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
public fun MapView(
mapTileProvider: MapTileProvider,
config: ViewConfig<Gmc>,
features: FeatureGroup<Gmc>,
initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null,
modifier: Modifier,
) {
val mapState = MapCanvasState.remember(mapTileProvider, config, initialViewPoint, initialRectangle)
MapView(mapState, mapTileProvider, features, modifier)
}
/**
* Draw a map using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined,
* 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 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 MapView(
mapTileProvider: MapTileProvider,
config: ViewConfig<Gmc> = ViewConfig(),
initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null,
modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: FeatureGroup<Gmc>.() -> Unit = {},
) {
val featureState = FeatureGroup.remember(WebMercatorSpace, buildFeatures)
val computedRectangle = initialRectangle ?: featureState.getBoundingBox()
MapView(mapTileProvider, config, featureState, initialViewPoint, computedRectangle, modifier)
}

View File

@ -1,9 +1,9 @@
package center.sciprog.maps.compose package space.kscience.maps.compose
import center.sciprog.maps.coordinates.GeodeticMapCoordinates import space.kscience.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.Gmc import space.kscience.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.WebMercatorProjection import space.kscience.maps.coordinates.WebMercatorProjection
import center.sciprog.maps.features.ViewPoint import space.kscience.maps.features.ViewPoint
/** /**
* Observable position on the map. Includes observation coordinate and [zoom] factor * Observable position on the map. Includes observation coordinate and [zoom] factor

View File

@ -1,14 +1,14 @@
package center.sciprog.maps.compose package space.kscience.maps.compose
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.maps.coordinates.*
import center.sciprog.maps.features.CoordinateSpace
import center.sciprog.maps.features.Rectangle
import center.sciprog.maps.features.ViewPoint
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.radians import space.kscience.kmath.geometry.radians
import space.kscience.maps.coordinates.*
import space.kscience.maps.features.CoordinateSpace
import space.kscience.maps.features.Rectangle
import space.kscience.maps.features.ViewPoint
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.pow import kotlin.math.pow
@ -19,7 +19,8 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
private fun tileScale(zoom: Float): Float = 2f.pow(zoom - floor(zoom)) private fun tileScale(zoom: Float): Float = 2f.pow(zoom - floor(zoom))
override fun Rectangle(first: Gmc, second: Gmc): Rectangle<Gmc> = GmcRectangle(first, second) override fun Rectangle(first: Gmc, second: Gmc): Rectangle<Gmc> =
space.kscience.maps.compose.GmcRectangle(first, second)
override fun Rectangle(center: Gmc, zoom: Float, size: DpSize): Rectangle<Gmc> { override fun Rectangle(center: Gmc, zoom: Float, size: DpSize): Rectangle<Gmc> {
val scale = WebMercatorProjection.scaleFactor(zoom) val scale = WebMercatorProjection.scaleFactor(zoom)
@ -62,7 +63,10 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
val maxLat = maxOf { it.top } val maxLat = maxOf { it.top }
val minLong = minOf { it.left } val minLong = minOf { it.left }
val maxLong = maxOf { it.right } val maxLong = maxOf { it.right }
return GmcRectangle(Gmc.normalized(minLat, minLong), Gmc.normalized(maxLat, maxLong)) return space.kscience.maps.compose.GmcRectangle(
Gmc.normalized(minLat, minLong),
Gmc.normalized(maxLat, maxLong)
)
} }
override fun Collection<Gmc>.wrapPoints(): Rectangle<Gmc>? { override fun Collection<Gmc>.wrapPoints(): Rectangle<Gmc>? {
@ -72,7 +76,10 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
val maxLat = maxOf { it.latitude } val maxLat = maxOf { it.latitude }
val minLong = minOf { it.longitude } val minLong = minOf { it.longitude }
val maxLong = maxOf { it.longitude } val maxLong = maxOf { it.longitude }
return GmcRectangle(Gmc.normalized(minLat, minLong), Gmc.normalized(maxLat, maxLong)) return space.kscience.maps.compose.GmcRectangle(
Gmc.normalized(minLat, minLong),
Gmc.normalized(maxLat, maxLong)
)
} }
override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset { override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset {
@ -132,5 +139,5 @@ public fun CoordinateSpace<Gmc>.Rectangle(
center.latitude + (height / 2), center.latitude + (height / 2),
center.longitude + (width / 2) center.longitude + (width / 2)
) )
return GmcRectangle(a, b) return space.kscience.maps.compose.GmcRectangle(a, b)
} }

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.compose package space.kscience.maps.compose
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
@ -6,12 +6,10 @@ 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 org.jetbrains.skia.Font
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.GmcCurve
import center.sciprog.maps.features.*
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.maps.coordinates.*
import space.kscience.maps.features.*
import kotlin.math.ceil import kotlin.math.ceil
@ -55,6 +53,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 +96,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,
@ -110,7 +140,7 @@ public fun FeatureGroup<Gmc>.icon(
public fun FeatureGroup<Gmc>.text( public fun FeatureGroup<Gmc>.text(
position: Pair<Double, Double>, position: Pair<Double, Double>,
text: String, text: String,
font: FeatureFont.() -> Unit = { size = 16f }, font: Font.() -> Unit = { size = 16f },
id: String? = null, id: String? = null,
): FeatureRef<Gmc, TextFeature<Gmc>> = feature( ): FeatureRef<Gmc, TextFeature<Gmc>> = feature(
id, id,

View File

@ -0,0 +1,91 @@
package center.sciprog.maps.compose
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.readBytes
import io.ktor.http.Url
import io.ktor.util.decodeBase64Bytes
import io.ktor.util.encodeBase64
import kotlinx.browser.window
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import org.jetbrains.skia.Image
import org.w3c.dom.Storage
/**
* A [MapTileProvider] based on Open Street Map API. With in-memory and file cache
*/
public class OpenStreetMapTileProvider(
private val client: HttpClient,
private val storage: Storage = window.localStorage,
parallelism: Int = 4,
cacheCapacity: Int = 200,
private val osmBaseUrl: String = "https://tile.openstreetmap.org",
) : MapTileProvider {
private val semaphore = Semaphore(parallelism)
private val cache = LruCache<TileId, Deferred<Image>>(cacheCapacity)
private fun TileId.osmUrl() = Url("$osmBaseUrl/${zoom}/${i}/${j}.png")
private fun TileId.imageName() = "${zoom}/${i}/${j}.png"
private fun TileId.readImage() = storage.getItem(imageName())
/**
* Download and cache the tile image
*/
private fun CoroutineScope.downloadImageAsync(id: TileId): Deferred<Image> = async {
id.readImage()?.let { imageString ->
try {
return@async Image.makeFromEncoded(imageString.decodeBase64Bytes())
} catch (ex: Exception) {
logger.debug { "Failed to load image from $imageString" }
storage.removeItem(id.imageName())
}
}
//semaphore works only for actual download
semaphore.withPermit {
val url = id.osmUrl()
val byteArray = client.get(url).readBytes()
logger.debug { "Finished downloading map tile with id $id from $url" }
val imageName = id.imageName()
logger.debug { "Caching map tile $id to $imageName" }
storage.setItem(imageName, byteArray.encodeBase64())
Image.makeFromEncoded(byteArray)
}
}
override fun CoroutineScope.loadTileAsync(
tileId: TileId,
): Deferred<MapTile> {
//start image download
val imageDeferred: Deferred<Image> = cache.getOrPut(tileId) {
downloadImageAsync(tileId)
}
//collect the result asynchronously
return async {
val image: Image = runCatching { imageDeferred.await() }.onFailure {
if (it !is CancellationException) {
logger.error(it) { "Failed to load tile image with id=$tileId" }
}
cache.remove(tileId)
}.getOrThrow()
MapTile(tileId, image)
}
}
public companion object {
private val logger = KotlinLogging.logger("OpenStreetMapCache")
}
}

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,15 +1,12 @@
package center.sciprog.maps.compose package space.kscience.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 {
if(it !is CancellationException) {
logger.error(it) { "Failed to load tile image with id=$tileId" } logger.error(it) { "Failed to load tile image with id=$tileId" }
}
cache.remove(tileId) cache.remove(tileId)
}.getOrThrow() }.getOrThrow()

View File

@ -1,15 +1,13 @@
package center.sciprog.maps.compose package space.kscience.maps.compose
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.nio.file.Files import java.nio.file.Files
import kotlin.test.assertFails import kotlin.test.assertFails
@OptIn(ExperimentalCoroutinesApi::class)
class OsmTileProviderTest { class OsmTileProviderTest {
// @get:Rule // @get:Rule
// val rule = createComposeRule() // val rule = createComposeRule()

View File

@ -9,19 +9,8 @@ The core interfaces of KMath.
## Artifact: ## Artifact:
The Maven coordinates of this project are `center.sciprog:maps-kt-core:0.2.2`. The Maven coordinates of this project are `space.kscience:maps-kt-core:0.3.0`.
**Gradle Groovy:**
```groovy
repositories {
maven { url 'https://repo.kotlin.link' }
mavenCentral()
}
dependencies {
implementation 'center.sciprog:maps-kt-core:0.2.2'
}
```
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
repositories { repositories {
@ -30,6 +19,6 @@ repositories {
} }
dependencies { dependencies {
implementation("center.sciprog:maps-kt-core:0.2.2") implementation("space.kscience:maps-kt-core:0.3.0")
} }
``` ```

View File

@ -8,6 +8,9 @@ val kmathVersion: String by rootProject.extra
kscience{ kscience{
jvm() jvm()
js() js()
native()
wasm()
useSerialization() useSerialization()
dependencies{ dependencies{

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.coordinates package space.kscience.maps.coordinates
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline

View File

@ -1,10 +1,8 @@
package center.sciprog.maps.coordinates package space.kscience.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

@ -1,4 +1,4 @@
package center.sciprog.maps.coordinates package space.kscience.maps.coordinates
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import space.kscience.kmath.geometry.* import space.kscience.kmath.geometry.*
@ -22,7 +22,6 @@ public class GeodeticMapCoordinates(
"Longitude $longitude is not in (-PI..PI) range" "Longitude $longitude is not in (-PI..PI) range"
} }
} }
override val x: Angle get() = longitude override val x: Angle get() = longitude
override val y: Angle get() = latitude override val y: Angle get() = latitude
@ -43,7 +42,7 @@ public class GeodeticMapCoordinates(
} }
override fun toString(): String { override fun toString(): String {
return "GMC(latitude=${latitude.degrees} deg, longitude=${longitude.degrees} deg)" return "GMC(latitude=${latitude.toDegrees().value} deg, longitude=${longitude.toDegrees().value} deg)"
} }

View File

@ -1,14 +1,14 @@
package center.sciprog.maps.coordinates package space.kscience.maps.coordinates
import space.kscience.kmath.geometry.* import space.kscience.kmath.geometry.*
import kotlin.math.* 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,
@ -64,8 +64,8 @@ public fun GeoEllipsoid.meridianCurve(
} }
return GmcCurve( return GmcCurve(
forward = GmcPose(Gmc.normalized(fromLatitude, longitude), if (up) Angle.zero else Angle.pi), forward = GmcPose(GeodeticMapCoordinates.normalized(fromLatitude, longitude), if (up) Angle.zero else Angle.pi),
backward = GmcPose(Gmc.normalized(toLatitude, longitude), if (up) Angle.pi else Angle.zero), backward = GmcPose(GeodeticMapCoordinates.normalized(toLatitude, longitude), if (up) Angle.pi else Angle.zero),
distance = s distance = s
) )
} }
@ -77,9 +77,9 @@ public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toL
require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" } require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
val right = toLongitude > fromLongitude val right = toLongitude > fromLongitude
return GmcCurve( return GmcCurve(
forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) Angle.piDiv2 else -Angle.piDiv2), forward = GmcPose(GeodeticMapCoordinates.normalized(latitude, fromLongitude), if (right) Angle.piDiv2 else -Angle.piDiv2),
backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -Angle.piDiv2 else Angle.piDiv2), backward = GmcPose(GeodeticMapCoordinates.normalized(latitude, toLongitude), if (right) -Angle.piDiv2 else Angle.piDiv2),
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians) distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).toRadians().value)
) )
} }
@ -193,7 +193,7 @@ public fun GeoEllipsoid.curveInDirection(
val L = lambda - (1 - C) * f * sinAlpha * val L = lambda - (1 - C) * f * sinAlpha *
(sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2))) (sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)))
val endPoint = Gmc.normalized(phi2, start.longitude + L.radians) val endPoint = GeodeticMapCoordinates.normalized(phi2, start.longitude + L.radians)
// eq. 12 // eq. 12

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.coordinates package space.kscience.maps.coordinates
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle

View File

@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
package center.sciprog.maps.coordinates package space.kscience.maps.coordinates
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import space.kscience.kmath.geometry.* import space.kscience.kmath.geometry.*
@ -57,12 +57,12 @@ public open class MercatorProjection(
return if (ellipsoid === GeoEllipsoid.sphere) { return if (ellipsoid === GeoEllipsoid.sphere) {
GeodeticMapCoordinates.ofRadians( GeodeticMapCoordinates.ofRadians(
atan(sinh(pc.y / ellipsoid.equatorRadius)), atan(sinh(pc.y / ellipsoid.equatorRadius)),
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius), baseLongitude.toRadians().value + (pc.x / ellipsoid.equatorRadius),
) )
} else { } else {
GeodeticMapCoordinates.ofRadians( GeodeticMapCoordinates.ofRadians(
cphi2(exp(-(pc.y / ellipsoid.equatorRadius))), cphi2(exp(-(pc.y / ellipsoid.equatorRadius))),
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius) baseLongitude.toRadians().value + (pc.x / ellipsoid.equatorRadius)
) )
} }
} }
@ -76,13 +76,13 @@ public open class MercatorProjection(
return if (ellipsoid === GeoEllipsoid.sphere) { return if (ellipsoid === GeoEllipsoid.sphere) {
ProjectionCoordinates( ProjectionCoordinates(
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians, x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).toRadians().value,
y = ellipsoid.equatorRadius * ln(tan(Angle.pi / 4 + gmc.latitude / 2)) y = ellipsoid.equatorRadius * ln(tan(Angle.pi / 4 + gmc.latitude / 2))
) )
} else { } else {
val sinPhi = sin(gmc.latitude) val sinPhi = sin(gmc.latitude)
ProjectionCoordinates( ProjectionCoordinates(
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians, x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).toRadians().value,
y = ellipsoid.equatorRadius * ln( y = ellipsoid.equatorRadius * ln(
tan(Angle.pi / 4 + gmc.latitude / 2) * ((1 - e * sinPhi) / (1 + e * sinPhi)).pow(e / 2) tan(Angle.pi / 4 + gmc.latitude / 2) * ((1 - e * sinPhi) / (1 + e * sinPhi)).pow(e / 2)
) )

View File

@ -3,10 +3,9 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
package center.sciprog.maps.coordinates package space.kscience.maps.coordinates
import space.kscience.kmath.geometry.abs import space.kscience.kmath.geometry.abs
import space.kscience.kmath.geometry.radians
import kotlin.math.* import kotlin.math.*
public data class WebMercatorCoordinates(val zoom: Int, val x: Float, val y: Float) public data class WebMercatorCoordinates(val zoom: Int, val x: Float, val y: Float)
@ -36,8 +35,8 @@ public object WebMercatorProjection {
val scaleFactor = scaleFactor(zoom.toFloat()) val scaleFactor = scaleFactor(zoom.toFloat())
return WebMercatorCoordinates( return WebMercatorCoordinates(
zoom = zoom, zoom = zoom,
x = scaleFactor * (gmc.longitude.radians + PI).toFloat(), x = scaleFactor * (gmc.longitude.toRadians().value + PI).toFloat(),
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians / 2))).toFloat() y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.toRadians().value / 2))).toFloat()
) )
} }

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.coordinates package space.kscience.maps.coordinates
import space.kscience.kmath.geometry.radians import space.kscience.kmath.geometry.radians
import kotlin.test.Test import kotlin.test.Test
@ -6,8 +6,8 @@ import kotlin.test.assertEquals
internal class DistanceTest { internal class DistanceTest {
companion object { companion object {
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869) val moscow = GeodeticMapCoordinates.ofDegrees(55.76058287719673, 37.60358622841869)
val spb = Gmc.ofDegrees(59.926686023580444, 30.36038109122013) val spb = GeodeticMapCoordinates.ofDegrees(59.926686023580444, 30.36038109122013)
} }
@Test @Test
@ -21,7 +21,7 @@ internal class DistanceTest {
val distance = curve.distance val distance = curve.distance
assertEquals(632.035426877, distance.kilometers, 0.0001) assertEquals(632.035426877, distance.kilometers, 0.0001)
assertEquals(-0.6947937116552751, curve.forward.bearing.radians, 0.0001) assertEquals(-0.6947937116552751, curve.forward.bearing.toRadians().value, 0.0001)
} }
@Test @Test
@ -30,7 +30,7 @@ internal class DistanceTest {
GmcPose(moscow, (-0.6947937116552751).radians), Distance(632.035426877) GmcPose(moscow, (-0.6947937116552751).radians), Distance(632.035426877)
) )
assertEquals(spb.latitude.radians, curve.backward.latitude.radians, 0.0001) assertEquals(spb.latitude.toRadians().value, curve.backward.latitude.toRadians().value, 0.0001)
assertEquals(spb.longitude.radians, curve.backward.longitude.radians, 0.0001) assertEquals(spb.longitude.toRadians().value, curve.backward.longitude.toRadians().value, 0.0001)
} }
} }

View File

@ -1,29 +1,28 @@
package center.sciprog.maps.coordinates package space.kscience.maps.coordinates
import space.kscience.kmath.geometry.degrees
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class MercatorTest { class MercatorTest {
@Test @Test
fun sphereForwardBackward(){ fun sphereForwardBackward(){
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869) val moscow = GeodeticMapCoordinates.ofDegrees(55.76058287719673, 37.60358622841869)
val mercator = MapProjection.epsg3857.toProjection(moscow) val mercator = MapProjection.epsg3857.toProjection(moscow)
//https://epsg.io/transform#s_srs=4326&t_srs=3857&x=37.6035862&y=55.7605829 //https://epsg.io/transform#s_srs=4326&t_srs=3857&x=37.6035862&y=55.7605829
assertEquals(4186.0120709, mercator.x.kilometers, 1e-4) assertEquals(4186.0120709, mercator.x.kilometers, 1e-4)
assertEquals(7510.9013658, mercator.y.kilometers, 1e-4) assertEquals(7510.9013658, mercator.y.kilometers, 1e-4)
val backwards = MapProjection.epsg3857.toGeodetic(mercator) val backwards = MapProjection.epsg3857.toGeodetic(mercator)
assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 1e-6) assertEquals(moscow.latitude.toDegrees().value, backwards.latitude.toDegrees().value, 1e-6)
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6) assertEquals(moscow.longitude.toDegrees().value, backwards.longitude.toDegrees().value, 1e-6)
} }
@Test @Test
fun ellipseForwardBackward(){ fun ellipseForwardBackward(){
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869) val moscow = GeodeticMapCoordinates.ofDegrees(55.76058287719673, 37.60358622841869)
val projection = MercatorProjection(ellipsoid = GeoEllipsoid.WGS84) val projection = MercatorProjection(ellipsoid = GeoEllipsoid.WGS84)
val mercator = projection.toProjection(moscow) val mercator = projection.toProjection(moscow)
val backwards = projection.toGeodetic(mercator) val backwards = projection.toGeodetic(mercator)
assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 1e-6) assertEquals(moscow.latitude.toDegrees().value, backwards.latitude.toDegrees().value, 1e-6)
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6) assertEquals(moscow.longitude.toDegrees().value, backwards.longitude.toDegrees().value, 1e-6)
} }
} }

View File

@ -6,19 +6,8 @@
## Artifact: ## Artifact:
The Maven coordinates of this project are `center.sciprog:maps-kt-features:0.2.2`. The Maven coordinates of this project are `space.kscience:maps-kt-features:0.3.0`.
**Gradle Groovy:**
```groovy
repositories {
maven { url 'https://repo.kotlin.link' }
mavenCentral()
}
dependencies {
implementation 'center.sciprog:maps-kt-features:0.2.2'
}
```
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
repositories { repositories {
@ -27,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("center.sciprog:maps-kt-features:0.2.2") implementation("space.kscience:maps-kt-features:0.3.0")
} }
``` ```

View File

@ -1,30 +1,39 @@
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.mpp")
id("org.jetbrains.compose") alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
`maven-publish` `maven-publish`
} }
val kmathVersion: String by rootProject.extra val kmathVersion: String by rootProject.extra
kscience{ kscience {
jvm() jvm()
js() // js()
useSerialization{ wasm{
browser {
testTask {
enabled = false
}
}
}
useCoroutines()
useSerialization {
json() json()
} }
useSerialization(sourceSet = space.kscience.gradle.DependencySourceSet.TEST){ useSerialization(sourceSet = space.kscience.gradle.DependencySourceSet.TEST) {
protobuf() protobuf()
} }
}
kotlin { commonMain{
sourceSets {
commonMain {
dependencies {
api(projects.trajectoryKt) api(projects.trajectoryKt)
api(compose.runtime)
api(compose.foundation) api(compose.foundation)
} api(compose.material)
} api(compose.ui)
api("io.github.oshai:kotlin-logging:6.0.3")
} }
} }

View File

@ -1,22 +0,0 @@
package center.sciprog.attributes
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
public interface Attribute<T>
public abstract class SerializableAttribute<T>(
public val serialId: String,
public val serializer: KSerializer<T>,
) : Attribute<T> {
override fun toString(): String = serialId
}
public interface AttributeWithDefault<T> : Attribute<T> {
public val default: T
}
public interface SetAttribute<V> : Attribute<Set<V>>
public object NameAttribute : SerializableAttribute<String>("name", String.serializer())

View File

@ -1,74 +0,0 @@
package center.sciprog.attributes
import center.sciprog.maps.features.Feature
import center.sciprog.maps.features.ZAttribute
import kotlin.jvm.JvmInline
@JvmInline
public value class Attributes internal constructor(public val content: Map<out Attribute<*>, Any>) {
public val keys: Set<Attribute<*>> get() = content.keys
@Suppress("UNCHECKED_CAST")
public operator fun <T> get(attribute: Attribute<T>): T? = content[attribute] as? T
override fun toString(): String = "Attributes(value=${content.entries})"
public companion object {
public val EMPTY: Attributes = Attributes(emptyMap())
}
}
public fun Attributes.isEmpty(): Boolean = content.isEmpty()
public fun <T> Attributes.getOrDefault(attribute: AttributeWithDefault<T>): T = get(attribute) ?: attribute.default
public fun <T, A : Attribute<T>> Attributes.withAttribute(
attribute: A,
attrValue: T?,
): Attributes = Attributes(
if (attrValue == null) {
content - attribute
} else {
content + (attribute to attrValue)
}
)
/**
* Add an element to a [SetAttribute]
*/
public fun <T, A : SetAttribute<T>> Attributes.withAttributeElement(
attribute: A,
attrValue: T,
): Attributes {
val currentSet: Set<T> = get(attribute) ?: emptySet()
return Attributes(
content + (attribute to (currentSet + attrValue))
)
}
/**
* Remove an element from [SetAttribute]
*/
public fun <T, A : SetAttribute<T>> Attributes.withoutAttributeElement(
attribute: A,
attrValue: T,
): Attributes {
val currentSet: Set<T> = get(attribute) ?: emptySet()
return Attributes(
content + (attribute to (currentSet - attrValue))
)
}
public fun <T : Any, A : Attribute<T>> Attributes(
attribute: A,
attrValue: T,
): Attributes = Attributes(mapOf(attribute to attrValue))
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,47 +0,0 @@
package center.sciprog.attributes
/**
* A safe builder for [Attributes]
*/
public class AttributesBuilder internal constructor(private val map: MutableMap<Attribute<*>, Any> = mutableMapOf()) {
@Suppress("UNCHECKED_CAST")
public operator fun <T> get(attribute: Attribute<T>): T? = map[attribute] as? T
public operator fun <V> Attribute<V>.invoke(value: V?) {
if (value == null) {
map.remove(this)
} else {
map[this] = value
}
}
public fun from(attributes: Attributes) {
map.putAll(attributes.content)
}
public fun <V> SetAttribute<V>.add(
attrValue: V,
) {
val currentSet: Set<V> = get(this) ?: emptySet()
map[this] = currentSet + attrValue
}
/**
* Remove an element from [SetAttribute]
*/
public fun <V> SetAttribute<V>.remove(
attrValue: V,
) {
val currentSet: Set<V> = get(this) ?: emptySet()
map[this] = currentSet - attrValue
}
public fun build(): Attributes = Attributes(map)
}
public fun AttributesBuilder(
attributes: Attributes,
): AttributesBuilder = AttributesBuilder(attributes.content.toMutableMap())
public fun Attributes(builder: AttributesBuilder.() -> Unit): Attributes = AttributesBuilder().apply(builder).build()

View File

@ -1,5 +0,0 @@
package center.sciprog.maps.features
public expect class FeatureFont {
public var size: Float
}

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.compose package space.kscience.maps.compose
import androidx.compose.foundation.gestures.drag import androidx.compose.foundation.gestures.drag
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -7,7 +7,7 @@ import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.maps.features.* import space.kscience.maps.features.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -16,8 +16,8 @@ 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) {
@ -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,4 +1,4 @@
package center.sciprog.maps.compose package space.kscience.maps.compose
import androidx.compose.foundation.gestures.GestureCancellationException import androidx.compose.foundation.gestures.GestureCancellationException
import androidx.compose.foundation.gestures.PressGestureScope import androidx.compose.foundation.gestures.PressGestureScope
@ -26,32 +26,9 @@ public val PointerEvent.position: Offset get() = firstChange.position
/** /**
* Detects tap, double-tap, and long press gestures and calls [onClick], [onDoubleClick], and * An alternative to [detectTapGestures] with reimplementation of internal logic
* [onLongClick], respectively, when detected. [onPress] is called when the press is detected
* and the [PressGestureScope.tryAwaitRelease] and [PressGestureScope.awaitRelease] can be
* used to detect when pointers have released or the gesture was canceled.
* The first pointer down and final pointer up are consumed, and in the
* case of long press, all changes after the long press is detected are consumed.
*
* Each function parameter receives an [Offset] representing the position relative to the containing
* element. The [Offset] can be outside the actual bounds of the element itself meaning the numbers
* can be negative or larger than the element bounds if the touch target is smaller than the
* [ViewConfiguration.minimumTouchTargetSize].
*
* When [onDoubleClick] is provided, the tap gesture is detected only after
* the [ViewConfiguration.doubleTapMinTimeMillis] has passed and [onDoubleClick] is called if the
* second tap is started before [ViewConfiguration.doubleTapTimeoutMillis]. If [onDoubleClick] is not
* provided, then [onClick] is called when the pointer up has been received.
*
* After the initial [onPress], if the pointer moves out of the input area, the position change
* is consumed, or another gesture consumes the down or up events, the gestures are considered
* canceled. That means [onDoubleClick], [onLongClick], and [onClick] will not be called after a
* gesture has been canceled.
*
* If the first down event is consumed somewhere else, the entire gesture will be skipped,
* including [onPress].
*/ */
public suspend fun PointerInputScope.detectClicks( internal suspend fun PointerInputScope.detectClicks(
onDoubleClick: (Density.(PointerEvent) -> Unit)? = null, onDoubleClick: (Density.(PointerEvent) -> Unit)? = null,
onLongClick: (Density.(PointerEvent) -> Unit)? = null, onLongClick: (Density.(PointerEvent) -> Unit)? = null,
onPress: suspend PressGestureScope.(event: PointerEvent) -> Unit = NoPressGesture, onPress: suspend PressGestureScope.(event: PointerEvent) -> Unit = NoPressGesture,

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.features package space.kscience.maps.features
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -7,10 +7,12 @@ 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 +22,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 +38,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

@ -1,4 +1,4 @@
package center.sciprog.maps.features package space.kscience.maps.features
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.features package space.kscience.maps.features
import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.isPrimaryPressed import androidx.compose.ui.input.pointer.isPrimaryPressed

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.features package space.kscience.maps.features
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
@ -17,8 +17,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
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.Attributes import org.jetbrains.skia.Font
import center.sciprog.attributes.NameAttribute import space.kscience.NameAttribute
import space.kscience.attributes.Attributes
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.nd.Structure2D import space.kscience.kmath.nd.Structure2D
@ -331,7 +332,7 @@ public data class TextFeature<T : Any>(
public val position: T, public val position: T,
public val text: String, public val text: String,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
public val fontConfig: FeatureFont.() -> Unit, public val fontConfig: Font.() -> Unit,
) : DraggableFeature<T> { ) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position) override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position)

View File

@ -0,0 +1,113 @@
package space.kscience.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 io.github.oshai.kotlinlogging.KotlinLogging
import space.kscience.attributes.Attributes
/**
* 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 open 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 ?: error("Text measurer not defined"), text, position)
} catch (ex: Exception) {
logger.error(ex) { "Failed to measure text" }
}
}
override fun painterFor(feature: PainterFeature<T>): Painter =
painterCache[feature] ?: error("Can't resolve painter for $feature")
public companion object {
private val logger = KotlinLogging.logger("ComposeFeatureDrawScope")
}
}
/**
* 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(0)
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 space.kscience.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,9 @@ 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 org.jetbrains.skia.Font
import space.kscience.attributes.Attribute
import space.kscience.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 +33,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 +66,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 +77,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 +107,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 +137,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 +148,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 +158,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 +166,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,8 +253,7 @@ 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,
@ -251,15 +262,14 @@ public fun <T : Any> FeatureGroup<T>.icon(
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)
} }
@ -276,7 +286,7 @@ public fun <T : Any> FeatureGroup<T>.scalableImage(
public fun <T : Any> FeatureGroup<T>.text( public fun <T : Any> FeatureGroup<T>.text(
position: T, position: T,
text: String, text: String,
font: FeatureFont.() -> Unit = { size = 16f }, font: Font.() -> Unit = { size = 16f },
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
id: String? = null, id: String? = null,
): FeatureRef<T, TextFeature<T>> = feature( ): FeatureRef<T, TextFeature<T>> = feature(
@ -284,14 +294,14 @@ public fun <T : Any> FeatureGroup<T>.text(
TextFeature(space, position, text, fontConfig = font, attributes = attributes) TextFeature(space, position, text, fontConfig = font, attributes = attributes)
) )
public fun <T> StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND<T> { //public fun <T> StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND<T> {
val strides = Strides(shape) // val strides = Strides(shape)
return BufferND(strides, Buffer.boxing(strides.linearSize) { initializer(strides.index(it)) }) // return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) })
} //}
public fun <T> Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D<T> { public inline fun <reified T> Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D<T> {
val strides = Strides(ShapeND(rows, columns)) val strides = Strides(ShapeND(rows, columns))
return BufferND(strides, Buffer.boxing(strides.linearSize) { initializer(strides.index(it)) }).as2D() return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }).as2D()
} }
public fun <T : Any> FeatureGroup<T>.pixelMap( public fun <T : Any> FeatureGroup<T>.pixelMap(
@ -303,3 +313,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

@ -1,4 +1,4 @@
package center.sciprog.maps.features package space.kscience.maps.features
import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.isPrimaryPressed import androidx.compose.ui.input.pointer.isPrimaryPressed

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.features package space.kscience.maps.features
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.features package space.kscience.maps.features
/** /**
* @param T type of coordinates used for the view point * @param T type of coordinates used for the view point

View File

@ -1,6 +1,6 @@
package center.sciprog.maps.features package space.kscience.maps.features
import center.sciprog.attributes.Attributes import space.kscience.attributes.Attributes
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
@ -18,9 +18,9 @@ public fun <T : Any> FeatureGroup<T>.draggableLine(
space, space,
aId.resolve().center, aId.resolve().center,
bId.resolve().center, bId.resolve().center,
Attributes { Attributes<FeatureGroup<T>> {
ZAttribute(-10f) ZAttribute(-10f)
lineId?.attributes?.let { from(it) } lineId?.attributes?.let { putAll(it) }
} }
) )
) )
@ -51,9 +51,9 @@ public fun <T : Any> FeatureGroup<T>.draggableMultiLine(
MultiLineFeature( MultiLineFeature(
space, space,
points.map { it.resolve().center }, points.map { it.resolve().center },
Attributes { Attributes<FeatureGroup<T>>{
ZAttribute(-10f) ZAttribute(-10f)
polygonId?.attributes?.let { from(it) } polygonId?.attributes?.let { putAll(it) }
} }
) )
) )

View File

@ -1,37 +1,38 @@
package center.sciprog.maps.features package space.kscience.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.* import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.Path
import center.sciprog.attributes.plus import androidx.compose.ui.graphics.PointMode
import org.jetbrains.skia.Font import androidx.compose.ui.graphics.drawscope.Stroke
import org.jetbrains.skia.Paint import androidx.compose.ui.graphics.drawscope.translate
import space.kscience.attributes.plus
import space.kscience.kmath.PerformancePitfall import space.kscience.kmath.PerformancePitfall
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) //avoid drawing invisible features
if(feature.attributes[VisibleAttribute] == false) return
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(),
center = feature.center.toOffset() center = feature.center.toOffset(),
alpha = alpha
) )
is RectangleFeature -> drawRect( is RectangleFeature -> drawRect(
@ -40,7 +41,8 @@ public fun <T : Any> DrawScope.drawFeature(
feature.size.width.toPx() / 2, feature.size.width.toPx() / 2,
feature.size.height.toPx() / 2 feature.size.height.toPx() / 2
), ),
size = feature.size.toSize() size = feature.size.toSize(),
alpha = alpha
) )
is LineFeature -> drawLine( is LineFeature -> drawLine(
@ -48,7 +50,8 @@ public fun <T : Any> DrawScope.drawFeature(
feature.a.toOffset(), feature.a.toOffset(),
feature.b.toOffset(), feature.b.toOffset(),
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pathEffect = feature.attributes[PathEffectAttribute] pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha
) )
is ArcFeature -> { is ArcFeature -> {
@ -58,8 +61,8 @@ public fun <T : Any> DrawScope.drawFeature(
drawArc( drawArc(
color = color, color = color,
startAngle = (feature.startAngle.degrees).toFloat(), startAngle = (feature.startAngle.toDegrees().value).toFloat(),
sweepAngle = (feature.arcLength.degrees).toFloat(), sweepAngle = (feature.arcLength.toDegrees().value).toFloat(),
useCenter = false, useCenter = false,
topLeft = dpRect.topLeft, topLeft = dpRect.topLeft,
size = size, size = size,
@ -69,28 +72,19 @@ public fun <T : Any> DrawScope.drawFeature(
} }
is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset()) is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset(), alpha = alpha)
is VectorIconFeature -> { is VectorIconFeature -> {
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) }, alpha = alpha)
} }
} }
} }
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()
@ -101,9 +95,11 @@ 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
}) }
)
} }
} }
@ -121,7 +117,7 @@ public fun <T : Any> DrawScope.drawFeature(
drawPoints( drawPoints(
points = points, points = points,
color = color, color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, strokeWidth = feature.attributes[StrokeAttribute] ?: 5f,
pointMode = PointMode.Points, pointMode = PointMode.Points,
pathEffect = feature.attributes[PathEffectAttribute], pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha alpha = alpha
@ -160,8 +156,8 @@ 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, alpha = alpha)
} }
} }
} }
@ -184,7 +180,8 @@ public fun <T : Any> DrawScope.drawFeature(
x = i * xStep, x = i * xStep,
y = rect.height - j * yStep y = rect.height - j * yStep
), ),
size = pixelSize size = pixelSize,
alpha = alpha
) )
} }
} }

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.features package space.kscience.maps.features
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher import androidx.compose.foundation.PointerMatcher
@ -6,13 +6,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import center.sciprog.attributes.Attribute import space.kscience.attributes.*
import center.sciprog.attributes.AttributesBuilder
import center.sciprog.attributes.SetAttribute
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,20 +46,25 @@ 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>.alpha(alpha: Float): FeatureRef<T, F> {
require(alpha in 0f..1f) { "Alpha value must be between 0 and 1" }
return modifyAttribute(AlphaAttribute, alpha)
}
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes(
modification: AttributesBuilder<F>.() -> Unit,
): FeatureRef<T, F> {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
parent.feature( parent.feature(
id, id,
resolve().withAttributes { resolve().withAttributes { modified(modification) } as F
AttributesBuilder(this).apply(modify).build()
} as F
) )
return this return this
} }
public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute( public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(
key: Attribute<V>, key: Attribute<V>,
value: V?, value: V,
): FeatureRef<T, F> { ): FeatureRef<T, F> {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F) parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F)
@ -170,3 +175,6 @@ public object StrokeAttribute : Attribute<Float>
public fun <T : Any, F : LineSegmentFeature<T>> FeatureRef<T, F>.stroke(width: Float): FeatureRef<T, F> = public fun <T : Any, F : LineSegmentFeature<T>> FeatureRef<T, F>.stroke(width: Float): FeatureRef<T, F> =
modifyAttribute(StrokeAttribute, width) modifyAttribute(StrokeAttribute, width)
public fun <T : Any, F : PointsFeature<T>> FeatureRef<T, F>.pointSize(width: Float): FeatureRef<T, F> =
modifyAttribute(StrokeAttribute, width)

View File

@ -1,13 +1,21 @@
package center.sciprog.attributes package center.sciprog.attributes
import kotlinx.serialization.* import kotlinx.serialization.Contextual
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual import kotlinx.serialization.modules.contextual
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import kotlinx.serialization.serializer
import space.kscience.AttributesSerializer
import space.kscience.NameAttribute
import space.kscience.SerializableAttribute
import space.kscience.attributes.Attributes
import kotlin.test.Ignore import kotlin.test.Ignore
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue
internal class AttributesSerializationTest { internal class AttributesSerializationTest {
@ -28,32 +36,36 @@ internal class AttributesSerializationTest {
override fun toString(): String = "test" override fun toString(): String = "test"
} }
val serializer = AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute))
@Test @Test
fun restoreFromJson() { fun restoreFromJson() {
val json = Json { val json = Json {
serializersModule = SerializersModule { serializersModule = SerializersModule {
contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute))) contextual(serializer)
} }
} }
val attributes = Attributes { val attributes = Attributes<Any> {
NameAttribute("myTest") NameAttribute("myTest")
TestAttribute(mapOf("a" to "aa", "b" to "bb")) TestAttribute(mapOf("a" to "aa", "b" to "bb"))
ContainerAttribute( ContainerAttribute(
Container( Container(
Attributes { Attributes<Any> {
TestAttribute(mapOf("a" to "aa", "b" to "bb")) TestAttribute(mapOf("a" to "aa", "b" to "bb"))
} }
) )
) )
} }
val serialized: String = json.encodeToString(attributes)
val serialized: String = json.encodeToString(serializer, attributes)
println(serialized) println(serialized)
val restored: Attributes = json.decodeFromString(serialized) val restored: Attributes = json.decodeFromString(serializer, serialized)
assertEquals(attributes, restored) assertTrue { Attributes.equals(attributes, restored) }
} }
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
@ -62,25 +74,25 @@ internal class AttributesSerializationTest {
fun restoreFromProtoBuf() { fun restoreFromProtoBuf() {
val protoBuf = ProtoBuf { val protoBuf = ProtoBuf {
serializersModule = SerializersModule { serializersModule = SerializersModule {
contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute))) contextual(serializer)
} }
} }
val attributes = Attributes { val attributes = Attributes<Any> {
NameAttribute("myTest") NameAttribute("myTest")
TestAttribute(mapOf("a" to "aa", "b" to "bb")) TestAttribute(mapOf("a" to "aa", "b" to "bb"))
ContainerAttribute( ContainerAttribute(
Container( Container(
Attributes { Attributes<Any> {
TestAttribute(mapOf("a" to "aa", "b" to "bb")) TestAttribute(mapOf("a" to "aa", "b" to "bb"))
} }
) )
) )
} }
val serialized = protoBuf.encodeToByteArray(attributes) val serialized = protoBuf.encodeToByteArray(serializer, attributes)
val restored: Attributes = protoBuf.decodeFromByteArray(serialized) val restored: Attributes = protoBuf.decodeFromByteArray(serializer, serialized)
assertEquals(attributes, restored) assertEquals(attributes, restored)
} }

View File

@ -1,5 +0,0 @@
package center.sciprog.maps.features
public actual class FeatureFont {
public actual var size: Float = 16f
}

View File

@ -1,5 +0,0 @@
package center.sciprog.maps.features
import org.jetbrains.skia.Font
public actual typealias FeatureFont = Font

View File

@ -6,19 +6,8 @@
## Artifact: ## Artifact:
The Maven coordinates of this project are `center.sciprog:maps-kt-geojson:0.2.2`. The Maven coordinates of this project are `space.kscience:maps-kt-geojson:0.3.0`.
**Gradle Groovy:**
```groovy
repositories {
maven { url 'https://repo.kotlin.link' }
mavenCentral()
}
dependencies {
implementation 'center.sciprog:maps-kt-geojson:0.2.2'
}
```
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
repositories { repositories {
@ -27,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("center.sciprog:maps-kt-geojson:0.2.2") implementation("space.kscience:maps-kt-geojson:0.3.0")
} }
``` ```

View File

@ -6,7 +6,9 @@ plugins {
kscience{ kscience{
jvm() jvm()
js() // js()
wasm()
useSerialization { useSerialization {
json() json()
} }

View File

@ -1,10 +1,10 @@
package center.sciprog.maps.geojson package space.kscience.maps.geojson
import center.sciprog.maps.geojson.GeoJson.Companion.PROPERTIES_KEY
import center.sciprog.maps.geojson.GeoJson.Companion.TYPE_KEY
import center.sciprog.maps.geojson.GeoJsonFeatureCollection.Companion.FEATURES_KEY
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import space.kscience.maps.geojson.GeoJson.Companion.PROPERTIES_KEY
import space.kscience.maps.geojson.GeoJson.Companion.TYPE_KEY
import space.kscience.maps.geojson.GeoJsonFeatureCollection.Companion.FEATURES_KEY
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
/** /**

View File

@ -1,10 +1,9 @@
package center.sciprog.maps.geojson package space.kscience.maps.geojson
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.meters
import center.sciprog.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import space.kscience.kmath.geometry.degrees import space.kscience.maps.coordinates.Gmc
import space.kscience.maps.coordinates.meters
import space.kscience.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
public sealed interface GeoJsonGeometry : GeoJson { public sealed interface GeoJsonGeometry : GeoJson {
@ -35,8 +34,8 @@ internal fun JsonElement.toGmc() = jsonArray.run {
} }
internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray { internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray {
add(longitude.degrees) add(longitude.toDegrees().value)
add(latitude.degrees) add(latitude.toDegrees().value)
elevation?.let { elevation?.let {
add(it.meters) add(it.meters)
} }

View File

@ -1,7 +1,7 @@
package center.sciprog.maps.geojson package space.kscience.maps.geojson
import center.sciprog.attributes.SerializableAttribute
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import space.kscience.SerializableAttribute
public object GeoJsonPropertiesAttribute : SerializableAttribute<JsonObject>("properties", serializer()) public object GeoJsonPropertiesAttribute : SerializableAttribute<JsonObject>("properties", serializer())

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.geojson package space.kscience.maps.geojson
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor

View File

@ -1,12 +1,12 @@
package center.sciprog.maps.geojson package space.kscience.maps.geojson
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import center.sciprog.attributes.NameAttribute
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.*
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import space.kscience.NameAttribute
import space.kscience.maps.coordinates.Gmc
import space.kscience.maps.features.*
/** /**

View File

@ -1,11 +1,11 @@
package center.sciprog.maps.geojson package space.kscience.maps.geojson
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.Feature
import center.sciprog.maps.features.FeatureGroup
import center.sciprog.maps.features.FeatureRef
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import space.kscience.maps.coordinates.Gmc
import space.kscience.maps.features.Feature
import space.kscience.maps.features.FeatureGroup
import space.kscience.maps.features.FeatureRef
import java.net.URL import java.net.URL
/** /**

View File

@ -1,9 +1,9 @@
package center.sciprog.maps.geotiff package center.sciprog.maps.geotiff
import center.sciprog.maps.coordinates.Gmc import space.kscience.maps.coordinates.Gmc
import center.sciprog.maps.features.Feature import space.kscience.maps.features.Feature
import center.sciprog.maps.features.FeatureGroup import space.kscience.maps.features.FeatureGroup
import center.sciprog.maps.features.FeatureRef import space.kscience.maps.features.FeatureRef
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import org.geotools.gce.geotiff.GeoTiffReader import org.geotools.gce.geotiff.GeoTiffReader

View File

@ -0,0 +1,31 @@
plugins {
id("space.kscience.gradle.mpp")
id("org.jetbrains.compose")
`maven-publish`
}
kscience{
js {
binaries.executable()
}
}
kotlin {
sourceSets {
val jsMain by getting {
dependencies {
implementation(projects.mapsKtCompose)
implementation(compose.runtime)
implementation(compose.html.core)
implementation(npm("@types/leaflet", "1.9.6"))
}
}
}
}
compose {
experimental.web{
application{}
}
// web{}
}

View File

@ -0,0 +1,7 @@
import org.jetbrains.skiko.wasm.onWasmReady
fun main() {
onWasmReady {
}
}

View File

@ -6,19 +6,8 @@
## Artifact: ## Artifact:
The Maven coordinates of this project are `center.sciprog:maps-kt-scheme:0.2.2`. The Maven coordinates of this project are `space.kscience:maps-kt-scheme:0.3.0`.
**Gradle Groovy:**
```groovy
repositories {
maven { url 'https://repo.kotlin.link' }
mavenCentral()
}
dependencies {
implementation 'center.sciprog:maps-kt-scheme:0.2.2'
}
```
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
repositories { repositories {
@ -27,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("center.sciprog:maps-kt-scheme:0.2.2") implementation("space.kscience:maps-kt-scheme:0.3.0")
} }
``` ```

View File

@ -1,31 +1,25 @@
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.mpp")
id("org.jetbrains.compose") alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
`maven-publish` `maven-publish`
} }
kscience{ kscience{
jvm() jvm()
} // js()
wasm()
kotlin { commonMain{
sourceSets {
commonMain {
dependencies {
api(projects.mapsKtFeatures) api(projects.mapsKtFeatures)
api("io.github.microutils:kotlin-logging:2.1.23")
api(compose.foundation)
} }
} jvmMain{
val jvmMain by getting {
dependencies {
implementation("org.jfree:org.jfree.svg:5.0.4") implementation("org.jfree:org.jfree.svg:5.0.4")
api(compose.desktop.currentOs) api(compose.desktop.currentOs)
} }
}
}
} }
//java { //java {
// targetCompatibility = JVM_TARGET // targetCompatibility = JVM_TARGET
//} //}

View File

@ -1,20 +1,12 @@
package center.sciprog.maps.scheme package space.kscience.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.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.unit.DpSize import androidx.compose.ui.unit.DpSize
import center.sciprog.attributes.z import io.github.oshai.kotlinlogging.KotlinLogging
import center.sciprog.maps.compose.mapControls import space.kscience.maps.compose.canvasControls
import center.sciprog.maps.features.* import space.kscience.maps.features.*
import mu.KotlinLogging
import kotlin.math.min import kotlin.math.min
@ -22,48 +14,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 +45,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 +70,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

@ -1,16 +1,18 @@
package center.sciprog.maps.scheme package space.kscience.maps.scheme
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.features.CoordinateSpace
import center.sciprog.maps.features.Rectangle
import center.sciprog.maps.features.ViewPoint
import space.kscience.kmath.geometry.Vector2D import space.kscience.kmath.geometry.Vector2D
import space.kscience.maps.features.CoordinateSpace
import space.kscience.maps.features.Rectangle
import space.kscience.maps.features.ViewPoint
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min 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,

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.scheme package space.kscience.maps.scheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -6,12 +6,12 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.maps.features.* import space.kscience.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
@ -26,12 +26,15 @@ public class XYViewScope(
) )
override fun computeViewPoint(rectangle: Rectangle<XY>): ViewPoint<XY> { override fun computeViewPoint(rectangle: Rectangle<XY>): ViewPoint<XY> {
val scale = min( val scale: Float = min(
canvasSize.width.value / rectangle.width, canvasSize.width.value / rectangle.width,
canvasSize.height.value / rectangle.height canvasSize.height.value / rectangle.height
) )
return if(scale.isInfinite()){
return XYViewPoint(rectangle.center, scale) XYViewPoint(rectangle.center, 1f)
} else {
XYViewPoint(rectangle.center, scale)
}
} }
override fun ViewPoint<XY>.moveBy(x: Dp, y: Dp): ViewPoint<XY> { override fun ViewPoint<XY>.moveBy(x: Dp, y: Dp): ViewPoint<XY> {
@ -51,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

@ -1,11 +1,11 @@
package center.sciprog.maps.scheme package space.kscience.maps.scheme
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.maps.features.CoordinateSpace import space.kscience.maps.features.CoordinateSpace
import center.sciprog.maps.features.Rectangle import space.kscience.maps.features.Rectangle
import center.sciprog.maps.features.ViewPoint import space.kscience.maps.features.ViewPoint
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.pow import kotlin.math.pow

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.scheme package space.kscience.maps.scheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -8,9 +8,9 @@ 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.Attributes import space.kscience.attributes.Attributes
import center.sciprog.maps.features.*
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.maps.features.*
import kotlin.math.ceil import kotlin.math.ceil
internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat()) internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat())
@ -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

@ -1,187 +0,0 @@
package center.sciprog.maps.svg
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Offset
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 center.sciprog.maps.features.*
import center.sciprog.maps.scheme.*
import org.jfree.svg.SVGGraphics2D
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 val features: Map<String, Feature<T>>,
internal val painterCache: Map<PainterFeature<T>, Painter>,
)
@Composable
public fun <T : Any> FeatureGroup<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
featureMap,
features.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
)
public fun FeatureStateSnapshot<XY>.generateSvg(
viewPoint: ViewPoint<XY>,
width: Double,
height: Double,
id: String? = null,
): String {
fun XY.toOffset(): Offset = Offset(
(width / 2 + (x - viewPoint.focus.x) * viewPoint.zoom).toFloat(),
(height / 2 + (viewPoint.focus.y - y) * viewPoint.zoom).toFloat()
)
fun SvgDrawScope.drawFeature(scale: Float, feature: Feature<XY>) {
val color = feature.color ?: Color.Red
val alpha = feature.attributes[AlphaAttribute] ?: 1f
when (feature) {
is ScalableImageFeature -> {
val offset = XY(feature.rectangle.left, feature.rectangle.top).toOffset()
val backgroundSize = Size(
(feature.rectangle.width * scale),
(feature.rectangle.height * scale)
)
translate(offset.x, offset.y) {
with(painterCache[feature]!!) {
draw(backgroundSize)
}
}
}
is FeatureSelector -> drawFeature(scale, feature.selector(scale))
is CircleFeature -> drawCircle(
color,
feature.radius.toPx(),
center = feature.center.toOffset(),
alpha = alpha
)
is LineFeature -> drawLine(
color,
feature.a.toOffset(),
feature.b.toOffset(),
alpha = alpha
)
is PointsFeature -> {
val points = feature.points.map { it.toOffset() }
drawPoints(
points = points,
color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Points,
pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha
)
}
is MultiLineFeature -> {
val points = feature.points.map { it.toOffset() }
drawPoints(
points = points,
color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Polygon,
pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha
)
}
is ArcFeature -> {
val topLeft = feature.oval.leftTop.toOffset()
val bottomRight = feature.oval.rightBottom.toOffset()
val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
drawArc(
color = color,
startAngle = feature.startAngle.degrees.toFloat(),
sweepAngle = feature.arcLength.degrees.toFloat(),
useCenter = false,
topLeft = topLeft,
size = size,
style = Stroke(),
alpha = alpha
)
}
is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset())
is VectorIconFeature -> {
val offset = feature.center.toOffset()
val imageSize = feature.size.toSize()
translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
with(painterCache[feature]!!) {
draw(imageSize, alpha = alpha)
}
}
}
is TextFeature -> drawIntoCanvas { _ ->
val offset = feature.position.toOffset()
drawText(
feature.text,
offset.x + 5,
offset.y - 5,
java.awt.Font(null, PLAIN, 16),
color
)
}
is DrawFeature -> {
val offset = feature.position.toOffset()
translate(offset.x, offset.y) {
feature.drawFeature(this)
}
}
is FeatureGroup -> {
feature.featureMap.values.forEach {
drawFeature(scale, it)
}
}
}
}
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(width, height)
val svgScope = SvgDrawScope(svgGraphics2D, Size(width.toFloat(), height.toFloat()))
svgScope.apply {
features.values.filterIsInstance<ScalableImageFeature<XY>>().forEach { background ->
drawFeature(viewPoint.zoom, background)
}
features.values.filter {
it !is ScalableImageFeature && viewPoint.zoom in it.zoomRange
}.forEach { feature ->
drawFeature(viewPoint.zoom, feature)
}
}
return svgGraphics2D.getSVGElement(id)
}
public fun FeatureStateSnapshot<XY>.exportToSvg(
viewPoint: ViewPoint<XY>,
width: Double,
height: Double,
path: java.nio.file.Path,
) {
val svgString = generateSvg(viewPoint, width, height)
SVGUtils.writeToSVG(path.toFile(), svgString)
}

View File

@ -1,4 +1,4 @@
package center.sciprog.maps.svg package space.kscience.maps.svg
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
@ -301,7 +301,7 @@ internal class SvgCanvas(val graphics: SVGGraphics2D) : Canvas {
} }
internal class SvgDrawContext(val graphics: SVGGraphics2D, override var size: Size) : DrawContext { internal class SvgDrawContext(val graphics: SVGGraphics2D, override var size: Size) : DrawContext {
override val canvas: Canvas = SvgCanvas(graphics) override var canvas: Canvas = SvgCanvas(graphics)
override val transform: DrawTransform = asDrawTransform() override val transform: DrawTransform = asDrawTransform()
} }

View File

@ -1,25 +1,32 @@
package center.sciprog.maps.svg package space.kscience.maps.svg
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
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.*
import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.graphics.drawscope.DrawContext
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
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 org.jfree.svg.SVGGraphics2D import org.jfree.svg.SVGGraphics2D
import space.kscience.attributes.Attributes
import space.kscience.maps.features.*
import space.kscience.maps.scheme.XY
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, private val painterCache: Map<PainterFeature<XY>, Painter>,
private val defaultStrokeWidth: Float = 1f, private val defaultStrokeWidth: Float = 1f,
) : DrawScope { ) : FeatureDrawScope<XY>(state) {
override val layoutDirection: LayoutDirection override val layoutDirection: LayoutDirection
get() = LayoutDirection.Ltr get() = LayoutDirection.Ltr
@ -459,18 +466,22 @@ 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 val drawContext: DrawContext = SvgDrawContext(graphics, size) 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, state.canvasSize.toSize())
} }

View File

@ -0,0 +1,182 @@
package space.kscience.maps.svg
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import org.jfree.svg.SVGGraphics2D
import org.jfree.svg.SVGUtils
import space.kscience.maps.features.*
import space.kscience.maps.scheme.XY
import space.kscience.maps.scheme.XYCanvasState
public class FeatureStateSnapshot<T : Any>(
public val features: Map<String, Feature<T>>,
internal val painterCache: Map<PainterFeature<T>, Painter>,
)
@Composable
public fun <T : Any> FeatureGroup<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
featureMap,
features.flatMap {
if (it is FeatureGroup) it.features else listOf(it)
}.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
)
public fun FeatureStateSnapshot<XY>.generateSvg(
viewPoint: ViewPoint<XY>,
width: Double,
height: Double,
id: String? = null,
): String {
// fun XY.toOffset(): Offset = Offset(
// (width / 2 + (x - viewPoint.focus.x) * viewPoint.zoom).toFloat(),
// (height / 2 + (viewPoint.focus.y - y) * viewPoint.zoom).toFloat()
// )
//
//
// fun SvgDrawScope.drawFeature(scale: Float, feature: Feature<XY>) {
//
// val color = feature.color ?: Color.Red
// val alpha = feature.attributes[AlphaAttribute] ?: 1f
//
// when (feature) {
// is ScalableImageFeature -> {
// val offset = XY(feature.rectangle.left, feature.rectangle.top).toOffset()
// val backgroundSize = Size(
// (feature.rectangle.width * scale),
// (feature.rectangle.height * scale)
// )
//
// translate(offset.x, offset.y) {
// with(painterCache[feature]!!) {
// draw(backgroundSize)
// }
// }
// }
//
// is FeatureSelector -> drawFeature(scale, feature.selector(scale))
//
// is CircleFeature -> drawCircle(
// color,
// feature.radius.toPx(),
// center = feature.center.toOffset(),
// alpha = alpha
// )
//
// is LineFeature -> drawLine(
// color,
// feature.a.toOffset(),
// feature.b.toOffset(),
// alpha = alpha
// )
//
// is PointsFeature -> {
// val points = feature.points.map { it.toOffset() }
// drawPoints(
// points = points,
// color = color,
// strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
// pointMode = PointMode.Points,
// pathEffect = feature.attributes[PathEffectAttribute],
// alpha = alpha
// )
// }
//
// is MultiLineFeature -> {
// val points = feature.points.map { it.toOffset() }
// drawPoints(
// points = points,
// color = color,
// strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
// pointMode = PointMode.Polygon,
// pathEffect = feature.attributes[PathEffectAttribute],
// alpha = alpha
// )
// }
//
// is ArcFeature -> {
// val topLeft = feature.oval.leftTop.toOffset()
// val bottomRight = feature.oval.rightBottom.toOffset()
//
// val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
//
// drawArc(
// color = color,
// startAngle = feature.startAngle.degrees.toFloat(),
// sweepAngle = feature.arcLength.degrees.toFloat(),
// useCenter = false,
// topLeft = topLeft,
// size = size,
// style = Stroke(),
// alpha = alpha
// )
// }
//
// is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset())
//
// is VectorIconFeature -> {
// val offset = feature.center.toOffset()
// val imageSize = feature.size.toSize()
// translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
// with(painterCache[feature]!!) {
// draw(imageSize, alpha = alpha)
// }
// }
// }
//
// is TextFeature -> drawIntoCanvas { _ ->
// val offset = feature.position.toOffset()
// drawText(
// feature.text,
// offset.x + 5,
// offset.y - 5,
// java.awt.Font(null, PLAIN, 16),
// color
// )
// }
//
// is DrawFeature -> {
// val offset = feature.position.toOffset()
// translate(offset.x, offset.y) {
// feature.drawFeature(this)
// }
// }
//
// is FeatureGroup -> {
// feature.featureMap.values.forEach {
// drawFeature(scale, it)
// }
// }
// }
// }
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(width, height)
val svgCanvasState: XYCanvasState = XYCanvasState(ViewConfig()).apply {
this.viewPoint = viewPoint
this.canvasSize = DpSize(width.dp, height.dp)
}
val svgScope = SvgDrawScope(svgCanvasState, svgGraphics2D, painterCache)
svgScope.apply {
features.values.sortedBy { it.z }
.filter { state.viewPoint.zoom in it.zoomRange }
.forEach { feature ->
this@apply.drawFeature(feature)
}
}
return svgGraphics2D.getSVGElement(id)
}
public fun FeatureStateSnapshot<XY>.exportToSvg(
viewPoint: ViewPoint<XY>,
width: Double,
height: Double,
path: java.nio.file.Path,
) {
val svgString: String = generateSvg(viewPoint, width, height)
SVGUtils.writeToSVG(path.toFile(), svgString)
}

View File

@ -16,13 +16,11 @@ 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("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
id("space.kscience.gradle.jvm") version toolsVersion id("space.kscience.gradle.jvm") version toolsVersion
id("space.kscience.gradle.js") version toolsVersion
} }
} }
@ -52,9 +50,11 @@ include(
":maps-kt-features", ":maps-kt-features",
":maps-kt-compose", ":maps-kt-compose",
":maps-kt-scheme", ":maps-kt-scheme",
// ":maps-kt-leaflet",
":demo:maps", ":demo:maps",
":demo:scheme", ":demo:scheme",
":demo:polygon-editor", ":demo:polygon-editor",
":demo:trajectory-playground" ":demo:trajectory-playground",
":demo:maps-wasm"
) )

View File

@ -5,19 +5,8 @@
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:trajectory-kt:0.2.2`. The Maven coordinates of this project are `space.kscience:trajectory-kt:0.3.0`.
**Gradle Groovy:**
```groovy
repositories {
maven { url 'https://repo.kotlin.link' }
mavenCentral()
}
dependencies {
implementation 'space.kscience:trajectory-kt:0.2.2'
}
```
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
repositories { repositories {
@ -26,7 +15,7 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:trajectory-kt:0.2.2") implementation("space.kscience:trajectory-kt:0.3.0")
} }
``` ```

View File

@ -11,9 +11,12 @@ kscience{
jvm() jvm()
js() js()
native() native()
wasm()
useContextReceivers() useContextReceivers()
useSerialization() useSerialization{
json()
}
dependencies { dependencies {
api("space.kscience:kmath-geometry:$kmathVersion") api("space.kscience:kmath-geometry:$kmathVersion")
} }

View File

@ -1,12 +1,15 @@
@file:Suppress("UNCHECKED_CAST") @file:Suppress("UNCHECKED_CAST")
package center.sciprog.attributes package space.kscience
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import space.kscience.attributes.Attribute
import space.kscience.attributes.Attributes
public class AttributesSerializer( public class AttributesSerializer(
private val serializableAttributes: Set<SerializableAttribute<*>>, private val serializableAttributes: Set<SerializableAttribute<*>>,
@ -29,12 +32,17 @@ public class AttributesSerializer(
attr to value attr to value
} }
return Attributes(attributeMap) return object : Attributes {
override val content: Map<out Attribute<*>, Any?> = attributeMap
override fun toString(): String = "Attributes(value=${content.entries})"
override fun equals(other: Any?): Boolean = other is Attributes && Attributes.equals(this, other)
override fun hashCode(): Int = content.hashCode()
}
} }
override fun serialize(encoder: Encoder, value: Attributes) { override fun serialize(encoder: Encoder, value: Attributes) {
val json = buildJsonObject { val json = buildJsonObject {
value.content.forEach { (key: Attribute<*>, value: Any) -> value.content.forEach { (key: Attribute<*>, value: Any?) ->
if (key !in serializableAttributes) error("An attribute key '$key' is not in the list of allowed attributes for this serializer") if (key !in serializableAttributes) error("An attribute key '$key' is not in the list of allowed attributes for this serializer")
val serializableKey = key as SerializableAttribute val serializableKey = key as SerializableAttribute
@ -46,10 +54,19 @@ public class AttributesSerializer(
put( put(
serializableKey.serialId, serializableKey.serialId,
json.encodeToJsonElement(serializableKey.serializer as KSerializer<Any>, value) json.encodeToJsonElement(serializableKey.serializer as KSerializer<Any?>, value)
) )
} }
} }
jsonSerializer.serialize(encoder, json) jsonSerializer.serialize(encoder, json)
} }
} }
public abstract class SerializableAttribute<T>(
public val serialId: String,
public val serializer: KSerializer<T>,
) : Attribute<T> {
override fun toString(): String = serialId
}
public object NameAttribute : SerializableAttribute<String>("name", String.serializer())

View File

@ -1,21 +1,25 @@
package space.kscience.kmath.geometry package space.kscience
import space.kscience.kmath.geometry.*
import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.structures.Float64
import space.kscience.trajectory.* import space.kscience.trajectory.*
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sign import kotlin.math.sign
import kotlin.math.sqrt import kotlin.math.sqrt
public fun Euclidean2DSpace.circle(x: Number, y: Number, radius: Number): Circle2D = public fun Float64Space2D.circle(x: Number, y: Number, radius: Number): Circle2D<Float64> =
Circle2D(vector(x, y), radius = radius.toDouble()) Circle2D(vector(x, y), radius = radius.toDouble())
public fun Euclidean2DSpace.segment(begin: DoubleVector2D, end: DoubleVector2D): LineSegment2D = public fun Float64Space2D.segment(begin: Vector2D<Float64>, end: Vector2D<Float64>): LineSegment2D =
LineSegment(begin, end) LineSegment(begin, end)
public fun Euclidean2DSpace.segment(x1: Number, y1: Number, x2: Number, y2: Number): LineSegment2D = public fun Float64Space2D.segment(x1: Number, y1: Number, x2: Number, y2: Number): LineSegment2D =
LineSegment(vector(x1, y1), vector(x2, y2)) LineSegment(vector(x1, y1), vector(x2, y2))
public fun Euclidean2DSpace.intersectsOrInside(circle1: Circle2D, circle2: Circle2D): Boolean { public fun Float64Space2D.intersectsOrInside(circle1: Circle2D<Float64>, circle2: Circle2D<Float64>): Boolean {
val distance = norm(circle2.center - circle1.center) val distance = norm(circle2.center - circle1.center)
return distance <= circle1.radius + circle2.radius return distance <= circle1.radius + circle2.radius
} }
@ -23,7 +27,7 @@ public fun Euclidean2DSpace.intersectsOrInside(circle1: Circle2D, circle2: Circl
/** /**
* https://mathworld.wolfram.com/Circle-LineIntersection.html * https://mathworld.wolfram.com/Circle-LineIntersection.html
*/ */
public fun Euclidean2DSpace.intersects(segment: LineSegment2D, circle: Circle2D): Boolean { public fun Float64Space2D.intersects(segment: LineSegment2D, circle: Circle2D<Float64>): Boolean {
val direction = segment.end - segment.begin val direction = segment.end - segment.begin
val radiusVector = segment.begin - circle.center val radiusVector = segment.begin - circle.center
@ -43,14 +47,14 @@ public fun Euclidean2DSpace.intersects(segment: LineSegment2D, circle: Circle2D)
} }
public fun Euclidean2DSpace.intersects(circle: Circle2D, segment: LineSegment2D): Boolean = public fun Float64Space2D.intersects(circle: Circle2D<Float64>, segment: LineSegment2D): Boolean =
intersects(segment, circle) intersects(segment, circle)
public fun Euclidean2DSpace.intersects(segment1: LineSegment2D, segment2: LineSegment2D): Boolean { public fun Float64Space2D.intersects(segment1: LineSegment2D, segment2: LineSegment2D): Boolean {
infix fun DoubleVector2D.cross(v2: DoubleVector2D): Double = x * v2.y - y * v2.x infix fun Vector2D<Float64>.cross(v2: Vector2D<Float64>): Double = x * v2.y - y * v2.x
infix fun DoubleVector2D.crossSign(v2: DoubleVector2D) = cross(v2).sign infix fun Vector2D<Float64>.crossSign(v2: Vector2D<Float64>) = cross(v2).sign
return with(Euclidean2DSpace) { return with(Float64Space2D) {
(segment2.begin - segment1.begin) crossSign (segment2.end - segment1.begin) != (segment2.begin - segment1.begin) crossSign (segment2.end - segment1.begin) !=
(segment2.begin - segment1.end) crossSign (segment2.end - segment1.end) && (segment2.begin - segment1.end) crossSign (segment2.end - segment1.end) &&
(segment1.begin - segment2.begin) crossSign (segment1.end - segment2.begin) != (segment1.begin - segment2.begin) crossSign (segment1.end - segment2.begin) !=
@ -58,7 +62,7 @@ public fun Euclidean2DSpace.intersects(segment1: LineSegment2D, segment2: LineSe
} }
} }
public fun Euclidean2DSpace.intersectsTrajectory(segment: LineSegment2D, trajectory: Trajectory2D): Boolean = public fun Float64Space2D.intersectsTrajectory(segment: LineSegment2D, trajectory: Trajectory2D): Boolean =
when (trajectory) { when (trajectory) {
is CircleTrajectory2D -> intersects(segment, trajectory.circle) is CircleTrajectory2D -> intersects(segment, trajectory.circle)
is StraightTrajectory2D -> intersects(segment, trajectory) is StraightTrajectory2D -> intersects(segment, trajectory)
@ -72,7 +76,7 @@ public fun Euclidean2DSpace.intersectsTrajectory(segment: LineSegment2D, traject
* *
* @param bearing is counted the same way as in [Pose2D], from positive y clockwise * @param bearing is counted the same way as in [Pose2D], from positive y clockwise
*/ */
public fun Circle2D.tangent(bearing: Angle, direction: Trajectory2D.Direction): Pose2D = with(Euclidean2DSpace) { public fun Circle2D<Float64>.tangent(bearing: Angle, direction: Trajectory2D.Direction): Pose2D = with(Float64Space2D) {
val coordinates: Vector2D<Double> = vector(center.x + radius * sin(bearing), center.y + radius * cos(bearing)) val coordinates: Vector2D<Double> = vector(center.x + radius * sin(bearing), center.y + radius * cos(bearing))
val tangentAngle = when (direction) { val tangentAngle = when (direction) {
Trajectory2D.R -> bearing + Angle.piDiv2 Trajectory2D.R -> bearing + Angle.piDiv2
@ -82,7 +86,7 @@ public fun Circle2D.tangent(bearing: Angle, direction: Trajectory2D.Direction):
} }
public fun CircleTrajectory2D.containsPoint(point: DoubleVector2D): Boolean = with(Euclidean2DSpace) { public fun CircleTrajectory2D.containsPoint(point: Vector2D<Float64>): Boolean = with(Float64Space2D) {
val radiusVector = point - center val radiusVector = point - center
if (abs(norm(radiusVector) - circle.radius) > 1e-4 * circle.radius) error("Wrong radius") if (abs(norm(radiusVector) - circle.radius) > 1e-4 * circle.radius) error("Wrong radius")
val radiusVectorBearing = radiusVector.bearing val radiusVectorBearing = radiusVector.bearing

View File

@ -1,21 +0,0 @@
package space.kscience.kmath.geometry
import space.kscience.kmath.misc.zipWithNextCircular
import space.kscience.trajectory.Trajectory2D
public fun Euclidean2DSpace.polygon(points: List<DoubleVector2D>): Polygon<Double> = object : Polygon<Double> {
override val points: List<Vector2D<Double>> get() = points
}
public fun Euclidean2DSpace.intersects(polygon: Polygon<Double>, segment: LineSegment2D): Boolean =
polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, segment) }
public fun Euclidean2DSpace.intersects(polygon: Polygon<Double>, circle: Circle2D): Boolean =
polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, circle) }
public fun Euclidean2DSpace.intersectsTrajectory(polygon: Polygon<Double>, trajectory: Trajectory2D): Boolean =
polygon.points.zipWithNextCircular { l, r ->
segment(l, r)
}.any { edge ->
intersectsTrajectory(edge, trajectory)
}

Some files were not shown because too many files have changed in this diff Show More