diff --git a/.gitignore b/.gitignore index 8bb26b1..cef5b19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ build/ .gradle/ .idea/ +.kotlin /*.iml mapCache/ \ No newline at end of file diff --git a/.space.kts b/.space.kts index bd47550..c5dd962 100644 --- a/.space.kts +++ b/.space.kts @@ -9,17 +9,17 @@ job("Publish") { gitPush { enabled = false } } container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") { - env["SPACE_USER"] = Secrets("space_user") - env["SPACE_TOKEN"] = Secrets("space_token") + env["SPACE_USER"] = "{{ project:space_user }}" + env["SPACE_TOKEN"] = "{{ project:space_token }}" kotlinScript { api -> val spaceUser = System.getenv("SPACE_USER") val spaceToken = System.getenv("SPACE_TOKEN") - // write version to the build directory + // write the version to the build directory 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 revisionSuffix = if (version.endsWith("SNAPSHOT")) { @@ -32,7 +32,7 @@ job("Publish") { project = api.projectIdentifier(), targetIdentifier = TargetIdentifier.Key("maps-kt"), 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 ) api.gradlew( diff --git a/CHANGELOG.md b/CHANGELOG.md index d2ba1bb..fea04df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,27 @@ ## Unreleased ### Added +- `alpha` extension for feature attribute builder ### Changed +- avoid drawing features with VisibleAttribute false ### Deprecated ### Removed ### Fixed +- Add alpha attribute comprehension for all standard features. ### Security + +## 0.3.0 - 2024-06-04 + +### Changed + +- Package changed to `space.kscience` +- Kotlin 2.0 + +### Fixed + +- Use of generated resources for Wasm diff --git a/README.md b/README.md index 583d397..85eab2d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ This repository is a work-in-progress implementation of Map-with-markers compone ### [demo](demo) -> > > **Maturity**: EXPERIMENTAL @@ -33,17 +32,14 @@ This repository is a work-in-progress implementation of Map-with-markers compone ### [maps-kt-features](maps-kt-features) -> > > **Maturity**: EXPERIMENTAL ### [maps-kt-geojson](maps-kt-geojson) -> > > **Maturity**: EXPERIMENTAL ### [maps-kt-scheme](maps-kt-scheme) -> > > **Maturity**: EXPERIMENTAL @@ -53,21 +49,21 @@ This repository is a work-in-progress implementation of Map-with-markers compone > **Maturity**: EXPERIMENTAL ### [demo/maps](demo/maps) -> +> +> **Maturity**: EXPERIMENTAL + +### [demo/maps-wasm](demo/maps-wasm) > > **Maturity**: EXPERIMENTAL ### [demo/polygon-editor](demo/polygon-editor) -> > > **Maturity**: EXPERIMENTAL ### [demo/scheme](demo/scheme) -> > > **Maturity**: EXPERIMENTAL ### [demo/trajectory-playground](demo/trajectory-playground) -> > > **Maturity**: EXPERIMENTAL diff --git a/build.gradle.kts b/build.gradle.kts index 2ee9a4d..fdc4c25 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,3 @@ -import space.kscience.gradle.isInDevelopment import space.kscience.gradle.useApache2Licence import space.kscience.gradle.useSPCTeam @@ -6,16 +5,15 @@ plugins { id("space.kscience.gradle.project") } -val kmathVersion: String by extra("0.3.1-dev-RC") +val kmathVersion: String by extra("0.4.0") allprojects { - group = "center.sciprog" - version = "0.2.2" + group = "space.kscience" + version = "0.3.1-dev" repositories { mavenLocal() maven("https://repo.kotlin.link") - maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } } @@ -24,20 +22,12 @@ ksciencePublish { useApache2Licence() useSPCTeam() } - github("SciProgCentre", "maps-kt") - space( - if (isInDevelopment) { - "https://maven.pkg.jetbrains.space/spc/p/sci/dev" - } else { - "https://maven.pkg.jetbrains.space/spc/p/sci/maven" - } - ) - sonatype() + repository("spc","https://maven.sciprog.center/kscience") + sonatype("https://oss.sonatype.org") } subprojects { repositories { - maven("https://maven.pkg.jetbrains.space/mipt-npm/p/sci/dev") google() mavenCentral() maven("https://repo.kotlin.link") @@ -48,4 +38,3 @@ subprojects { readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md") - diff --git a/demo/maps-wasm/README.md b/demo/maps-wasm/README.md new file mode 100644 index 0000000..bd67940 --- /dev/null +++ b/demo/maps-wasm/README.md @@ -0,0 +1,4 @@ +# Module maps-wasm + + + diff --git a/demo/maps-wasm/build.gradle.kts b/demo/maps-wasm/build.gradle.kts new file mode 100644 index 0000000..da1d0ed --- /dev/null +++ b/demo/maps-wasm/build.gradle.kts @@ -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 { + + } +} \ No newline at end of file diff --git a/demo/maps-wasm/src/commonMain/composeResources/drawable/middle-earth.jpg b/demo/maps-wasm/src/commonMain/composeResources/drawable/middle-earth.jpg new file mode 100644 index 0000000..4ed4735 Binary files /dev/null and b/demo/maps-wasm/src/commonMain/composeResources/drawable/middle-earth.jpg differ diff --git a/demo/maps-wasm/src/wasmJsMain/kotlin/Main.kt b/demo/maps-wasm/src/wasmJsMain/kotlin/Main.kt new file mode 100644 index 0000000..c6aee72 --- /dev/null +++ b/demo/maps-wasm/src/wasmJsMain/kotlin/Main.kt @@ -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 = 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 = remember { + features.getBoundingBox(1f)?.computeViewPoint() ?: XYViewPoint(XY(0f, 0f)) + } + + var viewPoint: ViewPoint 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() + } +} diff --git a/demo/maps-wasm/src/wasmJsMain/resources/index.html b/demo/maps-wasm/src/wasmJsMain/resources/index.html new file mode 100644 index 0000000..5b0eca0 --- /dev/null +++ b/demo/maps-wasm/src/wasmJsMain/resources/index.html @@ -0,0 +1,12 @@ + + + + + Compose App + + + + + + + \ No newline at end of file diff --git a/demo/maps/build.gradle.kts b/demo/maps/build.gradle.kts index c9a83f8..821c5ee 100644 --- a/demo/maps/build.gradle.kts +++ b/demo/maps/build.gradle.kts @@ -2,7 +2,8 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { kotlin("multiplatform") - id("org.jetbrains.compose") + alias(spclibs.plugins.compose.compiler) + alias(spclibs.plugins.compose.jb) } val ktorVersion: String by rootProject.extra @@ -17,7 +18,7 @@ kotlin { implementation(projects.mapsKtGeojson) implementation(compose.desktop.currentOs) implementation("io.ktor:ktor-client-cio") - implementation("ch.qos.logback:logback-classic:1.2.11") + implementation(spclibs.logback.classic) } } val jvmTest by getting diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index 235bcac..a33267e 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -13,11 +13,6 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window 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.engine.cio.CIO import kotlinx.coroutines.delay @@ -27,15 +22,22 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import space.kscience.attributes.Attributes import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.degrees 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 kotlin.math.PI import kotlin.random.Random 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) @@ -55,7 +57,6 @@ fun App() { val centerCoordinates = MutableStateFlow(null) - val pointOne = 55.568548 to 37.568604 val pointTwo = 55.929444 to 37.518434 // val pointThree = 60.929444 to 37.518434 @@ -71,14 +72,17 @@ fun App() { ) { geoJson(javaClass.getResource("/moscow.geo.json")!!) - .modifyAttribute(ColorAttribute, Color.Blue) + .color(Color.Blue) .modifyAttribute(AlphaAttribute, 0.4f) icon(pointOne, Icons.Filled.Home) - val marker1 = rectangle(55.744 to 38.614, 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) + val marker1 = rectangle(55.744 to 38.614, 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 { println("line 1 clicked") @@ -90,6 +94,7 @@ fun App() { println("line 3 clicked") } + multiLine( points = listOf( 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( centerCoordinates = pointTwo, ) @@ -120,9 +137,11 @@ fun App() { arc(pointOne, 10.0.kilometers, (PI / 4).radians, -Angle.pi / 2) + line(pointOne, pointTwo, id = "line") text(pointOne, "Home", font = { size = 32f }) + pixelMap( space.Rectangle( Gmc(latitude = 55.58461879539754.degrees, longitude = 37.8746197303493.degrees), @@ -132,10 +151,11 @@ fun App() { 0.005.degrees ) { gmc -> Color( - red = ((gmc.latitude + Angle.piDiv2).degrees*10 % 1f).toFloat(), - green = ((gmc.longitude + Angle.pi).degrees*10 % 1f).toFloat(), - blue = 0f - ).copy(alpha = 0.3f) + red = ((gmc.latitude + Angle.piDiv2).toDegrees().value * 10 % 1f).toFloat(), + green = ((gmc.longitude + Angle.pi).toDegrees().value * 10 % 1f).toFloat(), + blue = 0f, + alpha = 0.3f + ) } centerCoordinates.filterNotNull().onEach { @@ -159,6 +179,7 @@ fun App() { } } } +// println(toPrettyString()) } } } diff --git a/demo/polygon-editor/build.gradle.kts b/demo/polygon-editor/build.gradle.kts index b4bbb88..ec0333f 100644 --- a/demo/polygon-editor/build.gradle.kts +++ b/demo/polygon-editor/build.gradle.kts @@ -2,7 +2,8 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { kotlin("multiplatform") - id("org.jetbrains.compose") + alias(spclibs.plugins.compose.compiler) + alias(spclibs.plugins.compose.jb) } val ktorVersion: String by rootProject.extra diff --git a/demo/polygon-editor/src/jvmMain/kotlin/Main.kt b/demo/polygon-editor/src/jvmMain/kotlin/Main.kt index c6746aa..907be39 100644 --- a/demo/polygon-editor/src/jvmMain/kotlin/Main.kt +++ b/demo/polygon-editor/src/jvmMain/kotlin/Main.kt @@ -9,11 +9,11 @@ import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.input.pointer.isSecondaryPressed import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import center.sciprog.maps.features.* -import center.sciprog.maps.scheme.SchemeView -import center.sciprog.maps.scheme.XY -import center.sciprog.maps.scheme.XYCoordinateSpace -import center.sciprog.maps.scheme.XYViewScope +import space.kscience.maps.features.* +import space.kscience.maps.scheme.SchemeView +import space.kscience.maps.scheme.XY +import space.kscience.maps.scheme.XYCanvasState +import space.kscience.maps.scheme.XYCoordinateSpace @Composable @Preview @@ -31,7 +31,7 @@ fun App() { ) } - val mapState: XYViewScope = XYViewScope.remember( + val mapState: XYCanvasState = XYCanvasState.remember( config = ViewConfig( onClick = { event, point -> if (event.buttons.isSecondaryPressed) { diff --git a/demo/scheme/build.gradle.kts b/demo/scheme/build.gradle.kts index 845f9bd..56dfe85 100644 --- a/demo/scheme/build.gradle.kts +++ b/demo/scheme/build.gradle.kts @@ -2,7 +2,8 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { kotlin("multiplatform") - id("org.jetbrains.compose") + alias(spclibs.plugins.compose.compiler) + alias(spclibs.plugins.compose.jb) } val ktorVersion: String by rootProject.extra @@ -26,6 +27,7 @@ compose{ desktop { application { mainClass = "MainKt" + //mainClass = "Joker2023Kt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "scheme-compose-demo" diff --git a/demo/scheme/src/jvmMain/kotlin/Main.kt b/demo/scheme/src/jvmMain/kotlin/Main.kt index 991d6e5..32e1a18 100644 --- a/demo/scheme/src/jvmMain/kotlin/Main.kt +++ b/demo/scheme/src/jvmMain/kotlin/Main.kt @@ -8,18 +8,18 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import center.sciprog.maps.features.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.isActive import kotlinx.coroutines.launch 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.nio.file.Files @@ -29,7 +29,7 @@ fun App() { MaterialTheme { val scope = rememberCoroutineScope() - val schemeFeaturesState: FeatureGroup = FeatureGroup.remember(XYCoordinateSpace) { + val features: FeatureGroup = FeatureGroup.remember(XYCoordinateSpace) { background(1600f, 1200f) { painterResource("middle-earth.jpg") } circle(410.52737 to 868.7676).color(Color.Blue) text(410.52737 to 868.7676, "Shire").color(Color.Blue) @@ -53,7 +53,7 @@ fun App() { } val initialViewPoint: ViewPoint = remember { - schemeFeaturesState.getBoundingBox(1f)?.computeViewPoint() ?: XYViewPoint(XY(0f, 0f)) + features.getBoundingBox(1f)?.computeViewPoint() ?: XYViewPoint(XY(0f, 0f)) } var viewPoint: ViewPoint by remember { mutableStateOf(initialViewPoint) } @@ -61,7 +61,7 @@ fun App() { var snapshot: FeatureStateSnapshot? by remember { mutableStateOf(null) } if (snapshot == null) { - snapshot = schemeFeaturesState.snapshot() + snapshot = features.snapshot() } ContextMenuArea( @@ -78,7 +78,7 @@ fun App() { ) } ) { - val mapState: XYViewScope = XYViewScope.remember( + val mapState: XYCanvasState = XYCanvasState.remember( ViewConfig( onClick = { _, click -> println("${click.focus.x}, ${click.focus.y}") @@ -90,7 +90,7 @@ fun App() { SchemeView( mapState, - schemeFeaturesState, + features, ) } diff --git a/demo/scheme/src/jvmMain/kotlin/joker2023.kt b/demo/scheme/src/jvmMain/kotlin/joker2023.kt new file mode 100644 index 0000000..9273e1a --- /dev/null +++ b/demo/scheme/src/jvmMain/kotlin/joker2023.kt @@ -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) + } + } + } + } +} diff --git a/demo/scheme/src/jvmMain/resources/SPC-logo.png b/demo/scheme/src/jvmMain/resources/SPC-logo.png new file mode 100644 index 0000000..953de16 Binary files /dev/null and b/demo/scheme/src/jvmMain/resources/SPC-logo.png differ diff --git a/demo/scheme/src/jvmMain/resources/joker2023.png b/demo/scheme/src/jvmMain/resources/joker2023.png new file mode 100644 index 0000000..0b0dd84 Binary files /dev/null and b/demo/scheme/src/jvmMain/resources/joker2023.png differ diff --git a/demo/trajectory-playground/build.gradle.kts b/demo/trajectory-playground/build.gradle.kts index 6983ece..f849341 100644 --- a/demo/trajectory-playground/build.gradle.kts +++ b/demo/trajectory-playground/build.gradle.kts @@ -1,6 +1,7 @@ plugins { kotlin("multiplatform") - id("org.jetbrains.compose") + alias(spclibs.plugins.compose.compiler) + alias(spclibs.plugins.compose.jb) } val ktorVersion: String by rootProject.extra diff --git a/demo/trajectory-playground/src/jvmMain/kotlin/Main.kt b/demo/trajectory-playground/src/jvmMain/kotlin/Main.kt index 41550cf..2c088ce 100644 --- a/demo/trajectory-playground/src/jvmMain/kotlin/Main.kt +++ b/demo/trajectory-playground/src/jvmMain/kotlin/Main.kt @@ -8,17 +8,17 @@ import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.window.Window 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.Circle2D -import space.kscience.kmath.geometry.DoubleVector2D -import space.kscience.kmath.geometry.Euclidean2DSpace +import space.kscience.kmath.geometry.Vector2D +import space.kscience.kmath.geometry.euclidean2d.Circle2D +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 kotlin.random.Random -private fun DoubleVector2D.toXY() = XY(x.toFloat(), y.toFloat()) +private fun Vector2D.toXY() = XY(x.toFloat(), y.toFloat()) private val random = Random(123) @@ -32,7 +32,7 @@ fun FeatureGroup.trajectory( bCoordinates = trajectory.end.toXY(), ).color(colorPicker(trajectory)) - is CircleTrajectory2D -> with(Euclidean2DSpace) { + is CircleTrajectory2D -> with(Float64Space2D) { val topLeft = 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.obstacle(obstacle: Obstacle, colorPicker: (Trajectory2D) -> polygon(obstacle.arcs.map { it.center.toXY() }).color(Color.Gray) } -fun FeatureGroup.pose(pose2D: Pose2D) = with(Euclidean2DSpace) { +fun FeatureGroup.pose(pose2D: Pose2D) = with(Float64Space2D) { line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY()) } @Composable @Preview -fun closePoints() { +fun closePoints() = with(Float64Space2D){ SchemeView { val obstacle = Obstacle( - Circle2D(Euclidean2DSpace.vector(0.0, 0.0), 1.0), - Circle2D(Euclidean2DSpace.vector(0.0, 1.0), 1.0), - Circle2D(Euclidean2DSpace.vector(1.0, 1.0), 1.0), - Circle2D(Euclidean2DSpace.vector(1.0, 0.0), 1.0) + Circle2D(vector(0.0, 0.0), 1.0), + Circle2D(vector(0.0, 1.0), 1.0), + Circle2D(vector(1.0, 1.0), 1.0), + Circle2D(vector(1.0, 0.0), 1.0) ) val enter = Pose2D(-0.8, -0.8, Angle.pi) val exit = Pose2D(-0.8, -0.8, Angle.piDiv2) @@ -101,7 +101,7 @@ fun closePoints() { @Preview fun singleObstacle() { 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 exit = Pose2D(20, 4, Angle.pi * 3 / 4) @@ -123,19 +123,19 @@ fun singleObstacle() { @Composable @Preview -fun doubleObstacle() { +fun doubleObstacle() = with(Float64Space2D){ SchemeView { val obstacles = arrayOf( Obstacle( - Circle2D(Euclidean2DSpace.vector(1.0, 6.5), 0.5), - Circle2D(Euclidean2DSpace.vector(2.0, 1.0), 0.5), - Circle2D(Euclidean2DSpace.vector(6.0, 0.0), 0.5), - Circle2D(Euclidean2DSpace.vector(5.0, 5.0), 0.5) + Circle2D(vector(1.0, 6.5), 0.5), + Circle2D(vector(2.0, 1.0), 0.5), + Circle2D(vector(6.0, 0.0), 0.5), + Circle2D(vector(5.0, 5.0), 0.5) ), Obstacle( - Circle2D(Euclidean2DSpace.vector(10.0, 1.0), 0.5), - Circle2D(Euclidean2DSpace.vector(16.0, 0.0), 0.5), - Circle2D(Euclidean2DSpace.vector(14.0, 6.0), 0.5), - Circle2D(Euclidean2DSpace.vector(9.0, 4.0), 0.5) + Circle2D(vector(10.0, 1.0), 0.5), + Circle2D(vector(16.0, 0.0), 0.5), + Circle2D(vector(14.0, 6.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 @Preview @@ -165,6 +173,7 @@ fun playground() { "Close starting points", "Single obstacle", "Two obstacles", + "Single element" ) var currentExample by remember { mutableStateOf(examples.first()) } @@ -182,6 +191,7 @@ fun playground() { examples[0] -> closePoints() examples[1] -> singleObstacle() examples[2] -> doubleObstacle() + examples[3] -> singleElement() } } } diff --git a/gradle.properties b/gradle.properties index efda397..b07eae5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,5 @@ 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 -toolsVersion=0.14.6-kotlin-1.8.20 \ No newline at end of file +toolsVersion=0.15.4-kotlin-2.0.0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fae0804..17655d0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME 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 zipStorePath=wrapper/dists diff --git a/maps-kt-compose/README.md b/maps-kt-compose/README.md index f5ffb6c..1ca284b 100644 --- a/maps-kt-compose/README.md +++ b/maps-kt-compose/README.md @@ -7,19 +7,8 @@ The core interfaces of KMath. ## 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:** ```kotlin repositories { @@ -28,6 +17,6 @@ repositories { } dependencies { - implementation("center.sciprog:maps-kt-compose:0.2.2") + implementation("space.kscience:maps-kt-compose:0.3.0") } ``` diff --git a/maps-kt-compose/build.gradle.kts b/maps-kt-compose/build.gradle.kts index 8185226..e13e841 100644 --- a/maps-kt-compose/build.gradle.kts +++ b/maps-kt-compose/build.gradle.kts @@ -1,34 +1,32 @@ plugins { 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` } -kscience{ +kscience { jvm() -} + wasm() -kotlin { - sourceSets { - commonMain { - dependencies { - api(projects.mapsKtCore) - api(projects.mapsKtFeatures) - api(compose.foundation) - api(project.dependencies.platform(spclibs.ktor.bom)) - api("io.ktor:ktor-client-core") - api("io.github.microutils:kotlin-logging:2.1.23") - } - } - val jvmTest by getting { - dependencies { - implementation("io.ktor:ktor-client-cio") - implementation(compose.desktop.currentOs) - implementation(spclibs.kotlinx.coroutines.test) + useCoroutines() - implementation(spclibs.logback.classic) - } - } + commonMain{ + api(projects.mapsKtCore) + api(projects.mapsKtFeatures) + api(compose.foundation) + api(project.dependencies.platform(spclibs.ktor.bom)) + } + jvmMain{ + api("io.ktor:ktor-client-cio") + } + jvmTest{ + implementation("io.ktor:ktor-client-cio") + implementation(compose.desktop.currentOs) + implementation(spclibs.kotlinx.coroutines.test) + + implementation(spclibs.logback.classic) } } @@ -40,4 +38,8 @@ readme { feature( id = "osm", ) { "OpenStreetMap tile provider." } -} \ No newline at end of file +} + +//tasks.getByName("downloadWix"){ +// duplicatesStrategy = DuplicatesStrategy.WARN +//} diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt deleted file mode 100644 index 08b64b7..0000000 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt +++ /dev/null @@ -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, - modifier: Modifier = Modifier.fillMaxSize(), -) - -/** - * A builder for a Map with static features. - */ -@Composable -public fun MapView( - mapTileProvider: MapTileProvider, - features: FeatureGroup, - initialViewPoint: ViewPoint? = null, - initialRectangle: Rectangle? = null, - config: ViewConfig = 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? = null, - initialRectangle: Rectangle? = null, - config: ViewConfig = ViewConfig(), - modifier: Modifier = Modifier.fillMaxSize(), - buildFeatures: FeatureGroup.() -> 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) -} \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcRectangle.kt b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/GmcRectangle.kt similarity index 91% rename from maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcRectangle.kt rename to maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/GmcRectangle.kt index a151db9..971a5ee 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcRectangle.kt +++ b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/GmcRectangle.kt @@ -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.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 diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/LruCache.kt b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/LruCache.kt similarity index 92% rename from maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/LruCache.kt rename to maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/LruCache.kt index 4a776ee..d27fada 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/LruCache.kt +++ b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/LruCache.kt @@ -1,4 +1,6 @@ -package center.sciprog.maps.compose +@file:Suppress("DEPRECATION") + +package space.kscience.maps.compose import kotlin.jvm.Synchronized diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewScope.kt b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapCanvasState.kt similarity index 82% rename from maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewScope.kt rename to maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapCanvasState.kt index 38783d6..0a5cb20 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewScope.kt +++ b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapCanvasState.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.compose +package space.kscience.maps.compose import androidx.compose.runtime.Composable 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.DpRect 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.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.* -public class MapViewScope internal constructor( + +public class MapCanvasState private constructor( public val mapTileProvider: MapTileProvider, config: ViewConfig, -) : CoordinateViewScope(config) { +) : CanvasState(config) { override val space: CoordinateSpace get() = WebMercatorSpace private val scaleFactor: Float @@ -60,10 +61,10 @@ public class MapViewScope internal constructor( override fun computeViewPoint(rectangle: Rectangle): ViewPoint { val zoom = log2( min( - canvasSize.width.value / rectangle.longitudeDelta.radians, - canvasSize.height.value / rectangle.latitudeDelta.radians + canvasSize.width.value / rectangle.longitudeDelta.toRadians().value, + canvasSize.height.value / rectangle.latitudeDelta.toRadians().value ) * 2 * PI / mapTileProvider.tileSize - ) + ).coerceIn(0.0..22.0) return space.ViewPoint(rectangle.center, zoom.toFloat()) } @@ -87,14 +88,14 @@ public class MapViewScope internal constructor( config: ViewConfig = ViewConfig(), initialViewPoint: ViewPoint? = null, initialRectangle: Rectangle? = null, - ): MapViewScope = remember { - MapViewScope(mapTileProvider, config).also { mapState -> + ): MapCanvasState = remember { + MapCanvasState(mapTileProvider, config).apply { if (initialViewPoint != null) { - mapState.viewPoint = initialViewPoint + viewPoint = initialViewPoint } else if (initialRectangle != null) { - mapState.viewPoint = mapState.computeViewPoint(initialRectangle) + viewPoint = computeViewPoint(initialRectangle) } } } } -} \ No newline at end of file +} diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTileProvider.kt b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapTileProvider.kt similarity index 95% rename from maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTileProvider.kt rename to maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapTileProvider.kt index 3aeb0c8..1d206ba 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTileProvider.kt +++ b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapTileProvider.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.compose +package space.kscience.maps.compose import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred diff --git a/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapView.kt b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapView.kt new file mode 100644 index 0000000..eaef886 --- /dev/null +++ b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapView.kt @@ -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, + modifier: Modifier, +) { + val mapTiles = remember(mapTileProvider) { + mutableStateMapOf() + } + + 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, + features: FeatureGroup, + initialViewPoint: ViewPoint? = null, + initialRectangle: Rectangle? = 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 = ViewConfig(), + initialViewPoint: ViewPoint? = null, + initialRectangle: Rectangle? = null, + modifier: Modifier = Modifier.fillMaxSize(), + buildFeatures: FeatureGroup.() -> Unit = {}, +) { + val featureState = FeatureGroup.remember(WebMercatorSpace, buildFeatures) + val computedRectangle = initialRectangle ?: featureState.getBoundingBox() + MapView(mapTileProvider, config, featureState, initialViewPoint, computedRectangle, modifier) +} \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewPoint.kt b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapViewPoint.kt similarity index 63% rename from maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewPoint.kt rename to maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapViewPoint.kt index 6a83b3b..d91aa16 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewPoint.kt +++ b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapViewPoint.kt @@ -1,9 +1,9 @@ -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.coordinates.WebMercatorProjection -import center.sciprog.maps.features.ViewPoint +import space.kscience.maps.coordinates.GeodeticMapCoordinates +import space.kscience.maps.coordinates.Gmc +import space.kscience.maps.coordinates.WebMercatorProjection +import space.kscience.maps.features.ViewPoint /** * Observable position on the map. Includes observation coordinate and [zoom] factor diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/WebMercatorSpace.kt similarity index 87% rename from maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt rename to maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/WebMercatorSpace.kt index 90ab1ba..072fb88 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt +++ b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/WebMercatorSpace.kt @@ -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.DpSize 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.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.floor import kotlin.math.pow @@ -19,7 +19,8 @@ public object WebMercatorSpace : CoordinateSpace { private fun tileScale(zoom: Float): Float = 2f.pow(zoom - floor(zoom)) - override fun Rectangle(first: Gmc, second: Gmc): Rectangle = GmcRectangle(first, second) + override fun Rectangle(first: Gmc, second: Gmc): Rectangle = + space.kscience.maps.compose.GmcRectangle(first, second) override fun Rectangle(center: Gmc, zoom: Float, size: DpSize): Rectangle { val scale = WebMercatorProjection.scaleFactor(zoom) @@ -62,7 +63,10 @@ public object WebMercatorSpace : CoordinateSpace { val maxLat = maxOf { it.top } val minLong = minOf { it.left } 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.wrapPoints(): Rectangle? { @@ -72,7 +76,10 @@ public object WebMercatorSpace : CoordinateSpace { val maxLat = maxOf { it.latitude } val minLong = minOf { 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 { @@ -132,5 +139,5 @@ public fun CoordinateSpace.Rectangle( center.latitude + (height / 2), center.longitude + (width / 2) ) - return GmcRectangle(a, b) + return space.kscience.maps.compose.GmcRectangle(a, b) } \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/mapFeatures.kt similarity index 74% rename from maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt rename to maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/mapFeatures.kt index fa17d6c..da9cf36 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt +++ b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/mapFeatures.kt @@ -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.drawscope.DrawScope @@ -6,12 +6,10 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import center.sciprog.maps.coordinates.Distance -import center.sciprog.maps.coordinates.GeodeticMapCoordinates -import center.sciprog.maps.coordinates.Gmc -import center.sciprog.maps.coordinates.GmcCurve -import center.sciprog.maps.features.* +import org.jetbrains.skia.Font import space.kscience.kmath.geometry.Angle +import space.kscience.maps.coordinates.* +import space.kscience.maps.features.* import kotlin.math.ceil @@ -55,6 +53,39 @@ public fun FeatureGroup.line( LineFeature(space, curve.forward.coordinates, curve.backward.coordinates) ) +/** + * A segmented geodetic curve + */ +public fun FeatureGroup.geodeticLine( + curve: GmcCurve, + ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84, + maxLineDistance: Distance = 100.kilometers, + id: String? = null, +): FeatureRef> = 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 { + 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.geodeticLine( + from: Gmc, + to: Gmc, + ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84, + maxLineDistance: Distance = 100.kilometers, + id: String? = null, +): FeatureRef> = geodeticLine(ellipsoid.curveBetween(from, to), ellipsoid, maxLineDistance, id) public fun FeatureGroup.line( aCoordinates: Pair, @@ -65,7 +96,6 @@ public fun FeatureGroup.line( LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates)) ) - public fun FeatureGroup.arc( center: Pair, radius: Distance, @@ -110,7 +140,7 @@ public fun FeatureGroup.icon( public fun FeatureGroup.text( position: Pair, text: String, - font: FeatureFont.() -> Unit = { size = 16f }, + font: Font.() -> Unit = { size = 16f }, id: String? = null, ): FeatureRef> = feature( id, diff --git a/maps-kt-compose/src/jsMain/kotlin/compose/OpenStreetMapTileProvider.kt b/maps-kt-compose/src/jsMain/kotlin/compose/OpenStreetMapTileProvider.kt new file mode 100644 index 0000000..f6a6941 --- /dev/null +++ b/maps-kt-compose/src/jsMain/kotlin/compose/OpenStreetMapTileProvider.kt @@ -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>(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 = 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 { + + //start image download + val imageDeferred: Deferred = 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") + } +} \ No newline at end of file diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/mapViewJvm.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/mapViewJvm.kt deleted file mode 100644 index 82dda16..0000000 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/mapViewJvm.kt +++ /dev/null @@ -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, - modifier: Modifier, -): Unit = with(viewScope) { - val mapTiles = remember(mapTileProvider) { mutableStateMapOf() } - - // 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, Painter> = - features.features.filterIsInstance>().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) - ) - ) - } - } - } -} - diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/OpenStreetMapTileProvider.kt b/maps-kt-compose/src/jvmMain/kotlin/space/kscience/maps/compose/OpenStreetMapTileProvider.kt similarity index 90% rename from maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/OpenStreetMapTileProvider.kt rename to maps-kt-compose/src/jvmMain/kotlin/space/kscience/maps/compose/OpenStreetMapTileProvider.kt index 10581d2..b6b2fbb 100644 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/OpenStreetMapTileProvider.kt +++ b/maps-kt-compose/src/jvmMain/kotlin/space/kscience/maps/compose/OpenStreetMapTileProvider.kt @@ -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.request.get import io.ktor.client.statement.readBytes -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit -import mu.KotlinLogging import org.jetbrains.skia.Image import java.net.URL import java.nio.file.Path @@ -76,7 +73,9 @@ public class OpenStreetMapTileProvider( //collect the result asynchronously return async { val image: Image = runCatching { imageDeferred.await() }.onFailure { - logger.error(it) { "Failed to load tile image with id=$tileId" } + if(it !is CancellationException) { + logger.error(it) { "Failed to load tile image with id=$tileId" } + } cache.remove(tileId) }.getOrThrow() diff --git a/maps-kt-compose/src/jvmTest/kotlin/center/sciprog/maps/compose/OsmTileProviderTest.kt b/maps-kt-compose/src/jvmTest/kotlin/space/kscience/maps/compose/OsmTileProviderTest.kt similarity index 89% rename from maps-kt-compose/src/jvmTest/kotlin/center/sciprog/maps/compose/OsmTileProviderTest.kt rename to maps-kt-compose/src/jvmTest/kotlin/space/kscience/maps/compose/OsmTileProviderTest.kt index 4c9049c..aa1da5a 100644 --- a/maps-kt-compose/src/jvmTest/kotlin/center/sciprog/maps/compose/OsmTileProviderTest.kt +++ b/maps-kt-compose/src/jvmTest/kotlin/space/kscience/maps/compose/OsmTileProviderTest.kt @@ -1,15 +1,13 @@ -package center.sciprog.maps.compose +package space.kscience.maps.compose import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import java.nio.file.Files import kotlin.test.assertFails -@OptIn(ExperimentalCoroutinesApi::class) class OsmTileProviderTest { // @get:Rule // val rule = createComposeRule() diff --git a/maps-kt-core/README.md b/maps-kt-core/README.md index 09d5a0d..03aff2b 100644 --- a/maps-kt-core/README.md +++ b/maps-kt-core/README.md @@ -9,19 +9,8 @@ The core interfaces of KMath. ## 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:** ```kotlin repositories { @@ -30,6 +19,6 @@ repositories { } dependencies { - implementation("center.sciprog:maps-kt-core:0.2.2") + implementation("space.kscience:maps-kt-core:0.3.0") } ``` diff --git a/maps-kt-core/build.gradle.kts b/maps-kt-core/build.gradle.kts index a7f9a0e..75ea1e5 100644 --- a/maps-kt-core/build.gradle.kts +++ b/maps-kt-core/build.gradle.kts @@ -8,6 +8,9 @@ val kmathVersion: String by rootProject.extra kscience{ jvm() js() + native() + wasm() + useSerialization() dependencies{ diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/Distance.kt b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/Distance.kt similarity index 90% rename from maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/Distance.kt rename to maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/Distance.kt index 08d5f1b..27a274c 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/Distance.kt +++ b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/Distance.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.coordinates +package space.kscience.maps.coordinates import kotlinx.serialization.Serializable import kotlin.jvm.JvmInline diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeoEllipsoid.kt b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GeoEllipsoid.kt similarity index 55% rename from maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeoEllipsoid.kt rename to maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GeoEllipsoid.kt index 607aeca..375c278 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeoEllipsoid.kt +++ b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GeoEllipsoid.kt @@ -1,10 +1,8 @@ -package center.sciprog.maps.coordinates +package space.kscience.maps.coordinates import kotlinx.serialization.Serializable -import space.kscience.kmath.geometry.Angle -import space.kscience.kmath.geometry.tan -import kotlin.math.pow -import kotlin.math.sqrt +import space.kscience.kmath.geometry.* +import kotlin.math.* @Serializable 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) ) -// /** -// * 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) return equatorRadius / sqrt(1.0 + reducedLatitudeTan.pow(2)) } -// -// -///** -// * Compute distance between two map points using giv -// * https://en.wikipedia.org/wiki/Geographical_distance#Lambert's_formula_for_long_lines -// */ -//public fun GeoEllipsoid.lambertDistanceBetween(r1: GMC, r2: GMC): Distance { -// 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)) -//} \ No newline at end of file + + +/** + * Compute distance between two map points using giv + * https://en.wikipedia.org/wiki/Geographical_distance#Lambert's_formula_for_long_lines + */ +public fun GeoEllipsoid.lambertDistanceBetween(r1: Gmc, r2: Gmc): Distance { + + /** + * https://en.wikipedia.org/wiki/Great-circle_distance + */ + 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 + + 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)) +} \ No newline at end of file diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeodeticMapCoordinates.kt b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GeodeticMapCoordinates.kt similarity index 94% rename from maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeodeticMapCoordinates.kt rename to maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GeodeticMapCoordinates.kt index 8484b3b..8130192 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeodeticMapCoordinates.kt +++ b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GeodeticMapCoordinates.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.coordinates +package space.kscience.maps.coordinates import kotlinx.serialization.Serializable import space.kscience.kmath.geometry.* @@ -22,7 +22,6 @@ public class GeodeticMapCoordinates( "Longitude $longitude is not in (-PI..PI) range" } } - override val x: Angle get() = longitude override val y: Angle get() = latitude @@ -43,7 +42,7 @@ public class GeodeticMapCoordinates( } 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)" } diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcCurve.kt b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GmcCurve.kt similarity index 92% rename from maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcCurve.kt rename to maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GmcCurve.kt index a99c159..cad2bfe 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcCurve.kt +++ b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GmcCurve.kt @@ -1,14 +1,14 @@ -package center.sciprog.maps.coordinates +package space.kscience.maps.coordinates import space.kscience.kmath.geometry.* import kotlin.math.* /** * 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 backward coordinate of an end point with backward direction + * @param forward coordinate of a start point with the forward 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 backward: GmcPose, public val distance: Distance, @@ -64,8 +64,8 @@ public fun GeoEllipsoid.meridianCurve( } return GmcCurve( - forward = GmcPose(Gmc.normalized(fromLatitude, longitude), if (up) Angle.zero else Angle.pi), - backward = GmcPose(Gmc.normalized(toLatitude, longitude), if (up) Angle.pi else Angle.zero), + forward = GmcPose(GeodeticMapCoordinates.normalized(fromLatitude, longitude), if (up) Angle.zero else Angle.pi), + backward = GmcPose(GeodeticMapCoordinates.normalized(toLatitude, longitude), if (up) Angle.pi else Angle.zero), 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" } val right = toLongitude > fromLongitude return GmcCurve( - forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) Angle.piDiv2 else -Angle.piDiv2), - backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -Angle.piDiv2 else Angle.piDiv2), - distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians) + forward = GmcPose(GeodeticMapCoordinates.normalized(latitude, fromLongitude), 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).toRadians().value) ) } @@ -193,7 +193,7 @@ public fun GeoEllipsoid.curveInDirection( val L = lambda - (1 - C) * f * sinAlpha * (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 diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcPose.kt b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GmcPose.kt similarity index 92% rename from maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcPose.kt rename to maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GmcPose.kt index d66e66f..4bcdd60 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GmcPose.kt +++ b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/GmcPose.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.coordinates +package space.kscience.maps.coordinates import kotlinx.serialization.Serializable import space.kscience.kmath.geometry.Angle diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/MercatorProjection.kt b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/MercatorProjection.kt similarity index 92% rename from maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/MercatorProjection.kt rename to maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/MercatorProjection.kt index beaa8de..4afce1d 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/MercatorProjection.kt +++ b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/MercatorProjection.kt @@ -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. */ -package center.sciprog.maps.coordinates +package space.kscience.maps.coordinates import kotlinx.serialization.Serializable import space.kscience.kmath.geometry.* @@ -57,12 +57,12 @@ public open class MercatorProjection( return if (ellipsoid === GeoEllipsoid.sphere) { GeodeticMapCoordinates.ofRadians( atan(sinh(pc.y / ellipsoid.equatorRadius)), - baseLongitude.radians + (pc.x / ellipsoid.equatorRadius), + baseLongitude.toRadians().value + (pc.x / ellipsoid.equatorRadius), ) } else { GeodeticMapCoordinates.ofRadians( 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) { 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)) ) } else { val sinPhi = sin(gmc.latitude) 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) * ((1 - e * sinPhi) / (1 + e * sinPhi)).pow(e / 2) ) diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/WebMercatorProjection.kt similarity index 92% rename from maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt rename to maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/WebMercatorProjection.kt index 87134b4..f4ce989 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt +++ b/maps-kt-core/src/commonMain/kotlin/space/kscience/maps/coordinates/WebMercatorProjection.kt @@ -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. */ -package center.sciprog.maps.coordinates +package space.kscience.maps.coordinates import space.kscience.kmath.geometry.abs -import space.kscience.kmath.geometry.radians import kotlin.math.* 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()) return WebMercatorCoordinates( zoom = zoom, - x = scaleFactor * (gmc.longitude.radians + PI).toFloat(), - y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians / 2))).toFloat() + x = scaleFactor * (gmc.longitude.toRadians().value + PI).toFloat(), + y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.toRadians().value / 2))).toFloat() ) } diff --git a/maps-kt-core/src/commonTest/kotlin/center/sciprog/maps/coordinates/DistanceTest.kt b/maps-kt-core/src/commonTest/kotlin/space/kscience/maps/coordinates/DistanceTest.kt similarity index 62% rename from maps-kt-core/src/commonTest/kotlin/center/sciprog/maps/coordinates/DistanceTest.kt rename to maps-kt-core/src/commonTest/kotlin/space/kscience/maps/coordinates/DistanceTest.kt index a0ff074..5cb513b 100644 --- a/maps-kt-core/src/commonTest/kotlin/center/sciprog/maps/coordinates/DistanceTest.kt +++ b/maps-kt-core/src/commonTest/kotlin/space/kscience/maps/coordinates/DistanceTest.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.coordinates +package space.kscience.maps.coordinates import space.kscience.kmath.geometry.radians import kotlin.test.Test @@ -6,8 +6,8 @@ import kotlin.test.assertEquals internal class DistanceTest { companion object { - val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869) - val spb = Gmc.ofDegrees(59.926686023580444, 30.36038109122013) + val moscow = GeodeticMapCoordinates.ofDegrees(55.76058287719673, 37.60358622841869) + val spb = GeodeticMapCoordinates.ofDegrees(59.926686023580444, 30.36038109122013) } @Test @@ -21,7 +21,7 @@ internal class DistanceTest { val distance = curve.distance 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 @@ -30,7 +30,7 @@ internal class DistanceTest { GmcPose(moscow, (-0.6947937116552751).radians), Distance(632.035426877) ) - assertEquals(spb.latitude.radians, curve.backward.latitude.radians, 0.0001) - assertEquals(spb.longitude.radians, curve.backward.longitude.radians, 0.0001) + assertEquals(spb.latitude.toRadians().value, curve.backward.latitude.toRadians().value, 0.0001) + assertEquals(spb.longitude.toRadians().value, curve.backward.longitude.toRadians().value, 0.0001) } } \ No newline at end of file diff --git a/maps-kt-core/src/commonTest/kotlin/center/sciprog/maps/coordinates/MercatorTest.kt b/maps-kt-core/src/commonTest/kotlin/space/kscience/maps/coordinates/MercatorTest.kt similarity index 53% rename from maps-kt-core/src/commonTest/kotlin/center/sciprog/maps/coordinates/MercatorTest.kt rename to maps-kt-core/src/commonTest/kotlin/space/kscience/maps/coordinates/MercatorTest.kt index 06576b5..04d4e08 100644 --- a/maps-kt-core/src/commonTest/kotlin/center/sciprog/maps/coordinates/MercatorTest.kt +++ b/maps-kt-core/src/commonTest/kotlin/space/kscience/maps/coordinates/MercatorTest.kt @@ -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.assertEquals class MercatorTest { @Test fun sphereForwardBackward(){ - val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869) + val moscow = GeodeticMapCoordinates.ofDegrees(55.76058287719673, 37.60358622841869) val mercator = MapProjection.epsg3857.toProjection(moscow) //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(7510.9013658, mercator.y.kilometers, 1e-4) val backwards = MapProjection.epsg3857.toGeodetic(mercator) - assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 1e-6) - assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6) + assertEquals(moscow.latitude.toDegrees().value, backwards.latitude.toDegrees().value, 1e-6) + assertEquals(moscow.longitude.toDegrees().value, backwards.longitude.toDegrees().value, 1e-6) } @Test 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 mercator = projection.toProjection(moscow) val backwards = projection.toGeodetic(mercator) - assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 1e-6) - assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6) + assertEquals(moscow.latitude.toDegrees().value, backwards.latitude.toDegrees().value, 1e-6) + assertEquals(moscow.longitude.toDegrees().value, backwards.longitude.toDegrees().value, 1e-6) } } \ No newline at end of file diff --git a/maps-kt-features/README.md b/maps-kt-features/README.md index e72643f..1028f1d 100644 --- a/maps-kt-features/README.md +++ b/maps-kt-features/README.md @@ -6,19 +6,8 @@ ## 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:** ```kotlin repositories { @@ -27,6 +16,6 @@ repositories { } dependencies { - implementation("center.sciprog:maps-kt-features:0.2.2") + implementation("space.kscience:maps-kt-features:0.3.0") } ``` diff --git a/maps-kt-features/build.gradle.kts b/maps-kt-features/build.gradle.kts index d761986..c1e4895 100644 --- a/maps-kt-features/build.gradle.kts +++ b/maps-kt-features/build.gradle.kts @@ -1,30 +1,39 @@ plugins { id("space.kscience.gradle.mpp") - id("org.jetbrains.compose") + alias(spclibs.plugins.compose.compiler) + alias(spclibs.plugins.compose.jb) `maven-publish` } val kmathVersion: String by rootProject.extra -kscience{ +kscience { jvm() - js() - useSerialization{ - json() - } - - useSerialization(sourceSet = space.kscience.gradle.DependencySourceSet.TEST){ - protobuf() - } -} - -kotlin { - sourceSets { - commonMain { - dependencies { - api(projects.trajectoryKt) - api(compose.foundation) +// js() + wasm{ + browser { + testTask { + enabled = false } } } + + useCoroutines() + + useSerialization { + json() + } + + useSerialization(sourceSet = space.kscience.gradle.DependencySourceSet.TEST) { + protobuf() + } + + commonMain{ + api(projects.trajectoryKt) + api(compose.runtime) + api(compose.foundation) + api(compose.material) + api(compose.ui) + api("io.github.oshai:kotlin-logging:6.0.3") + } } \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/Attribute.kt b/maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/Attribute.kt deleted file mode 100644 index 492b74f..0000000 --- a/maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/Attribute.kt +++ /dev/null @@ -1,22 +0,0 @@ -package center.sciprog.attributes - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.serializer - -public interface Attribute - -public abstract class SerializableAttribute( - public val serialId: String, - public val serializer: KSerializer, -) : Attribute { - override fun toString(): String = serialId -} - -public interface AttributeWithDefault : Attribute { - public val default: T -} - -public interface SetAttribute : Attribute> - -public object NameAttribute : SerializableAttribute("name", String.serializer()) - diff --git a/maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/Attributes.kt b/maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/Attributes.kt deleted file mode 100644 index 68fb15a..0000000 --- a/maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/Attributes.kt +++ /dev/null @@ -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, Any>) { - - public val keys: Set> get() = content.keys - - @Suppress("UNCHECKED_CAST") - public operator fun get(attribute: Attribute): 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 Attributes.getOrDefault(attribute: AttributeWithDefault): T = get(attribute) ?: attribute.default - -public fun > 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 > Attributes.withAttributeElement( - attribute: A, - attrValue: T, -): Attributes { - val currentSet: Set = get(attribute) ?: emptySet() - return Attributes( - content + (attribute to (currentSet + attrValue)) - ) -} - -/** - * Remove an element from [SetAttribute] - */ -public fun > Attributes.withoutAttributeElement( - attribute: A, - attrValue: T, -): Attributes { - val currentSet: Set = get(attribute) ?: emptySet() - return Attributes( - content + (attribute to (currentSet - attrValue)) - ) -} - -public fun > 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 -// } \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/AttributesBuilder.kt b/maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/AttributesBuilder.kt deleted file mode 100644 index c61e20e..0000000 --- a/maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/AttributesBuilder.kt +++ /dev/null @@ -1,47 +0,0 @@ -package center.sciprog.attributes - -/** - * A safe builder for [Attributes] - */ -public class AttributesBuilder internal constructor(private val map: MutableMap, Any> = mutableMapOf()) { - - @Suppress("UNCHECKED_CAST") - public operator fun get(attribute: Attribute): T? = map[attribute] as? T - - public operator fun Attribute.invoke(value: V?) { - if (value == null) { - map.remove(this) - } else { - map[this] = value - } - } - - public fun from(attributes: Attributes) { - map.putAll(attributes.content) - } - - public fun SetAttribute.add( - attrValue: V, - ) { - val currentSet: Set = get(this) ?: emptySet() - map[this] = currentSet + attrValue - } - - /** - * Remove an element from [SetAttribute] - */ - public fun SetAttribute.remove( - attrValue: V, - ) { - val currentSet: Set = 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() \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureFont.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureFont.kt deleted file mode 100644 index 04838c2..0000000 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureFont.kt +++ /dev/null @@ -1,5 +0,0 @@ -package center.sciprog.maps.features - -public expect class FeatureFont { - public var size: Float -} \ No newline at end of file diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/mapControls.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/canvasControls.kt similarity index 86% rename from maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/mapControls.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/canvasControls.kt index 5aec2fa..cc650aa 100644 --- a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/mapControls.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/canvasControls.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.compose +package space.kscience.maps.compose import androidx.compose.foundation.gestures.drag 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.DpRect import androidx.compose.ui.unit.dp -import center.sciprog.maps.features.* +import space.kscience.maps.features.* import kotlin.math.max import kotlin.math.min @@ -16,8 +16,8 @@ import kotlin.math.min * Create a modifier for Map/Scheme canvas controls on desktop * @param features a collection of features to be rendered in descending [ZAttribute] order */ -public fun Modifier.mapControls( - state: CoordinateViewScope, +public fun Modifier.canvasControls( + state: CanvasState, features: FeatureGroup, ): Modifier = with(state) { @@ -32,8 +32,8 @@ public fun Modifier.mapControls( awaitPointerEventScope { while (true) { val event = awaitPointerEvent() - val coordinates = event.changes.first().position.toCoordinates(this) - val point = space.ViewPoint(coordinates, zoom) + val coordinates = toCoordinates(event.changes.first().position, this) + val point = state.space.ViewPoint(coordinates, zoom) if (event.type == PointerEventType.Move) { features.forEachWithAttribute(HoverListenerAttribute) { _, feature, listeners -> @@ -47,9 +47,9 @@ public fun Modifier.mapControls( } }.pointerInput(Unit) { detectClicks( - onDoubleClick = if (state.config.zoomOnDoubleClick) { + onDoubleClick = if (viewConfig.zoomOnDoubleClick) { { event -> - val invariant = event.position.toCoordinates(this) + val invariant = toCoordinates(event.position, this) viewPoint = with(space) { viewPoint.zoomBy( if (event.buttons.isPrimaryPressed) 1f else if (event.buttons.isSecondaryPressed) -1f else 0f, @@ -59,10 +59,10 @@ public fun Modifier.mapControls( } } else null, onClick = { event -> - val coordinates = event.position.toCoordinates(this) + val coordinates = toCoordinates(event.position, this) val point = space.ViewPoint(coordinates, zoom) - config.onClick?.handle( + viewConfig.onClick?.handle( event, point ) @@ -88,7 +88,7 @@ public fun Modifier.mapControls( //compute invariant point of translation val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates() viewPoint = with(space) { - viewPoint.zoomBy(-change.scrollDelta.y * config.zoomSpeed, invariant) + viewPoint.zoomBy(-change.scrollDelta.y * viewConfig.zoomSpeed, invariant) } change.consume() } @@ -110,14 +110,14 @@ public fun Modifier.mapControls( //apply drag handle and check if it prohibits the drag even propagation if (selectionStart == null) { val dragStart = space.ViewPoint( - dragChange.previousPosition.toCoordinates(this), + toCoordinates(dragChange.previousPosition, this), zoom ) val dragEnd = space.ViewPoint( - dragChange.position.toCoordinates(this), + toCoordinates(dragChange.position, this), zoom ) - val dragResult = config.dragHandle?.handle(event, dragStart, dragEnd) + val dragResult = viewConfig.dragHandle?.handle(event, dragStart, dragEnd) if (dragResult?.handleNext == false) return@drag var continueAfter = true @@ -132,7 +132,7 @@ public fun Modifier.mapControls( } if (event.buttons.isPrimaryPressed) { - //If selection process is started, modify the frame + //If the selection process is started, modify the frame selectionStart?.let { start -> val offset = dragChange.position selectRect = DpRect( @@ -161,8 +161,8 @@ public fun Modifier.mapControls( rect.topLeft.toCoordinates(), rect.bottomRight.toCoordinates() ) - config.onSelect(coordinateRect) - if (config.zoomOnSelect) { + viewConfig.onSelect(coordinateRect) + if (viewConfig.zoomOnSelect) { viewPoint = computeViewPoint(coordinateRect) } selectRect = null diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/clickGestures.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/clickGestures.kt similarity index 85% rename from maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/clickGestures.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/clickGestures.kt index 2939a0d..43e8929 100644 --- a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/clickGestures.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/clickGestures.kt @@ -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.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 - * [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]. + * An alternative to [detectTapGestures] with reimplementation of internal logic */ -public suspend fun PointerInputScope.detectClicks( +internal suspend fun PointerInputScope.detectClicks( onDoubleClick: (Density.(PointerEvent) -> Unit)? = null, onLongClick: (Density.(PointerEvent) -> Unit)? = null, onPress: suspend PressGestureScope.(event: PointerEvent) -> Unit = NoPressGesture, diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewScope.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/CanvasState.kt similarity index 67% rename from maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewScope.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/CanvasState.kt index 03631ec..5e31fe9 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewScope.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/CanvasState.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.features +package space.kscience.maps.features import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue @@ -7,10 +7,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.* -public abstract class CoordinateViewScope( - public val config: ViewConfig, -) { - +/** + * A state holder for current canvas size and view point. Allows transformation from coordinates to pixels and back + */ +public abstract class CanvasState( + public val viewConfig: ViewConfig +){ public abstract val space: CoordinateSpace private var canvasSizeState: MutableState = mutableStateOf(null) @@ -20,13 +22,14 @@ public abstract class CoordinateViewScope( get() = canvasSizeState.value ?: DpSize(512.dp, 512.dp) set(value) { canvasSizeState.value = value + viewConfig.onCanvasSizeChange(value) } public var viewPoint: ViewPoint get() = viewPointState.value ?: space.defaultViewPoint set(value) { viewPointState.value = value - config.onViewChange(viewPoint) + viewConfig.onViewChange(viewPoint) } public val zoom: Float get() = viewPoint.zoom @@ -35,28 +38,28 @@ public abstract class CoordinateViewScope( // Selection rectangle. If null - no selection 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.toDpRect(): DpRect public abstract fun ViewPoint.moveBy(x: Dp, y: Dp): ViewPoint public abstract fun computeViewPoint(rectangle: Rectangle): ViewPoint -} + 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) diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/CoordinateSpace.kt similarity index 96% rename from maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/CoordinateSpace.kt index 809f81a..cec0f5b 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/CoordinateSpace.kt @@ -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.DpOffset diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/DragHandle.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/DragHandle.kt similarity index 98% rename from maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/DragHandle.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/DragHandle.kt index 239909b..2f58aed 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/DragHandle.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/DragHandle.kt @@ -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.isPrimaryPressed diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/Feature.kt similarity index 98% rename from maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/Feature.kt index 110a3a1..57417d0 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/Feature.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.features +package space.kscience.maps.features import androidx.compose.runtime.Composable 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.DpSize import androidx.compose.ui.unit.dp -import center.sciprog.attributes.Attributes -import center.sciprog.attributes.NameAttribute +import org.jetbrains.skia.Font +import space.kscience.NameAttribute +import space.kscience.attributes.Attributes import space.kscience.kmath.geometry.Angle import space.kscience.kmath.nd.Structure2D @@ -331,7 +332,7 @@ public data class TextFeature( public val position: T, public val text: String, override val attributes: Attributes = Attributes.EMPTY, - public val fontConfig: FeatureFont.() -> Unit, + public val fontConfig: Font.() -> Unit, ) : DraggableFeature { override fun getBoundingBox(zoom: Float): Rectangle = space.Rectangle(position, position) diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureDrawScope.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureDrawScope.kt new file mode 100644 index 0000000..a8d81be --- /dev/null +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureDrawScope.kt @@ -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( + public val state: CanvasState, +) : 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.toDpRect(): DpRect = with(state) { toDpRect() } + + public abstract fun painterFor(feature: PainterFeature): 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( + drawScope: DrawScope, + state: CanvasState, + private val painterCache: Map, Painter>, + private val textMeasurer: TextMeasurer?, +) : FeatureDrawScope(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): 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 FeatureCanvas( + state: CanvasState, + features: FeatureGroup, + modifier: Modifier = Modifier, + draw: FeatureDrawScope.() -> Unit = {}, +) { + val textMeasurer = rememberTextMeasurer(0) + + val painterCache: Map, Painter> = features.features.flatMap { + if (it is FeatureGroup) it.features else listOf(it) + }.filterIsInstance>().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) + ) + ) + } + } +} diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureGroup.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureGroup.kt similarity index 75% rename from maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureGroup.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureGroup.kt index 8cbb6ff..d218c22 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureGroup.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureGroup.kt @@ -1,8 +1,6 @@ -package center.sciprog.maps.features +package space.kscience.maps.features -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.graphics.Color 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.DpSize 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.nd.* import space.kscience.kmath.structures.Buffer @@ -33,8 +33,12 @@ public val > FeatureRef.attributes: Attributes get public data class FeatureGroup( override val space: CoordinateSpace, public val featureMap: SnapshotStateMap> = mutableStateMapOf(), - override val attributes: Attributes = Attributes.EMPTY, ) : CoordinateSpace by space, Feature { + + private val attributesState: MutableState = mutableStateOf(Attributes.EMPTY) + + override val attributes: Attributes get() = attributesState.value + // // @Suppress("UNCHECKED_CAST") // public operator fun > get(id: FeatureId): F = @@ -62,26 +66,8 @@ public data class FeatureGroup( public val features: Collection> get() = featureMap.values.sortedByDescending { it.z } - public fun visit(visitor: FeatureGroup.(id: String, feature: Feature) -> Unit) { - featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) -> - if (feature is FeatureGroup) { - feature.visit(visitor) - } else { - visitor(this, key, feature) - } - } - } - public fun visitUntil(visitor: FeatureGroup.(id: String, feature: Feature) -> Boolean) { - featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) -> - if (feature is FeatureGroup) { - feature.visitUntil(visitor) - } else { - if (!visitor(this, key, feature)) return@visitUntil - } - } - } -// + // // @Suppress("UNCHECKED_CAST") // public fun getAttribute(id: FeatureId>, key: Attribute): A? = // get(id).attributes[key] @@ -91,7 +77,10 @@ public data class FeatureGroup( featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() } - override fun withAttributes(modify: Attributes.() -> Attributes): Feature = copy(attributes = modify(attributes)) + override fun withAttributes(modify: Attributes.() -> Attributes): Feature { + attributesState.value = attributes.modify() + return this + } public companion object { @@ -118,6 +107,29 @@ public data class FeatureGroup( } } +/** + * Recursively search for feature until function returns true + */ +public fun FeatureGroup.forEachUntil(visitor: FeatureGroup.(id: String, feature: Feature) -> Boolean) { + featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) -> + if (feature is FeatureGroup) { + feature.forEachUntil(visitor) + } else { + if (!visitor(this, key, feature)) return@forEachUntil + } + } +} + +/** + * Recursively visit all features in this group + */ +public fun FeatureGroup.forEach( + visitor: FeatureGroup.(id: String, feature: Feature) -> Unit, +): Unit = forEachUntil { id, feature -> + visitor(id, feature) + true +} + /** * Process all features with a given attribute from the one with highest [z] to lowest */ @@ -125,7 +137,7 @@ public fun FeatureGroup.forEachWithAttribute( key: Attribute, block: FeatureGroup.(id: String, feature: Feature, attributeValue: A) -> Unit, ) { - visit { id, feature -> + forEach { id, feature -> feature.attributes[key]?.let { block(id, feature, it) } @@ -136,7 +148,7 @@ public fun FeatureGroup.forEachWithAttributeUntil( key: Attribute, block: FeatureGroup.(id: String, feature: Feature, attributeValue: A) -> Boolean, ) { - visitUntil { id, feature -> + forEachUntil { id, feature -> feature.attributes[key]?.let { block(id, feature, it) } ?: true @@ -146,7 +158,7 @@ public fun FeatureGroup.forEachWithAttributeUntil( public inline fun > FeatureGroup.forEachWithType( crossinline block: (FeatureRef) -> Unit, ) { - visit { id, feature -> + forEach { id, feature -> if (feature is F) block(FeatureRef(id, this)) } } @@ -154,7 +166,7 @@ public inline fun > FeatureGroup.forEachWithT public inline fun > FeatureGroup.forEachWithTypeUntil( crossinline block: (FeatureRef) -> Boolean, ) { - visitUntil { id, feature -> + forEachUntil { id, feature -> if (feature is F) block(FeatureRef(id, this)) else true } } @@ -241,25 +253,23 @@ public fun FeatureGroup.icon( size: DpSize = DpSize(image.defaultWidth, image.defaultHeight), attributes: Attributes = Attributes.EMPTY, id: String? = null, -): FeatureRef> = - feature( - id, - VectorIconFeature( - space, - position, - size, - image, - attributes - ) +): FeatureRef> = feature( + id, + VectorIconFeature( + space, + position, + size, + image, + attributes ) +) public fun FeatureGroup.group( - attributes: Attributes = Attributes.EMPTY, id: String? = null, builder: FeatureGroup.() -> Unit, ): FeatureRef> { val collection = FeatureGroup(space).apply(builder) - val feature = FeatureGroup(space, collection.featureMap, attributes) + val feature = FeatureGroup(space, collection.featureMap) return feature(id, feature) } @@ -276,7 +286,7 @@ public fun FeatureGroup.scalableImage( public fun FeatureGroup.text( position: T, text: String, - font: FeatureFont.() -> Unit = { size = 16f }, + font: Font.() -> Unit = { size = 16f }, attributes: Attributes = Attributes.EMPTY, id: String? = null, ): FeatureRef> = feature( @@ -284,14 +294,14 @@ public fun FeatureGroup.text( TextFeature(space, position, text, fontConfig = font, attributes = attributes) ) -public fun StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND { - val strides = Strides(shape) - return BufferND(strides, Buffer.boxing(strides.linearSize) { initializer(strides.index(it)) }) -} +//public fun StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND { +// val strides = Strides(shape) +// return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }) +//} -public fun Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D { +public inline fun Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D { 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 FeatureGroup.pixelMap( @@ -302,4 +312,23 @@ public fun FeatureGroup.pixelMap( ): FeatureRef> = feature( id, PixelMapFeature(space, rectangle, pixelMap, attributes = attributes) -) \ No newline at end of file +) + +/** + * 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, "") + } +} \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/MouseListener.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/MouseListener.kt similarity index 95% rename from maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/MouseListener.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/MouseListener.kt index dc66b02..2c077d0 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/MouseListener.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/MouseListener.kt @@ -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.isPrimaryPressed diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/ViewConfig.kt similarity index 91% rename from maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/ViewConfig.kt index aca3d4f..db377da 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/ViewConfig.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.features +package space.kscience.maps.features import androidx.compose.ui.unit.DpSize diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewPoint.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/ViewPoint.kt similarity index 80% rename from maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewPoint.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/ViewPoint.kt index bcbfbc7..5f38d80 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewPoint.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/ViewPoint.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.features +package space.kscience.maps.features /** * @param T type of coordinates used for the view point diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/compositeFeatures.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/compositeFeatures.kt similarity index 85% rename from maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/compositeFeatures.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/compositeFeatures.kt index ac360e6..de92173 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/compositeFeatures.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/compositeFeatures.kt @@ -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 @@ -18,9 +18,9 @@ public fun FeatureGroup.draggableLine( space, aId.resolve().center, bId.resolve().center, - Attributes { + Attributes> { ZAttribute(-10f) - lineId?.attributes?.let { from(it) } + lineId?.attributes?.let { putAll(it) } } ) ) @@ -51,9 +51,9 @@ public fun FeatureGroup.draggableMultiLine( MultiLineFeature( space, points.map { it.resolve().center }, - Attributes { + Attributes>{ ZAttribute(-10f) - polygonId?.attributes?.let { from(it) } + polygonId?.attributes?.let { putAll(it) } } ) ) diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/drawFeature.kt similarity index 74% rename from maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/drawFeature.kt index 8948f79..6af5d06 100644 --- a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/drawFeature.kt @@ -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.Size -import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.drawscope.* -import androidx.compose.ui.graphics.painter.Painter -import center.sciprog.attributes.plus -import org.jetbrains.skia.Font -import org.jetbrains.skia.Paint +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PointMode +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.translate +import space.kscience.attributes.plus import space.kscience.kmath.PerformancePitfall -import space.kscience.kmath.geometry.degrees -internal fun Color.toPaint(): Paint = Paint().apply { - isAntiAlias = true - color = toArgb() -} +//internal fun Color.toPaint(): Paint = Paint().apply { +// isAntiAlias = true +// color = toArgb() +//} -public fun DrawScope.drawFeature( - state: CoordinateViewScope, - painterCache: Map, Painter>, + +public fun FeatureDrawScope.drawFeature( feature: Feature, -): Unit = with(state) { +): Unit { val color = feature.color ?: Color.Red 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) { - is FeatureSelector -> drawFeature(state, painterCache, feature.selector(state.zoom)) + is FeatureSelector -> drawFeature(feature.selector(state.zoom)) is CircleFeature -> drawCircle( color, feature.radius.toPx(), - center = feature.center.toOffset() + center = feature.center.toOffset(), + alpha = alpha ) is RectangleFeature -> drawRect( @@ -40,7 +41,8 @@ public fun DrawScope.drawFeature( feature.size.width.toPx() / 2, feature.size.height.toPx() / 2 ), - size = feature.size.toSize() + size = feature.size.toSize(), + alpha = alpha ) is LineFeature -> drawLine( @@ -48,7 +50,8 @@ public fun DrawScope.drawFeature( feature.a.toOffset(), feature.b.toOffset(), strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, - pathEffect = feature.attributes[PathEffectAttribute] + pathEffect = feature.attributes[PathEffectAttribute], + alpha = alpha ) is ArcFeature -> { @@ -58,8 +61,8 @@ public fun DrawScope.drawFeature( drawArc( color = color, - startAngle = (feature.startAngle.degrees).toFloat(), - sweepAngle = (feature.arcLength.degrees).toFloat(), + startAngle = (feature.startAngle.toDegrees().value).toFloat(), + sweepAngle = (feature.arcLength.toDegrees().value).toFloat(), useCenter = false, topLeft = dpRect.topLeft, size = size, @@ -69,28 +72,19 @@ public fun DrawScope.drawFeature( } - is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset()) + is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset(), alpha = alpha) is VectorIconFeature -> { val offset = feature.center.toOffset() val size = feature.size.toSize() translate(offset.x - size.width / 2, offset.y - size.height / 2) { - with(painterCache[feature]!!) { - draw(size) + with(this@drawFeature.painterFor(feature)) { + draw(size, colorFilter = feature.color?.let { ColorFilter.tint(it) }, alpha = alpha) } } } - is TextFeature -> drawIntoCanvas { canvas -> - val offset = feature.position.toOffset() - canvas.nativeCanvas.drawString( - feature.text, - offset.x + 5, - offset.y - 5, - Font().apply(feature.fontConfig), - (feature.color ?: Color.Black).toPaint() - ) - } + is TextFeature -> drawText(feature.text, feature.position.toOffset(), feature.attributes) is DrawFeature -> { val offset = feature.position.toOffset() @@ -101,9 +95,11 @@ public fun DrawScope.drawFeature( is FeatureGroup -> { feature.featureMap.values.forEach { - drawFeature(state, painterCache, it.withAttributes { - feature.attributes + this - }) + drawFeature( + it.withAttributes { + feature.attributes + this + } + ) } } @@ -121,7 +117,7 @@ public fun DrawScope.drawFeature( drawPoints( points = points, color = color, - strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, + strokeWidth = feature.attributes[StrokeAttribute] ?: 5f, pointMode = PointMode.Points, pathEffect = feature.attributes[PathEffectAttribute], alpha = alpha @@ -160,8 +156,8 @@ public fun DrawScope.drawFeature( val offset = rect.topLeft translate(offset.x, offset.y) { - with(painterCache[feature]!!) { - draw(rect.size) + with(this@drawFeature.painterFor(feature)) { + draw(rect.size, alpha = alpha) } } } @@ -184,7 +180,8 @@ public fun DrawScope.drawFeature( x = i * xStep, y = rect.height - j * yStep ), - size = pixelSize + size = pixelSize, + alpha = alpha ) } } diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/mapFeatureAttributes.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/mapFeatureAttributes.kt similarity index 88% rename from maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/mapFeatureAttributes.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/mapFeatureAttributes.kt index 9b85e82..8c10a05 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/mapFeatureAttributes.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/mapFeatureAttributes.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.features +package space.kscience.maps.features import androidx.compose.foundation.ExperimentalFoundationApi 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.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerKeyboardModifiers -import center.sciprog.attributes.Attribute -import center.sciprog.attributes.AttributesBuilder -import center.sciprog.attributes.SetAttribute -import center.sciprog.attributes.withAttribute +import space.kscience.attributes.* public object ZAttribute : Attribute +public val Feature<*>.z: Float + get() = attributes[ZAttribute] ?: 0f + public object DraggableAttribute : Attribute> public object DragListenerAttribute : SetAttribute> @@ -46,20 +46,25 @@ public fun > FeatureRef.zoomRange(range: FloatRang public object AlphaAttribute : Attribute -public fun > FeatureRef.modifyAttributes(modify: AttributesBuilder.() -> Unit): FeatureRef { +public fun > FeatureRef.alpha(alpha: Float): FeatureRef { + require(alpha in 0f..1f) { "Alpha value must be between 0 and 1" } + return modifyAttribute(AlphaAttribute, alpha) +} + +public fun > FeatureRef.modifyAttributes( + modification: AttributesBuilder.() -> Unit, +): FeatureRef { @Suppress("UNCHECKED_CAST") parent.feature( id, - resolve().withAttributes { - AttributesBuilder(this).apply(modify).build() - } as F + resolve().withAttributes { modified(modification) } as F ) return this } public fun , V> FeatureRef.modifyAttribute( key: Attribute, - value: V?, + value: V, ): FeatureRef { @Suppress("UNCHECKED_CAST") parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F) @@ -169,4 +174,7 @@ public fun FeatureRef>.pathEffect(effect: Pat public object StrokeAttribute : Attribute public fun > FeatureRef.stroke(width: Float): FeatureRef = + modifyAttribute(StrokeAttribute, width) + +public fun > FeatureRef.pointSize(width: Float): FeatureRef = modifyAttribute(StrokeAttribute, width) \ No newline at end of file diff --git a/maps-kt-features/src/commonTest/kotlin/center/sciprog/attributes/AttributesSerializationTest.kt b/maps-kt-features/src/commonTest/kotlin/center/sciprog/attributes/AttributesSerializationTest.kt index 9c4c84e..f00507d 100644 --- a/maps-kt-features/src/commonTest/kotlin/center/sciprog/attributes/AttributesSerializationTest.kt +++ b/maps-kt-features/src/commonTest/kotlin/center/sciprog/attributes/AttributesSerializationTest.kt @@ -1,13 +1,21 @@ 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.modules.SerializersModule import kotlinx.serialization.modules.contextual 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.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue internal class AttributesSerializationTest { @@ -28,32 +36,36 @@ internal class AttributesSerializationTest { override fun toString(): String = "test" } + val serializer = AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute)) + @Test fun restoreFromJson() { + val json = Json { serializersModule = SerializersModule { - contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute))) + contextual(serializer) } } - val attributes = Attributes { + val attributes = Attributes { NameAttribute("myTest") TestAttribute(mapOf("a" to "aa", "b" to "bb")) ContainerAttribute( Container( - Attributes { + Attributes { TestAttribute(mapOf("a" to "aa", "b" to "bb")) } ) ) } - val serialized: String = json.encodeToString(attributes) + + val serialized: String = json.encodeToString(serializer, attributes) 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) @@ -62,25 +74,25 @@ internal class AttributesSerializationTest { fun restoreFromProtoBuf() { val protoBuf = ProtoBuf { serializersModule = SerializersModule { - contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute))) + contextual(serializer) } } - val attributes = Attributes { + val attributes = Attributes { NameAttribute("myTest") TestAttribute(mapOf("a" to "aa", "b" to "bb")) ContainerAttribute( Container( - Attributes { + Attributes { 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) } diff --git a/maps-kt-features/src/jsMain/kotlin/center/sciprog/maps/features/FeatureFont.kt b/maps-kt-features/src/jsMain/kotlin/center/sciprog/maps/features/FeatureFont.kt deleted file mode 100644 index 3bca24e..0000000 --- a/maps-kt-features/src/jsMain/kotlin/center/sciprog/maps/features/FeatureFont.kt +++ /dev/null @@ -1,5 +0,0 @@ -package center.sciprog.maps.features - -public actual class FeatureFont { - public actual var size: Float = 16f -} \ No newline at end of file diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/FeatureFont.kt b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/FeatureFont.kt deleted file mode 100644 index 6ec7512..0000000 --- a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/FeatureFont.kt +++ /dev/null @@ -1,5 +0,0 @@ -package center.sciprog.maps.features - -import org.jetbrains.skia.Font - -public actual typealias FeatureFont = Font \ No newline at end of file diff --git a/maps-kt-geojson/README.md b/maps-kt-geojson/README.md index 2e7e374..a998441 100644 --- a/maps-kt-geojson/README.md +++ b/maps-kt-geojson/README.md @@ -6,19 +6,8 @@ ## 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:** ```kotlin repositories { @@ -27,6 +16,6 @@ repositories { } dependencies { - implementation("center.sciprog:maps-kt-geojson:0.2.2") + implementation("space.kscience:maps-kt-geojson:0.3.0") } ``` diff --git a/maps-kt-geojson/build.gradle.kts b/maps-kt-geojson/build.gradle.kts index 2203034..dd243fa 100644 --- a/maps-kt-geojson/build.gradle.kts +++ b/maps-kt-geojson/build.gradle.kts @@ -6,7 +6,9 @@ plugins { kscience{ jvm() - js() +// js() + wasm() + useSerialization { json() } diff --git a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJson.kt b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJson.kt similarity index 93% rename from maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJson.kt rename to maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJson.kt index e5f7480..8951fce 100644 --- a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJson.kt +++ b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJson.kt @@ -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.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 /** diff --git a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonGeometry.kt b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJsonGeometry.kt similarity index 95% rename from maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonGeometry.kt rename to maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJsonGeometry.kt index 1c461a5..ceb79f3 100644 --- a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonGeometry.kt +++ b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJsonGeometry.kt @@ -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 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 public sealed interface GeoJsonGeometry : GeoJson { @@ -35,8 +34,8 @@ internal fun JsonElement.toGmc() = jsonArray.run { } internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray { - add(longitude.degrees) - add(latitude.degrees) + add(longitude.toDegrees().value) + add(latitude.toDegrees().value) elevation?.let { add(it.meters) } diff --git a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonPropertiesAttribute.kt b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJsonPropertiesAttribute.kt similarity index 53% rename from maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonPropertiesAttribute.kt rename to maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJsonPropertiesAttribute.kt index 2643e96..8b1b0c7 100644 --- a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonPropertiesAttribute.kt +++ b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJsonPropertiesAttribute.kt @@ -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.serializer +import space.kscience.SerializableAttribute public object GeoJsonPropertiesAttribute : SerializableAttribute("properties", serializer()) \ No newline at end of file diff --git a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonSerializer.kt b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJsonSerializer.kt similarity index 94% rename from maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonSerializer.kt rename to maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJsonSerializer.kt index 3ba3bd9..5b71cd8 100644 --- a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/GeoJsonSerializer.kt +++ b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/GeoJsonSerializer.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.geojson +package space.kscience.maps.geojson import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.SerialDescriptor diff --git a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/geoJsonToMap.kt b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/geoJsonToMap.kt similarity index 92% rename from maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/geoJsonToMap.kt rename to maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/geoJsonToMap.kt index 7b91d3b..352376b 100644 --- a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/geoJsonToMap.kt +++ b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/geoJsonToMap.kt @@ -1,12 +1,12 @@ -package center.sciprog.maps.geojson +package space.kscience.maps.geojson 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.intOrNull import kotlinx.serialization.json.jsonPrimitive +import space.kscience.NameAttribute +import space.kscience.maps.coordinates.Gmc +import space.kscience.maps.features.* /** diff --git a/maps-kt-geojson/src/jvmMain/kotlin/center/sciprog/maps/geojson/geoJsonFeatureJvm.kt b/maps-kt-geojson/src/jvmMain/kotlin/space/kscience/maps/geojson/geoJsonFeatureJvm.kt similarity index 66% rename from maps-kt-geojson/src/jvmMain/kotlin/center/sciprog/maps/geojson/geoJsonFeatureJvm.kt rename to maps-kt-geojson/src/jvmMain/kotlin/space/kscience/maps/geojson/geoJsonFeatureJvm.kt index da84729..5a1fbe3 100644 --- a/maps-kt-geojson/src/jvmMain/kotlin/center/sciprog/maps/geojson/geoJsonFeatureJvm.kt +++ b/maps-kt-geojson/src/jvmMain/kotlin/space/kscience/maps/geojson/geoJsonFeatureJvm.kt @@ -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.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 /** diff --git a/maps-kt-geotiff/src/main/kotlin/center/sciprog/maps/geotiff/geotiff.kt b/maps-kt-geotiff/src/main/kotlin/center/sciprog/maps/geotiff/geotiff.kt index e77ba3f..a1e43ef 100644 --- a/maps-kt-geotiff/src/main/kotlin/center/sciprog/maps/geotiff/geotiff.kt +++ b/maps-kt-geotiff/src/main/kotlin/center/sciprog/maps/geotiff/geotiff.kt @@ -1,9 +1,9 @@ package center.sciprog.maps.geotiff -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 space.kscience.maps.coordinates.Gmc +import space.kscience.maps.features.Feature +import space.kscience.maps.features.FeatureGroup +import space.kscience.maps.features.FeatureRef import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import org.geotools.gce.geotiff.GeoTiffReader diff --git a/maps-kt-leaflet/build.gradle.kts b/maps-kt-leaflet/build.gradle.kts new file mode 100644 index 0000000..6644712 --- /dev/null +++ b/maps-kt-leaflet/build.gradle.kts @@ -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{} +} \ No newline at end of file diff --git a/maps-kt-leaflet/src/jsMain/kotlin/main.kt b/maps-kt-leaflet/src/jsMain/kotlin/main.kt new file mode 100644 index 0000000..e6d832d --- /dev/null +++ b/maps-kt-leaflet/src/jsMain/kotlin/main.kt @@ -0,0 +1,7 @@ +import org.jetbrains.skiko.wasm.onWasmReady + +fun main() { + onWasmReady { + + } +} \ No newline at end of file diff --git a/maps-kt-scheme/README.md b/maps-kt-scheme/README.md index b95b404..038ddc1 100644 --- a/maps-kt-scheme/README.md +++ b/maps-kt-scheme/README.md @@ -6,19 +6,8 @@ ## 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:** ```kotlin repositories { @@ -27,6 +16,6 @@ repositories { } dependencies { - implementation("center.sciprog:maps-kt-scheme:0.2.2") + implementation("space.kscience:maps-kt-scheme:0.3.0") } ``` diff --git a/maps-kt-scheme/build.gradle.kts b/maps-kt-scheme/build.gradle.kts index 48c27b8..e9adb9d 100644 --- a/maps-kt-scheme/build.gradle.kts +++ b/maps-kt-scheme/build.gradle.kts @@ -1,31 +1,25 @@ plugins { id("space.kscience.gradle.mpp") - id("org.jetbrains.compose") + alias(spclibs.plugins.compose.compiler) + alias(spclibs.plugins.compose.jb) `maven-publish` } kscience{ jvm() -} +// js() + wasm() -kotlin { - sourceSets { - commonMain { - dependencies { - api(projects.mapsKtFeatures) - api("io.github.microutils:kotlin-logging:2.1.23") - api(compose.foundation) - } - } - val jvmMain by getting { - dependencies { - implementation("org.jfree:org.jfree.svg:5.0.4") - api(compose.desktop.currentOs) - } - } + commonMain{ + api(projects.mapsKtFeatures) + } + jvmMain{ + implementation("org.jfree:org.jfree.svg:5.0.4") + api(compose.desktop.currentOs) } } + //java { // targetCompatibility = JVM_TARGET //} \ No newline at end of file diff --git a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/scheme/SchemeView.kt b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/SchemeView.kt similarity index 53% rename from maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/scheme/SchemeView.kt rename to maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/SchemeView.kt index f482df8..2caf3e5 100644 --- a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/scheme/SchemeView.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/SchemeView.kt @@ -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.runtime.Composable -import androidx.compose.runtime.key 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 center.sciprog.attributes.z -import center.sciprog.maps.compose.mapControls -import center.sciprog.maps.features.* -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging +import space.kscience.maps.compose.canvasControls +import space.kscience.maps.features.* import kotlin.math.min @@ -22,48 +14,14 @@ private val logger = KotlinLogging.logger("SchemeView") @Composable public fun SchemeView( - state: XYViewScope, + state: XYCanvasState, features: FeatureGroup, modifier: Modifier = Modifier.fillMaxSize(), -): Unit = key(state, features) { - with(state) { - //Can't do that inside canvas - val painterCache: Map, Painter> = - features.features.filterIsInstance>().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) - ) - ) - } - } - } - +): Unit { + FeatureCanvas(state, features, modifier = modifier.canvasControls(state, features)) } + public fun Rectangle.computeViewPoint( canvasSize: DpSize = defaultCanvasSize, ): ViewPoint { @@ -87,7 +45,7 @@ public fun SchemeView( modifier: Modifier = Modifier.fillMaxSize(), ) { - val state = XYViewScope.remember( + val state = XYCanvasState.remember( config, initialViewPoint = initialViewPoint, initialRectangle = initialRectangle ?: features.getBoundingBox(Float.MAX_VALUE), @@ -112,7 +70,7 @@ public fun SchemeView( buildFeatures: FeatureGroup.() -> Unit = {}, ) { val featureState = FeatureGroup.remember(XYCoordinateSpace, buildFeatures) - val mapState: XYViewScope = XYViewScope.remember( + val mapState: XYCanvasState = XYCanvasState.remember( config, initialViewPoint = initialViewPoint, initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox( diff --git a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XY.kt b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/XY.kt similarity index 86% rename from maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XY.kt rename to maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/XY.kt index 8895cbd..f54a736 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XY.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/XY.kt @@ -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.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.maps.features.CoordinateSpace +import space.kscience.maps.features.Rectangle +import space.kscience.maps.features.ViewPoint import kotlin.math.abs import kotlin.math.max import kotlin.math.min -public data class XY(override val x: Float, override val y: Float): Vector2D +public data class XY(override val x: Float, override val y: Float) : Vector2D + +public fun XY(x: Number, y: Number): XY = XY(x.toFloat(), y.toFloat()) internal data class XYRectangle( override val a: XY, diff --git a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYViewScope.kt b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/XYCanvasState.kt similarity index 77% rename from maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYViewScope.kt rename to maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/XYCanvasState.kt index 550a66a..efc7879 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYViewScope.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/XYCanvasState.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.scheme +package space.kscience.maps.scheme import androidx.compose.runtime.Composable 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.DpRect import androidx.compose.ui.unit.dp -import center.sciprog.maps.features.* +import space.kscience.maps.features.* import kotlin.math.min -public class XYViewScope( +public class XYCanvasState( config: ViewConfig, -) : CoordinateViewScope(config) { +) : CanvasState(config) { override val space: CoordinateSpace get() = XYCoordinateSpace @@ -26,12 +26,15 @@ public class XYViewScope( ) override fun computeViewPoint(rectangle: Rectangle): ViewPoint { - val scale = min( + val scale: Float = min( canvasSize.width.value / rectangle.width, canvasSize.height.value / rectangle.height ) - - return XYViewPoint(rectangle.center, scale) + return if(scale.isInfinite()){ + XYViewPoint(rectangle.center, 1f) + } else { + XYViewPoint(rectangle.center, scale) + } } override fun ViewPoint.moveBy(x: Dp, y: Dp): ViewPoint { @@ -51,12 +54,12 @@ public class XYViewScope( config: ViewConfig = ViewConfig(), initialViewPoint: ViewPoint? = null, initialRectangle: Rectangle? = null, - ): XYViewScope = remember { - XYViewScope(config).also { mapState-> + ): XYCanvasState = remember { + XYCanvasState(config).apply { if (initialViewPoint != null) { - mapState.viewPoint = initialViewPoint + viewPoint = initialViewPoint } else if (initialRectangle != null) { - mapState.viewPoint = mapState.computeViewPoint(initialRectangle) + viewPoint = computeViewPoint(initialRectangle) } } } diff --git a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYCoordinateSpace.kt b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/XYCoordinateSpace.kt similarity index 93% rename from maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYCoordinateSpace.kt rename to maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/XYCoordinateSpace.kt index 7aced67..af82b51 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYCoordinateSpace.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/XYCoordinateSpace.kt @@ -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.DpSize import androidx.compose.ui.unit.dp -import center.sciprog.maps.features.CoordinateSpace -import center.sciprog.maps.features.Rectangle -import center.sciprog.maps.features.ViewPoint +import space.kscience.maps.features.CoordinateSpace +import space.kscience.maps.features.Rectangle +import space.kscience.maps.features.ViewPoint import kotlin.math.abs import kotlin.math.pow diff --git a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/schemeFeatures.kt b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/schemeFeatures.kt similarity index 75% rename from maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/schemeFeatures.kt rename to maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/schemeFeatures.kt index 1ceaa6d..c795eeb 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/schemeFeatures.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/schemeFeatures.kt @@ -1,4 +1,4 @@ -package center.sciprog.maps.scheme +package space.kscience.maps.scheme import androidx.compose.runtime.Composable 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.DpSize import androidx.compose.ui.unit.dp -import center.sciprog.attributes.Attributes -import center.sciprog.maps.features.* +import space.kscience.attributes.Attributes import space.kscience.kmath.geometry.Angle +import space.kscience.maps.features.* import kotlin.math.ceil internal fun Pair.toCoordinates(): XY = XY(first.toFloat(), second.toFloat()) @@ -41,7 +41,7 @@ public fun FeatureGroup.circle( centerCoordinates: Pair, size: Dp = 5.dp, id: String? = null, -): FeatureRef> = circle(centerCoordinates.toCoordinates(), size, id = id) +): FeatureRef> = circle(centerCoordinates.toCoordinates(), size, id = id) public fun FeatureGroup.draw( position: Pair, @@ -63,7 +63,7 @@ public fun FeatureGroup.arc( arcLength: Angle, id: String? = null, ): FeatureRef> = arc( - oval = XYCoordinateSpace.Rectangle(center.toCoordinates(), 2*radius, 2*radius), + oval = XYCoordinateSpace.Rectangle(center.toCoordinates(), 2 * radius, 2 * radius), startAngle = startAngle, arcLength = arcLength, id = id @@ -108,4 +108,33 @@ public fun FeatureGroup.pixelMap( ) ) +public fun FeatureGroup.rectanglePolygon( + left: Number, right: Number, + bottom: Number, top: Number, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = 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.rectanglePolygon( + rectangle: Rectangle, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = polygon( + listOf( + XY(rectangle.left, rectangle.top), + XY(rectangle.right, rectangle.top), + XY(rectangle.right, rectangle.bottom), + XY(rectangle.left, rectangle.bottom) + ), + attributes, id +) + diff --git a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/svg/exportToSvg.kt b/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/svg/exportToSvg.kt deleted file mode 100644 index 0424bbf..0000000 --- a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/svg/exportToSvg.kt +++ /dev/null @@ -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( - public val features: Map>, - internal val painterCache: Map, Painter>, -) - -@Composable -public fun FeatureGroup.snapshot(): FeatureStateSnapshot = FeatureStateSnapshot( - featureMap, - features.filterIsInstance>().associateWith { it.getPainter() } -) - -public fun FeatureStateSnapshot.generateSvg( - viewPoint: ViewPoint, - 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) { - - 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>().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.exportToSvg( - viewPoint: ViewPoint, - width: Double, - height: Double, - path: java.nio.file.Path, -) { - - val svgString = generateSvg(viewPoint, width, height) - - SVGUtils.writeToSVG(path.toFile(), svgString) -} \ No newline at end of file diff --git a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/svg/SvgCanvas.kt b/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/SvgCanvas.kt similarity index 99% rename from maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/svg/SvgCanvas.kt rename to maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/SvgCanvas.kt index d90b11a..96e1f82 100644 --- a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/svg/SvgCanvas.kt +++ b/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/SvgCanvas.kt @@ -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.Rect @@ -301,7 +301,7 @@ internal class SvgCanvas(val graphics: SVGGraphics2D) : Canvas { } 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() } \ No newline at end of file diff --git a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/svg/SvgDrawScope.kt b/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/SvgDrawScope.kt similarity index 91% rename from maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/svg/SvgDrawScope.kt rename to maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/SvgDrawScope.kt index 024a4ea..ac67577 100644 --- a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/svg/SvgDrawScope.kt +++ b/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/SvgDrawScope.kt @@ -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.Offset import androidx.compose.ui.geometry.Size 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.IntSize import androidx.compose.ui.unit.LayoutDirection 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.Font import java.awt.geom.* import java.awt.image.AffineTransformOp import java.awt.Color as AWTColor public class SvgDrawScope( + state: CanvasState, private val graphics: SVGGraphics2D, - size: Size, + private val painterCache: Map, Painter>, private val defaultStrokeWidth: Float = 1f, -) : DrawScope { +) : FeatureDrawScope(state) { override val layoutDirection: LayoutDirection get() = LayoutDirection.Ltr @@ -459,18 +466,22 @@ public class SvgDrawScope( } } - public fun drawText( - text: String, - x: Float, - y: Float, - font: Font, - color: Color, + public fun renderText( + textFeature: TextFeature, ) { - setupColor(color) - graphics.font = font - graphics.drawString(text, x, y) + textFeature.color?.let { setupColor(it) } + graphics.drawString(textFeature.text, textFeature.position.x, textFeature.position.y) } - override val drawContext: DrawContext = SvgDrawContext(graphics, size) + override fun painterFor(feature: PainterFeature): 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()) } \ No newline at end of file diff --git a/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/exportToSvg.kt b/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/exportToSvg.kt new file mode 100644 index 0000000..0fc4131 --- /dev/null +++ b/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/exportToSvg.kt @@ -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( + public val features: Map>, + internal val painterCache: Map, Painter>, +) + +@Composable +public fun FeatureGroup.snapshot(): FeatureStateSnapshot = FeatureStateSnapshot( + featureMap, + features.flatMap { + if (it is FeatureGroup) it.features else listOf(it) + }.filterIsInstance>().associateWith { it.getPainter() } +) + + +public fun FeatureStateSnapshot.generateSvg( + viewPoint: ViewPoint, + 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) { +// +// 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.exportToSvg( + viewPoint: ViewPoint, + width: Double, + height: Double, + path: java.nio.file.Path, +) { + val svgString: String = generateSvg(viewPoint, width, height) + SVGUtils.writeToSVG(path.toFile(), svgString) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 172c19b..fc64058 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,13 +16,11 @@ pluginManagement { } plugins { - id("com.android.application").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("com.android.application").version(extra["agp.version"] as String) +// id("com.android.library").version(extra["agp.version"] as String) id("space.kscience.gradle.project") version toolsVersion id("space.kscience.gradle.mpp") 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-compose", ":maps-kt-scheme", +// ":maps-kt-leaflet", ":demo:maps", ":demo:scheme", ":demo:polygon-editor", - ":demo:trajectory-playground" + ":demo:trajectory-playground", + ":demo:maps-wasm" ) diff --git a/trajectory-kt/README.md b/trajectory-kt/README.md index 0936738..3d9ecdb 100644 --- a/trajectory-kt/README.md +++ b/trajectory-kt/README.md @@ -5,19 +5,8 @@ ## 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:** ```kotlin repositories { @@ -26,7 +15,7 @@ repositories { } dependencies { - implementation("space.kscience:trajectory-kt:0.2.2") + implementation("space.kscience:trajectory-kt:0.3.0") } ``` diff --git a/trajectory-kt/build.gradle.kts b/trajectory-kt/build.gradle.kts index 45512f0..0ce04c3 100644 --- a/trajectory-kt/build.gradle.kts +++ b/trajectory-kt/build.gradle.kts @@ -11,9 +11,12 @@ kscience{ jvm() js() native() + wasm() useContextReceivers() - useSerialization() + useSerialization{ + json() + } dependencies { api("space.kscience:kmath-geometry:$kmathVersion") } diff --git a/maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/AttributesSerializer.kt b/trajectory-kt/src/commonMain/kotlin/space/kscience/AttributesSerializer.kt similarity index 70% rename from maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/AttributesSerializer.kt rename to trajectory-kt/src/commonMain/kotlin/space/kscience/AttributesSerializer.kt index 690f923..b3f67d5 100644 --- a/maps-kt-features/src/commonMain/kotlin/center.sciprog.attributes/AttributesSerializer.kt +++ b/trajectory-kt/src/commonMain/kotlin/space/kscience/AttributesSerializer.kt @@ -1,12 +1,15 @@ @file:Suppress("UNCHECKED_CAST") -package center.sciprog.attributes +package space.kscience import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* +import space.kscience.attributes.Attribute +import space.kscience.attributes.Attributes public class AttributesSerializer( private val serializableAttributes: Set>, @@ -29,12 +32,17 @@ public class AttributesSerializer( attr to value } - return Attributes(attributeMap) + return object : Attributes { + override val content: Map, 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) { 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") val serializableKey = key as SerializableAttribute @@ -46,10 +54,19 @@ public class AttributesSerializer( put( serializableKey.serialId, - json.encodeToJsonElement(serializableKey.serializer as KSerializer, value) + json.encodeToJsonElement(serializableKey.serializer as KSerializer, value) ) } } jsonSerializer.serialize(encoder, json) } -} \ No newline at end of file +} + +public abstract class SerializableAttribute( + public val serialId: String, + public val serializer: KSerializer, +) : Attribute { + override fun toString(): String = serialId +} + +public object NameAttribute : SerializableAttribute("name", String.serializer()) \ No newline at end of file diff --git a/trajectory-kt/src/commonMain/kotlin/space/kscience/kmath/geometry/geometryExtensions.kt b/trajectory-kt/src/commonMain/kotlin/space/kscience/geometryExtensions.kt similarity index 64% rename from trajectory-kt/src/commonMain/kotlin/space/kscience/kmath/geometry/geometryExtensions.kt rename to trajectory-kt/src/commonMain/kotlin/space/kscience/geometryExtensions.kt index 3a06630..dc33e89 100644 --- a/trajectory-kt/src/commonMain/kotlin/space/kscience/kmath/geometry/geometryExtensions.kt +++ b/trajectory-kt/src/commonMain/kotlin/space/kscience/geometryExtensions.kt @@ -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 kotlin.math.abs import kotlin.math.pow import kotlin.math.sign 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 = Circle2D(vector(x, y), radius = radius.toDouble()) -public fun Euclidean2DSpace.segment(begin: DoubleVector2D, end: DoubleVector2D): LineSegment2D = +public fun Float64Space2D.segment(begin: Vector2D, end: Vector2D): LineSegment2D = 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)) -public fun Euclidean2DSpace.intersectsOrInside(circle1: Circle2D, circle2: Circle2D): Boolean { +public fun Float64Space2D.intersectsOrInside(circle1: Circle2D, circle2: Circle2D): Boolean { val distance = norm(circle2.center - circle1.center) 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 */ -public fun Euclidean2DSpace.intersects(segment: LineSegment2D, circle: Circle2D): Boolean { +public fun Float64Space2D.intersects(segment: LineSegment2D, circle: Circle2D): Boolean { val direction = segment.end - segment.begin 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, segment: LineSegment2D): Boolean = intersects(segment, circle) -public fun Euclidean2DSpace.intersects(segment1: LineSegment2D, segment2: LineSegment2D): Boolean { - infix fun DoubleVector2D.cross(v2: DoubleVector2D): Double = x * v2.y - y * v2.x - infix fun DoubleVector2D.crossSign(v2: DoubleVector2D) = cross(v2).sign +public fun Float64Space2D.intersects(segment1: LineSegment2D, segment2: LineSegment2D): Boolean { + infix fun Vector2D.cross(v2: Vector2D): Double = x * v2.y - y * v2.x + infix fun Vector2D.crossSign(v2: Vector2D) = cross(v2).sign - return with(Euclidean2DSpace) { + return with(Float64Space2D) { (segment2.begin - segment1.begin) crossSign (segment2.end - segment1.begin) != (segment2.begin - segment1.end) crossSign (segment2.end - segment1.end) && (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) { is CircleTrajectory2D -> intersects(segment, trajectory.circle) 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 */ -public fun Circle2D.tangent(bearing: Angle, direction: Trajectory2D.Direction): Pose2D = with(Euclidean2DSpace) { +public fun Circle2D.tangent(bearing: Angle, direction: Trajectory2D.Direction): Pose2D = with(Float64Space2D) { val coordinates: Vector2D = vector(center.x + radius * sin(bearing), center.y + radius * cos(bearing)) val tangentAngle = when (direction) { 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): Boolean = with(Float64Space2D) { val radiusVector = point - center if (abs(norm(radiusVector) - circle.radius) > 1e-4 * circle.radius) error("Wrong radius") val radiusVectorBearing = radiusVector.bearing diff --git a/trajectory-kt/src/commonMain/kotlin/space/kscience/kmath/geometry/polygonExtensions.kt b/trajectory-kt/src/commonMain/kotlin/space/kscience/kmath/geometry/polygonExtensions.kt deleted file mode 100644 index efa3277..0000000 --- a/trajectory-kt/src/commonMain/kotlin/space/kscience/kmath/geometry/polygonExtensions.kt +++ /dev/null @@ -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): Polygon = object : Polygon { - override val points: List> get() = points -} - -public fun Euclidean2DSpace.intersects(polygon: Polygon, segment: LineSegment2D): Boolean = - polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, segment) } - -public fun Euclidean2DSpace.intersects(polygon: Polygon, circle: Circle2D): Boolean = - polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, circle) } - -public fun Euclidean2DSpace.intersectsTrajectory(polygon: Polygon, trajectory: Trajectory2D): Boolean = - polygon.points.zipWithNextCircular { l, r -> - segment(l, r) - }.any { edge -> - intersectsTrajectory(edge, trajectory) - } \ No newline at end of file diff --git a/trajectory-kt/src/commonMain/kotlin/space/kscience/polygonExtensions.kt b/trajectory-kt/src/commonMain/kotlin/space/kscience/polygonExtensions.kt new file mode 100644 index 0000000..205db56 --- /dev/null +++ b/trajectory-kt/src/commonMain/kotlin/space/kscience/polygonExtensions.kt @@ -0,0 +1,28 @@ +package space.kscience + +import space.kscience.kmath.geometry.LineSegment2D +import space.kscience.kmath.geometry.Polygon +import space.kscience.kmath.geometry.Vector2D +import space.kscience.kmath.geometry.euclidean2d.Circle2D +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D +import space.kscience.kmath.misc.zipWithNextCircular +import space.kscience.kmath.structures.Float64 +import space.kscience.trajectory.Trajectory2D + +public fun Float64Space2D.polygon(points: List>): Polygon> = + object : Polygon> { + override val points: List> get() = points + } + +public fun Float64Space2D.intersects(polygon: Polygon>, segment: LineSegment2D): Boolean = + polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, segment) } + +public fun Float64Space2D.intersects(polygon: Polygon>, circle: Circle2D): Boolean = + polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, circle) } + +public fun Float64Space2D.intersectsTrajectory(polygon: Polygon>, trajectory: Trajectory2D): Boolean = + polygon.points.zipWithNextCircular { l, r -> + segment(l, r) + }.any { edge -> + intersectsTrajectory(edge, trajectory) + } \ No newline at end of file diff --git a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/DubinsPath.kt b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/DubinsPath.kt index a4736e9..ae870c9 100644 --- a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/DubinsPath.kt +++ b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/DubinsPath.kt @@ -5,61 +5,70 @@ package space.kscience.trajectory -import space.kscience.kmath.geometry.* -import space.kscience.kmath.geometry.Euclidean2DSpace.distanceTo +import space.kscience.kmath.geometry.cos +import space.kscience.kmath.geometry.euclidean2d.Circle2D +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D.distanceTo +import space.kscience.kmath.geometry.normalized +import space.kscience.kmath.geometry.radians +import space.kscience.kmath.geometry.sin +import space.kscience.kmath.structures.Float64 import space.kscience.trajectory.Trajectory2D.* import kotlin.math.acos -internal fun Pose2D.getLeftCircle(radius: Double): Circle2D = getTangentCircles(radius).first +internal fun Pose2D.getLeftCircle(radius: Double): Circle2D = getTangentCircles(radius).first -internal fun Pose2D.getRightCircle(radius: Double): Circle2D = getTangentCircles(radius).second +internal fun Pose2D.getRightCircle(radius: Double): Circle2D = getTangentCircles(radius).second -internal fun Pose2D.getTangentCircles(radius: Double): Pair = with(Euclidean2DSpace) { - val dX = radius * cos(bearing) - val dY = radius * sin(bearing) - return Circle2D(vector(x - dX, y + dY), radius) to Circle2D(vector(x + dX, y - dY), radius) -} +internal fun Pose2D.getTangentCircles(radius: Double): Pair, Circle2D> = + with(Float64Space2D) { + val dX = radius * cos(bearing) + val dY = radius * sin(bearing) + return Circle2D(vector(x - dX, y + dY), radius) to Circle2D(vector(x + dX, y - dY), radius) + } -private fun outerTangent(from: Circle2D, to: Circle2D, direction: Direction): StraightTrajectory2D = - with(Euclidean2DSpace) { - val centers = StraightTrajectory2D(from.center, to.center) - val p1 = when (direction) { - L -> vector( - from.center.x - from.radius * cos(centers.bearing), - from.center.y + from.radius * sin(centers.bearing) - ) +private fun Float64Space2D.outerTangent( + from: Circle2D, + to: Circle2D, + direction: Direction, +): StraightTrajectory2D { + val centers = StraightTrajectory2D(from.center, to.center) + val p1 = when (direction) { + L -> vector( + from.center.x - from.radius * cos(centers.bearing), + from.center.y + from.radius * sin(centers.bearing) + ) - R -> vector( - from.center.x + from.radius * cos(centers.bearing), - from.center.y - from.radius * sin(centers.bearing) - ) - } - return StraightTrajectory2D( - p1, - vector(p1.x + (centers.end.x - centers.begin.x), p1.y + (centers.end.y - centers.begin.y)) + R -> vector( + from.center.x + from.radius * cos(centers.bearing), + from.center.y - from.radius * sin(centers.bearing) ) } + return StraightTrajectory2D( + p1, + vector(p1.x + (centers.end.x - centers.begin.x), p1.y + (centers.end.y - centers.begin.y)) + ) +} -private fun innerTangent( - from: Circle2D, - to: Circle2D, +private fun Float64Space2D.innerTangent( + from: Circle2D, + to: Circle2D, direction: Direction, -): StraightTrajectory2D? = - with(Euclidean2DSpace) { - val centers = StraightTrajectory2D(from.center, to.center) - if (centers.length < from.radius * 2) return null - val angle = when (direction) { - L -> centers.bearing + acos(from.radius * 2 / centers.length).radians - R -> centers.bearing - acos(from.radius * 2 / centers.length).radians - }.normalized() +): StraightTrajectory2D? { + val centers = StraightTrajectory2D(from.center, to.center) + if (centers.length < from.radius * 2) return null + val angle = when (direction) { + L -> centers.bearing + acos(from.radius * 2 / centers.length).radians + R -> centers.bearing - acos(from.radius * 2 / centers.length).radians + }.normalized() - val dX = from.radius * sin(angle) - val dY = from.radius * cos(angle) - val p1 = vector(from.center.x + dX, from.center.y + dY) - val p2 = vector(to.center.x - dX, to.center.y - dY) - return StraightTrajectory2D(p1, p2) - } + val dX = from.radius * sin(angle) + val dY = from.radius * cos(angle) + val p1 = vector(from.center.x + dX, from.center.y + dY) + val p2 = vector(to.center.x - dX, to.center.y - dY) + return StraightTrajectory2D(p1, p2) +} @Suppress("DuplicatedCode") @@ -118,7 +127,7 @@ public object DubinsPath { all(start, end, turningRadius).minBy { it.length } public fun rlr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? = - with(Euclidean2DSpace) { + with(Float64Space2D) { val c1 = start.getRightCircle(turningRadius) val c2 = end.getRightCircle(turningRadius) val centers = StraightTrajectory2D(c1.center, c2.center) @@ -162,7 +171,7 @@ public object DubinsPath { } public fun lrl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? = - with(Euclidean2DSpace) { + with(Float64Space2D) { val c1 = start.getLeftCircle(turningRadius) val c2 = end.getLeftCircle(turningRadius) val centers = StraightTrajectory2D(c1.center, c2.center) @@ -208,7 +217,7 @@ public object DubinsPath { public fun rsr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D { val c1 = start.getRightCircle(turningRadius) val c2 = end.getRightCircle(turningRadius) - val s = outerTangent(c1, c2, L) + val s = Float64Space2D.outerTangent(c1, c2, L) val a1 = CircleTrajectory2D(c1.center, start, s.begin, R) val a3 = CircleTrajectory2D(c2.center, s.end, end, R) return CompositeTrajectory2D(a1, s, a3) @@ -217,7 +226,7 @@ public object DubinsPath { public fun lsl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D { val c1 = start.getLeftCircle(turningRadius) val c2 = end.getLeftCircle(turningRadius) - val s = outerTangent(c1, c2, R) + val s = Float64Space2D.outerTangent(c1, c2, R) val a1 = CircleTrajectory2D(c1.center, start, s.begin, L) val a3 = CircleTrajectory2D(c2.center, s.end, end, L) return CompositeTrajectory2D(a1, s, a3) @@ -226,7 +235,7 @@ public object DubinsPath { public fun rsl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? { val c1 = start.getRightCircle(turningRadius) val c2 = end.getLeftCircle(turningRadius) - val s = innerTangent(c1, c2, R) + val s = Float64Space2D.innerTangent(c1, c2, R) if (s == null || c1.center.distanceTo(c2.center) < turningRadius * 2) return null val a1 = CircleTrajectory2D(c1.center, start, s.begin, R) @@ -237,7 +246,7 @@ public object DubinsPath { public fun lsr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? { val c1 = start.getLeftCircle(turningRadius) val c2 = end.getRightCircle(turningRadius) - val s = innerTangent(c1, c2, L) + val s = Float64Space2D.innerTangent(c1, c2, L) if (s == null || c1.center.distanceTo(c2.center) < turningRadius * 2) return null val a1 = CircleTrajectory2D(c1.center, start, s.begin, L) diff --git a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Obstacle.kt b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Obstacle.kt index d52038b..9de8253 100644 --- a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Obstacle.kt +++ b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Obstacle.kt @@ -5,8 +5,15 @@ package space.kscience.trajectory -import space.kscience.kmath.geometry.* +import space.kscience.intersectsTrajectory +import space.kscience.kmath.geometry.Angle +import space.kscience.kmath.geometry.Polygon +import space.kscience.kmath.geometry.Vector2D +import space.kscience.kmath.geometry.euclidean2d.Circle2D +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D import space.kscience.kmath.misc.zipWithNextCircular +import space.kscience.kmath.structures.Float64 +import space.kscience.polygon public interface Obstacle { @@ -30,7 +37,7 @@ public interface Obstacle { } } -private class CircleObstacle(val circle: Circle2D) : Obstacle { +private class CircleObstacle(val circle: Circle2D) : Obstacle { override val center: Vector2D get() = circle.center override val arcs: List @@ -41,7 +48,7 @@ private class CircleObstacle(val circle: Circle2D) : Obstacle { override fun intersectsTrajectory(trajectory: Trajectory2D): Boolean = - Euclidean2DSpace.intersectsTrajectory(circumvention, trajectory) + Float64Space2D.intersectsTrajectory(circumvention, trajectory) override fun equals(other: Any?): Boolean { if (this === other) return true @@ -67,18 +74,18 @@ private class CoreObstacle(override val circumvention: CompositeTrajectory2D) : } override val center: Vector2D by lazy { - Euclidean2DSpace.vector( + Float64Space2D.vector( arcs.sumOf { it.center.x } / arcs.size, arcs.sumOf { it.center.y } / arcs.size ) } - val core: Polygon by lazy { - Euclidean2DSpace.polygon(arcs.map { it.circle.center }) + val core: Polygon> by lazy { + Float64Space2D.polygon(arcs.map { it.circle.center }) } override fun intersectsTrajectory(trajectory: Trajectory2D): Boolean = - Euclidean2DSpace.intersectsTrajectory(core, trajectory) + Float64Space2D.intersectsTrajectory(core, trajectory) override fun equals(other: Any?): Boolean { @@ -99,7 +106,7 @@ private class CoreObstacle(override val circumvention: CompositeTrajectory2D) : } } -public fun Obstacle(circles: List): Obstacle = with(Euclidean2DSpace) { +public fun Obstacle(circles: List>): Obstacle = with(Float64Space2D) { require(circles.isNotEmpty()) { "Can't create circumvention for an empty obstacle" } //Create a single circle obstacle if(circles.size == 1) return CircleObstacle(circles.first()) @@ -123,7 +130,7 @@ public fun Obstacle(circles: List): Obstacle = with(Euclidean2DSpace) (it.center - center).bearing } - val tangents = convex.zipWithNextCircular { a: Circle2D, b: Circle2D -> + val tangents = convex.zipWithNextCircular { a: Circle2D, b: Circle2D -> tangentsBetweenCircles(a, b)[DubinsPath.Type.RSR] ?: error("Can't find right handed circumvention") } @@ -144,7 +151,7 @@ public fun Obstacle(circles: List): Obstacle = with(Euclidean2DSpace) } -public fun Obstacle(vararg circles: Circle2D): Obstacle = Obstacle(listOf(*circles)) +public fun Obstacle(vararg circles: Circle2D): Obstacle = Obstacle(listOf(*circles)) public fun Obstacle(points: List>, radius: Double): Obstacle = Obstacle(points.map { Circle2D(it, radius) }) diff --git a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Obstacles.kt b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Obstacles.kt index 060a189..1a4ccf7 100644 --- a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Obstacles.kt +++ b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Obstacles.kt @@ -1,13 +1,21 @@ package space.kscience.trajectory -import space.kscience.kmath.geometry.* +import space.kscience.containsPoint +import space.kscience.intersects +import space.kscience.intersectsOrInside +import space.kscience.kmath.geometry.Angle +import space.kscience.kmath.geometry.Polygon +import space.kscience.kmath.geometry.Vector2D +import space.kscience.kmath.geometry.euclidean2d.Circle2D +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D +import space.kscience.kmath.structures.Float64 import kotlin.collections.component1 import kotlin.collections.component2 /** * The same as [intersectsTrajectory], but bypasses same circles or same straights */ -private fun Euclidean2DSpace.intersectsOtherTrajectory(a: Trajectory2D, b: Trajectory2D): Boolean = when (a) { +private fun Float64Space2D.intersectsOtherTrajectory(a: Trajectory2D, b: Trajectory2D): Boolean = when (a) { is CircleTrajectory2D -> when (b) { is CircleTrajectory2D -> a != b && intersectsOrInside(a.circle, b.circle) is StraightTrajectory2D -> intersects(a.circle, b) @@ -32,7 +40,7 @@ public class Obstacles(public val obstacles: List) { val direction: Trajectory2D.Direction, ) { val obstacle: Obstacle get() = obstacles[obstacleIndex] - val circle: Circle2D get() = obstacle.arcs[nodeIndex].circle + val circle: Circle2D get() = obstacle.arcs[nodeIndex].circle } private inner class ObstacleTangent( @@ -44,7 +52,7 @@ public class Obstacles(public val obstacles: List) { * If false, this tangent intersects another obstacle */ val isValid by lazy { - with(Euclidean2DSpace) { + with(Float64Space2D) { obstacles.indices.none { it != from?.obstacleIndex && it != to?.obstacleIndex && obstacles[it].intersectsTrajectory( tangentTrajectory @@ -64,7 +72,7 @@ public class Obstacles(public val obstacles: List) { private fun tangentsBetween( firstIndex: Int, secondIndex: Int, - ): Map = with(Euclidean2DSpace) { + ): Map = with(Float64Space2D) { val first = obstacles[firstIndex] val second = obstacles[secondIndex] buildMap { @@ -99,7 +107,7 @@ public class Obstacles(public val obstacles: List) { private fun tangentsFromArc( arc: CircleTrajectory2D, obstacleIndex: Int, - ): Map = with(Euclidean2DSpace) { + ): Map = with(Float64Space2D) { val obstacle = obstacles[obstacleIndex] buildMap { for (circleIndex in obstacle.arcs.indices) { @@ -127,7 +135,7 @@ public class Obstacles(public val obstacles: List) { obstacleIndex: Int, obstacleDirection: Trajectory2D.Direction, arc: CircleTrajectory2D, - ): ObstacleTangent? = with(Euclidean2DSpace) { + ): ObstacleTangent? = with(Float64Space2D) { val obstacle = obstacles[obstacleIndex] for (circleIndex in obstacle.arcs.indices) { val obstacleArc = obstacle.arcs[circleIndex] @@ -207,7 +215,7 @@ public class Obstacles(public val obstacles: List) { private fun avoiding( dubinsPath: CompositeTrajectory2D, - ): Collection = with(Euclidean2DSpace) { + ): Collection = with(Float64Space2D) { //fast return if no obstacles intersect the direct path if (obstacles.none { it.intersectsTrajectory(dubinsPath) }) return listOf(dubinsPath) @@ -337,7 +345,7 @@ public class Obstacles(public val obstacles: List) { start: Pose2D, finish: Pose2D, radius: Double, - vararg polygons: Polygon, + vararg polygons: Polygon>, ): List { val obstacles: List = polygons.map { polygon -> Obstacle(polygon.points, radius) @@ -350,7 +358,7 @@ public class Obstacles(public val obstacles: List) { start: Pose2D, finish: Pose2D, radius: Double, - polygons: Collection>, + polygons: Collection>>, ): List { val obstacles: List = polygons.map { polygon -> Obstacle(polygon.points, radius) diff --git a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Pose2D.kt b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Pose2D.kt index 9e86f77..0007739 100644 --- a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Pose2D.kt +++ b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Pose2D.kt @@ -2,7 +2,7 @@ * Copyright 2018-2022 KMath contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ -@file:UseSerializers(Euclidean2DSpace.VectorSerializer::class) +@file:UseSerializers(Float64Space2D.VectorSerializer::class) package space.kscience.trajectory @@ -14,14 +14,16 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import space.kscience.kmath.geometry.* +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D +import space.kscience.kmath.structures.Float64 import kotlin.math.atan2 /** * Combination of [Vector] and its view angle (clockwise from positive y-axis direction) */ @Serializable(Pose2DSerializer::class) -public interface Pose2D : DoubleVector2D { - public val coordinates: DoubleVector2D +public interface Pose2D : Vector2D { + public val coordinates: Vector2D public val bearing: Angle /** @@ -31,9 +33,9 @@ public interface Pose2D : DoubleVector2D { public companion object { public fun bearingToVector(bearing: Angle): Vector2D = - Euclidean2DSpace.vector(cos(bearing), sin(bearing)) + Float64Space2D.vector(cos(bearing), sin(bearing)) - public fun vectorToBearing(vector2D: DoubleVector2D): Angle { + public fun vectorToBearing(vector2D: Vector2D): Angle { require(vector2D.x != 0.0 || vector2D.y != 0.0) { "Can't get bearing of zero vector" } return atan2(vector2D.y, vector2D.x).radians } @@ -43,25 +45,24 @@ public interface Pose2D : DoubleVector2D { @Serializable public class PhaseVector2D( - override val coordinates: DoubleVector2D, - public val velocity: DoubleVector2D, -) : Pose2D, DoubleVector2D by coordinates { + override val coordinates: Vector2D, + public val velocity: Vector2D, +) : Pose2D, Vector2D by coordinates { override val bearing: Angle get() = atan2(velocity.x, velocity.y).radians - override fun reversed(): Pose2D = with(Euclidean2DSpace) { PhaseVector2D(coordinates, -velocity) } + override fun reversed(): Pose2D = with(Float64Space2D) { PhaseVector2D(coordinates, -velocity) } } @Serializable @SerialName("DubinsPose2D") private class Pose2DImpl( - override val coordinates: DoubleVector2D, + override val coordinates: Vector2D, override val bearing: Angle, -) : Pose2D, DoubleVector2D by coordinates { +) : Pose2D, Vector2D by coordinates { override fun reversed(): Pose2D = Pose2DImpl(coordinates, bearing.plus(Angle.pi).normalized()) - override fun toString(): String = "Pose2D(x=$x, y=$y, bearing=$bearing)" override fun equals(other: Any?): Boolean { if (this === other) return true @@ -96,11 +97,11 @@ public object Pose2DSerializer : KSerializer { } } -public fun Pose2D(coordinate: DoubleVector2D, bearing: Angle): Pose2D = +public fun Pose2D(coordinate: Vector2D, bearing: Angle): Pose2D = Pose2DImpl(coordinate, bearing) -public fun Pose2D(point: DoubleVector2D, direction: DoubleVector2D): Pose2D = +public fun Pose2D(point: Vector2D, direction: Vector2D): Pose2D = Pose2D(point, Pose2D.vectorToBearing(direction)) public fun Pose2D(x: Number, y: Number, bearing: Angle): Pose2D = - Pose2DImpl(Euclidean2DSpace.vector(x, y), bearing) \ No newline at end of file + Pose2DImpl(Float64Space2D.vector(x, y), bearing) \ No newline at end of file diff --git a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Trajectory2D.kt b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Trajectory2D.kt index 1254bd1..ddf7a67 100644 --- a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Trajectory2D.kt +++ b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/Trajectory2D.kt @@ -2,16 +2,21 @@ * Copyright 2018-2022 KMath contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ -@file:UseSerializers(Euclidean2DSpace.VectorSerializer::class) +@file:UseSerializers(Float64Space2D.VectorSerializer::class) package space.kscience.trajectory import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers +import space.kscience.intersects +import space.kscience.intersectsOrInside import space.kscience.kmath.geometry.* -import space.kscience.kmath.geometry.Euclidean2DSpace.distanceTo -import space.kscience.kmath.geometry.Euclidean2DSpace.minus +import space.kscience.kmath.geometry.euclidean2d.Circle2D +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D.distanceTo +import space.kscience.kmath.structures.Float64 +import space.kscience.tangent import kotlin.math.atan2 @Serializable @@ -44,7 +49,7 @@ public sealed interface Trajectory2D { } -public val DoubleVector2D.bearing: Angle get() = (atan2(x, y).radians).normalized() +public val Vector2D.bearing: Angle get() = (atan2(x, y).radians).normalized() /** * Straight path segment. The order of start and end defines the direction @@ -52,13 +57,13 @@ public val DoubleVector2D.bearing: Angle get() = (atan2(x, y).radians).normalize @Serializable @SerialName("straight") public data class StraightTrajectory2D( - override val begin: DoubleVector2D, - override val end: DoubleVector2D, + override val begin: Vector2D, + override val end: Vector2D, ) : Trajectory2D, LineSegment2D { override val length: Double get() = begin.distanceTo(end) - public val bearing: Angle get() = (end - begin).bearing + public val bearing: Angle get() = with(Float64Space2D) { (end - begin).bearing } override val beginPose: Pose2D get() = Pose2D(begin, bearing) override val endPose: Pose2D get() = Pose2D(end, bearing) @@ -75,7 +80,7 @@ public fun StraightTrajectory2D(segment: LineSegment2D): StraightTrajectory2D = @Serializable @SerialName("arc") public data class CircleTrajectory2D( - public val circle: Circle2D, + public val circle: Circle2D, public val arcStart: Angle, public val arcAngle: Angle, ) : Trajectory2D { @@ -86,7 +91,7 @@ public data class CircleTrajectory2D( override val endPose: Pose2D get() = circle.tangent(arcEnd, direction) override val length: Double by lazy { - circle.radius * kotlin.math.abs(arcAngle.radians) + circle.radius * kotlin.math.abs(arcAngle.toRadians().value) } val center: Vector2D get() = circle.center @@ -98,11 +103,11 @@ public data class CircleTrajectory2D( } public fun CircleTrajectory2D( - center: DoubleVector2D, - start: DoubleVector2D, - end: DoubleVector2D, + center: Vector2D, + start: Vector2D, + end: Vector2D, direction: Trajectory2D.Direction, -): CircleTrajectory2D = with(Euclidean2DSpace) { +): CircleTrajectory2D = with(Float64Space2D) { val startVector = start - center val endVector = end - center val startRadius = norm(startVector) @@ -131,11 +136,11 @@ public fun CircleTrajectory2D( } public fun CircleTrajectory2D( - circle: Circle2D, - start: DoubleVector2D, - end: DoubleVector2D, + circle: Circle2D, + start: Vector2D, + end: Vector2D, direction: Trajectory2D.Direction, -): CircleTrajectory2D = with(Euclidean2DSpace) { +): CircleTrajectory2D = with(Float64Space2D) { val startVector = start - circle.center val endVector = end - circle.center val startBearing = startVector.bearing @@ -161,10 +166,10 @@ public fun CircleTrajectory2D( @Deprecated("Use angle notation instead") public fun CircleTrajectory2D( - circle: Circle2D, + circle: Circle2D, beginPose: Pose2D, endPose: Pose2D, -): CircleTrajectory2D = with(Euclidean2DSpace) { +): CircleTrajectory2D = with(Float64Space2D) { val vectorToBegin = beginPose - circle.center val vectorToEnd = endPose - circle.center //TODO check pose bearing @@ -185,7 +190,7 @@ public class CompositeTrajectory2D(public val segments: List) : Tr public fun CompositeTrajectory2D(vararg segments: Trajectory2D): CompositeTrajectory2D = CompositeTrajectory2D(segments.toList()) -public fun Euclidean2DSpace.intersectsTrajectory(a: Trajectory2D, b: Trajectory2D): Boolean = when (a) { +public fun Float64Space2D.intersectsTrajectory(a: Trajectory2D, b: Trajectory2D): Boolean = when (a) { is CircleTrajectory2D -> when (b) { is CircleTrajectory2D -> intersectsOrInside(a.circle, b.circle) is StraightTrajectory2D -> intersects(a.circle, b) diff --git a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/circumvention.kt b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/circumvention.kt index 968c595..6286dbb 100644 --- a/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/circumvention.kt +++ b/trajectory-kt/src/commonMain/kotlin/space/kscience/trajectory/circumvention.kt @@ -1,6 +1,10 @@ package space.kscience.trajectory +import space.kscience.containsPoint 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.DubinsPath.Type import kotlin.math.* @@ -10,9 +14,9 @@ import kotlin.math.* * This method returns a map of segments using [DubinsPath] connection type notation. */ internal fun tangentsBetweenCircles( - first: Circle2D, - second: Circle2D, -): Map = with(Euclidean2DSpace) { + first: Circle2D, + second: Circle2D, +): Map = with(Float64Space2D) { // Distance between centers val distanceBetweenCenters: Double = first.center.distanceTo(second.center) diff --git a/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/ArcTests.kt b/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/ArcTests.kt index 793c367..8517dfc 100644 --- a/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/ArcTests.kt +++ b/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/ArcTests.kt @@ -5,6 +5,11 @@ package space.kscience.kmath.geometry +import space.kscience.circle +import space.kscience.containsPoint +import space.kscience.kmath.geometry.euclidean2d.Circle2D +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D +import space.kscience.kmath.geometry.euclidean2d.circumference import space.kscience.trajectory.CircleTrajectory2D import space.kscience.trajectory.Trajectory2D import kotlin.math.PI @@ -16,7 +21,7 @@ import kotlin.test.assertTrue class ArcTests { @Test - fun arc() = with(Euclidean2DSpace) { + fun arc() = with(Float64Space2D) { val circle = Circle2D(vector(0.0, 0.0), 2.0) val arc = CircleTrajectory2D( circle.center, @@ -25,12 +30,12 @@ class ArcTests { Trajectory2D.R ) assertEquals(circle.circumference / 4, arc.length, 1.0) - assertEquals(0.0, arc.beginPose.bearing.degrees) - assertEquals(90.0, arc.endPose.bearing.degrees) + assertEquals(0.0, arc.beginPose.bearing.toDegrees().value) + assertEquals(90.0, arc.endPose.bearing.toDegrees().value) } @Test - fun quarter() = with(Euclidean2DSpace) { + fun quarter() = with(Float64Space2D) { val circle = circle(1, 0, 1) val arc = CircleTrajectory2D( circle, @@ -38,11 +43,11 @@ class ArcTests { (PI/2).radians ) assertEquals(Trajectory2D.R, arc.direction) - assertEquals(PI, arc.arcEnd.radians, 1e-4) + assertEquals(PI, arc.arcEnd.toRadians().value, 1e-4) } @Test - fun arcContains() = with(Euclidean2DSpace) { + fun arcContains() = with(Float64Space2D) { val circle = circle(0, 0, 1.0) val arc1 = CircleTrajectory2D(circle, Angle.pi / 4, Angle.piDiv2) diff --git a/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/CircleTests.kt b/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/CircleTests.kt index 4c0bc15..5191824 100644 --- a/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/CircleTests.kt +++ b/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/CircleTests.kt @@ -5,6 +5,14 @@ package space.kscience.kmath.geometry +import space.kscience.circle +import space.kscience.intersects +import space.kscience.intersectsOrInside +import space.kscience.kmath.geometry.euclidean2d.Circle2D +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D +import space.kscience.kmath.geometry.euclidean2d.circumference +import space.kscience.kmath.structures.Float64 +import space.kscience.segment import kotlin.math.pow import kotlin.math.sqrt import kotlin.test.Test @@ -16,7 +24,7 @@ class CircleTests { @Test fun circle() { - val center = Euclidean2DSpace.vector(0.0, 0.0) + val center = Float64Space2D.vector(0.0, 0.0) val radius = 2.0 val expectedCircumference = 12.56637 val circle = Circle2D(center, radius) @@ -24,7 +32,7 @@ class CircleTests { } @Test - fun circleIntersection() = with(Euclidean2DSpace) { + fun circleIntersection() = with(Float64Space2D) { assertTrue { intersectsOrInside( circle(0.0, 0.0, 1.0), @@ -46,7 +54,7 @@ class CircleTests { } @Test - fun circleLineIntersection() = with(Euclidean2DSpace) { + fun circleLineIntersection() = with(Float64Space2D) { assertTrue { intersects(circle(0, 0, 1.0), segment(1, 1, -1, 1)) } @@ -84,7 +92,7 @@ class CircleTests { } } - private fun Euclidean2DSpace.oldIntersect(circle: Circle2D, segment: LineSegment2D): Boolean{ + private fun Float64Space2D.oldIntersect(circle: Circle2D, segment: LineSegment2D): Boolean{ val begin = segment.begin val end = segment.end val lengthSquared = (begin.x - end.x).pow(2) + (begin.y - end.y).pow(2) @@ -111,7 +119,7 @@ class CircleTests { } @Test - fun oldCircleLineIntersection() = with(Euclidean2DSpace){ + fun oldCircleLineIntersection() = with(Float64Space2D){ assertTrue { oldIntersect(circle(0, 0, 1.1), segment(1, 1, -1, 1)) } diff --git a/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/LineTests.kt b/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/LineTests.kt index 480e598..eaea514 100644 --- a/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/LineTests.kt +++ b/trajectory-kt/src/commonTest/kotlin/space/kscience/kmath/geometry/LineTests.kt @@ -5,6 +5,7 @@ package space.kscience.kmath.geometry +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D import space.kscience.trajectory.StraightTrajectory2D import kotlin.math.pow import kotlin.math.sqrt @@ -14,22 +15,22 @@ import kotlin.test.assertEquals class LineTests { @Test - fun lineTest() = with(Euclidean2DSpace){ + fun lineTest() = with(Float64Space2D){ val straight = StraightTrajectory2D(vector(0.0, 0.0), vector(100.0, 100.0)) assertEquals(sqrt(100.0.pow(2) + 100.0.pow(2)), straight.length) - assertEquals(45.0, straight.bearing.degrees) + assertEquals(45.0, straight.bearing.toDegrees().value) } @Test - fun lineAngleTest() = with(Euclidean2DSpace){ + fun lineAngleTest() = with(Float64Space2D){ //val zero = Vector2D(0.0, 0.0) val north = StraightTrajectory2D(zero, vector(0.0, 2.0)) - assertEquals(0.0, north.bearing.degrees) + assertEquals(0.0, north.bearing.toDegrees().value) val east = StraightTrajectory2D(zero, vector(2.0, 0.0)) - assertEquals(90.0, east.bearing.degrees) + assertEquals(90.0, east.bearing.toDegrees().value) val south = StraightTrajectory2D(zero, vector(0.0, -2.0)) - assertEquals(180.0, south.bearing.degrees) + assertEquals(180.0, south.bearing.toDegrees().value) val west = StraightTrajectory2D(zero, vector(-2.0, 0.0)) - assertEquals(270.0, west.bearing.degrees) + assertEquals(270.0, west.bearing.toDegrees().value) } } diff --git a/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/DubinsTests.kt b/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/DubinsTests.kt index 77617d4..82b507a 100644 --- a/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/DubinsTests.kt +++ b/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/DubinsTests.kt @@ -5,7 +5,7 @@ package space.kscience.trajectory -import space.kscience.kmath.geometry.Euclidean2DSpace +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -14,7 +14,7 @@ import kotlin.test.assertNotNull class DubinsTests { @Test - fun dubinsTest() = with(Euclidean2DSpace) { + fun dubinsTest() = with(Float64Space2D) { val straight = StraightTrajectory2D(vector(0.0, 0.0), vector(100.0, 100.0)) val lineP1 = straight.shift(1, 10.0).inverse() @@ -53,7 +53,7 @@ class DubinsTests { assertEquals(c.beginPose, b.endPose, 1e-4) } else if (b is StraightTrajectory2D) { assertEquals(a.endPose, Pose2D(b.begin, b.bearing), 1e-4) - assertEquals(c.beginPose, Pose2D(b.end, b.bearing),1e-4) + assertEquals(c.beginPose, Pose2D(b.end, b.bearing), 1e-4) } } } diff --git a/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/ObstacleTest.kt b/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/ObstacleTest.kt index dd67436..ce08217 100644 --- a/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/ObstacleTest.kt +++ b/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/ObstacleTest.kt @@ -6,10 +6,10 @@ package space.kscience.trajectory import space.kscience.kmath.geometry.Angle -import space.kscience.kmath.geometry.Circle2D import space.kscience.kmath.geometry.Degrees -import space.kscience.kmath.geometry.Euclidean2DSpace.vector import space.kscience.kmath.geometry.degrees +import space.kscience.kmath.geometry.euclidean2d.Circle2D +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D import kotlin.math.PI import kotlin.test.Test import kotlin.test.assertEquals @@ -18,7 +18,7 @@ import kotlin.test.assertTrue class ObstacleTest { @Test - fun equalObstacles() { + fun equalObstacles() = with(Float64Space2D) { val circle1 = Circle2D(vector(1.0, 6.5), 0.5) val circle2 = Circle2D(vector(1.0, 6.5), 0.5) assertEquals(circle1, circle2) @@ -28,7 +28,7 @@ class ObstacleTest { } @Test - fun singePoint() { + fun singePoint() = with(Float64Space2D) { val outputTangents: List = Obstacles.avoidObstacles( Pose2D(-5, -1, Angle.pi / 4), Pose2D(20, 4, Angle.pi * 3 / 4), @@ -41,7 +41,7 @@ class ObstacleTest { } @Test - fun twoObstacles() { + fun twoObstacles() = with(Float64Space2D) { val paths = Obstacles.avoidObstacles( Pose2D(-5, -1, Angle.pi / 4), Pose2D(20, 4, Angle.pi * 3 / 4), @@ -64,7 +64,7 @@ class ObstacleTest { } @Test - fun circumvention() { + fun circumvention() = with(Float64Space2D) { val obstacle = Obstacle( Circle2D(vector(0.0, 0.0), 1.0), Circle2D(vector(0.0, 1.0), 1.0), @@ -80,7 +80,7 @@ class ObstacleTest { } @Test - fun closePoints() { + fun closePoints() = with(Float64Space2D) { val obstacle = Obstacle( Circle2D(vector(0.0, 0.0), 1.0), Circle2D(vector(0.0, 1.0), 1.0), @@ -100,7 +100,7 @@ class ObstacleTest { } @Test - fun largeCoordinates() { + fun largeCoordinates() = with(Float64Space2D) { val startPoints = listOf( Pose2D(x = 484149.535516561, y = 2995086.2534208703, bearing = Degrees(3.401475378237137)) ) diff --git a/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/TangentTest.kt b/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/TangentTest.kt index e5831b0..56c9b4b 100644 --- a/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/TangentTest.kt +++ b/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/TangentTest.kt @@ -5,18 +5,17 @@ package space.kscience.trajectory -import space.kscience.kmath.geometry.Circle2D -import space.kscience.kmath.geometry.Euclidean2DSpace -import space.kscience.kmath.geometry.Euclidean2DSpace.vector import space.kscience.kmath.geometry.LineSegment import space.kscience.kmath.geometry.equalsLine +import space.kscience.kmath.geometry.euclidean2d.Circle2D +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class TangentTest { @Test - fun tangents() { + fun tangents() = with(Float64Space2D) { val c1 = Circle2D(vector(0.0, 0.0), 1.0) val c2 = Circle2D(vector(4.0, 0.0), 1.0) val routes = listOf( @@ -50,12 +49,12 @@ class TangentTest { assertEquals(routes, tangentMapKeys) for (i in segments.indices) { - assertTrue(segments[i].equalsLine(Euclidean2DSpace, tangentMapValues[i])) + assertTrue(segments[i].equalsLine(Float64Space2D, tangentMapValues[i])) } } @Test - fun concentric(){ + fun concentric() = with(Float64Space2D) { val c1 = Circle2D(vector(0.0, 0.0), 10.0) val c2 = Circle2D(vector(0.0, 0.0), 1.0) assertEquals(emptyMap(), tangentsBetweenCircles(c1, c2)) diff --git a/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/testutils.kt b/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/testutils.kt index 86f8054..5014411 100644 --- a/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/testutils.kt +++ b/trajectory-kt/src/commonTest/kotlin/space/kscience/trajectory/testutils.kt @@ -5,20 +5,19 @@ package space.kscience.trajectory -import space.kscience.kmath.geometry.Euclidean2DSpace -import space.kscience.kmath.geometry.radians +import space.kscience.kmath.geometry.euclidean2d.Float64Space2D import space.kscience.kmath.geometry.sin -fun assertEquals(expected: Pose2D, actual: Pose2D, precision: Double = 1e-6){ +fun assertEquals(expected: Pose2D, actual: Pose2D, precision: Double = 1e-6) { kotlin.test.assertEquals(expected.x, actual.x, precision) kotlin.test.assertEquals(expected.y, actual.y, precision) - kotlin.test.assertEquals(expected.bearing.radians, actual.bearing.radians, precision) + kotlin.test.assertEquals(expected.bearing.toRadians().value, actual.bearing.toRadians().value, precision) } fun StraightTrajectory2D.inverse() = StraightTrajectory2D(end, begin) -fun StraightTrajectory2D.shift(shift: Int, width: Double): StraightTrajectory2D = with(Euclidean2DSpace) { +fun StraightTrajectory2D.shift(shift: Int, width: Double): StraightTrajectory2D = with(Float64Space2D) { val dX = width * sin(inverse().bearing) val dY = width * sin(bearing)