Merge pull request #24 from SciProgCentre/dev
0.3.0
This commit is contained in:
commit
bd6d8e2f8e
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
|||||||
build/
|
build/
|
||||||
.gradle/
|
.gradle/
|
||||||
.idea/
|
.idea/
|
||||||
|
.kotlin
|
||||||
/*.iml
|
/*.iml
|
||||||
|
|
||||||
mapCache/
|
mapCache/
|
10
.space.kts
10
.space.kts
@ -9,17 +9,17 @@ job("Publish") {
|
|||||||
gitPush { enabled = false }
|
gitPush { enabled = false }
|
||||||
}
|
}
|
||||||
container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") {
|
container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") {
|
||||||
env["SPACE_USER"] = Secrets("space_user")
|
env["SPACE_USER"] = "{{ project:space_user }}"
|
||||||
env["SPACE_TOKEN"] = Secrets("space_token")
|
env["SPACE_TOKEN"] = "{{ project:space_token }}"
|
||||||
kotlinScript { api ->
|
kotlinScript { api ->
|
||||||
|
|
||||||
val spaceUser = System.getenv("SPACE_USER")
|
val spaceUser = System.getenv("SPACE_USER")
|
||||||
val spaceToken = System.getenv("SPACE_TOKEN")
|
val spaceToken = System.getenv("SPACE_TOKEN")
|
||||||
|
|
||||||
// write version to the build directory
|
// write the version to the build directory
|
||||||
api.gradlew("version")
|
api.gradlew("version")
|
||||||
|
|
||||||
//read version from build file
|
//read the version from build file
|
||||||
val version = java.nio.file.Path.of("build/project-version.txt").readText()
|
val version = java.nio.file.Path.of("build/project-version.txt").readText()
|
||||||
|
|
||||||
val revisionSuffix = if (version.endsWith("SNAPSHOT")) {
|
val revisionSuffix = if (version.endsWith("SNAPSHOT")) {
|
||||||
@ -32,7 +32,7 @@ job("Publish") {
|
|||||||
project = api.projectIdentifier(),
|
project = api.projectIdentifier(),
|
||||||
targetIdentifier = TargetIdentifier.Key("maps-kt"),
|
targetIdentifier = TargetIdentifier.Key("maps-kt"),
|
||||||
version = version+revisionSuffix,
|
version = version+revisionSuffix,
|
||||||
// automatically update deployment status based on a status of a job
|
// automatically update deployment status based on the status of a job
|
||||||
syncWithAutomationJob = true
|
syncWithAutomationJob = true
|
||||||
)
|
)
|
||||||
api.gradlew(
|
api.gradlew(
|
||||||
|
14
CHANGELOG.md
14
CHANGELOG.md
@ -3,13 +3,27 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- `alpha` extension for feature attribute builder
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- avoid drawing features with VisibleAttribute false
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Add alpha attribute comprehension for all standard features.
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
|
## 0.3.0 - 2024-06-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Package changed to `space.kscience`
|
||||||
|
- Kotlin 2.0
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Use of generated resources for Wasm
|
||||||
|
10
README.md
10
README.md
@ -9,7 +9,6 @@ This repository is a work-in-progress implementation of Map-with-markers compone
|
|||||||
|
|
||||||
### [demo](demo)
|
### [demo](demo)
|
||||||
>
|
>
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [maps-kt-compose](maps-kt-compose)
|
### [maps-kt-compose](maps-kt-compose)
|
||||||
@ -34,17 +33,14 @@ This repository is a work-in-progress implementation of Map-with-markers compone
|
|||||||
|
|
||||||
### [maps-kt-features](maps-kt-features)
|
### [maps-kt-features](maps-kt-features)
|
||||||
>
|
>
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [maps-kt-geojson](maps-kt-geojson)
|
### [maps-kt-geojson](maps-kt-geojson)
|
||||||
>
|
>
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [maps-kt-scheme](maps-kt-scheme)
|
### [maps-kt-scheme](maps-kt-scheme)
|
||||||
>
|
>
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [trajectory-kt](trajectory-kt)
|
### [trajectory-kt](trajectory-kt)
|
||||||
@ -54,20 +50,20 @@ This repository is a work-in-progress implementation of Map-with-markers compone
|
|||||||
|
|
||||||
### [demo/maps](demo/maps)
|
### [demo/maps](demo/maps)
|
||||||
>
|
>
|
||||||
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
|
### [demo/maps-wasm](demo/maps-wasm)
|
||||||
>
|
>
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [demo/polygon-editor](demo/polygon-editor)
|
### [demo/polygon-editor](demo/polygon-editor)
|
||||||
>
|
>
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [demo/scheme](demo/scheme)
|
### [demo/scheme](demo/scheme)
|
||||||
>
|
>
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
|
||||||
### [demo/trajectory-playground](demo/trajectory-playground)
|
### [demo/trajectory-playground](demo/trajectory-playground)
|
||||||
>
|
>
|
||||||
>
|
|
||||||
> **Maturity**: EXPERIMENTAL
|
> **Maturity**: EXPERIMENTAL
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import space.kscience.gradle.isInDevelopment
|
|
||||||
import space.kscience.gradle.useApache2Licence
|
import space.kscience.gradle.useApache2Licence
|
||||||
import space.kscience.gradle.useSPCTeam
|
import space.kscience.gradle.useSPCTeam
|
||||||
|
|
||||||
@ -6,16 +5,15 @@ plugins {
|
|||||||
id("space.kscience.gradle.project")
|
id("space.kscience.gradle.project")
|
||||||
}
|
}
|
||||||
|
|
||||||
val kmathVersion: String by extra("0.3.1-dev-RC")
|
val kmathVersion: String by extra("0.4.0")
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "center.sciprog"
|
group = "space.kscience"
|
||||||
version = "0.2.2"
|
version = "0.3.1-dev"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,20 +22,12 @@ ksciencePublish {
|
|||||||
useApache2Licence()
|
useApache2Licence()
|
||||||
useSPCTeam()
|
useSPCTeam()
|
||||||
}
|
}
|
||||||
github("SciProgCentre", "maps-kt")
|
repository("spc","https://maven.sciprog.center/kscience")
|
||||||
space(
|
sonatype("https://oss.sonatype.org")
|
||||||
if (isInDevelopment) {
|
|
||||||
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
|
|
||||||
} else {
|
|
||||||
"https://maven.pkg.jetbrains.space/spc/p/sci/maven"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
sonatype()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://maven.pkg.jetbrains.space/mipt-npm/p/sci/dev")
|
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
@ -48,4 +38,3 @@ subprojects {
|
|||||||
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4
demo/maps-wasm/README.md
Normal file
4
demo/maps-wasm/README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Module maps-wasm
|
||||||
|
|
||||||
|
|
||||||
|
|
37
demo/maps-wasm/build.gradle.kts
Normal file
37
demo/maps-wasm/build.gradle.kts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
alias(spclibs.plugins.compose.compiler)
|
||||||
|
alias(spclibs.plugins.compose.jb)
|
||||||
|
}
|
||||||
|
|
||||||
|
//val ktorVersion: String by rootProject.extra
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
binaries.executable()
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
|
||||||
|
api(compose.components.resources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wasmJsMain {
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.mapsKtScheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compose {
|
||||||
|
web {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 469 KiB |
82
demo/maps-wasm/src/wasmJsMain/kotlin/Main.kt
Normal file
82
demo/maps-wasm/src/wasmJsMain/kotlin/Main.kt
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
@file:OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class)
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.window.CanvasBasedWindow
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||||
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
import space.kscience.kmath.geometry.Angle
|
||||||
|
import space.kscience.maps.features.FeatureGroup
|
||||||
|
import space.kscience.maps.features.ViewConfig
|
||||||
|
import space.kscience.maps.features.ViewPoint
|
||||||
|
import space.kscience.maps.features.color
|
||||||
|
import space.kscience.maps.scheme.*
|
||||||
|
import space.kscience.maps_wasm.generated.resources.Res
|
||||||
|
import space.kscience.maps_wasm.generated.resources.middle_earth
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun App() {
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|
||||||
|
val features: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) {
|
||||||
|
background(1600f, 1200f) {
|
||||||
|
painterResource(Res.drawable.middle_earth)
|
||||||
|
}
|
||||||
|
circle(410.52737 to 868.7676).color(Color.Blue)
|
||||||
|
text(410.52737 to 868.7676, "Shire").color(Color.Blue)
|
||||||
|
circle(1132.0881 to 394.99127).color(Color.Red)
|
||||||
|
text(1132.0881 to 394.99127, "Ordruin").color(Color.Red)
|
||||||
|
arc(center = 1132.0881 to 394.99127, radius = 20f, startAngle = Angle.zero, Angle.piTimes2)
|
||||||
|
|
||||||
|
//circle(410.52737 to 868.7676, id = "hobbit")
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
var t = 0.0
|
||||||
|
while (isActive) {
|
||||||
|
val x = 410.52737 + t * (1132.0881 - 410.52737)
|
||||||
|
val y = 868.7676 + t * (394.99127 - 868.7676)
|
||||||
|
circle(x to y, id = "hobbit").color(Color.Green)
|
||||||
|
delay(100)
|
||||||
|
t += 0.005
|
||||||
|
if (t >= 1.0) t = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val initialViewPoint: ViewPoint<XY> = remember {
|
||||||
|
features.getBoundingBox(1f)?.computeViewPoint() ?: XYViewPoint(XY(0f, 0f))
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
|
||||||
|
|
||||||
|
val mapState: XYCanvasState = XYCanvasState.remember(
|
||||||
|
ViewConfig(
|
||||||
|
onClick = { _, click ->
|
||||||
|
println("${click.focus.x}, ${click.focus.y}")
|
||||||
|
},
|
||||||
|
onViewChange = { viewPoint = this }
|
||||||
|
),
|
||||||
|
initialViewPoint = initialViewPoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
SchemeView(
|
||||||
|
mapState,
|
||||||
|
features,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
// renderComposable(rootElementId = "root") {
|
||||||
|
CanvasBasedWindow("Maps demo", canvasElementId = "ComposeTarget") {
|
||||||
|
App()
|
||||||
|
}
|
||||||
|
}
|
12
demo/maps-wasm/src/wasmJsMain/resources/index.html
Normal file
12
demo/maps-wasm/src/wasmJsMain/resources/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Compose App</title>
|
||||||
|
<script type="application/javascript" src="skiko.js"></script>
|
||||||
|
<script type="application/javascript" src="maps-wasm.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="ComposeTarget"></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -2,7 +2,8 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform")
|
kotlin("multiplatform")
|
||||||
id("org.jetbrains.compose")
|
alias(spclibs.plugins.compose.compiler)
|
||||||
|
alias(spclibs.plugins.compose.jb)
|
||||||
}
|
}
|
||||||
|
|
||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
@ -17,7 +18,7 @@ kotlin {
|
|||||||
implementation(projects.mapsKtGeojson)
|
implementation(projects.mapsKtGeojson)
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
implementation("io.ktor:ktor-client-cio")
|
implementation("io.ktor:ktor-client-cio")
|
||||||
implementation("ch.qos.logback:logback-classic:1.2.11")
|
implementation(spclibs.logback.classic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jvmTest by getting
|
val jvmTest by getting
|
||||||
|
@ -13,11 +13,6 @@ import androidx.compose.ui.unit.DpSize
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import center.sciprog.attributes.Attributes
|
|
||||||
import center.sciprog.maps.compose.*
|
|
||||||
import center.sciprog.maps.coordinates.*
|
|
||||||
import center.sciprog.maps.features.*
|
|
||||||
import center.sciprog.maps.geojson.geoJson
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -27,15 +22,22 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import space.kscience.attributes.Attributes
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
import space.kscience.kmath.geometry.degrees
|
import space.kscience.kmath.geometry.degrees
|
||||||
import space.kscience.kmath.geometry.radians
|
import space.kscience.kmath.geometry.radians
|
||||||
|
import space.kscience.maps.compose.*
|
||||||
|
import space.kscience.maps.coordinates.GeodeticMapCoordinates
|
||||||
|
import space.kscience.maps.coordinates.Gmc
|
||||||
|
import space.kscience.maps.coordinates.kilometers
|
||||||
|
import space.kscience.maps.features.*
|
||||||
|
import space.kscience.maps.geojson.geoJson
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
public fun GeodeticMapCoordinates.toShortString(): String =
|
public fun GeodeticMapCoordinates.toShortString(): String =
|
||||||
"${(latitude.degrees).toString().take(6)}:${(longitude.degrees).toString().take(6)}"
|
"${(latitude.toDegrees().value).toString().take(6)}:${(longitude.toDegrees().value).toString().take(6)}"
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@ -55,7 +57,6 @@ fun App() {
|
|||||||
|
|
||||||
val centerCoordinates = MutableStateFlow<Gmc?>(null)
|
val centerCoordinates = MutableStateFlow<Gmc?>(null)
|
||||||
|
|
||||||
|
|
||||||
val pointOne = 55.568548 to 37.568604
|
val pointOne = 55.568548 to 37.568604
|
||||||
val pointTwo = 55.929444 to 37.518434
|
val pointTwo = 55.929444 to 37.518434
|
||||||
// val pointThree = 60.929444 to 37.518434
|
// val pointThree = 60.929444 to 37.518434
|
||||||
@ -71,14 +72,17 @@ fun App() {
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
geoJson(javaClass.getResource("/moscow.geo.json")!!)
|
geoJson(javaClass.getResource("/moscow.geo.json")!!)
|
||||||
.modifyAttribute(ColorAttribute, Color.Blue)
|
.color(Color.Blue)
|
||||||
.modifyAttribute(AlphaAttribute, 0.4f)
|
.modifyAttribute(AlphaAttribute, 0.4f)
|
||||||
|
|
||||||
icon(pointOne, Icons.Filled.Home)
|
icon(pointOne, Icons.Filled.Home)
|
||||||
|
|
||||||
val marker1 = rectangle(55.744 to 38.614, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
|
val marker1 = rectangle(55.744 to 38.614, size = DpSize(10.dp, 10.dp))
|
||||||
val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
|
.color(Color.Magenta)
|
||||||
val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
|
val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp))
|
||||||
|
.color(Color.Magenta)
|
||||||
|
val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp))
|
||||||
|
.color(Color.Magenta)
|
||||||
|
|
||||||
draggableLine(marker1, marker2, id = "line 1").color(Color.Red).onClick {
|
draggableLine(marker1, marker2, id = "line 1").color(Color.Red).onClick {
|
||||||
println("line 1 clicked")
|
println("line 1 clicked")
|
||||||
@ -90,6 +94,7 @@ fun App() {
|
|||||||
println("line 3 clicked")
|
println("line 3 clicked")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
multiLine(
|
multiLine(
|
||||||
points = listOf(
|
points = listOf(
|
||||||
55.742465 to 37.615812,
|
55.742465 to 37.615812,
|
||||||
@ -101,7 +106,19 @@ fun App() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
//remember feature ID
|
// points(
|
||||||
|
// points = listOf(
|
||||||
|
// 55.744 to 38.614,
|
||||||
|
// 55.8 to 38.5,
|
||||||
|
// 56.0 to 38.5,
|
||||||
|
// )
|
||||||
|
// ).pointSize(5f)
|
||||||
|
|
||||||
|
// geodeticLine(Gmc.ofDegrees(40.7128, -74.0060), Gmc.ofDegrees(55.742465, 37.615812)).color(Color.Blue)
|
||||||
|
// line(Gmc.ofDegrees(40.7128, -74.0060), Gmc.ofDegrees(55.742465, 37.615812))
|
||||||
|
|
||||||
|
|
||||||
|
//remember feature ref
|
||||||
val circleId = circle(
|
val circleId = circle(
|
||||||
centerCoordinates = pointTwo,
|
centerCoordinates = pointTwo,
|
||||||
)
|
)
|
||||||
@ -120,9 +137,11 @@ fun App() {
|
|||||||
|
|
||||||
arc(pointOne, 10.0.kilometers, (PI / 4).radians, -Angle.pi / 2)
|
arc(pointOne, 10.0.kilometers, (PI / 4).radians, -Angle.pi / 2)
|
||||||
|
|
||||||
|
|
||||||
line(pointOne, pointTwo, id = "line")
|
line(pointOne, pointTwo, id = "line")
|
||||||
text(pointOne, "Home", font = { size = 32f })
|
text(pointOne, "Home", font = { size = 32f })
|
||||||
|
|
||||||
|
|
||||||
pixelMap(
|
pixelMap(
|
||||||
space.Rectangle(
|
space.Rectangle(
|
||||||
Gmc(latitude = 55.58461879539754.degrees, longitude = 37.8746197303493.degrees),
|
Gmc(latitude = 55.58461879539754.degrees, longitude = 37.8746197303493.degrees),
|
||||||
@ -132,10 +151,11 @@ fun App() {
|
|||||||
0.005.degrees
|
0.005.degrees
|
||||||
) { gmc ->
|
) { gmc ->
|
||||||
Color(
|
Color(
|
||||||
red = ((gmc.latitude + Angle.piDiv2).degrees*10 % 1f).toFloat(),
|
red = ((gmc.latitude + Angle.piDiv2).toDegrees().value * 10 % 1f).toFloat(),
|
||||||
green = ((gmc.longitude + Angle.pi).degrees*10 % 1f).toFloat(),
|
green = ((gmc.longitude + Angle.pi).toDegrees().value * 10 % 1f).toFloat(),
|
||||||
blue = 0f
|
blue = 0f,
|
||||||
).copy(alpha = 0.3f)
|
alpha = 0.3f
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
centerCoordinates.filterNotNull().onEach {
|
centerCoordinates.filterNotNull().onEach {
|
||||||
@ -159,6 +179,7 @@ fun App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// println(toPrettyString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,8 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform")
|
kotlin("multiplatform")
|
||||||
id("org.jetbrains.compose")
|
alias(spclibs.plugins.compose.compiler)
|
||||||
|
alias(spclibs.plugins.compose.jb)
|
||||||
}
|
}
|
||||||
|
|
||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
|
@ -9,11 +9,11 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
|
|||||||
import androidx.compose.ui.input.pointer.isSecondaryPressed
|
import androidx.compose.ui.input.pointer.isSecondaryPressed
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import center.sciprog.maps.features.*
|
import space.kscience.maps.features.*
|
||||||
import center.sciprog.maps.scheme.SchemeView
|
import space.kscience.maps.scheme.SchemeView
|
||||||
import center.sciprog.maps.scheme.XY
|
import space.kscience.maps.scheme.XY
|
||||||
import center.sciprog.maps.scheme.XYCoordinateSpace
|
import space.kscience.maps.scheme.XYCanvasState
|
||||||
import center.sciprog.maps.scheme.XYViewScope
|
import space.kscience.maps.scheme.XYCoordinateSpace
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
@ -31,7 +31,7 @@ fun App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val mapState: XYViewScope = XYViewScope.remember(
|
val mapState: XYCanvasState = XYCanvasState.remember(
|
||||||
config = ViewConfig<XY>(
|
config = ViewConfig<XY>(
|
||||||
onClick = { event, point ->
|
onClick = { event, point ->
|
||||||
if (event.buttons.isSecondaryPressed) {
|
if (event.buttons.isSecondaryPressed) {
|
||||||
|
@ -2,7 +2,8 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform")
|
kotlin("multiplatform")
|
||||||
id("org.jetbrains.compose")
|
alias(spclibs.plugins.compose.compiler)
|
||||||
|
alias(spclibs.plugins.compose.jb)
|
||||||
}
|
}
|
||||||
|
|
||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
@ -26,6 +27,7 @@ compose{
|
|||||||
desktop {
|
desktop {
|
||||||
application {
|
application {
|
||||||
mainClass = "MainKt"
|
mainClass = "MainKt"
|
||||||
|
//mainClass = "Joker2023Kt"
|
||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||||
packageName = "scheme-compose-demo"
|
packageName = "scheme-compose-demo"
|
||||||
|
@ -8,18 +8,18 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import center.sciprog.maps.features.FeatureGroup
|
|
||||||
import center.sciprog.maps.features.ViewConfig
|
|
||||||
import center.sciprog.maps.features.ViewPoint
|
|
||||||
import center.sciprog.maps.features.color
|
|
||||||
import center.sciprog.maps.scheme.*
|
|
||||||
import center.sciprog.maps.svg.FeatureStateSnapshot
|
|
||||||
import center.sciprog.maps.svg.exportToSvg
|
|
||||||
import center.sciprog.maps.svg.snapshot
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
|
import space.kscience.maps.features.FeatureGroup
|
||||||
|
import space.kscience.maps.features.ViewConfig
|
||||||
|
import space.kscience.maps.features.ViewPoint
|
||||||
|
import space.kscience.maps.features.color
|
||||||
|
import space.kscience.maps.scheme.*
|
||||||
|
import space.kscience.maps.svg.FeatureStateSnapshot
|
||||||
|
import space.kscience.maps.svg.exportToSvg
|
||||||
|
import space.kscience.maps.svg.snapshot
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ fun App() {
|
|||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val schemeFeaturesState: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) {
|
val features: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) {
|
||||||
background(1600f, 1200f) { painterResource("middle-earth.jpg") }
|
background(1600f, 1200f) { painterResource("middle-earth.jpg") }
|
||||||
circle(410.52737 to 868.7676).color(Color.Blue)
|
circle(410.52737 to 868.7676).color(Color.Blue)
|
||||||
text(410.52737 to 868.7676, "Shire").color(Color.Blue)
|
text(410.52737 to 868.7676, "Shire").color(Color.Blue)
|
||||||
@ -53,7 +53,7 @@ fun App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val initialViewPoint: ViewPoint<XY> = remember {
|
val initialViewPoint: ViewPoint<XY> = remember {
|
||||||
schemeFeaturesState.getBoundingBox(1f)?.computeViewPoint() ?: XYViewPoint(XY(0f, 0f))
|
features.getBoundingBox(1f)?.computeViewPoint() ?: XYViewPoint(XY(0f, 0f))
|
||||||
}
|
}
|
||||||
|
|
||||||
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
|
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
|
||||||
@ -61,7 +61,7 @@ fun App() {
|
|||||||
var snapshot: FeatureStateSnapshot<XY>? by remember { mutableStateOf(null) }
|
var snapshot: FeatureStateSnapshot<XY>? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
if (snapshot == null) {
|
if (snapshot == null) {
|
||||||
snapshot = schemeFeaturesState.snapshot()
|
snapshot = features.snapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextMenuArea(
|
ContextMenuArea(
|
||||||
@ -78,7 +78,7 @@ fun App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val mapState: XYViewScope = XYViewScope.remember(
|
val mapState: XYCanvasState = XYCanvasState.remember(
|
||||||
ViewConfig(
|
ViewConfig(
|
||||||
onClick = { _, click ->
|
onClick = { _, click ->
|
||||||
println("${click.focus.x}, ${click.focus.y}")
|
println("${click.focus.x}, ${click.focus.y}")
|
||||||
@ -90,7 +90,7 @@ fun App() {
|
|||||||
|
|
||||||
SchemeView(
|
SchemeView(
|
||||||
mapState,
|
mapState,
|
||||||
schemeFeaturesState,
|
features,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
76
demo/scheme/src/jvmMain/kotlin/joker2023.kt
Normal file
76
demo/scheme/src/jvmMain/kotlin/joker2023.kt
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Face
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
import space.kscience.maps.features.*
|
||||||
|
import space.kscience.maps.scheme.*
|
||||||
|
import space.kscience.maps.scheme.XYCoordinateSpace.Rectangle
|
||||||
|
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
Window(onCloseRequest = ::exitApplication, title = "Joker2023 demo", icon = painterResource("SPC-logo.png")) {
|
||||||
|
MaterialTheme {
|
||||||
|
|
||||||
|
SchemeView(
|
||||||
|
initialRectangle = Rectangle(XY(0f, 0f), XY(1734f, 724f)),
|
||||||
|
config = ViewConfig(
|
||||||
|
onClick = { _, pointer ->
|
||||||
|
println("(${pointer.focus.x}, ${pointer.focus.y})")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
background(1734f, 724f, id = "background") { painterResource("joker2023.png") }
|
||||||
|
group(id = "hall_1") {
|
||||||
|
polygon(
|
||||||
|
listOf(
|
||||||
|
XY(1582.0042, 210.29636),
|
||||||
|
XY(1433.7021, 127.79796),
|
||||||
|
XY(1370.7639, 127.79796),
|
||||||
|
XY(1315.293, 222.73865),
|
||||||
|
XY(1314.2262, 476.625),
|
||||||
|
XY(1364.3635, 570.4984),
|
||||||
|
XY(1434.7689, 570.4984),
|
||||||
|
XY(1579.8469, 493.69244),
|
||||||
|
)
|
||||||
|
).modifyAttributes {
|
||||||
|
ColorAttribute(Color.Blue)
|
||||||
|
AlphaAttribute(0.4f)
|
||||||
|
}.onClick {
|
||||||
|
println("hall_1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group(id = "hall_2") {
|
||||||
|
rectanglePolygon(
|
||||||
|
left = 893, right = 1103,
|
||||||
|
bottom = 223, top = 406,
|
||||||
|
).modifyAttributes {
|
||||||
|
ColorAttribute(Color.Blue)
|
||||||
|
AlphaAttribute(0.4f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group(id = "hall_3") {
|
||||||
|
rectanglePolygon(
|
||||||
|
Rectangle(XY(460f, 374f), width = 140f, height = 122f),
|
||||||
|
).modifyAttributes {
|
||||||
|
ColorAttribute(Color.Blue)
|
||||||
|
AlphaAttribute(0.4f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group(id = "people") {
|
||||||
|
icon(XY(815.60535, 342.71313), Icons.Default.Face).color(Color.Red)
|
||||||
|
icon(XY(743.751, 381.09064), Icons.Default.Face).color(Color.Red)
|
||||||
|
icon(XY(1349.6648, 417.36014), Icons.Default.Face).color(Color.Red)
|
||||||
|
icon(XY (1362.4658, 287.21667), Icons.Default.Face).color(Color.Red)
|
||||||
|
icon(XY(208.24274, 317.08566), Icons.Default.Face).color(Color.Red)
|
||||||
|
icon(XY (293.5827, 319.21915), Icons.Default.Face).color(Color.Red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
demo/scheme/src/jvmMain/resources/SPC-logo.png
Normal file
BIN
demo/scheme/src/jvmMain/resources/SPC-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
demo/scheme/src/jvmMain/resources/joker2023.png
Normal file
BIN
demo/scheme/src/jvmMain/resources/joker2023.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 968 KiB |
@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform")
|
kotlin("multiplatform")
|
||||||
id("org.jetbrains.compose")
|
alias(spclibs.plugins.compose.compiler)
|
||||||
|
alias(spclibs.plugins.compose.jb)
|
||||||
}
|
}
|
||||||
|
|
||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
|
@ -8,17 +8,17 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import center.sciprog.maps.features.*
|
|
||||||
import center.sciprog.maps.scheme.SchemeView
|
|
||||||
import center.sciprog.maps.scheme.XY
|
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
import space.kscience.kmath.geometry.Circle2D
|
import space.kscience.kmath.geometry.Vector2D
|
||||||
import space.kscience.kmath.geometry.DoubleVector2D
|
import space.kscience.kmath.geometry.euclidean2d.Circle2D
|
||||||
import space.kscience.kmath.geometry.Euclidean2DSpace
|
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
|
||||||
|
import space.kscience.maps.features.*
|
||||||
|
import space.kscience.maps.scheme.SchemeView
|
||||||
|
import space.kscience.maps.scheme.XY
|
||||||
import space.kscience.trajectory.*
|
import space.kscience.trajectory.*
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
private fun DoubleVector2D.toXY() = XY(x.toFloat(), y.toFloat())
|
private fun Vector2D<out Number>.toXY() = XY(x.toFloat(), y.toFloat())
|
||||||
|
|
||||||
private val random = Random(123)
|
private val random = Random(123)
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ fun FeatureGroup<XY>.trajectory(
|
|||||||
bCoordinates = trajectory.end.toXY(),
|
bCoordinates = trajectory.end.toXY(),
|
||||||
).color(colorPicker(trajectory))
|
).color(colorPicker(trajectory))
|
||||||
|
|
||||||
is CircleTrajectory2D -> with(Euclidean2DSpace) {
|
is CircleTrajectory2D -> with(Float64Space2D) {
|
||||||
val topLeft = trajectory.circle.center + vector(-trajectory.circle.radius, trajectory.circle.radius)
|
val topLeft = trajectory.circle.center + vector(-trajectory.circle.radius, trajectory.circle.radius)
|
||||||
val bottomRight = trajectory.circle.center + vector(trajectory.circle.radius, -trajectory.circle.radius)
|
val bottomRight = trajectory.circle.center + vector(trajectory.circle.radius, -trajectory.circle.radius)
|
||||||
|
|
||||||
@ -59,20 +59,20 @@ fun FeatureGroup<XY>.obstacle(obstacle: Obstacle, colorPicker: (Trajectory2D) ->
|
|||||||
polygon(obstacle.arcs.map { it.center.toXY() }).color(Color.Gray)
|
polygon(obstacle.arcs.map { it.center.toXY() }).color(Color.Gray)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun FeatureGroup<XY>.pose(pose2D: Pose2D) = with(Euclidean2DSpace) {
|
fun FeatureGroup<XY>.pose(pose2D: Pose2D) = with(Float64Space2D) {
|
||||||
line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY())
|
line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun closePoints() {
|
fun closePoints() = with(Float64Space2D){
|
||||||
SchemeView {
|
SchemeView {
|
||||||
|
|
||||||
val obstacle = Obstacle(
|
val obstacle = Obstacle(
|
||||||
Circle2D(Euclidean2DSpace.vector(0.0, 0.0), 1.0),
|
Circle2D(vector(0.0, 0.0), 1.0),
|
||||||
Circle2D(Euclidean2DSpace.vector(0.0, 1.0), 1.0),
|
Circle2D(vector(0.0, 1.0), 1.0),
|
||||||
Circle2D(Euclidean2DSpace.vector(1.0, 1.0), 1.0),
|
Circle2D(vector(1.0, 1.0), 1.0),
|
||||||
Circle2D(Euclidean2DSpace.vector(1.0, 0.0), 1.0)
|
Circle2D(vector(1.0, 0.0), 1.0)
|
||||||
)
|
)
|
||||||
val enter = Pose2D(-0.8, -0.8, Angle.pi)
|
val enter = Pose2D(-0.8, -0.8, Angle.pi)
|
||||||
val exit = Pose2D(-0.8, -0.8, Angle.piDiv2)
|
val exit = Pose2D(-0.8, -0.8, Angle.piDiv2)
|
||||||
@ -101,7 +101,7 @@ fun closePoints() {
|
|||||||
@Preview
|
@Preview
|
||||||
fun singleObstacle() {
|
fun singleObstacle() {
|
||||||
SchemeView {
|
SchemeView {
|
||||||
val obstacle = Obstacle(Circle2D(Euclidean2DSpace.vector(7.0, 1.0), 5.0))
|
val obstacle = Obstacle(Circle2D(Float64Space2D.vector(7.0, 1.0), 5.0))
|
||||||
val enter = Pose2D(-5, -1, Angle.pi / 4)
|
val enter = Pose2D(-5, -1, Angle.pi / 4)
|
||||||
val exit = Pose2D(20, 4, Angle.pi * 3 / 4)
|
val exit = Pose2D(20, 4, Angle.pi * 3 / 4)
|
||||||
|
|
||||||
@ -123,19 +123,19 @@ fun singleObstacle() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun doubleObstacle() {
|
fun doubleObstacle() = with(Float64Space2D){
|
||||||
SchemeView {
|
SchemeView {
|
||||||
val obstacles = arrayOf(
|
val obstacles = arrayOf(
|
||||||
Obstacle(
|
Obstacle(
|
||||||
Circle2D(Euclidean2DSpace.vector(1.0, 6.5), 0.5),
|
Circle2D(vector(1.0, 6.5), 0.5),
|
||||||
Circle2D(Euclidean2DSpace.vector(2.0, 1.0), 0.5),
|
Circle2D(vector(2.0, 1.0), 0.5),
|
||||||
Circle2D(Euclidean2DSpace.vector(6.0, 0.0), 0.5),
|
Circle2D(vector(6.0, 0.0), 0.5),
|
||||||
Circle2D(Euclidean2DSpace.vector(5.0, 5.0), 0.5)
|
Circle2D(vector(5.0, 5.0), 0.5)
|
||||||
), Obstacle(
|
), Obstacle(
|
||||||
Circle2D(Euclidean2DSpace.vector(10.0, 1.0), 0.5),
|
Circle2D(vector(10.0, 1.0), 0.5),
|
||||||
Circle2D(Euclidean2DSpace.vector(16.0, 0.0), 0.5),
|
Circle2D(vector(16.0, 0.0), 0.5),
|
||||||
Circle2D(Euclidean2DSpace.vector(14.0, 6.0), 0.5),
|
Circle2D(vector(14.0, 6.0), 0.5),
|
||||||
Circle2D(Euclidean2DSpace.vector(9.0, 4.0), 0.5)
|
Circle2D(vector(9.0, 4.0), 0.5)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -157,6 +157,14 @@ fun doubleObstacle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun singleElement() {
|
||||||
|
SchemeView {
|
||||||
|
points(listOf(XY(1f,1f)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
@ -165,6 +173,7 @@ fun playground() {
|
|||||||
"Close starting points",
|
"Close starting points",
|
||||||
"Single obstacle",
|
"Single obstacle",
|
||||||
"Two obstacles",
|
"Two obstacles",
|
||||||
|
"Single element"
|
||||||
)
|
)
|
||||||
|
|
||||||
var currentExample by remember { mutableStateOf(examples.first()) }
|
var currentExample by remember { mutableStateOf(examples.first()) }
|
||||||
@ -182,6 +191,7 @@ fun playground() {
|
|||||||
examples[0] -> closePoints()
|
examples[0] -> closePoints()
|
||||||
examples[1] -> singleObstacle()
|
examples[1] -> singleObstacle()
|
||||||
examples[2] -> doubleObstacle()
|
examples[2] -> doubleObstacle()
|
||||||
|
examples[3] -> singleElement()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
|
||||||
compose.version=1.4.0
|
|
||||||
agp.version=7.4.2
|
|
||||||
android.useAndroidX=true
|
|
||||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
|
||||||
|
|
||||||
org.gradle.jvmargs=-Xmx4096m
|
org.gradle.jvmargs=-Xmx4096m
|
||||||
|
|
||||||
toolsVersion=0.14.6-kotlin-1.8.20
|
toolsVersion=0.15.4-kotlin-2.0.0
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -7,19 +7,8 @@ The core interfaces of KMath.
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `center.sciprog:maps-kt-compose:0.2.2`.
|
The Maven coordinates of this project are `space.kscience:maps-kt-compose:0.3.0`.
|
||||||
|
|
||||||
**Gradle Groovy:**
|
|
||||||
```groovy
|
|
||||||
repositories {
|
|
||||||
maven { url 'https://repo.kotlin.link' }
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'center.sciprog:maps-kt-compose:0.2.2'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
@ -28,6 +17,6 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("center.sciprog:maps-kt-compose:0.2.2")
|
implementation("space.kscience:maps-kt-compose:0.3.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("space.kscience.gradle.mpp")
|
id("space.kscience.gradle.mpp")
|
||||||
id("org.jetbrains.compose")
|
alias(spclibs.plugins.compose.compiler)
|
||||||
|
alias(spclibs.plugins.compose.jb)
|
||||||
|
// id("com.android.library")
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
jvm()
|
jvm()
|
||||||
}
|
wasm()
|
||||||
|
|
||||||
|
useCoroutines()
|
||||||
|
|
||||||
kotlin {
|
|
||||||
sourceSets {
|
|
||||||
commonMain{
|
commonMain{
|
||||||
dependencies {
|
|
||||||
api(projects.mapsKtCore)
|
api(projects.mapsKtCore)
|
||||||
api(projects.mapsKtFeatures)
|
api(projects.mapsKtFeatures)
|
||||||
api(compose.foundation)
|
api(compose.foundation)
|
||||||
api(project.dependencies.platform(spclibs.ktor.bom))
|
api(project.dependencies.platform(spclibs.ktor.bom))
|
||||||
api("io.ktor:ktor-client-core")
|
|
||||||
api("io.github.microutils:kotlin-logging:2.1.23")
|
|
||||||
}
|
}
|
||||||
|
jvmMain{
|
||||||
|
api("io.ktor:ktor-client-cio")
|
||||||
}
|
}
|
||||||
val jvmTest by getting {
|
jvmTest{
|
||||||
dependencies {
|
|
||||||
implementation("io.ktor:ktor-client-cio")
|
implementation("io.ktor:ktor-client-cio")
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
implementation(spclibs.kotlinx.coroutines.test)
|
implementation(spclibs.kotlinx.coroutines.test)
|
||||||
@ -29,8 +29,6 @@ kotlin {
|
|||||||
implementation(spclibs.logback.classic)
|
implementation(spclibs.logback.classic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readme {
|
readme {
|
||||||
description = "Compose-multiplaform implementation for web-mercator tiled maps"
|
description = "Compose-multiplaform implementation for web-mercator tiled maps"
|
||||||
@ -41,3 +39,7 @@ readme {
|
|||||||
id = "osm",
|
id = "osm",
|
||||||
) { "OpenStreetMap tile provider." }
|
) { "OpenStreetMap tile provider." }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//tasks.getByName<Copy>("downloadWix"){
|
||||||
|
// duplicatesStrategy = DuplicatesStrategy.WARN
|
||||||
|
//}
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
package center.sciprog.maps.compose
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
|
||||||
import center.sciprog.maps.features.*
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
public expect fun MapView(
|
|
||||||
viewScope: MapViewScope,
|
|
||||||
features: FeatureGroup<Gmc>,
|
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A builder for a Map with static features.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
public fun MapView(
|
|
||||||
mapTileProvider: MapTileProvider,
|
|
||||||
features: FeatureGroup<Gmc>,
|
|
||||||
initialViewPoint: ViewPoint<Gmc>? = null,
|
|
||||||
initialRectangle: Rectangle<Gmc>? = null,
|
|
||||||
config: ViewConfig<Gmc> = ViewConfig(),
|
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
val mapState: MapViewScope = MapViewScope.remember(
|
|
||||||
mapTileProvider,
|
|
||||||
config,
|
|
||||||
initialViewPoint = initialViewPoint,
|
|
||||||
initialRectangle = initialRectangle ?: features.getBoundingBox(Float.MAX_VALUE),
|
|
||||||
)
|
|
||||||
|
|
||||||
MapView(mapState, features, modifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a map using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined,
|
|
||||||
* use map features to infer view region.
|
|
||||||
* @param initialViewPoint The view point of the map using center and zoom. Is used if provided
|
|
||||||
* @param initialRectangle The rectangle to be used for view point computation. Used if [initialViewPoint] is not defined.
|
|
||||||
* @param buildFeatures - a builder for features
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
public fun MapView(
|
|
||||||
mapTileProvider: MapTileProvider,
|
|
||||||
initialViewPoint: ViewPoint<Gmc>? = null,
|
|
||||||
initialRectangle: Rectangle<Gmc>? = null,
|
|
||||||
config: ViewConfig<Gmc> = ViewConfig(),
|
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
|
||||||
buildFeatures: FeatureGroup<Gmc>.() -> Unit = {},
|
|
||||||
) {
|
|
||||||
|
|
||||||
val featureState = FeatureGroup.remember(WebMercatorSpace, buildFeatures)
|
|
||||||
|
|
||||||
val mapState: MapViewScope = MapViewScope.remember(
|
|
||||||
mapTileProvider,
|
|
||||||
config,
|
|
||||||
initialViewPoint = initialViewPoint,
|
|
||||||
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox(
|
|
||||||
WebMercatorSpace,
|
|
||||||
Float.MAX_VALUE
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
MapView(mapState, featureState, modifier)
|
|
||||||
}
|
|
@ -1,10 +1,10 @@
|
|||||||
package center.sciprog.maps.compose
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
|
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
|
||||||
import center.sciprog.maps.features.Rectangle
|
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
import space.kscience.kmath.geometry.abs
|
import space.kscience.kmath.geometry.abs
|
||||||
|
import space.kscience.maps.coordinates.GeodeticMapCoordinates
|
||||||
|
import space.kscience.maps.coordinates.Gmc
|
||||||
|
import space.kscience.maps.features.Rectangle
|
||||||
|
|
||||||
internal fun Angle.isBetween(a: Angle, b: Angle) = this in a..b || this in b..a
|
internal fun Angle.isBetween(a: Angle, b: Angle) = this in a..b || this in b..a
|
||||||
|
|
@ -1,4 +1,6 @@
|
|||||||
package center.sciprog.maps.compose
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
import kotlin.jvm.Synchronized
|
import kotlin.jvm.Synchronized
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.compose
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -6,18 +6,19 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.DpRect
|
import androidx.compose.ui.unit.DpRect
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
|
||||||
import center.sciprog.maps.coordinates.MercatorProjection
|
|
||||||
import center.sciprog.maps.coordinates.WebMercatorCoordinates
|
|
||||||
import center.sciprog.maps.coordinates.WebMercatorProjection
|
|
||||||
import center.sciprog.maps.features.*
|
|
||||||
import space.kscience.kmath.geometry.radians
|
import space.kscience.kmath.geometry.radians
|
||||||
|
import space.kscience.maps.coordinates.Gmc
|
||||||
|
import space.kscience.maps.coordinates.MercatorProjection
|
||||||
|
import space.kscience.maps.coordinates.WebMercatorCoordinates
|
||||||
|
import space.kscience.maps.coordinates.WebMercatorProjection
|
||||||
|
import space.kscience.maps.features.*
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
public class MapViewScope internal constructor(
|
|
||||||
|
public class MapCanvasState private constructor(
|
||||||
public val mapTileProvider: MapTileProvider,
|
public val mapTileProvider: MapTileProvider,
|
||||||
config: ViewConfig<Gmc>,
|
config: ViewConfig<Gmc>,
|
||||||
) : CoordinateViewScope<Gmc>(config) {
|
) : CanvasState<Gmc>(config) {
|
||||||
override val space: CoordinateSpace<Gmc> get() = WebMercatorSpace
|
override val space: CoordinateSpace<Gmc> get() = WebMercatorSpace
|
||||||
|
|
||||||
private val scaleFactor: Float
|
private val scaleFactor: Float
|
||||||
@ -60,10 +61,10 @@ public class MapViewScope internal constructor(
|
|||||||
override fun computeViewPoint(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> {
|
override fun computeViewPoint(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> {
|
||||||
val zoom = log2(
|
val zoom = log2(
|
||||||
min(
|
min(
|
||||||
canvasSize.width.value / rectangle.longitudeDelta.radians,
|
canvasSize.width.value / rectangle.longitudeDelta.toRadians().value,
|
||||||
canvasSize.height.value / rectangle.latitudeDelta.radians
|
canvasSize.height.value / rectangle.latitudeDelta.toRadians().value
|
||||||
) * 2 * PI / mapTileProvider.tileSize
|
) * 2 * PI / mapTileProvider.tileSize
|
||||||
)
|
).coerceIn(0.0..22.0)
|
||||||
return space.ViewPoint(rectangle.center, zoom.toFloat())
|
return space.ViewPoint(rectangle.center, zoom.toFloat())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,12 +88,12 @@ public class MapViewScope internal constructor(
|
|||||||
config: ViewConfig<Gmc> = ViewConfig(),
|
config: ViewConfig<Gmc> = ViewConfig(),
|
||||||
initialViewPoint: ViewPoint<Gmc>? = null,
|
initialViewPoint: ViewPoint<Gmc>? = null,
|
||||||
initialRectangle: Rectangle<Gmc>? = null,
|
initialRectangle: Rectangle<Gmc>? = null,
|
||||||
): MapViewScope = remember {
|
): MapCanvasState = remember {
|
||||||
MapViewScope(mapTileProvider, config).also { mapState ->
|
MapCanvasState(mapTileProvider, config).apply {
|
||||||
if (initialViewPoint != null) {
|
if (initialViewPoint != null) {
|
||||||
mapState.viewPoint = initialViewPoint
|
viewPoint = initialViewPoint
|
||||||
} else if (initialRectangle != null) {
|
} else if (initialRectangle != null) {
|
||||||
mapState.viewPoint = mapState.computeViewPoint(initialRectangle)
|
viewPoint = computeViewPoint(initialRectangle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.compose
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
@ -0,0 +1,149 @@
|
|||||||
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
|
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import org.jetbrains.skia.Image
|
||||||
|
import space.kscience.maps.coordinates.Gmc
|
||||||
|
import space.kscience.maps.features.*
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
|
||||||
|
private fun IntRange.intersect(other: IntRange) = kotlin.math.max(first, other.first)..kotlin.math.min(last, other.last)
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger("MapView")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component that renders map and provides basic map manipulation capabilities
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
public fun MapView(
|
||||||
|
mapState: MapCanvasState,
|
||||||
|
mapTileProvider: MapTileProvider,
|
||||||
|
features: FeatureGroup<Gmc>,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
val mapTiles = remember(mapTileProvider) {
|
||||||
|
mutableStateMapOf<TileId, Image>()
|
||||||
|
}
|
||||||
|
|
||||||
|
with(mapState) {
|
||||||
|
|
||||||
|
// Load tiles asynchronously
|
||||||
|
LaunchedEffect(viewPoint, canvasSize) {
|
||||||
|
with(mapTileProvider) {
|
||||||
|
val indexRange = 0 until 2.0.pow(intZoom).toInt()
|
||||||
|
|
||||||
|
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
|
||||||
|
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
|
||||||
|
val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
|
||||||
|
|
||||||
|
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
|
||||||
|
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
|
||||||
|
val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
|
||||||
|
|
||||||
|
for (j in verticalIndices) {
|
||||||
|
for (i in horizontalIndices) {
|
||||||
|
val id = TileId(intZoom, i, j)
|
||||||
|
//ensure that failed tiles do not fail the application
|
||||||
|
supervisorScope {
|
||||||
|
//start all
|
||||||
|
val deferred = loadTileAsync(id)
|
||||||
|
//wait asynchronously for it to finish
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
val tile = deferred.await()
|
||||||
|
mapTiles[tile.id] = tile.image
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
//displaying the error is maps responsibility
|
||||||
|
if (ex !is CancellationException) {
|
||||||
|
logger.error(ex) { "Failed to load tile with id=$id" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mapTiles.keys.filter {
|
||||||
|
it.zoom != intZoom || it.j !in verticalIndices || it.i !in horizontalIndices
|
||||||
|
}.forEach {
|
||||||
|
mapTiles.remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FeatureCanvas(mapState, features, modifier = modifier.canvasControls(mapState, features)) {
|
||||||
|
val tileScale = mapState.tileScale
|
||||||
|
|
||||||
|
clipRect {
|
||||||
|
val tileSize = IntSize(
|
||||||
|
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt(),
|
||||||
|
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt()
|
||||||
|
)
|
||||||
|
mapTiles.forEach { (id, image) ->
|
||||||
|
//converting back from tile index to screen offset
|
||||||
|
val offset = IntOffset(
|
||||||
|
(mapState.canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - mapState.centerCoordinates.x.dp) * tileScale).roundToPx(),
|
||||||
|
(mapState.canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - mapState.centerCoordinates.y.dp) * tileScale).roundToPx()
|
||||||
|
)
|
||||||
|
drawImage(
|
||||||
|
image = image.toComposeImageBitmap(),
|
||||||
|
dstOffset = offset,
|
||||||
|
dstSize = tileSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [MapView] with given [features] group.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
public fun MapView(
|
||||||
|
mapTileProvider: MapTileProvider,
|
||||||
|
config: ViewConfig<Gmc>,
|
||||||
|
features: FeatureGroup<Gmc>,
|
||||||
|
initialViewPoint: ViewPoint<Gmc>? = null,
|
||||||
|
initialRectangle: Rectangle<Gmc>? = null,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
val mapState = MapCanvasState.remember(mapTileProvider, config, initialViewPoint, initialRectangle)
|
||||||
|
MapView(mapState, mapTileProvider, features, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a map using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined,
|
||||||
|
* use map features to infer the view region.
|
||||||
|
* @param initialViewPoint The view point of the map using center and zoom. Is used if provided
|
||||||
|
* @param initialRectangle The rectangle to be used for view point computation. Used if [initialViewPoint] is not defined.
|
||||||
|
* @param buildFeatures - a builder for features
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
public fun MapView(
|
||||||
|
mapTileProvider: MapTileProvider,
|
||||||
|
config: ViewConfig<Gmc> = ViewConfig(),
|
||||||
|
initialViewPoint: ViewPoint<Gmc>? = null,
|
||||||
|
initialRectangle: Rectangle<Gmc>? = null,
|
||||||
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
|
buildFeatures: FeatureGroup<Gmc>.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
val featureState = FeatureGroup.remember(WebMercatorSpace, buildFeatures)
|
||||||
|
val computedRectangle = initialRectangle ?: featureState.getBoundingBox()
|
||||||
|
MapView(mapTileProvider, config, featureState, initialViewPoint, computedRectangle, modifier)
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
package center.sciprog.maps.compose
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
|
import space.kscience.maps.coordinates.GeodeticMapCoordinates
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
import space.kscience.maps.coordinates.Gmc
|
||||||
import center.sciprog.maps.coordinates.WebMercatorProjection
|
import space.kscience.maps.coordinates.WebMercatorProjection
|
||||||
import center.sciprog.maps.features.ViewPoint
|
import space.kscience.maps.features.ViewPoint
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable position on the map. Includes observation coordinate and [zoom] factor
|
* Observable position on the map. Includes observation coordinate and [zoom] factor
|
@ -1,14 +1,14 @@
|
|||||||
package center.sciprog.maps.compose
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.coordinates.*
|
|
||||||
import center.sciprog.maps.features.CoordinateSpace
|
|
||||||
import center.sciprog.maps.features.Rectangle
|
|
||||||
import center.sciprog.maps.features.ViewPoint
|
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
import space.kscience.kmath.geometry.radians
|
import space.kscience.kmath.geometry.radians
|
||||||
|
import space.kscience.maps.coordinates.*
|
||||||
|
import space.kscience.maps.features.CoordinateSpace
|
||||||
|
import space.kscience.maps.features.Rectangle
|
||||||
|
import space.kscience.maps.features.ViewPoint
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
@ -19,7 +19,8 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
|||||||
private fun tileScale(zoom: Float): Float = 2f.pow(zoom - floor(zoom))
|
private fun tileScale(zoom: Float): Float = 2f.pow(zoom - floor(zoom))
|
||||||
|
|
||||||
|
|
||||||
override fun Rectangle(first: Gmc, second: Gmc): Rectangle<Gmc> = GmcRectangle(first, second)
|
override fun Rectangle(first: Gmc, second: Gmc): Rectangle<Gmc> =
|
||||||
|
space.kscience.maps.compose.GmcRectangle(first, second)
|
||||||
|
|
||||||
override fun Rectangle(center: Gmc, zoom: Float, size: DpSize): Rectangle<Gmc> {
|
override fun Rectangle(center: Gmc, zoom: Float, size: DpSize): Rectangle<Gmc> {
|
||||||
val scale = WebMercatorProjection.scaleFactor(zoom)
|
val scale = WebMercatorProjection.scaleFactor(zoom)
|
||||||
@ -62,7 +63,10 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
|||||||
val maxLat = maxOf { it.top }
|
val maxLat = maxOf { it.top }
|
||||||
val minLong = minOf { it.left }
|
val minLong = minOf { it.left }
|
||||||
val maxLong = maxOf { it.right }
|
val maxLong = maxOf { it.right }
|
||||||
return GmcRectangle(Gmc.normalized(minLat, minLong), Gmc.normalized(maxLat, maxLong))
|
return space.kscience.maps.compose.GmcRectangle(
|
||||||
|
Gmc.normalized(minLat, minLong),
|
||||||
|
Gmc.normalized(maxLat, maxLong)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun Collection<Gmc>.wrapPoints(): Rectangle<Gmc>? {
|
override fun Collection<Gmc>.wrapPoints(): Rectangle<Gmc>? {
|
||||||
@ -72,7 +76,10 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
|||||||
val maxLat = maxOf { it.latitude }
|
val maxLat = maxOf { it.latitude }
|
||||||
val minLong = minOf { it.longitude }
|
val minLong = minOf { it.longitude }
|
||||||
val maxLong = maxOf { it.longitude }
|
val maxLong = maxOf { it.longitude }
|
||||||
return GmcRectangle(Gmc.normalized(minLat, minLong), Gmc.normalized(maxLat, maxLong))
|
return space.kscience.maps.compose.GmcRectangle(
|
||||||
|
Gmc.normalized(minLat, minLong),
|
||||||
|
Gmc.normalized(maxLat, maxLong)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset {
|
override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset {
|
||||||
@ -132,5 +139,5 @@ public fun CoordinateSpace<Gmc>.Rectangle(
|
|||||||
center.latitude + (height / 2),
|
center.latitude + (height / 2),
|
||||||
center.longitude + (width / 2)
|
center.longitude + (width / 2)
|
||||||
)
|
)
|
||||||
return GmcRectangle(a, b)
|
return space.kscience.maps.compose.GmcRectangle(a, b)
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.compose
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
@ -6,12 +6,10 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.coordinates.Distance
|
import org.jetbrains.skia.Font
|
||||||
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
|
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
|
||||||
import center.sciprog.maps.coordinates.GmcCurve
|
|
||||||
import center.sciprog.maps.features.*
|
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
|
import space.kscience.maps.coordinates.*
|
||||||
|
import space.kscience.maps.features.*
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
|
||||||
@ -55,6 +53,39 @@ public fun FeatureGroup<Gmc>.line(
|
|||||||
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates)
|
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A segmented geodetic curve
|
||||||
|
*/
|
||||||
|
public fun FeatureGroup<Gmc>.geodeticLine(
|
||||||
|
curve: GmcCurve,
|
||||||
|
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
|
||||||
|
maxLineDistance: Distance = 100.kilometers,
|
||||||
|
id: String? = null,
|
||||||
|
): FeatureRef<Gmc, Feature<Gmc>> = if (curve.distance < maxLineDistance) {
|
||||||
|
feature(
|
||||||
|
id,
|
||||||
|
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val segments = ceil(curve.distance / maxLineDistance).toInt()
|
||||||
|
val segmentSize = curve.distance / segments
|
||||||
|
val points = buildList<GmcPose> {
|
||||||
|
add(curve.forward)
|
||||||
|
repeat(segments) {
|
||||||
|
val segment = ellipsoid.curveInDirection(this.last(), segmentSize, 1e-2)
|
||||||
|
add(segment.backward)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
multiLine(points.map { it.coordinates }, id = id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun FeatureGroup<Gmc>.geodeticLine(
|
||||||
|
from: Gmc,
|
||||||
|
to: Gmc,
|
||||||
|
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
|
||||||
|
maxLineDistance: Distance = 100.kilometers,
|
||||||
|
id: String? = null,
|
||||||
|
): FeatureRef<Gmc, Feature<Gmc>> = geodeticLine(ellipsoid.curveBetween(from, to), ellipsoid, maxLineDistance, id)
|
||||||
|
|
||||||
public fun FeatureGroup<Gmc>.line(
|
public fun FeatureGroup<Gmc>.line(
|
||||||
aCoordinates: Pair<Double, Double>,
|
aCoordinates: Pair<Double, Double>,
|
||||||
@ -65,7 +96,6 @@ public fun FeatureGroup<Gmc>.line(
|
|||||||
LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates))
|
LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
public fun FeatureGroup<Gmc>.arc(
|
public fun FeatureGroup<Gmc>.arc(
|
||||||
center: Pair<Double, Double>,
|
center: Pair<Double, Double>,
|
||||||
radius: Distance,
|
radius: Distance,
|
||||||
@ -110,7 +140,7 @@ public fun FeatureGroup<Gmc>.icon(
|
|||||||
public fun FeatureGroup<Gmc>.text(
|
public fun FeatureGroup<Gmc>.text(
|
||||||
position: Pair<Double, Double>,
|
position: Pair<Double, Double>,
|
||||||
text: String,
|
text: String,
|
||||||
font: FeatureFont.() -> Unit = { size = 16f },
|
font: Font.() -> Unit = { size = 16f },
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
): FeatureRef<Gmc, TextFeature<Gmc>> = feature(
|
): FeatureRef<Gmc, TextFeature<Gmc>> = feature(
|
||||||
id,
|
id,
|
@ -0,0 +1,91 @@
|
|||||||
|
package center.sciprog.maps.compose
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.readBytes
|
||||||
|
import io.ktor.http.Url
|
||||||
|
import io.ktor.util.decodeBase64Bytes
|
||||||
|
import io.ktor.util.encodeBase64
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import org.jetbrains.skia.Image
|
||||||
|
import org.w3c.dom.Storage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [MapTileProvider] based on Open Street Map API. With in-memory and file cache
|
||||||
|
*/
|
||||||
|
public class OpenStreetMapTileProvider(
|
||||||
|
private val client: HttpClient,
|
||||||
|
private val storage: Storage = window.localStorage,
|
||||||
|
parallelism: Int = 4,
|
||||||
|
cacheCapacity: Int = 200,
|
||||||
|
private val osmBaseUrl: String = "https://tile.openstreetmap.org",
|
||||||
|
) : MapTileProvider {
|
||||||
|
private val semaphore = Semaphore(parallelism)
|
||||||
|
private val cache = LruCache<TileId, Deferred<Image>>(cacheCapacity)
|
||||||
|
|
||||||
|
private fun TileId.osmUrl() = Url("$osmBaseUrl/${zoom}/${i}/${j}.png")
|
||||||
|
|
||||||
|
private fun TileId.imageName() = "${zoom}/${i}/${j}.png"
|
||||||
|
|
||||||
|
private fun TileId.readImage() = storage.getItem(imageName())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download and cache the tile image
|
||||||
|
*/
|
||||||
|
private fun CoroutineScope.downloadImageAsync(id: TileId): Deferred<Image> = async {
|
||||||
|
|
||||||
|
id.readImage()?.let { imageString ->
|
||||||
|
try {
|
||||||
|
return@async Image.makeFromEncoded(imageString.decodeBase64Bytes())
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
logger.debug { "Failed to load image from $imageString" }
|
||||||
|
storage.removeItem(id.imageName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//semaphore works only for actual download
|
||||||
|
semaphore.withPermit {
|
||||||
|
val url = id.osmUrl()
|
||||||
|
val byteArray = client.get(url).readBytes()
|
||||||
|
logger.debug { "Finished downloading map tile with id $id from $url" }
|
||||||
|
val imageName = id.imageName()
|
||||||
|
logger.debug { "Caching map tile $id to $imageName" }
|
||||||
|
storage.setItem(imageName, byteArray.encodeBase64())
|
||||||
|
Image.makeFromEncoded(byteArray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun CoroutineScope.loadTileAsync(
|
||||||
|
tileId: TileId,
|
||||||
|
): Deferred<MapTile> {
|
||||||
|
|
||||||
|
//start image download
|
||||||
|
val imageDeferred: Deferred<Image> = cache.getOrPut(tileId) {
|
||||||
|
downloadImageAsync(tileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
//collect the result asynchronously
|
||||||
|
return async {
|
||||||
|
val image: Image = runCatching { imageDeferred.await() }.onFailure {
|
||||||
|
if (it !is CancellationException) {
|
||||||
|
logger.error(it) { "Failed to load tile image with id=$tileId" }
|
||||||
|
}
|
||||||
|
cache.remove(tileId)
|
||||||
|
}.getOrThrow()
|
||||||
|
|
||||||
|
MapTile(tileId, image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
private val logger = KotlinLogging.logger("OpenStreetMapCache")
|
||||||
|
}
|
||||||
|
}
|
@ -1,146 +0,0 @@
|
|||||||
package center.sciprog.maps.compose
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.PathEffect
|
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
||||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.IntSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import center.sciprog.attributes.z
|
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
|
||||||
import center.sciprog.maps.features.FeatureGroup
|
|
||||||
import center.sciprog.maps.features.PainterFeature
|
|
||||||
import center.sciprog.maps.features.drawFeature
|
|
||||||
import center.sciprog.maps.features.zoomRange
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.supervisorScope
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import org.jetbrains.skia.Image
|
|
||||||
import org.jetbrains.skia.Paint
|
|
||||||
import kotlin.math.ceil
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
|
|
||||||
private fun Color.toPaint(): Paint = Paint().apply {
|
|
||||||
isAntiAlias = true
|
|
||||||
color = toArgb()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun IntRange.intersect(other: IntRange) = max(first, other.first)..min(last, other.last)
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger("MapView")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A component that renders map and provides basic map manipulation capabilities
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
public actual fun MapView(
|
|
||||||
viewScope: MapViewScope,
|
|
||||||
features: FeatureGroup<Gmc>,
|
|
||||||
modifier: Modifier,
|
|
||||||
): Unit = with(viewScope) {
|
|
||||||
val mapTiles = remember(mapTileProvider) { mutableStateMapOf<TileId, Image>() }
|
|
||||||
|
|
||||||
// Load tiles asynchronously
|
|
||||||
LaunchedEffect(viewPoint, canvasSize) {
|
|
||||||
with(mapTileProvider) {
|
|
||||||
val indexRange = 0 until 2.0.pow(intZoom).toInt()
|
|
||||||
|
|
||||||
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
|
|
||||||
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
|
|
||||||
val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
|
|
||||||
|
|
||||||
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
|
|
||||||
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
|
|
||||||
val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
|
|
||||||
|
|
||||||
for (j in verticalIndices) {
|
|
||||||
for (i in horizontalIndices) {
|
|
||||||
val id = TileId(intZoom, i, j)
|
|
||||||
//ensure that failed tiles do not fail the application
|
|
||||||
supervisorScope {
|
|
||||||
//start all
|
|
||||||
val deferred = loadTileAsync(id)
|
|
||||||
//wait asynchronously for it to finish
|
|
||||||
launch {
|
|
||||||
try {
|
|
||||||
val tile = deferred.await()
|
|
||||||
mapTiles[tile.id] = tile.image
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
//displaying the error is maps responsibility
|
|
||||||
logger.error(ex) { "Failed to load tile with id=$id" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mapTiles.keys.filter {
|
|
||||||
it.zoom != intZoom || it.j !in verticalIndices || it.i !in horizontalIndices
|
|
||||||
}.forEach {
|
|
||||||
mapTiles.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
key(viewScope, features) {
|
|
||||||
val painterCache: Map<PainterFeature<Gmc>, Painter> =
|
|
||||||
features.features.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.getPainter() }
|
|
||||||
|
|
||||||
Canvas(modifier = modifier.mapControls(viewScope, features)) {
|
|
||||||
|
|
||||||
if (canvasSize != size.toDpSize()) {
|
|
||||||
logger.debug { "Recalculate canvas. Size: $size" }
|
|
||||||
config.onCanvasSizeChange(canvasSize)
|
|
||||||
canvasSize = size.toDpSize()
|
|
||||||
}
|
|
||||||
|
|
||||||
clipRect {
|
|
||||||
val tileSize = IntSize(
|
|
||||||
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt(),
|
|
||||||
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt()
|
|
||||||
)
|
|
||||||
mapTiles.forEach { (id, image) ->
|
|
||||||
//converting back from tile index to screen offset
|
|
||||||
val offset = IntOffset(
|
|
||||||
(canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale).roundToPx(),
|
|
||||||
(canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale).roundToPx()
|
|
||||||
)
|
|
||||||
drawImage(
|
|
||||||
image = image.toComposeImageBitmap(),
|
|
||||||
dstOffset = offset,
|
|
||||||
dstSize = tileSize
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
features.featureMap.values.sortedBy { it.z }
|
|
||||||
.filter { viewPoint.zoom in it.zoomRange }
|
|
||||||
.forEach { feature ->
|
|
||||||
drawFeature(viewScope, painterCache, feature)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectRect?.let { dpRect ->
|
|
||||||
val rect = dpRect.toRect()
|
|
||||||
drawRect(
|
|
||||||
color = Color.Blue,
|
|
||||||
topLeft = rect.topLeft,
|
|
||||||
size = rect.size,
|
|
||||||
alpha = 0.5f,
|
|
||||||
style = Stroke(
|
|
||||||
width = 2f,
|
|
||||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
|||||||
package center.sciprog.maps.compose
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.statement.readBytes
|
import io.ktor.client.statement.readBytes
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import mu.KotlinLogging
|
|
||||||
import org.jetbrains.skia.Image
|
import org.jetbrains.skia.Image
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
@ -76,7 +73,9 @@ public class OpenStreetMapTileProvider(
|
|||||||
//collect the result asynchronously
|
//collect the result asynchronously
|
||||||
return async {
|
return async {
|
||||||
val image: Image = runCatching { imageDeferred.await() }.onFailure {
|
val image: Image = runCatching { imageDeferred.await() }.onFailure {
|
||||||
|
if(it !is CancellationException) {
|
||||||
logger.error(it) { "Failed to load tile image with id=$tileId" }
|
logger.error(it) { "Failed to load tile image with id=$tileId" }
|
||||||
|
}
|
||||||
cache.remove(tileId)
|
cache.remove(tileId)
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
|
|
@ -1,15 +1,13 @@
|
|||||||
package center.sciprog.maps.compose
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import kotlin.test.assertFails
|
import kotlin.test.assertFails
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
class OsmTileProviderTest {
|
class OsmTileProviderTest {
|
||||||
// @get:Rule
|
// @get:Rule
|
||||||
// val rule = createComposeRule()
|
// val rule = createComposeRule()
|
@ -9,19 +9,8 @@ The core interfaces of KMath.
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `center.sciprog:maps-kt-core:0.2.2`.
|
The Maven coordinates of this project are `space.kscience:maps-kt-core:0.3.0`.
|
||||||
|
|
||||||
**Gradle Groovy:**
|
|
||||||
```groovy
|
|
||||||
repositories {
|
|
||||||
maven { url 'https://repo.kotlin.link' }
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'center.sciprog:maps-kt-core:0.2.2'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
@ -30,6 +19,6 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("center.sciprog:maps-kt-core:0.2.2")
|
implementation("space.kscience:maps-kt-core:0.3.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -8,6 +8,9 @@ val kmathVersion: String by rootProject.extra
|
|||||||
kscience{
|
kscience{
|
||||||
jvm()
|
jvm()
|
||||||
js()
|
js()
|
||||||
|
native()
|
||||||
|
wasm()
|
||||||
|
|
||||||
useSerialization()
|
useSerialization()
|
||||||
|
|
||||||
dependencies{
|
dependencies{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package space.kscience.maps.coordinates
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.jvm.JvmInline
|
import kotlin.jvm.JvmInline
|
@ -1,10 +1,8 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package space.kscience.maps.coordinates
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.*
|
||||||
import space.kscience.kmath.geometry.tan
|
import kotlin.math.*
|
||||||
import kotlin.math.pow
|
|
||||||
import kotlin.math.sqrt
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRadius: Distance) {
|
public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRadius: Distance) {
|
||||||
@ -43,12 +41,7 @@ public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRa
|
|||||||
polarRadius = Distance(6378.137)
|
polarRadius = Distance(6378.137)
|
||||||
)
|
)
|
||||||
|
|
||||||
// /**
|
|
||||||
// * https://en.wikipedia.org/wiki/Great-circle_distance
|
|
||||||
// */
|
|
||||||
// public fun greatCircleAngleBetween(r1: GMC, r2: GMC): Radians = acos(
|
|
||||||
// sin(r1.latitude) * sin(r2.latitude) + cos(r1.latitude) * cos(r2.latitude) * cos(r1.longitude - r2.longitude)
|
|
||||||
// ).radians
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,22 +52,35 @@ public fun GeoEllipsoid.reducedRadius(latitude: Angle): Distance {
|
|||||||
val reducedLatitudeTan = (1 - f) * tan(latitude)
|
val reducedLatitudeTan = (1 - f) * tan(latitude)
|
||||||
return equatorRadius / sqrt(1.0 + reducedLatitudeTan.pow(2))
|
return equatorRadius / sqrt(1.0 + reducedLatitudeTan.pow(2))
|
||||||
}
|
}
|
||||||
//
|
|
||||||
//
|
|
||||||
///**
|
/**
|
||||||
// * Compute distance between two map points using giv
|
* Compute distance between two map points using giv
|
||||||
// * https://en.wikipedia.org/wiki/Geographical_distance#Lambert's_formula_for_long_lines
|
* https://en.wikipedia.org/wiki/Geographical_distance#Lambert's_formula_for_long_lines
|
||||||
// */
|
*/
|
||||||
//public fun GeoEllipsoid.lambertDistanceBetween(r1: GMC, r2: GMC): Distance {
|
public fun GeoEllipsoid.lambertDistanceBetween(r1: Gmc, r2: Gmc): Distance {
|
||||||
// val s = greatCircleAngleBetween(r1, r2)
|
|
||||||
//
|
/**
|
||||||
// val b1: Double = (1 - f) * tan(r1.latitude)
|
* https://en.wikipedia.org/wiki/Great-circle_distance
|
||||||
// val b2: Double = (1 - f) * tan(r2.latitude)
|
*/
|
||||||
// val p = (b1 + b2) / 2
|
fun greatCircleAngleBetween(
|
||||||
// val q = (b2 - b1) / 2
|
r1: Gmc,
|
||||||
//
|
r2: Gmc,
|
||||||
// val x = (s.value - sin(s)) * sin(p).pow(2) * cos(q).pow(2) / cos(s / 2).pow(2)
|
): Radians = acos(
|
||||||
// val y = (s.value + sin(s)) * cos(p).pow(2) * sin(q).pow(2) / sin(s / 2).pow(2)
|
sin(r1.latitude) * sin(r2.latitude) +
|
||||||
//
|
cos(r1.latitude) * cos(r2.latitude) *
|
||||||
// return equatorRadius * (s.value - f / 2 * (x + y))
|
cos(r1.longitude - r2.longitude)
|
||||||
//}
|
).radians
|
||||||
|
|
||||||
|
val s = greatCircleAngleBetween(r1, r2)
|
||||||
|
|
||||||
|
val b1: Double = (1 - f) * tan(r1.latitude)
|
||||||
|
val b2: Double = (1 - f) * tan(r2.latitude)
|
||||||
|
val p = (b1 + b2) / 2
|
||||||
|
val q = (b2 - b1) / 2
|
||||||
|
|
||||||
|
val x = (s.value - sin(s)) * sin(p).pow(2) * cos(q).pow(2) / cos(s / 2).pow(2)
|
||||||
|
val y = (s.value + sin(s)) * cos(p).pow(2) * sin(q).pow(2) / sin(s / 2).pow(2)
|
||||||
|
|
||||||
|
return equatorRadius * (s.value - f / 2 * (x + y))
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package space.kscience.maps.coordinates
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.kmath.geometry.*
|
import space.kscience.kmath.geometry.*
|
||||||
@ -22,7 +22,6 @@ public class GeodeticMapCoordinates(
|
|||||||
"Longitude $longitude is not in (-PI..PI) range"
|
"Longitude $longitude is not in (-PI..PI) range"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val x: Angle get() = longitude
|
override val x: Angle get() = longitude
|
||||||
|
|
||||||
override val y: Angle get() = latitude
|
override val y: Angle get() = latitude
|
||||||
@ -43,7 +42,7 @@ public class GeodeticMapCoordinates(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "GMC(latitude=${latitude.degrees} deg, longitude=${longitude.degrees} deg)"
|
return "GMC(latitude=${latitude.toDegrees().value} deg, longitude=${longitude.toDegrees().value} deg)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package space.kscience.maps.coordinates
|
||||||
|
|
||||||
import space.kscience.kmath.geometry.*
|
import space.kscience.kmath.geometry.*
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A directed straight (geodetic) segment on a spheroid with given start, direction, end point and distance.
|
* A directed straight (geodetic) segment on a spheroid with given start, direction, end point and distance.
|
||||||
* @param forward coordinate of a start point with forward direction
|
* @param forward coordinate of a start point with the forward direction
|
||||||
* @param backward coordinate of an end point with backward direction
|
* @param backward coordinate of an end point with the backward direction
|
||||||
*/
|
*/
|
||||||
public class GmcCurve(
|
public class GmcCurve internal constructor(
|
||||||
public val forward: GmcPose,
|
public val forward: GmcPose,
|
||||||
public val backward: GmcPose,
|
public val backward: GmcPose,
|
||||||
public val distance: Distance,
|
public val distance: Distance,
|
||||||
@ -64,8 +64,8 @@ public fun GeoEllipsoid.meridianCurve(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return GmcCurve(
|
return GmcCurve(
|
||||||
forward = GmcPose(Gmc.normalized(fromLatitude, longitude), if (up) Angle.zero else Angle.pi),
|
forward = GmcPose(GeodeticMapCoordinates.normalized(fromLatitude, longitude), if (up) Angle.zero else Angle.pi),
|
||||||
backward = GmcPose(Gmc.normalized(toLatitude, longitude), if (up) Angle.pi else Angle.zero),
|
backward = GmcPose(GeodeticMapCoordinates.normalized(toLatitude, longitude), if (up) Angle.pi else Angle.zero),
|
||||||
distance = s
|
distance = s
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -77,9 +77,9 @@ public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toL
|
|||||||
require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
|
require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
|
||||||
val right = toLongitude > fromLongitude
|
val right = toLongitude > fromLongitude
|
||||||
return GmcCurve(
|
return GmcCurve(
|
||||||
forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) Angle.piDiv2 else -Angle.piDiv2),
|
forward = GmcPose(GeodeticMapCoordinates.normalized(latitude, fromLongitude), if (right) Angle.piDiv2 else -Angle.piDiv2),
|
||||||
backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -Angle.piDiv2 else Angle.piDiv2),
|
backward = GmcPose(GeodeticMapCoordinates.normalized(latitude, toLongitude), if (right) -Angle.piDiv2 else Angle.piDiv2),
|
||||||
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians)
|
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).toRadians().value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +193,7 @@ public fun GeoEllipsoid.curveInDirection(
|
|||||||
val L = lambda - (1 - C) * f * sinAlpha *
|
val L = lambda - (1 - C) * f * sinAlpha *
|
||||||
(sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)))
|
(sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)))
|
||||||
|
|
||||||
val endPoint = Gmc.normalized(phi2, start.longitude + L.radians)
|
val endPoint = GeodeticMapCoordinates.normalized(phi2, start.longitude + L.radians)
|
||||||
|
|
||||||
// eq. 12
|
// eq. 12
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package space.kscience.maps.coordinates
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
@ -3,7 +3,7 @@
|
|||||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package center.sciprog.maps.coordinates
|
package space.kscience.maps.coordinates
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.kmath.geometry.*
|
import space.kscience.kmath.geometry.*
|
||||||
@ -57,12 +57,12 @@ public open class MercatorProjection(
|
|||||||
return if (ellipsoid === GeoEllipsoid.sphere) {
|
return if (ellipsoid === GeoEllipsoid.sphere) {
|
||||||
GeodeticMapCoordinates.ofRadians(
|
GeodeticMapCoordinates.ofRadians(
|
||||||
atan(sinh(pc.y / ellipsoid.equatorRadius)),
|
atan(sinh(pc.y / ellipsoid.equatorRadius)),
|
||||||
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius),
|
baseLongitude.toRadians().value + (pc.x / ellipsoid.equatorRadius),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
GeodeticMapCoordinates.ofRadians(
|
GeodeticMapCoordinates.ofRadians(
|
||||||
cphi2(exp(-(pc.y / ellipsoid.equatorRadius))),
|
cphi2(exp(-(pc.y / ellipsoid.equatorRadius))),
|
||||||
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius)
|
baseLongitude.toRadians().value + (pc.x / ellipsoid.equatorRadius)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,13 +76,13 @@ public open class MercatorProjection(
|
|||||||
|
|
||||||
return if (ellipsoid === GeoEllipsoid.sphere) {
|
return if (ellipsoid === GeoEllipsoid.sphere) {
|
||||||
ProjectionCoordinates(
|
ProjectionCoordinates(
|
||||||
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians,
|
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).toRadians().value,
|
||||||
y = ellipsoid.equatorRadius * ln(tan(Angle.pi / 4 + gmc.latitude / 2))
|
y = ellipsoid.equatorRadius * ln(tan(Angle.pi / 4 + gmc.latitude / 2))
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val sinPhi = sin(gmc.latitude)
|
val sinPhi = sin(gmc.latitude)
|
||||||
ProjectionCoordinates(
|
ProjectionCoordinates(
|
||||||
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians,
|
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).toRadians().value,
|
||||||
y = ellipsoid.equatorRadius * ln(
|
y = ellipsoid.equatorRadius * ln(
|
||||||
tan(Angle.pi / 4 + gmc.latitude / 2) * ((1 - e * sinPhi) / (1 + e * sinPhi)).pow(e / 2)
|
tan(Angle.pi / 4 + gmc.latitude / 2) * ((1 - e * sinPhi) / (1 + e * sinPhi)).pow(e / 2)
|
||||||
)
|
)
|
@ -3,10 +3,9 @@
|
|||||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package center.sciprog.maps.coordinates
|
package space.kscience.maps.coordinates
|
||||||
|
|
||||||
import space.kscience.kmath.geometry.abs
|
import space.kscience.kmath.geometry.abs
|
||||||
import space.kscience.kmath.geometry.radians
|
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
public data class WebMercatorCoordinates(val zoom: Int, val x: Float, val y: Float)
|
public data class WebMercatorCoordinates(val zoom: Int, val x: Float, val y: Float)
|
||||||
@ -36,8 +35,8 @@ public object WebMercatorProjection {
|
|||||||
val scaleFactor = scaleFactor(zoom.toFloat())
|
val scaleFactor = scaleFactor(zoom.toFloat())
|
||||||
return WebMercatorCoordinates(
|
return WebMercatorCoordinates(
|
||||||
zoom = zoom,
|
zoom = zoom,
|
||||||
x = scaleFactor * (gmc.longitude.radians + PI).toFloat(),
|
x = scaleFactor * (gmc.longitude.toRadians().value + PI).toFloat(),
|
||||||
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians / 2))).toFloat()
|
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.toRadians().value / 2))).toFloat()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package space.kscience.maps.coordinates
|
||||||
|
|
||||||
import space.kscience.kmath.geometry.radians
|
import space.kscience.kmath.geometry.radians
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
@ -6,8 +6,8 @@ import kotlin.test.assertEquals
|
|||||||
|
|
||||||
internal class DistanceTest {
|
internal class DistanceTest {
|
||||||
companion object {
|
companion object {
|
||||||
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869)
|
val moscow = GeodeticMapCoordinates.ofDegrees(55.76058287719673, 37.60358622841869)
|
||||||
val spb = Gmc.ofDegrees(59.926686023580444, 30.36038109122013)
|
val spb = GeodeticMapCoordinates.ofDegrees(59.926686023580444, 30.36038109122013)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -21,7 +21,7 @@ internal class DistanceTest {
|
|||||||
val distance = curve.distance
|
val distance = curve.distance
|
||||||
|
|
||||||
assertEquals(632.035426877, distance.kilometers, 0.0001)
|
assertEquals(632.035426877, distance.kilometers, 0.0001)
|
||||||
assertEquals(-0.6947937116552751, curve.forward.bearing.radians, 0.0001)
|
assertEquals(-0.6947937116552751, curve.forward.bearing.toRadians().value, 0.0001)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -30,7 +30,7 @@ internal class DistanceTest {
|
|||||||
GmcPose(moscow, (-0.6947937116552751).radians), Distance(632.035426877)
|
GmcPose(moscow, (-0.6947937116552751).radians), Distance(632.035426877)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(spb.latitude.radians, curve.backward.latitude.radians, 0.0001)
|
assertEquals(spb.latitude.toRadians().value, curve.backward.latitude.toRadians().value, 0.0001)
|
||||||
assertEquals(spb.longitude.radians, curve.backward.longitude.radians, 0.0001)
|
assertEquals(spb.longitude.toRadians().value, curve.backward.longitude.toRadians().value, 0.0001)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,29 +1,28 @@
|
|||||||
package center.sciprog.maps.coordinates
|
package space.kscience.maps.coordinates
|
||||||
|
|
||||||
import space.kscience.kmath.geometry.degrees
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class MercatorTest {
|
class MercatorTest {
|
||||||
@Test
|
@Test
|
||||||
fun sphereForwardBackward(){
|
fun sphereForwardBackward(){
|
||||||
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869)
|
val moscow = GeodeticMapCoordinates.ofDegrees(55.76058287719673, 37.60358622841869)
|
||||||
val mercator = MapProjection.epsg3857.toProjection(moscow)
|
val mercator = MapProjection.epsg3857.toProjection(moscow)
|
||||||
//https://epsg.io/transform#s_srs=4326&t_srs=3857&x=37.6035862&y=55.7605829
|
//https://epsg.io/transform#s_srs=4326&t_srs=3857&x=37.6035862&y=55.7605829
|
||||||
assertEquals(4186.0120709, mercator.x.kilometers, 1e-4)
|
assertEquals(4186.0120709, mercator.x.kilometers, 1e-4)
|
||||||
assertEquals(7510.9013658, mercator.y.kilometers, 1e-4)
|
assertEquals(7510.9013658, mercator.y.kilometers, 1e-4)
|
||||||
val backwards = MapProjection.epsg3857.toGeodetic(mercator)
|
val backwards = MapProjection.epsg3857.toGeodetic(mercator)
|
||||||
assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 1e-6)
|
assertEquals(moscow.latitude.toDegrees().value, backwards.latitude.toDegrees().value, 1e-6)
|
||||||
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6)
|
assertEquals(moscow.longitude.toDegrees().value, backwards.longitude.toDegrees().value, 1e-6)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun ellipseForwardBackward(){
|
fun ellipseForwardBackward(){
|
||||||
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869)
|
val moscow = GeodeticMapCoordinates.ofDegrees(55.76058287719673, 37.60358622841869)
|
||||||
val projection = MercatorProjection(ellipsoid = GeoEllipsoid.WGS84)
|
val projection = MercatorProjection(ellipsoid = GeoEllipsoid.WGS84)
|
||||||
val mercator = projection.toProjection(moscow)
|
val mercator = projection.toProjection(moscow)
|
||||||
val backwards = projection.toGeodetic(mercator)
|
val backwards = projection.toGeodetic(mercator)
|
||||||
assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 1e-6)
|
assertEquals(moscow.latitude.toDegrees().value, backwards.latitude.toDegrees().value, 1e-6)
|
||||||
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6)
|
assertEquals(moscow.longitude.toDegrees().value, backwards.longitude.toDegrees().value, 1e-6)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,19 +6,8 @@
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `center.sciprog:maps-kt-features:0.2.2`.
|
The Maven coordinates of this project are `space.kscience:maps-kt-features:0.3.0`.
|
||||||
|
|
||||||
**Gradle Groovy:**
|
|
||||||
```groovy
|
|
||||||
repositories {
|
|
||||||
maven { url 'https://repo.kotlin.link' }
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'center.sciprog:maps-kt-features:0.2.2'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
@ -27,6 +16,6 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("center.sciprog:maps-kt-features:0.2.2")
|
implementation("space.kscience:maps-kt-features:0.3.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("space.kscience.gradle.mpp")
|
id("space.kscience.gradle.mpp")
|
||||||
id("org.jetbrains.compose")
|
alias(spclibs.plugins.compose.compiler)
|
||||||
|
alias(spclibs.plugins.compose.jb)
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8,7 +9,17 @@ val kmathVersion: String by rootProject.extra
|
|||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
jvm()
|
jvm()
|
||||||
js()
|
// js()
|
||||||
|
wasm{
|
||||||
|
browser {
|
||||||
|
testTask {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useCoroutines()
|
||||||
|
|
||||||
useSerialization {
|
useSerialization {
|
||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
@ -16,15 +27,13 @@ kscience{
|
|||||||
useSerialization(sourceSet = space.kscience.gradle.DependencySourceSet.TEST) {
|
useSerialization(sourceSet = space.kscience.gradle.DependencySourceSet.TEST) {
|
||||||
protobuf()
|
protobuf()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
sourceSets {
|
|
||||||
commonMain{
|
commonMain{
|
||||||
dependencies {
|
|
||||||
api(projects.trajectoryKt)
|
api(projects.trajectoryKt)
|
||||||
|
api(compose.runtime)
|
||||||
api(compose.foundation)
|
api(compose.foundation)
|
||||||
}
|
api(compose.material)
|
||||||
}
|
api(compose.ui)
|
||||||
|
api("io.github.oshai:kotlin-logging:6.0.3")
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,22 +0,0 @@
|
|||||||
package center.sciprog.attributes
|
|
||||||
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
|
|
||||||
public interface Attribute<T>
|
|
||||||
|
|
||||||
public abstract class SerializableAttribute<T>(
|
|
||||||
public val serialId: String,
|
|
||||||
public val serializer: KSerializer<T>,
|
|
||||||
) : Attribute<T> {
|
|
||||||
override fun toString(): String = serialId
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface AttributeWithDefault<T> : Attribute<T> {
|
|
||||||
public val default: T
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface SetAttribute<V> : Attribute<Set<V>>
|
|
||||||
|
|
||||||
public object NameAttribute : SerializableAttribute<String>("name", String.serializer())
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
|||||||
package center.sciprog.attributes
|
|
||||||
|
|
||||||
import center.sciprog.maps.features.Feature
|
|
||||||
import center.sciprog.maps.features.ZAttribute
|
|
||||||
import kotlin.jvm.JvmInline
|
|
||||||
|
|
||||||
@JvmInline
|
|
||||||
public value class Attributes internal constructor(public val content: Map<out Attribute<*>, Any>) {
|
|
||||||
|
|
||||||
public val keys: Set<Attribute<*>> get() = content.keys
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
public operator fun <T> get(attribute: Attribute<T>): T? = content[attribute] as? T
|
|
||||||
|
|
||||||
override fun toString(): String = "Attributes(value=${content.entries})"
|
|
||||||
|
|
||||||
public companion object {
|
|
||||||
public val EMPTY: Attributes = Attributes(emptyMap())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun Attributes.isEmpty(): Boolean = content.isEmpty()
|
|
||||||
|
|
||||||
public fun <T> Attributes.getOrDefault(attribute: AttributeWithDefault<T>): T = get(attribute) ?: attribute.default
|
|
||||||
|
|
||||||
public fun <T, A : Attribute<T>> Attributes.withAttribute(
|
|
||||||
attribute: A,
|
|
||||||
attrValue: T?,
|
|
||||||
): Attributes = Attributes(
|
|
||||||
if (attrValue == null) {
|
|
||||||
content - attribute
|
|
||||||
} else {
|
|
||||||
content + (attribute to attrValue)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an element to a [SetAttribute]
|
|
||||||
*/
|
|
||||||
public fun <T, A : SetAttribute<T>> Attributes.withAttributeElement(
|
|
||||||
attribute: A,
|
|
||||||
attrValue: T,
|
|
||||||
): Attributes {
|
|
||||||
val currentSet: Set<T> = get(attribute) ?: emptySet()
|
|
||||||
return Attributes(
|
|
||||||
content + (attribute to (currentSet + attrValue))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an element from [SetAttribute]
|
|
||||||
*/
|
|
||||||
public fun <T, A : SetAttribute<T>> Attributes.withoutAttributeElement(
|
|
||||||
attribute: A,
|
|
||||||
attrValue: T,
|
|
||||||
): Attributes {
|
|
||||||
val currentSet: Set<T> = get(attribute) ?: emptySet()
|
|
||||||
return Attributes(
|
|
||||||
content + (attribute to (currentSet - attrValue))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <T : Any, A : Attribute<T>> Attributes(
|
|
||||||
attribute: A,
|
|
||||||
attrValue: T,
|
|
||||||
): Attributes = Attributes(mapOf(attribute to attrValue))
|
|
||||||
|
|
||||||
public operator fun Attributes.plus(other: Attributes): Attributes = Attributes(content + other.content)
|
|
||||||
|
|
||||||
public val Feature<*>.z: Float
|
|
||||||
get() = attributes[ZAttribute] ?: 0f
|
|
||||||
// set(value) {
|
|
||||||
// attributes[ZAttribute] = value
|
|
||||||
// }
|
|
@ -1,47 +0,0 @@
|
|||||||
package center.sciprog.attributes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A safe builder for [Attributes]
|
|
||||||
*/
|
|
||||||
public class AttributesBuilder internal constructor(private val map: MutableMap<Attribute<*>, Any> = mutableMapOf()) {
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
public operator fun <T> get(attribute: Attribute<T>): T? = map[attribute] as? T
|
|
||||||
|
|
||||||
public operator fun <V> Attribute<V>.invoke(value: V?) {
|
|
||||||
if (value == null) {
|
|
||||||
map.remove(this)
|
|
||||||
} else {
|
|
||||||
map[this] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun from(attributes: Attributes) {
|
|
||||||
map.putAll(attributes.content)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <V> SetAttribute<V>.add(
|
|
||||||
attrValue: V,
|
|
||||||
) {
|
|
||||||
val currentSet: Set<V> = get(this) ?: emptySet()
|
|
||||||
map[this] = currentSet + attrValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an element from [SetAttribute]
|
|
||||||
*/
|
|
||||||
public fun <V> SetAttribute<V>.remove(
|
|
||||||
attrValue: V,
|
|
||||||
) {
|
|
||||||
val currentSet: Set<V> = get(this) ?: emptySet()
|
|
||||||
map[this] = currentSet - attrValue
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun build(): Attributes = Attributes(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun AttributesBuilder(
|
|
||||||
attributes: Attributes,
|
|
||||||
): AttributesBuilder = AttributesBuilder(attributes.content.toMutableMap())
|
|
||||||
|
|
||||||
public fun Attributes(builder: AttributesBuilder.() -> Unit): Attributes = AttributesBuilder().apply(builder).build()
|
|
@ -1,5 +0,0 @@
|
|||||||
package center.sciprog.maps.features
|
|
||||||
|
|
||||||
public expect class FeatureFont {
|
|
||||||
public var size: Float
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.compose
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.drag
|
import androidx.compose.foundation.gestures.drag
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -7,7 +7,7 @@ import androidx.compose.ui.input.pointer.*
|
|||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.DpRect
|
import androidx.compose.ui.unit.DpRect
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.features.*
|
import space.kscience.maps.features.*
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@ -16,8 +16,8 @@ import kotlin.math.min
|
|||||||
* Create a modifier for Map/Scheme canvas controls on desktop
|
* Create a modifier for Map/Scheme canvas controls on desktop
|
||||||
* @param features a collection of features to be rendered in descending [ZAttribute] order
|
* @param features a collection of features to be rendered in descending [ZAttribute] order
|
||||||
*/
|
*/
|
||||||
public fun <T : Any> Modifier.mapControls(
|
public fun <T : Any> Modifier.canvasControls(
|
||||||
state: CoordinateViewScope<T>,
|
state: CanvasState<T>,
|
||||||
features: FeatureGroup<T>,
|
features: FeatureGroup<T>,
|
||||||
): Modifier = with(state) {
|
): Modifier = with(state) {
|
||||||
|
|
||||||
@ -32,8 +32,8 @@ public fun <T : Any> Modifier.mapControls(
|
|||||||
awaitPointerEventScope {
|
awaitPointerEventScope {
|
||||||
while (true) {
|
while (true) {
|
||||||
val event = awaitPointerEvent()
|
val event = awaitPointerEvent()
|
||||||
val coordinates = event.changes.first().position.toCoordinates(this)
|
val coordinates = toCoordinates(event.changes.first().position, this)
|
||||||
val point = space.ViewPoint(coordinates, zoom)
|
val point = state.space.ViewPoint(coordinates, zoom)
|
||||||
|
|
||||||
if (event.type == PointerEventType.Move) {
|
if (event.type == PointerEventType.Move) {
|
||||||
features.forEachWithAttribute(HoverListenerAttribute) { _, feature, listeners ->
|
features.forEachWithAttribute(HoverListenerAttribute) { _, feature, listeners ->
|
||||||
@ -47,9 +47,9 @@ public fun <T : Any> Modifier.mapControls(
|
|||||||
}
|
}
|
||||||
}.pointerInput(Unit) {
|
}.pointerInput(Unit) {
|
||||||
detectClicks(
|
detectClicks(
|
||||||
onDoubleClick = if (state.config.zoomOnDoubleClick) {
|
onDoubleClick = if (viewConfig.zoomOnDoubleClick) {
|
||||||
{ event ->
|
{ event ->
|
||||||
val invariant = event.position.toCoordinates(this)
|
val invariant = toCoordinates(event.position, this)
|
||||||
viewPoint = with(space) {
|
viewPoint = with(space) {
|
||||||
viewPoint.zoomBy(
|
viewPoint.zoomBy(
|
||||||
if (event.buttons.isPrimaryPressed) 1f else if (event.buttons.isSecondaryPressed) -1f else 0f,
|
if (event.buttons.isPrimaryPressed) 1f else if (event.buttons.isSecondaryPressed) -1f else 0f,
|
||||||
@ -59,10 +59,10 @@ public fun <T : Any> Modifier.mapControls(
|
|||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
onClick = { event ->
|
onClick = { event ->
|
||||||
val coordinates = event.position.toCoordinates(this)
|
val coordinates = toCoordinates(event.position, this)
|
||||||
val point = space.ViewPoint(coordinates, zoom)
|
val point = space.ViewPoint(coordinates, zoom)
|
||||||
|
|
||||||
config.onClick?.handle(
|
viewConfig.onClick?.handle(
|
||||||
event,
|
event,
|
||||||
point
|
point
|
||||||
)
|
)
|
||||||
@ -88,7 +88,7 @@ public fun <T : Any> Modifier.mapControls(
|
|||||||
//compute invariant point of translation
|
//compute invariant point of translation
|
||||||
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
|
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
|
||||||
viewPoint = with(space) {
|
viewPoint = with(space) {
|
||||||
viewPoint.zoomBy(-change.scrollDelta.y * config.zoomSpeed, invariant)
|
viewPoint.zoomBy(-change.scrollDelta.y * viewConfig.zoomSpeed, invariant)
|
||||||
}
|
}
|
||||||
change.consume()
|
change.consume()
|
||||||
}
|
}
|
||||||
@ -110,14 +110,14 @@ public fun <T : Any> Modifier.mapControls(
|
|||||||
//apply drag handle and check if it prohibits the drag even propagation
|
//apply drag handle and check if it prohibits the drag even propagation
|
||||||
if (selectionStart == null) {
|
if (selectionStart == null) {
|
||||||
val dragStart = space.ViewPoint(
|
val dragStart = space.ViewPoint(
|
||||||
dragChange.previousPosition.toCoordinates(this),
|
toCoordinates(dragChange.previousPosition, this),
|
||||||
zoom
|
zoom
|
||||||
)
|
)
|
||||||
val dragEnd = space.ViewPoint(
|
val dragEnd = space.ViewPoint(
|
||||||
dragChange.position.toCoordinates(this),
|
toCoordinates(dragChange.position, this),
|
||||||
zoom
|
zoom
|
||||||
)
|
)
|
||||||
val dragResult = config.dragHandle?.handle(event, dragStart, dragEnd)
|
val dragResult = viewConfig.dragHandle?.handle(event, dragStart, dragEnd)
|
||||||
if (dragResult?.handleNext == false) return@drag
|
if (dragResult?.handleNext == false) return@drag
|
||||||
|
|
||||||
var continueAfter = true
|
var continueAfter = true
|
||||||
@ -132,7 +132,7 @@ public fun <T : Any> Modifier.mapControls(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.buttons.isPrimaryPressed) {
|
if (event.buttons.isPrimaryPressed) {
|
||||||
//If selection process is started, modify the frame
|
//If the selection process is started, modify the frame
|
||||||
selectionStart?.let { start ->
|
selectionStart?.let { start ->
|
||||||
val offset = dragChange.position
|
val offset = dragChange.position
|
||||||
selectRect = DpRect(
|
selectRect = DpRect(
|
||||||
@ -161,8 +161,8 @@ public fun <T : Any> Modifier.mapControls(
|
|||||||
rect.topLeft.toCoordinates(),
|
rect.topLeft.toCoordinates(),
|
||||||
rect.bottomRight.toCoordinates()
|
rect.bottomRight.toCoordinates()
|
||||||
)
|
)
|
||||||
config.onSelect(coordinateRect)
|
viewConfig.onSelect(coordinateRect)
|
||||||
if (config.zoomOnSelect) {
|
if (viewConfig.zoomOnSelect) {
|
||||||
viewPoint = computeViewPoint(coordinateRect)
|
viewPoint = computeViewPoint(coordinateRect)
|
||||||
}
|
}
|
||||||
selectRect = null
|
selectRect = null
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.compose
|
package space.kscience.maps.compose
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.GestureCancellationException
|
import androidx.compose.foundation.gestures.GestureCancellationException
|
||||||
import androidx.compose.foundation.gestures.PressGestureScope
|
import androidx.compose.foundation.gestures.PressGestureScope
|
||||||
@ -26,32 +26,9 @@ public val PointerEvent.position: Offset get() = firstChange.position
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects tap, double-tap, and long press gestures and calls [onClick], [onDoubleClick], and
|
* An alternative to [detectTapGestures] with reimplementation of internal logic
|
||||||
* [onLongClick], respectively, when detected. [onPress] is called when the press is detected
|
|
||||||
* and the [PressGestureScope.tryAwaitRelease] and [PressGestureScope.awaitRelease] can be
|
|
||||||
* used to detect when pointers have released or the gesture was canceled.
|
|
||||||
* The first pointer down and final pointer up are consumed, and in the
|
|
||||||
* case of long press, all changes after the long press is detected are consumed.
|
|
||||||
*
|
|
||||||
* Each function parameter receives an [Offset] representing the position relative to the containing
|
|
||||||
* element. The [Offset] can be outside the actual bounds of the element itself meaning the numbers
|
|
||||||
* can be negative or larger than the element bounds if the touch target is smaller than the
|
|
||||||
* [ViewConfiguration.minimumTouchTargetSize].
|
|
||||||
*
|
|
||||||
* When [onDoubleClick] is provided, the tap gesture is detected only after
|
|
||||||
* the [ViewConfiguration.doubleTapMinTimeMillis] has passed and [onDoubleClick] is called if the
|
|
||||||
* second tap is started before [ViewConfiguration.doubleTapTimeoutMillis]. If [onDoubleClick] is not
|
|
||||||
* provided, then [onClick] is called when the pointer up has been received.
|
|
||||||
*
|
|
||||||
* After the initial [onPress], if the pointer moves out of the input area, the position change
|
|
||||||
* is consumed, or another gesture consumes the down or up events, the gestures are considered
|
|
||||||
* canceled. That means [onDoubleClick], [onLongClick], and [onClick] will not be called after a
|
|
||||||
* gesture has been canceled.
|
|
||||||
*
|
|
||||||
* If the first down event is consumed somewhere else, the entire gesture will be skipped,
|
|
||||||
* including [onPress].
|
|
||||||
*/
|
*/
|
||||||
public suspend fun PointerInputScope.detectClicks(
|
internal suspend fun PointerInputScope.detectClicks(
|
||||||
onDoubleClick: (Density.(PointerEvent) -> Unit)? = null,
|
onDoubleClick: (Density.(PointerEvent) -> Unit)? = null,
|
||||||
onLongClick: (Density.(PointerEvent) -> Unit)? = null,
|
onLongClick: (Density.(PointerEvent) -> Unit)? = null,
|
||||||
onPress: suspend PressGestureScope.(event: PointerEvent) -> Unit = NoPressGesture,
|
onPress: suspend PressGestureScope.(event: PointerEvent) -> Unit = NoPressGesture,
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.features
|
package space.kscience.maps.features
|
||||||
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@ -7,10 +7,12 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
|
|
||||||
public abstract class CoordinateViewScope<T : Any>(
|
/**
|
||||||
public val config: ViewConfig<T>,
|
* A state holder for current canvas size and view point. Allows transformation from coordinates to pixels and back
|
||||||
|
*/
|
||||||
|
public abstract class CanvasState<T: Any>(
|
||||||
|
public val viewConfig: ViewConfig<T>
|
||||||
){
|
){
|
||||||
|
|
||||||
public abstract val space: CoordinateSpace<T>
|
public abstract val space: CoordinateSpace<T>
|
||||||
|
|
||||||
private var canvasSizeState: MutableState<DpSize?> = mutableStateOf(null)
|
private var canvasSizeState: MutableState<DpSize?> = mutableStateOf(null)
|
||||||
@ -20,13 +22,14 @@ public abstract class CoordinateViewScope<T : Any>(
|
|||||||
get() = canvasSizeState.value ?: DpSize(512.dp, 512.dp)
|
get() = canvasSizeState.value ?: DpSize(512.dp, 512.dp)
|
||||||
set(value) {
|
set(value) {
|
||||||
canvasSizeState.value = value
|
canvasSizeState.value = value
|
||||||
|
viewConfig.onCanvasSizeChange(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var viewPoint: ViewPoint<T>
|
public var viewPoint: ViewPoint<T>
|
||||||
get() = viewPointState.value ?: space.defaultViewPoint
|
get() = viewPointState.value ?: space.defaultViewPoint
|
||||||
set(value) {
|
set(value) {
|
||||||
viewPointState.value = value
|
viewPointState.value = value
|
||||||
config.onViewChange(viewPoint)
|
viewConfig.onViewChange(viewPoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
public val zoom: Float get() = viewPoint.zoom
|
public val zoom: Float get() = viewPoint.zoom
|
||||||
@ -35,28 +38,28 @@ public abstract class CoordinateViewScope<T : Any>(
|
|||||||
// Selection rectangle. If null - no selection
|
// Selection rectangle. If null - no selection
|
||||||
public var selectRect: DpRect? by mutableStateOf(null)
|
public var selectRect: DpRect? by mutableStateOf(null)
|
||||||
|
|
||||||
public abstract fun DpOffset.toCoordinates(): T
|
|
||||||
|
|
||||||
|
|
||||||
public abstract fun T.toDpOffset(): DpOffset
|
|
||||||
|
|
||||||
public fun T.toOffset(density: Density): Offset = with(density) {
|
|
||||||
val dpOffset = this@toOffset.toDpOffset()
|
|
||||||
Offset(dpOffset.x.toPx(), dpOffset.y.toPx())
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun Offset.toCoordinates(density: Density): T = with(density) {
|
|
||||||
val dpOffset = DpOffset(x.toDp(), y.toDp())
|
|
||||||
dpOffset.toCoordinates()
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract fun Rectangle<T>.toDpRect(): DpRect
|
public abstract fun Rectangle<T>.toDpRect(): DpRect
|
||||||
|
|
||||||
public abstract fun ViewPoint<T>.moveBy(x: Dp, y: Dp): ViewPoint<T>
|
public abstract fun ViewPoint<T>.moveBy(x: Dp, y: Dp): ViewPoint<T>
|
||||||
|
|
||||||
public abstract fun computeViewPoint(rectangle: Rectangle<T>): ViewPoint<T>
|
public abstract fun computeViewPoint(rectangle: Rectangle<T>): ViewPoint<T>
|
||||||
|
|
||||||
|
public abstract fun DpOffset.toCoordinates(): T
|
||||||
|
|
||||||
|
|
||||||
|
public abstract fun T.toDpOffset(): DpOffset
|
||||||
|
|
||||||
|
public fun toCoordinates(offset: Offset, density: Density): T = with(density){
|
||||||
|
val dpOffset = DpOffset(offset.x.toDp(), offset.y.toDp())
|
||||||
|
dpOffset.toCoordinates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun toOffset(coordinates: T, density: Density): Offset = with(density){
|
||||||
|
val dpOffset = coordinates.toDpOffset()
|
||||||
|
return Offset(dpOffset.x.toPx(), dpOffset.y.toPx())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public val DpRect.topLeft: DpOffset get() = DpOffset(left, top)
|
public val DpRect.topLeft: DpOffset get() = DpOffset(left, top)
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.features
|
package space.kscience.maps.features
|
||||||
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.features
|
package space.kscience.maps.features
|
||||||
|
|
||||||
import androidx.compose.ui.input.pointer.PointerEvent
|
import androidx.compose.ui.input.pointer.PointerEvent
|
||||||
import androidx.compose.ui.input.pointer.isPrimaryPressed
|
import androidx.compose.ui.input.pointer.isPrimaryPressed
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.features
|
package space.kscience.maps.features
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
@ -17,8 +17,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.attributes.Attributes
|
import org.jetbrains.skia.Font
|
||||||
import center.sciprog.attributes.NameAttribute
|
import space.kscience.NameAttribute
|
||||||
|
import space.kscience.attributes.Attributes
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
import space.kscience.kmath.nd.Structure2D
|
import space.kscience.kmath.nd.Structure2D
|
||||||
|
|
||||||
@ -331,7 +332,7 @@ public data class TextFeature<T : Any>(
|
|||||||
public val position: T,
|
public val position: T,
|
||||||
public val text: String,
|
public val text: String,
|
||||||
override val attributes: Attributes = Attributes.EMPTY,
|
override val attributes: Attributes = Attributes.EMPTY,
|
||||||
public val fontConfig: FeatureFont.() -> Unit,
|
public val fontConfig: Font.() -> Unit,
|
||||||
) : DraggableFeature<T> {
|
) : DraggableFeature<T> {
|
||||||
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position)
|
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position)
|
||||||
|
|
@ -0,0 +1,113 @@
|
|||||||
|
package space.kscience.maps.features
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScopeMarker
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.text.TextMeasurer
|
||||||
|
import androidx.compose.ui.text.drawText
|
||||||
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
|
import androidx.compose.ui.unit.DpRect
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import space.kscience.attributes.Attributes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extension of [DrawScope] to include map-specific features
|
||||||
|
*/
|
||||||
|
@DrawScopeMarker
|
||||||
|
public abstract class FeatureDrawScope<T : Any>(
|
||||||
|
public val state: CanvasState<T>,
|
||||||
|
) : DrawScope {
|
||||||
|
public fun Offset.toCoordinates(): T = with(state) {
|
||||||
|
toCoordinates(this@toCoordinates, this@FeatureDrawScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
public open fun T.toOffset(): Offset = with(state) {
|
||||||
|
toOffset(this@toOffset, this@FeatureDrawScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun Rectangle<T>.toDpRect(): DpRect = with(state) { toDpRect() }
|
||||||
|
|
||||||
|
public abstract fun painterFor(feature: PainterFeature<T>): Painter
|
||||||
|
|
||||||
|
public abstract fun drawText(text: String, position: Offset, attributes: Attributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of FeatureDrawScope to be used in Compose (both schemes and Maps)
|
||||||
|
*/
|
||||||
|
@DrawScopeMarker
|
||||||
|
public class ComposeFeatureDrawScope<T : Any>(
|
||||||
|
drawScope: DrawScope,
|
||||||
|
state: CanvasState<T>,
|
||||||
|
private val painterCache: Map<PainterFeature<T>, Painter>,
|
||||||
|
private val textMeasurer: TextMeasurer?,
|
||||||
|
) : FeatureDrawScope<T>(state), DrawScope by drawScope {
|
||||||
|
override fun drawText(text: String, position: Offset, attributes: Attributes) {
|
||||||
|
try {
|
||||||
|
drawText(textMeasurer ?: error("Text measurer not defined"), text, position)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
logger.error(ex) { "Failed to measure text" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun painterFor(feature: PainterFeature<T>): Painter =
|
||||||
|
painterCache[feature] ?: error("Can't resolve painter for $feature")
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
private val logger = KotlinLogging.logger("ComposeFeatureDrawScope")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a canvas with extended functionality (e.g., drawing text)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
public fun <T : Any> FeatureCanvas(
|
||||||
|
state: CanvasState<T>,
|
||||||
|
features: FeatureGroup<T>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
draw: FeatureDrawScope<T>.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
val textMeasurer = rememberTextMeasurer(0)
|
||||||
|
|
||||||
|
val painterCache: Map<PainterFeature<T>, Painter> = features.features.flatMap {
|
||||||
|
if (it is FeatureGroup) it.features else listOf(it)
|
||||||
|
}.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
|
||||||
|
|
||||||
|
Canvas(modifier) {
|
||||||
|
if (state.canvasSize != size.toDpSize()) {
|
||||||
|
state.canvasSize = size.toDpSize()
|
||||||
|
}
|
||||||
|
ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply {
|
||||||
|
clipRect {
|
||||||
|
features.featureMap.values.sortedBy { it.z }
|
||||||
|
.filter { state.viewPoint.zoom in it.zoomRange }
|
||||||
|
.forEach { feature ->
|
||||||
|
this@apply.drawFeature(feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.selectRect?.let { dpRect ->
|
||||||
|
val rect = dpRect.toRect()
|
||||||
|
drawRect(
|
||||||
|
color = Color.Blue,
|
||||||
|
topLeft = rect.topLeft,
|
||||||
|
size = rect.size,
|
||||||
|
alpha = 0.5f,
|
||||||
|
style = Stroke(
|
||||||
|
width = 2f,
|
||||||
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
package center.sciprog.maps.features
|
package space.kscience.maps.features
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
@ -11,7 +9,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.attributes.*
|
import org.jetbrains.skia.Font
|
||||||
|
import space.kscience.attributes.Attribute
|
||||||
|
import space.kscience.attributes.Attributes
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
import space.kscience.kmath.nd.*
|
import space.kscience.kmath.nd.*
|
||||||
import space.kscience.kmath.structures.Buffer
|
import space.kscience.kmath.structures.Buffer
|
||||||
@ -33,8 +33,12 @@ public val <T : Any, F : Feature<T>> FeatureRef<T, F>.attributes: Attributes get
|
|||||||
public data class FeatureGroup<T : Any>(
|
public data class FeatureGroup<T : Any>(
|
||||||
override val space: CoordinateSpace<T>,
|
override val space: CoordinateSpace<T>,
|
||||||
public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(),
|
public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(),
|
||||||
override val attributes: Attributes = Attributes.EMPTY,
|
|
||||||
) : CoordinateSpace<T> by space, Feature<T> {
|
) : CoordinateSpace<T> by space, Feature<T> {
|
||||||
|
|
||||||
|
private val attributesState: MutableState<Attributes> = mutableStateOf(Attributes.EMPTY)
|
||||||
|
|
||||||
|
override val attributes: Attributes get() = attributesState.value
|
||||||
|
|
||||||
//
|
//
|
||||||
// @Suppress("UNCHECKED_CAST")
|
// @Suppress("UNCHECKED_CAST")
|
||||||
// public operator fun <F : Feature<T>> get(id: FeatureId<F>): F =
|
// public operator fun <F : Feature<T>> get(id: FeatureId<F>): F =
|
||||||
@ -62,25 +66,7 @@ public data class FeatureGroup<T : Any>(
|
|||||||
|
|
||||||
public val features: Collection<Feature<T>> get() = featureMap.values.sortedByDescending { it.z }
|
public val features: Collection<Feature<T>> get() = featureMap.values.sortedByDescending { it.z }
|
||||||
|
|
||||||
public fun visit(visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Unit) {
|
|
||||||
featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
|
|
||||||
if (feature is FeatureGroup<T>) {
|
|
||||||
feature.visit(visitor)
|
|
||||||
} else {
|
|
||||||
visitor(this, key, feature)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun visitUntil(visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Boolean) {
|
|
||||||
featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
|
|
||||||
if (feature is FeatureGroup<T>) {
|
|
||||||
feature.visitUntil(visitor)
|
|
||||||
} else {
|
|
||||||
if (!visitor(this, key, feature)) return@visitUntil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
//
|
||||||
// @Suppress("UNCHECKED_CAST")
|
// @Suppress("UNCHECKED_CAST")
|
||||||
// public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
|
// public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
|
||||||
@ -91,7 +77,10 @@ public data class FeatureGroup<T : Any>(
|
|||||||
featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
|
featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes))
|
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> {
|
||||||
|
attributesState.value = attributes.modify()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
@ -118,6 +107,29 @@ public data class FeatureGroup<T : Any>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively search for feature until function returns true
|
||||||
|
*/
|
||||||
|
public fun <T : Any> FeatureGroup<T>.forEachUntil(visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Boolean) {
|
||||||
|
featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
|
||||||
|
if (feature is FeatureGroup<T>) {
|
||||||
|
feature.forEachUntil(visitor)
|
||||||
|
} else {
|
||||||
|
if (!visitor(this, key, feature)) return@forEachUntil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively visit all features in this group
|
||||||
|
*/
|
||||||
|
public fun <T : Any> FeatureGroup<T>.forEach(
|
||||||
|
visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Unit,
|
||||||
|
): Unit = forEachUntil { id, feature ->
|
||||||
|
visitor(id, feature)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process all features with a given attribute from the one with highest [z] to lowest
|
* Process all features with a given attribute from the one with highest [z] to lowest
|
||||||
*/
|
*/
|
||||||
@ -125,7 +137,7 @@ public fun <T : Any, A> FeatureGroup<T>.forEachWithAttribute(
|
|||||||
key: Attribute<A>,
|
key: Attribute<A>,
|
||||||
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Unit,
|
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Unit,
|
||||||
) {
|
) {
|
||||||
visit { id, feature ->
|
forEach { id, feature ->
|
||||||
feature.attributes[key]?.let {
|
feature.attributes[key]?.let {
|
||||||
block(id, feature, it)
|
block(id, feature, it)
|
||||||
}
|
}
|
||||||
@ -136,7 +148,7 @@ public fun <T : Any, A> FeatureGroup<T>.forEachWithAttributeUntil(
|
|||||||
key: Attribute<A>,
|
key: Attribute<A>,
|
||||||
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Boolean,
|
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Boolean,
|
||||||
) {
|
) {
|
||||||
visitUntil { id, feature ->
|
forEachUntil { id, feature ->
|
||||||
feature.attributes[key]?.let {
|
feature.attributes[key]?.let {
|
||||||
block(id, feature, it)
|
block(id, feature, it)
|
||||||
} ?: true
|
} ?: true
|
||||||
@ -146,7 +158,7 @@ public fun <T : Any, A> FeatureGroup<T>.forEachWithAttributeUntil(
|
|||||||
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithType(
|
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithType(
|
||||||
crossinline block: (FeatureRef<T, F>) -> Unit,
|
crossinline block: (FeatureRef<T, F>) -> Unit,
|
||||||
) {
|
) {
|
||||||
visit { id, feature ->
|
forEach { id, feature ->
|
||||||
if (feature is F) block(FeatureRef(id, this))
|
if (feature is F) block(FeatureRef(id, this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,7 +166,7 @@ public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithT
|
|||||||
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithTypeUntil(
|
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithTypeUntil(
|
||||||
crossinline block: (FeatureRef<T, F>) -> Boolean,
|
crossinline block: (FeatureRef<T, F>) -> Boolean,
|
||||||
) {
|
) {
|
||||||
visitUntil { id, feature ->
|
forEachUntil { id, feature ->
|
||||||
if (feature is F) block(FeatureRef(id, this)) else true
|
if (feature is F) block(FeatureRef(id, this)) else true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,8 +253,7 @@ public fun <T : Any> FeatureGroup<T>.icon(
|
|||||||
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
|
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
|
||||||
attributes: Attributes = Attributes.EMPTY,
|
attributes: Attributes = Attributes.EMPTY,
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
): FeatureRef<T, VectorIconFeature<T>> =
|
): FeatureRef<T, VectorIconFeature<T>> = feature(
|
||||||
feature(
|
|
||||||
id,
|
id,
|
||||||
VectorIconFeature(
|
VectorIconFeature(
|
||||||
space,
|
space,
|
||||||
@ -254,12 +265,11 @@ public fun <T : Any> FeatureGroup<T>.icon(
|
|||||||
)
|
)
|
||||||
|
|
||||||
public fun <T : Any> FeatureGroup<T>.group(
|
public fun <T : Any> FeatureGroup<T>.group(
|
||||||
attributes: Attributes = Attributes.EMPTY,
|
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
builder: FeatureGroup<T>.() -> Unit,
|
builder: FeatureGroup<T>.() -> Unit,
|
||||||
): FeatureRef<T, FeatureGroup<T>> {
|
): FeatureRef<T, FeatureGroup<T>> {
|
||||||
val collection = FeatureGroup(space).apply(builder)
|
val collection = FeatureGroup(space).apply(builder)
|
||||||
val feature = FeatureGroup(space, collection.featureMap, attributes)
|
val feature = FeatureGroup(space, collection.featureMap)
|
||||||
return feature(id, feature)
|
return feature(id, feature)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,7 +286,7 @@ public fun <T : Any> FeatureGroup<T>.scalableImage(
|
|||||||
public fun <T : Any> FeatureGroup<T>.text(
|
public fun <T : Any> FeatureGroup<T>.text(
|
||||||
position: T,
|
position: T,
|
||||||
text: String,
|
text: String,
|
||||||
font: FeatureFont.() -> Unit = { size = 16f },
|
font: Font.() -> Unit = { size = 16f },
|
||||||
attributes: Attributes = Attributes.EMPTY,
|
attributes: Attributes = Attributes.EMPTY,
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
): FeatureRef<T, TextFeature<T>> = feature(
|
): FeatureRef<T, TextFeature<T>> = feature(
|
||||||
@ -284,14 +294,14 @@ public fun <T : Any> FeatureGroup<T>.text(
|
|||||||
TextFeature(space, position, text, fontConfig = font, attributes = attributes)
|
TextFeature(space, position, text, fontConfig = font, attributes = attributes)
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun <T> StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND<T> {
|
//public fun <T> StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND<T> {
|
||||||
val strides = Strides(shape)
|
// val strides = Strides(shape)
|
||||||
return BufferND(strides, Buffer.boxing(strides.linearSize) { initializer(strides.index(it)) })
|
// return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) })
|
||||||
}
|
//}
|
||||||
|
|
||||||
public fun <T> Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D<T> {
|
public inline fun <reified T> Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D<T> {
|
||||||
val strides = Strides(ShapeND(rows, columns))
|
val strides = Strides(ShapeND(rows, columns))
|
||||||
return BufferND(strides, Buffer.boxing(strides.linearSize) { initializer(strides.index(it)) }).as2D()
|
return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }).as2D()
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <T : Any> FeatureGroup<T>.pixelMap(
|
public fun <T : Any> FeatureGroup<T>.pixelMap(
|
||||||
@ -303,3 +313,22 @@ public fun <T : Any> FeatureGroup<T>.pixelMap(
|
|||||||
id,
|
id,
|
||||||
PixelMapFeature(space, rectangle, pixelMap, attributes = attributes)
|
PixelMapFeature(space, rectangle, pixelMap, attributes = attributes)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a pretty tree-like representation of this feature group
|
||||||
|
*/
|
||||||
|
public fun FeatureGroup<*>.toPrettyString(): String {
|
||||||
|
fun StringBuilder.printGroup(id: String, group: FeatureGroup<*>, prefix: String) {
|
||||||
|
appendLine("${prefix}* [group] $id")
|
||||||
|
group.featureMap.forEach { (id, feature) ->
|
||||||
|
if (feature is FeatureGroup<*>) {
|
||||||
|
printGroup(id, feature, " ")
|
||||||
|
} else {
|
||||||
|
appendLine("$prefix * [${feature::class.simpleName}] $id ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buildString {
|
||||||
|
printGroup("root", this@toPrettyString, "")
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.features
|
package space.kscience.maps.features
|
||||||
|
|
||||||
import androidx.compose.ui.input.pointer.PointerEvent
|
import androidx.compose.ui.input.pointer.PointerEvent
|
||||||
import androidx.compose.ui.input.pointer.isPrimaryPressed
|
import androidx.compose.ui.input.pointer.isPrimaryPressed
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.features
|
package space.kscience.maps.features
|
||||||
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.features
|
package space.kscience.maps.features
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param T type of coordinates used for the view point
|
* @param T type of coordinates used for the view point
|
@ -1,6 +1,6 @@
|
|||||||
package center.sciprog.maps.features
|
package space.kscience.maps.features
|
||||||
|
|
||||||
import center.sciprog.attributes.Attributes
|
import space.kscience.attributes.Attributes
|
||||||
import kotlin.jvm.JvmName
|
import kotlin.jvm.JvmName
|
||||||
|
|
||||||
|
|
||||||
@ -18,9 +18,9 @@ public fun <T : Any> FeatureGroup<T>.draggableLine(
|
|||||||
space,
|
space,
|
||||||
aId.resolve().center,
|
aId.resolve().center,
|
||||||
bId.resolve().center,
|
bId.resolve().center,
|
||||||
Attributes {
|
Attributes<FeatureGroup<T>> {
|
||||||
ZAttribute(-10f)
|
ZAttribute(-10f)
|
||||||
lineId?.attributes?.let { from(it) }
|
lineId?.attributes?.let { putAll(it) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -51,9 +51,9 @@ public fun <T : Any> FeatureGroup<T>.draggableMultiLine(
|
|||||||
MultiLineFeature(
|
MultiLineFeature(
|
||||||
space,
|
space,
|
||||||
points.map { it.resolve().center },
|
points.map { it.resolve().center },
|
||||||
Attributes {
|
Attributes<FeatureGroup<T>>{
|
||||||
ZAttribute(-10f)
|
ZAttribute(-10f)
|
||||||
polygonId?.attributes?.let { from(it) }
|
polygonId?.attributes?.let { putAll(it) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
@ -1,37 +1,38 @@
|
|||||||
package center.sciprog.maps.features
|
package space.kscience.maps.features
|
||||||
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.*
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.Path
|
||||||
import center.sciprog.attributes.plus
|
import androidx.compose.ui.graphics.PointMode
|
||||||
import org.jetbrains.skia.Font
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import org.jetbrains.skia.Paint
|
import androidx.compose.ui.graphics.drawscope.translate
|
||||||
|
import space.kscience.attributes.plus
|
||||||
import space.kscience.kmath.PerformancePitfall
|
import space.kscience.kmath.PerformancePitfall
|
||||||
import space.kscience.kmath.geometry.degrees
|
|
||||||
|
|
||||||
|
|
||||||
internal fun Color.toPaint(): Paint = Paint().apply {
|
//internal fun Color.toPaint(): Paint = Paint().apply {
|
||||||
isAntiAlias = true
|
// isAntiAlias = true
|
||||||
color = toArgb()
|
// color = toArgb()
|
||||||
}
|
//}
|
||||||
|
|
||||||
public fun <T : Any> DrawScope.drawFeature(
|
|
||||||
state: CoordinateViewScope<T>,
|
public fun <T : Any> FeatureDrawScope<T>.drawFeature(
|
||||||
painterCache: Map<PainterFeature<T>, Painter>,
|
|
||||||
feature: Feature<T>,
|
feature: Feature<T>,
|
||||||
): Unit = with(state) {
|
): Unit {
|
||||||
val color = feature.color ?: Color.Red
|
val color = feature.color ?: Color.Red
|
||||||
val alpha = feature.attributes[AlphaAttribute] ?: 1f
|
val alpha = feature.attributes[AlphaAttribute] ?: 1f
|
||||||
fun T.toOffset(): Offset = toOffset(this@drawFeature)
|
//avoid drawing invisible features
|
||||||
|
if(feature.attributes[VisibleAttribute] == false) return
|
||||||
|
|
||||||
when (feature) {
|
when (feature) {
|
||||||
is FeatureSelector -> drawFeature(state, painterCache, feature.selector(state.zoom))
|
is FeatureSelector -> drawFeature(feature.selector(state.zoom))
|
||||||
is CircleFeature -> drawCircle(
|
is CircleFeature -> drawCircle(
|
||||||
color,
|
color,
|
||||||
feature.radius.toPx(),
|
feature.radius.toPx(),
|
||||||
center = feature.center.toOffset()
|
center = feature.center.toOffset(),
|
||||||
|
alpha = alpha
|
||||||
)
|
)
|
||||||
|
|
||||||
is RectangleFeature -> drawRect(
|
is RectangleFeature -> drawRect(
|
||||||
@ -40,7 +41,8 @@ public fun <T : Any> DrawScope.drawFeature(
|
|||||||
feature.size.width.toPx() / 2,
|
feature.size.width.toPx() / 2,
|
||||||
feature.size.height.toPx() / 2
|
feature.size.height.toPx() / 2
|
||||||
),
|
),
|
||||||
size = feature.size.toSize()
|
size = feature.size.toSize(),
|
||||||
|
alpha = alpha
|
||||||
)
|
)
|
||||||
|
|
||||||
is LineFeature -> drawLine(
|
is LineFeature -> drawLine(
|
||||||
@ -48,7 +50,8 @@ public fun <T : Any> DrawScope.drawFeature(
|
|||||||
feature.a.toOffset(),
|
feature.a.toOffset(),
|
||||||
feature.b.toOffset(),
|
feature.b.toOffset(),
|
||||||
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
||||||
pathEffect = feature.attributes[PathEffectAttribute]
|
pathEffect = feature.attributes[PathEffectAttribute],
|
||||||
|
alpha = alpha
|
||||||
)
|
)
|
||||||
|
|
||||||
is ArcFeature -> {
|
is ArcFeature -> {
|
||||||
@ -58,8 +61,8 @@ public fun <T : Any> DrawScope.drawFeature(
|
|||||||
|
|
||||||
drawArc(
|
drawArc(
|
||||||
color = color,
|
color = color,
|
||||||
startAngle = (feature.startAngle.degrees).toFloat(),
|
startAngle = (feature.startAngle.toDegrees().value).toFloat(),
|
||||||
sweepAngle = (feature.arcLength.degrees).toFloat(),
|
sweepAngle = (feature.arcLength.toDegrees().value).toFloat(),
|
||||||
useCenter = false,
|
useCenter = false,
|
||||||
topLeft = dpRect.topLeft,
|
topLeft = dpRect.topLeft,
|
||||||
size = size,
|
size = size,
|
||||||
@ -69,28 +72,19 @@ public fun <T : Any> DrawScope.drawFeature(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset())
|
is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset(), alpha = alpha)
|
||||||
|
|
||||||
is VectorIconFeature -> {
|
is VectorIconFeature -> {
|
||||||
val offset = feature.center.toOffset()
|
val offset = feature.center.toOffset()
|
||||||
val size = feature.size.toSize()
|
val size = feature.size.toSize()
|
||||||
translate(offset.x - size.width / 2, offset.y - size.height / 2) {
|
translate(offset.x - size.width / 2, offset.y - size.height / 2) {
|
||||||
with(painterCache[feature]!!) {
|
with(this@drawFeature.painterFor(feature)) {
|
||||||
draw(size)
|
draw(size, colorFilter = feature.color?.let { ColorFilter.tint(it) }, alpha = alpha)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is TextFeature -> drawIntoCanvas { canvas ->
|
is TextFeature -> drawText(feature.text, feature.position.toOffset(), feature.attributes)
|
||||||
val offset = feature.position.toOffset()
|
|
||||||
canvas.nativeCanvas.drawString(
|
|
||||||
feature.text,
|
|
||||||
offset.x + 5,
|
|
||||||
offset.y - 5,
|
|
||||||
Font().apply(feature.fontConfig),
|
|
||||||
(feature.color ?: Color.Black).toPaint()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is DrawFeature -> {
|
is DrawFeature -> {
|
||||||
val offset = feature.position.toOffset()
|
val offset = feature.position.toOffset()
|
||||||
@ -101,9 +95,11 @@ public fun <T : Any> DrawScope.drawFeature(
|
|||||||
|
|
||||||
is FeatureGroup -> {
|
is FeatureGroup -> {
|
||||||
feature.featureMap.values.forEach {
|
feature.featureMap.values.forEach {
|
||||||
drawFeature(state, painterCache, it.withAttributes {
|
drawFeature(
|
||||||
|
it.withAttributes {
|
||||||
feature.attributes + this
|
feature.attributes + this
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +117,7 @@ public fun <T : Any> DrawScope.drawFeature(
|
|||||||
drawPoints(
|
drawPoints(
|
||||||
points = points,
|
points = points,
|
||||||
color = color,
|
color = color,
|
||||||
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
strokeWidth = feature.attributes[StrokeAttribute] ?: 5f,
|
||||||
pointMode = PointMode.Points,
|
pointMode = PointMode.Points,
|
||||||
pathEffect = feature.attributes[PathEffectAttribute],
|
pathEffect = feature.attributes[PathEffectAttribute],
|
||||||
alpha = alpha
|
alpha = alpha
|
||||||
@ -160,8 +156,8 @@ public fun <T : Any> DrawScope.drawFeature(
|
|||||||
val offset = rect.topLeft
|
val offset = rect.topLeft
|
||||||
|
|
||||||
translate(offset.x, offset.y) {
|
translate(offset.x, offset.y) {
|
||||||
with(painterCache[feature]!!) {
|
with(this@drawFeature.painterFor(feature)) {
|
||||||
draw(rect.size)
|
draw(rect.size, alpha = alpha)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,7 +180,8 @@ public fun <T : Any> DrawScope.drawFeature(
|
|||||||
x = i * xStep,
|
x = i * xStep,
|
||||||
y = rect.height - j * yStep
|
y = rect.height - j * yStep
|
||||||
),
|
),
|
||||||
size = pixelSize
|
size = pixelSize,
|
||||||
|
alpha = alpha
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.features
|
package space.kscience.maps.features
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.PointerMatcher
|
import androidx.compose.foundation.PointerMatcher
|
||||||
@ -6,13 +6,13 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.PathEffect
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
import androidx.compose.ui.input.pointer.PointerEvent
|
import androidx.compose.ui.input.pointer.PointerEvent
|
||||||
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
|
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
|
||||||
import center.sciprog.attributes.Attribute
|
import space.kscience.attributes.*
|
||||||
import center.sciprog.attributes.AttributesBuilder
|
|
||||||
import center.sciprog.attributes.SetAttribute
|
|
||||||
import center.sciprog.attributes.withAttribute
|
|
||||||
|
|
||||||
public object ZAttribute : Attribute<Float>
|
public object ZAttribute : Attribute<Float>
|
||||||
|
|
||||||
|
public val Feature<*>.z: Float
|
||||||
|
get() = attributes[ZAttribute] ?: 0f
|
||||||
|
|
||||||
public object DraggableAttribute : Attribute<DragHandle<Any>>
|
public object DraggableAttribute : Attribute<DragHandle<Any>>
|
||||||
|
|
||||||
public object DragListenerAttribute : SetAttribute<DragListener<Any>>
|
public object DragListenerAttribute : SetAttribute<DragListener<Any>>
|
||||||
@ -46,20 +46,25 @@ public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.zoomRange(range: FloatRang
|
|||||||
|
|
||||||
public object AlphaAttribute : Attribute<Float>
|
public object AlphaAttribute : Attribute<Float>
|
||||||
|
|
||||||
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes(modify: AttributesBuilder.() -> Unit): FeatureRef<T, F> {
|
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.alpha(alpha: Float): FeatureRef<T, F> {
|
||||||
|
require(alpha in 0f..1f) { "Alpha value must be between 0 and 1" }
|
||||||
|
return modifyAttribute(AlphaAttribute, alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes(
|
||||||
|
modification: AttributesBuilder<F>.() -> Unit,
|
||||||
|
): FeatureRef<T, F> {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
parent.feature(
|
parent.feature(
|
||||||
id,
|
id,
|
||||||
resolve().withAttributes {
|
resolve().withAttributes { modified(modification) } as F
|
||||||
AttributesBuilder(this).apply(modify).build()
|
|
||||||
} as F
|
|
||||||
)
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(
|
public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(
|
||||||
key: Attribute<V>,
|
key: Attribute<V>,
|
||||||
value: V?,
|
value: V,
|
||||||
): FeatureRef<T, F> {
|
): FeatureRef<T, F> {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F)
|
parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F)
|
||||||
@ -170,3 +175,6 @@ public object StrokeAttribute : Attribute<Float>
|
|||||||
|
|
||||||
public fun <T : Any, F : LineSegmentFeature<T>> FeatureRef<T, F>.stroke(width: Float): FeatureRef<T, F> =
|
public fun <T : Any, F : LineSegmentFeature<T>> FeatureRef<T, F>.stroke(width: Float): FeatureRef<T, F> =
|
||||||
modifyAttribute(StrokeAttribute, width)
|
modifyAttribute(StrokeAttribute, width)
|
||||||
|
|
||||||
|
public fun <T : Any, F : PointsFeature<T>> FeatureRef<T, F>.pointSize(width: Float): FeatureRef<T, F> =
|
||||||
|
modifyAttribute(StrokeAttribute, width)
|
@ -1,13 +1,21 @@
|
|||||||
package center.sciprog.attributes
|
package center.sciprog.attributes
|
||||||
|
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import kotlinx.serialization.modules.contextual
|
import kotlinx.serialization.modules.contextual
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import kotlinx.serialization.serializer
|
||||||
|
import space.kscience.AttributesSerializer
|
||||||
|
import space.kscience.NameAttribute
|
||||||
|
import space.kscience.SerializableAttribute
|
||||||
|
import space.kscience.attributes.Attributes
|
||||||
import kotlin.test.Ignore
|
import kotlin.test.Ignore
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
internal class AttributesSerializationTest {
|
internal class AttributesSerializationTest {
|
||||||
|
|
||||||
@ -28,32 +36,36 @@ internal class AttributesSerializationTest {
|
|||||||
override fun toString(): String = "test"
|
override fun toString(): String = "test"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val serializer = AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute))
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun restoreFromJson() {
|
fun restoreFromJson() {
|
||||||
|
|
||||||
val json = Json {
|
val json = Json {
|
||||||
serializersModule = SerializersModule {
|
serializersModule = SerializersModule {
|
||||||
contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute)))
|
contextual(serializer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val attributes = Attributes {
|
val attributes = Attributes<Any> {
|
||||||
NameAttribute("myTest")
|
NameAttribute("myTest")
|
||||||
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
|
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
|
||||||
ContainerAttribute(
|
ContainerAttribute(
|
||||||
Container(
|
Container(
|
||||||
Attributes {
|
Attributes<Any> {
|
||||||
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
|
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val serialized: String = json.encodeToString(attributes)
|
|
||||||
|
val serialized: String = json.encodeToString(serializer, attributes)
|
||||||
println(serialized)
|
println(serialized)
|
||||||
|
|
||||||
val restored: Attributes = json.decodeFromString(serialized)
|
val restored: Attributes = json.decodeFromString(serializer, serialized)
|
||||||
|
|
||||||
assertEquals(attributes, restored)
|
assertTrue { Attributes.equals(attributes, restored) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@ -62,25 +74,25 @@ internal class AttributesSerializationTest {
|
|||||||
fun restoreFromProtoBuf() {
|
fun restoreFromProtoBuf() {
|
||||||
val protoBuf = ProtoBuf {
|
val protoBuf = ProtoBuf {
|
||||||
serializersModule = SerializersModule {
|
serializersModule = SerializersModule {
|
||||||
contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute)))
|
contextual(serializer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val attributes = Attributes {
|
val attributes = Attributes<Any> {
|
||||||
NameAttribute("myTest")
|
NameAttribute("myTest")
|
||||||
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
|
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
|
||||||
ContainerAttribute(
|
ContainerAttribute(
|
||||||
Container(
|
Container(
|
||||||
Attributes {
|
Attributes<Any> {
|
||||||
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
|
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val serialized = protoBuf.encodeToByteArray(attributes)
|
val serialized = protoBuf.encodeToByteArray(serializer, attributes)
|
||||||
|
|
||||||
val restored: Attributes = protoBuf.decodeFromByteArray(serialized)
|
val restored: Attributes = protoBuf.decodeFromByteArray(serializer, serialized)
|
||||||
|
|
||||||
assertEquals(attributes, restored)
|
assertEquals(attributes, restored)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
package center.sciprog.maps.features
|
|
||||||
|
|
||||||
public actual class FeatureFont {
|
|
||||||
public actual var size: Float = 16f
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
package center.sciprog.maps.features
|
|
||||||
|
|
||||||
import org.jetbrains.skia.Font
|
|
||||||
|
|
||||||
public actual typealias FeatureFont = Font
|
|
@ -6,19 +6,8 @@
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `center.sciprog:maps-kt-geojson:0.2.2`.
|
The Maven coordinates of this project are `space.kscience:maps-kt-geojson:0.3.0`.
|
||||||
|
|
||||||
**Gradle Groovy:**
|
|
||||||
```groovy
|
|
||||||
repositories {
|
|
||||||
maven { url 'https://repo.kotlin.link' }
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'center.sciprog:maps-kt-geojson:0.2.2'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
@ -27,6 +16,6 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("center.sciprog:maps-kt-geojson:0.2.2")
|
implementation("space.kscience:maps-kt-geojson:0.3.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -6,7 +6,9 @@ plugins {
|
|||||||
|
|
||||||
kscience{
|
kscience{
|
||||||
jvm()
|
jvm()
|
||||||
js()
|
// js()
|
||||||
|
wasm()
|
||||||
|
|
||||||
useSerialization {
|
useSerialization {
|
||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package center.sciprog.maps.geojson
|
package space.kscience.maps.geojson
|
||||||
|
|
||||||
import center.sciprog.maps.geojson.GeoJson.Companion.PROPERTIES_KEY
|
|
||||||
import center.sciprog.maps.geojson.GeoJson.Companion.TYPE_KEY
|
|
||||||
import center.sciprog.maps.geojson.GeoJsonFeatureCollection.Companion.FEATURES_KEY
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
|
import space.kscience.maps.geojson.GeoJson.Companion.PROPERTIES_KEY
|
||||||
|
import space.kscience.maps.geojson.GeoJson.Companion.TYPE_KEY
|
||||||
|
import space.kscience.maps.geojson.GeoJsonFeatureCollection.Companion.FEATURES_KEY
|
||||||
import kotlin.jvm.JvmInline
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
/**
|
/**
|
@ -1,10 +1,9 @@
|
|||||||
package center.sciprog.maps.geojson
|
package space.kscience.maps.geojson
|
||||||
|
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
|
||||||
import center.sciprog.maps.coordinates.meters
|
|
||||||
import center.sciprog.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY
|
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import space.kscience.kmath.geometry.degrees
|
import space.kscience.maps.coordinates.Gmc
|
||||||
|
import space.kscience.maps.coordinates.meters
|
||||||
|
import space.kscience.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY
|
||||||
import kotlin.jvm.JvmInline
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
public sealed interface GeoJsonGeometry : GeoJson {
|
public sealed interface GeoJsonGeometry : GeoJson {
|
||||||
@ -35,8 +34,8 @@ internal fun JsonElement.toGmc() = jsonArray.run {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray {
|
internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray {
|
||||||
add(longitude.degrees)
|
add(longitude.toDegrees().value)
|
||||||
add(latitude.degrees)
|
add(latitude.toDegrees().value)
|
||||||
elevation?.let {
|
elevation?.let {
|
||||||
add(it.meters)
|
add(it.meters)
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
package center.sciprog.maps.geojson
|
package space.kscience.maps.geojson
|
||||||
|
|
||||||
import center.sciprog.attributes.SerializableAttribute
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
|
import space.kscience.SerializableAttribute
|
||||||
|
|
||||||
public object GeoJsonPropertiesAttribute : SerializableAttribute<JsonObject>("properties", serializer())
|
public object GeoJsonPropertiesAttribute : SerializableAttribute<JsonObject>("properties", serializer())
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.geojson
|
package space.kscience.maps.geojson
|
||||||
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
@ -1,12 +1,12 @@
|
|||||||
package center.sciprog.maps.geojson
|
package space.kscience.maps.geojson
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import center.sciprog.attributes.NameAttribute
|
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
|
||||||
import center.sciprog.maps.features.*
|
|
||||||
import kotlinx.serialization.json.contentOrNull
|
import kotlinx.serialization.json.contentOrNull
|
||||||
import kotlinx.serialization.json.intOrNull
|
import kotlinx.serialization.json.intOrNull
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import space.kscience.NameAttribute
|
||||||
|
import space.kscience.maps.coordinates.Gmc
|
||||||
|
import space.kscience.maps.features.*
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
@ -1,11 +1,11 @@
|
|||||||
package center.sciprog.maps.geojson
|
package space.kscience.maps.geojson
|
||||||
|
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
|
||||||
import center.sciprog.maps.features.Feature
|
|
||||||
import center.sciprog.maps.features.FeatureGroup
|
|
||||||
import center.sciprog.maps.features.FeatureRef
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import space.kscience.maps.coordinates.Gmc
|
||||||
|
import space.kscience.maps.features.Feature
|
||||||
|
import space.kscience.maps.features.FeatureGroup
|
||||||
|
import space.kscience.maps.features.FeatureRef
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
/**
|
/**
|
@ -1,9 +1,9 @@
|
|||||||
package center.sciprog.maps.geotiff
|
package center.sciprog.maps.geotiff
|
||||||
|
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
import space.kscience.maps.coordinates.Gmc
|
||||||
import center.sciprog.maps.features.Feature
|
import space.kscience.maps.features.Feature
|
||||||
import center.sciprog.maps.features.FeatureGroup
|
import space.kscience.maps.features.FeatureGroup
|
||||||
import center.sciprog.maps.features.FeatureRef
|
import space.kscience.maps.features.FeatureRef
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import org.geotools.gce.geotiff.GeoTiffReader
|
import org.geotools.gce.geotiff.GeoTiffReader
|
||||||
|
31
maps-kt-leaflet/build.gradle.kts
Normal file
31
maps-kt-leaflet/build.gradle.kts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
plugins {
|
||||||
|
id("space.kscience.gradle.mpp")
|
||||||
|
id("org.jetbrains.compose")
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
kscience{
|
||||||
|
js {
|
||||||
|
binaries.executable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
val jsMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.mapsKtCompose)
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.html.core)
|
||||||
|
implementation(npm("@types/leaflet", "1.9.6"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compose {
|
||||||
|
experimental.web{
|
||||||
|
application{}
|
||||||
|
}
|
||||||
|
// web{}
|
||||||
|
}
|
7
maps-kt-leaflet/src/jsMain/kotlin/main.kt
Normal file
7
maps-kt-leaflet/src/jsMain/kotlin/main.kt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import org.jetbrains.skiko.wasm.onWasmReady
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
onWasmReady {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -6,19 +6,8 @@
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `center.sciprog:maps-kt-scheme:0.2.2`.
|
The Maven coordinates of this project are `space.kscience:maps-kt-scheme:0.3.0`.
|
||||||
|
|
||||||
**Gradle Groovy:**
|
|
||||||
```groovy
|
|
||||||
repositories {
|
|
||||||
maven { url 'https://repo.kotlin.link' }
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'center.sciprog:maps-kt-scheme:0.2.2'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
@ -27,6 +16,6 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("center.sciprog:maps-kt-scheme:0.2.2")
|
implementation("space.kscience:maps-kt-scheme:0.3.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1,30 +1,24 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("space.kscience.gradle.mpp")
|
id("space.kscience.gradle.mpp")
|
||||||
id("org.jetbrains.compose")
|
alias(spclibs.plugins.compose.compiler)
|
||||||
|
alias(spclibs.plugins.compose.jb)
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
kscience{
|
kscience{
|
||||||
jvm()
|
jvm()
|
||||||
}
|
// js()
|
||||||
|
wasm()
|
||||||
|
|
||||||
kotlin {
|
|
||||||
sourceSets {
|
|
||||||
commonMain{
|
commonMain{
|
||||||
dependencies {
|
|
||||||
api(projects.mapsKtFeatures)
|
api(projects.mapsKtFeatures)
|
||||||
api("io.github.microutils:kotlin-logging:2.1.23")
|
|
||||||
api(compose.foundation)
|
|
||||||
}
|
}
|
||||||
}
|
jvmMain{
|
||||||
val jvmMain by getting {
|
|
||||||
dependencies {
|
|
||||||
implementation("org.jfree:org.jfree.svg:5.0.4")
|
implementation("org.jfree:org.jfree.svg:5.0.4")
|
||||||
api(compose.desktop.currentOs)
|
api(compose.desktop.currentOs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//java {
|
//java {
|
||||||
// targetCompatibility = JVM_TARGET
|
// targetCompatibility = JVM_TARGET
|
||||||
|
@ -1,20 +1,12 @@
|
|||||||
package center.sciprog.maps.scheme
|
package space.kscience.maps.scheme
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.key
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.PathEffect
|
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
||||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import center.sciprog.attributes.z
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import center.sciprog.maps.compose.mapControls
|
import space.kscience.maps.compose.canvasControls
|
||||||
import center.sciprog.maps.features.*
|
import space.kscience.maps.features.*
|
||||||
import mu.KotlinLogging
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
@ -22,47 +14,13 @@ private val logger = KotlinLogging.logger("SchemeView")
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
public fun SchemeView(
|
public fun SchemeView(
|
||||||
state: XYViewScope,
|
state: XYCanvasState,
|
||||||
features: FeatureGroup<XY>,
|
features: FeatureGroup<XY>,
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
): Unit = key(state, features) {
|
): Unit {
|
||||||
with(state) {
|
FeatureCanvas(state, features, modifier = modifier.canvasControls(state, features))
|
||||||
//Can't do that inside canvas
|
|
||||||
val painterCache: Map<PainterFeature<XY>, Painter> =
|
|
||||||
features.features.filterIsInstance<PainterFeature<XY>>().associateWith { it.getPainter() }
|
|
||||||
|
|
||||||
Canvas(modifier = modifier.mapControls(state, features)) {
|
|
||||||
|
|
||||||
if (canvasSize != size.toDpSize()) {
|
|
||||||
canvasSize = size.toDpSize()
|
|
||||||
logger.debug { "Recalculate canvas. Size: $size" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clipRect {
|
|
||||||
features.featureMap.values.sortedBy { it.z }
|
|
||||||
.filter { viewPoint.zoom in it.zoomRange }
|
|
||||||
.forEach { feature ->
|
|
||||||
drawFeature(state, painterCache, feature)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectRect?.let { dpRect ->
|
|
||||||
val rect = dpRect.toRect()
|
|
||||||
drawRect(
|
|
||||||
color = Color.Blue,
|
|
||||||
topLeft = rect.topLeft,
|
|
||||||
size = rect.size,
|
|
||||||
alpha = 0.5f,
|
|
||||||
style = Stroke(
|
|
||||||
width = 2f,
|
|
||||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun Rectangle<XY>.computeViewPoint(
|
public fun Rectangle<XY>.computeViewPoint(
|
||||||
canvasSize: DpSize = defaultCanvasSize,
|
canvasSize: DpSize = defaultCanvasSize,
|
||||||
@ -87,7 +45,7 @@ public fun SchemeView(
|
|||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val state = XYViewScope.remember(
|
val state = XYCanvasState.remember(
|
||||||
config,
|
config,
|
||||||
initialViewPoint = initialViewPoint,
|
initialViewPoint = initialViewPoint,
|
||||||
initialRectangle = initialRectangle ?: features.getBoundingBox(Float.MAX_VALUE),
|
initialRectangle = initialRectangle ?: features.getBoundingBox(Float.MAX_VALUE),
|
||||||
@ -112,7 +70,7 @@ public fun SchemeView(
|
|||||||
buildFeatures: FeatureGroup<XY>.() -> Unit = {},
|
buildFeatures: FeatureGroup<XY>.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val featureState = FeatureGroup.remember(XYCoordinateSpace, buildFeatures)
|
val featureState = FeatureGroup.remember(XYCoordinateSpace, buildFeatures)
|
||||||
val mapState: XYViewScope = XYViewScope.remember(
|
val mapState: XYCanvasState = XYCanvasState.remember(
|
||||||
config,
|
config,
|
||||||
initialViewPoint = initialViewPoint,
|
initialViewPoint = initialViewPoint,
|
||||||
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox(
|
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox(
|
@ -1,17 +1,19 @@
|
|||||||
package center.sciprog.maps.scheme
|
package space.kscience.maps.scheme
|
||||||
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.features.CoordinateSpace
|
|
||||||
import center.sciprog.maps.features.Rectangle
|
|
||||||
import center.sciprog.maps.features.ViewPoint
|
|
||||||
import space.kscience.kmath.geometry.Vector2D
|
import space.kscience.kmath.geometry.Vector2D
|
||||||
|
import space.kscience.maps.features.CoordinateSpace
|
||||||
|
import space.kscience.maps.features.Rectangle
|
||||||
|
import space.kscience.maps.features.ViewPoint
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
public data class XY(override val x: Float, override val y: Float) : Vector2D<Float>
|
public data class XY(override val x: Float, override val y: Float) : Vector2D<Float>
|
||||||
|
|
||||||
|
public fun XY(x: Number, y: Number): XY = XY(x.toFloat(), y.toFloat())
|
||||||
|
|
||||||
internal data class XYRectangle(
|
internal data class XYRectangle(
|
||||||
override val a: XY,
|
override val a: XY,
|
||||||
override val b: XY,
|
override val b: XY,
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.scheme
|
package space.kscience.maps.scheme
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -6,12 +6,12 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.DpRect
|
import androidx.compose.ui.unit.DpRect
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.features.*
|
import space.kscience.maps.features.*
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
public class XYViewScope(
|
public class XYCanvasState(
|
||||||
config: ViewConfig<XY>,
|
config: ViewConfig<XY>,
|
||||||
) : CoordinateViewScope<XY>(config) {
|
) : CanvasState<XY>(config) {
|
||||||
override val space: CoordinateSpace<XY>
|
override val space: CoordinateSpace<XY>
|
||||||
get() = XYCoordinateSpace
|
get() = XYCoordinateSpace
|
||||||
|
|
||||||
@ -26,12 +26,15 @@ public class XYViewScope(
|
|||||||
)
|
)
|
||||||
|
|
||||||
override fun computeViewPoint(rectangle: Rectangle<XY>): ViewPoint<XY> {
|
override fun computeViewPoint(rectangle: Rectangle<XY>): ViewPoint<XY> {
|
||||||
val scale = min(
|
val scale: Float = min(
|
||||||
canvasSize.width.value / rectangle.width,
|
canvasSize.width.value / rectangle.width,
|
||||||
canvasSize.height.value / rectangle.height
|
canvasSize.height.value / rectangle.height
|
||||||
)
|
)
|
||||||
|
return if(scale.isInfinite()){
|
||||||
return XYViewPoint(rectangle.center, scale)
|
XYViewPoint(rectangle.center, 1f)
|
||||||
|
} else {
|
||||||
|
XYViewPoint(rectangle.center, scale)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun ViewPoint<XY>.moveBy(x: Dp, y: Dp): ViewPoint<XY> {
|
override fun ViewPoint<XY>.moveBy(x: Dp, y: Dp): ViewPoint<XY> {
|
||||||
@ -51,12 +54,12 @@ public class XYViewScope(
|
|||||||
config: ViewConfig<XY> = ViewConfig(),
|
config: ViewConfig<XY> = ViewConfig(),
|
||||||
initialViewPoint: ViewPoint<XY>? = null,
|
initialViewPoint: ViewPoint<XY>? = null,
|
||||||
initialRectangle: Rectangle<XY>? = null,
|
initialRectangle: Rectangle<XY>? = null,
|
||||||
): XYViewScope = remember {
|
): XYCanvasState = remember {
|
||||||
XYViewScope(config).also { mapState->
|
XYCanvasState(config).apply {
|
||||||
if (initialViewPoint != null) {
|
if (initialViewPoint != null) {
|
||||||
mapState.viewPoint = initialViewPoint
|
viewPoint = initialViewPoint
|
||||||
} else if (initialRectangle != null) {
|
} else if (initialRectangle != null) {
|
||||||
mapState.viewPoint = mapState.computeViewPoint(initialRectangle)
|
viewPoint = computeViewPoint(initialRectangle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,11 +1,11 @@
|
|||||||
package center.sciprog.maps.scheme
|
package space.kscience.maps.scheme
|
||||||
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.features.CoordinateSpace
|
import space.kscience.maps.features.CoordinateSpace
|
||||||
import center.sciprog.maps.features.Rectangle
|
import space.kscience.maps.features.Rectangle
|
||||||
import center.sciprog.maps.features.ViewPoint
|
import space.kscience.maps.features.ViewPoint
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.scheme
|
package space.kscience.maps.scheme
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@ -8,9 +8,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.attributes.Attributes
|
import space.kscience.attributes.Attributes
|
||||||
import center.sciprog.maps.features.*
|
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
|
import space.kscience.maps.features.*
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat())
|
internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat())
|
||||||
@ -108,4 +108,33 @@ public fun FeatureGroup<XY>.pixelMap(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
public fun FeatureGroup<XY>.rectanglePolygon(
|
||||||
|
left: Number, right: Number,
|
||||||
|
bottom: Number, top: Number,
|
||||||
|
attributes: Attributes = Attributes.EMPTY,
|
||||||
|
id: String? = null,
|
||||||
|
): FeatureRef<XY, PolygonFeature<XY>> = polygon(
|
||||||
|
listOf(
|
||||||
|
XY(left.toFloat(), top.toFloat()),
|
||||||
|
XY(right.toFloat(), top.toFloat()),
|
||||||
|
XY(right.toFloat(), bottom.toFloat()),
|
||||||
|
XY(left.toFloat(), bottom.toFloat())
|
||||||
|
),
|
||||||
|
attributes, id
|
||||||
|
)
|
||||||
|
|
||||||
|
public fun FeatureGroup<XY>.rectanglePolygon(
|
||||||
|
rectangle: Rectangle<XY>,
|
||||||
|
attributes: Attributes = Attributes.EMPTY,
|
||||||
|
id: String? = null,
|
||||||
|
): FeatureRef<XY, PolygonFeature<XY>> = polygon(
|
||||||
|
listOf(
|
||||||
|
XY(rectangle.left, rectangle.top),
|
||||||
|
XY(rectangle.right, rectangle.top),
|
||||||
|
XY(rectangle.right, rectangle.bottom),
|
||||||
|
XY(rectangle.left, rectangle.bottom)
|
||||||
|
),
|
||||||
|
attributes, id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
|||||||
package center.sciprog.maps.svg
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.PointMode
|
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
|
||||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
|
||||||
import androidx.compose.ui.graphics.drawscope.translate
|
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import center.sciprog.maps.features.*
|
|
||||||
import center.sciprog.maps.scheme.*
|
|
||||||
import org.jfree.svg.SVGGraphics2D
|
|
||||||
import org.jfree.svg.SVGUtils
|
|
||||||
import space.kscience.kmath.geometry.degrees
|
|
||||||
import java.awt.Font.PLAIN
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
|
|
||||||
public class FeatureStateSnapshot<T : Any>(
|
|
||||||
public val features: Map<String, Feature<T>>,
|
|
||||||
internal val painterCache: Map<PainterFeature<T>, Painter>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
public fun <T : Any> FeatureGroup<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
|
|
||||||
featureMap,
|
|
||||||
features.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
|
|
||||||
)
|
|
||||||
|
|
||||||
public fun FeatureStateSnapshot<XY>.generateSvg(
|
|
||||||
viewPoint: ViewPoint<XY>,
|
|
||||||
width: Double,
|
|
||||||
height: Double,
|
|
||||||
id: String? = null,
|
|
||||||
): String {
|
|
||||||
|
|
||||||
fun XY.toOffset(): Offset = Offset(
|
|
||||||
(width / 2 + (x - viewPoint.focus.x) * viewPoint.zoom).toFloat(),
|
|
||||||
(height / 2 + (viewPoint.focus.y - y) * viewPoint.zoom).toFloat()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
fun SvgDrawScope.drawFeature(scale: Float, feature: Feature<XY>) {
|
|
||||||
|
|
||||||
val color = feature.color ?: Color.Red
|
|
||||||
val alpha = feature.attributes[AlphaAttribute] ?: 1f
|
|
||||||
|
|
||||||
when (feature) {
|
|
||||||
is ScalableImageFeature -> {
|
|
||||||
val offset = XY(feature.rectangle.left, feature.rectangle.top).toOffset()
|
|
||||||
val backgroundSize = Size(
|
|
||||||
(feature.rectangle.width * scale),
|
|
||||||
(feature.rectangle.height * scale)
|
|
||||||
)
|
|
||||||
|
|
||||||
translate(offset.x, offset.y) {
|
|
||||||
with(painterCache[feature]!!) {
|
|
||||||
draw(backgroundSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is FeatureSelector -> drawFeature(scale, feature.selector(scale))
|
|
||||||
|
|
||||||
is CircleFeature -> drawCircle(
|
|
||||||
color,
|
|
||||||
feature.radius.toPx(),
|
|
||||||
center = feature.center.toOffset(),
|
|
||||||
alpha = alpha
|
|
||||||
)
|
|
||||||
|
|
||||||
is LineFeature -> drawLine(
|
|
||||||
color,
|
|
||||||
feature.a.toOffset(),
|
|
||||||
feature.b.toOffset(),
|
|
||||||
alpha = alpha
|
|
||||||
)
|
|
||||||
|
|
||||||
is PointsFeature -> {
|
|
||||||
val points = feature.points.map { it.toOffset() }
|
|
||||||
drawPoints(
|
|
||||||
points = points,
|
|
||||||
color = color,
|
|
||||||
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
|
||||||
pointMode = PointMode.Points,
|
|
||||||
pathEffect = feature.attributes[PathEffectAttribute],
|
|
||||||
alpha = alpha
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is MultiLineFeature -> {
|
|
||||||
val points = feature.points.map { it.toOffset() }
|
|
||||||
drawPoints(
|
|
||||||
points = points,
|
|
||||||
color = color,
|
|
||||||
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
|
||||||
pointMode = PointMode.Polygon,
|
|
||||||
pathEffect = feature.attributes[PathEffectAttribute],
|
|
||||||
alpha = alpha
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is ArcFeature -> {
|
|
||||||
val topLeft = feature.oval.leftTop.toOffset()
|
|
||||||
val bottomRight = feature.oval.rightBottom.toOffset()
|
|
||||||
|
|
||||||
val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
|
|
||||||
|
|
||||||
drawArc(
|
|
||||||
color = color,
|
|
||||||
startAngle = feature.startAngle.degrees.toFloat(),
|
|
||||||
sweepAngle = feature.arcLength.degrees.toFloat(),
|
|
||||||
useCenter = false,
|
|
||||||
topLeft = topLeft,
|
|
||||||
size = size,
|
|
||||||
style = Stroke(),
|
|
||||||
alpha = alpha
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset())
|
|
||||||
|
|
||||||
is VectorIconFeature -> {
|
|
||||||
val offset = feature.center.toOffset()
|
|
||||||
val imageSize = feature.size.toSize()
|
|
||||||
translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
|
|
||||||
with(painterCache[feature]!!) {
|
|
||||||
draw(imageSize, alpha = alpha)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is TextFeature -> drawIntoCanvas { _ ->
|
|
||||||
val offset = feature.position.toOffset()
|
|
||||||
drawText(
|
|
||||||
feature.text,
|
|
||||||
offset.x + 5,
|
|
||||||
offset.y - 5,
|
|
||||||
java.awt.Font(null, PLAIN, 16),
|
|
||||||
color
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is DrawFeature -> {
|
|
||||||
val offset = feature.position.toOffset()
|
|
||||||
translate(offset.x, offset.y) {
|
|
||||||
feature.drawFeature(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is FeatureGroup -> {
|
|
||||||
feature.featureMap.values.forEach {
|
|
||||||
drawFeature(scale, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(width, height)
|
|
||||||
val svgScope = SvgDrawScope(svgGraphics2D, Size(width.toFloat(), height.toFloat()))
|
|
||||||
|
|
||||||
svgScope.apply {
|
|
||||||
features.values.filterIsInstance<ScalableImageFeature<XY>>().forEach { background ->
|
|
||||||
drawFeature(viewPoint.zoom, background)
|
|
||||||
}
|
|
||||||
features.values.filter {
|
|
||||||
it !is ScalableImageFeature && viewPoint.zoom in it.zoomRange
|
|
||||||
}.forEach { feature ->
|
|
||||||
drawFeature(viewPoint.zoom, feature)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return svgGraphics2D.getSVGElement(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun FeatureStateSnapshot<XY>.exportToSvg(
|
|
||||||
viewPoint: ViewPoint<XY>,
|
|
||||||
width: Double,
|
|
||||||
height: Double,
|
|
||||||
path: java.nio.file.Path,
|
|
||||||
) {
|
|
||||||
|
|
||||||
val svgString = generateSvg(viewPoint, width, height)
|
|
||||||
|
|
||||||
SVGUtils.writeToSVG(path.toFile(), svgString)
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
package center.sciprog.maps.svg
|
package space.kscience.maps.svg
|
||||||
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
@ -301,7 +301,7 @@ internal class SvgCanvas(val graphics: SVGGraphics2D) : Canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal class SvgDrawContext(val graphics: SVGGraphics2D, override var size: Size) : DrawContext {
|
internal class SvgDrawContext(val graphics: SVGGraphics2D, override var size: Size) : DrawContext {
|
||||||
override val canvas: Canvas = SvgCanvas(graphics)
|
override var canvas: Canvas = SvgCanvas(graphics)
|
||||||
|
|
||||||
override val transform: DrawTransform = asDrawTransform()
|
override val transform: DrawTransform = asDrawTransform()
|
||||||
}
|
}
|
@ -1,25 +1,32 @@
|
|||||||
package center.sciprog.maps.svg
|
package space.kscience.maps.svg
|
||||||
|
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.graphics.drawscope.*
|
import androidx.compose.ui.graphics.drawscope.DrawContext
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Fill
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import org.jfree.svg.SVGGraphics2D
|
import org.jfree.svg.SVGGraphics2D
|
||||||
|
import space.kscience.attributes.Attributes
|
||||||
|
import space.kscience.maps.features.*
|
||||||
|
import space.kscience.maps.scheme.XY
|
||||||
import java.awt.BasicStroke
|
import java.awt.BasicStroke
|
||||||
import java.awt.Font
|
|
||||||
import java.awt.geom.*
|
import java.awt.geom.*
|
||||||
import java.awt.image.AffineTransformOp
|
import java.awt.image.AffineTransformOp
|
||||||
import java.awt.Color as AWTColor
|
import java.awt.Color as AWTColor
|
||||||
|
|
||||||
public class SvgDrawScope(
|
public class SvgDrawScope(
|
||||||
|
state: CanvasState<XY>,
|
||||||
private val graphics: SVGGraphics2D,
|
private val graphics: SVGGraphics2D,
|
||||||
size: Size,
|
private val painterCache: Map<PainterFeature<XY>, Painter>,
|
||||||
private val defaultStrokeWidth: Float = 1f,
|
private val defaultStrokeWidth: Float = 1f,
|
||||||
) : DrawScope {
|
) : FeatureDrawScope<XY>(state) {
|
||||||
|
|
||||||
override val layoutDirection: LayoutDirection
|
override val layoutDirection: LayoutDirection
|
||||||
get() = LayoutDirection.Ltr
|
get() = LayoutDirection.Ltr
|
||||||
@ -459,18 +466,22 @@ public class SvgDrawScope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun drawText(
|
public fun renderText(
|
||||||
text: String,
|
textFeature: TextFeature<XY>,
|
||||||
x: Float,
|
|
||||||
y: Float,
|
|
||||||
font: Font,
|
|
||||||
color: Color,
|
|
||||||
) {
|
) {
|
||||||
setupColor(color)
|
textFeature.color?.let { setupColor(it) }
|
||||||
graphics.font = font
|
graphics.drawString(textFeature.text, textFeature.position.x, textFeature.position.y)
|
||||||
graphics.drawString(text, x, y)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val drawContext: DrawContext = SvgDrawContext(graphics, size)
|
override fun painterFor(feature: PainterFeature<XY>): Painter {
|
||||||
|
return painterCache[feature]!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun drawText(text: String, position: Offset, attributes: Attributes) {
|
||||||
|
attributes[ColorAttribute]?.let { setupColor(it) }
|
||||||
|
graphics.drawString(text, position.x, position.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val drawContext: DrawContext = SvgDrawContext(graphics, state.canvasSize.toSize())
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,182 @@
|
|||||||
|
package space.kscience.maps.svg
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.jfree.svg.SVGGraphics2D
|
||||||
|
import org.jfree.svg.SVGUtils
|
||||||
|
import space.kscience.maps.features.*
|
||||||
|
import space.kscience.maps.scheme.XY
|
||||||
|
import space.kscience.maps.scheme.XYCanvasState
|
||||||
|
|
||||||
|
|
||||||
|
public class FeatureStateSnapshot<T : Any>(
|
||||||
|
public val features: Map<String, Feature<T>>,
|
||||||
|
internal val painterCache: Map<PainterFeature<T>, Painter>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun <T : Any> FeatureGroup<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
|
||||||
|
featureMap,
|
||||||
|
features.flatMap {
|
||||||
|
if (it is FeatureGroup) it.features else listOf(it)
|
||||||
|
}.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
public fun FeatureStateSnapshot<XY>.generateSvg(
|
||||||
|
viewPoint: ViewPoint<XY>,
|
||||||
|
width: Double,
|
||||||
|
height: Double,
|
||||||
|
id: String? = null,
|
||||||
|
): String {
|
||||||
|
|
||||||
|
// fun XY.toOffset(): Offset = Offset(
|
||||||
|
// (width / 2 + (x - viewPoint.focus.x) * viewPoint.zoom).toFloat(),
|
||||||
|
// (height / 2 + (viewPoint.focus.y - y) * viewPoint.zoom).toFloat()
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// fun SvgDrawScope.drawFeature(scale: Float, feature: Feature<XY>) {
|
||||||
|
//
|
||||||
|
// val color = feature.color ?: Color.Red
|
||||||
|
// val alpha = feature.attributes[AlphaAttribute] ?: 1f
|
||||||
|
//
|
||||||
|
// when (feature) {
|
||||||
|
// is ScalableImageFeature -> {
|
||||||
|
// val offset = XY(feature.rectangle.left, feature.rectangle.top).toOffset()
|
||||||
|
// val backgroundSize = Size(
|
||||||
|
// (feature.rectangle.width * scale),
|
||||||
|
// (feature.rectangle.height * scale)
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// translate(offset.x, offset.y) {
|
||||||
|
// with(painterCache[feature]!!) {
|
||||||
|
// draw(backgroundSize)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// is FeatureSelector -> drawFeature(scale, feature.selector(scale))
|
||||||
|
//
|
||||||
|
// is CircleFeature -> drawCircle(
|
||||||
|
// color,
|
||||||
|
// feature.radius.toPx(),
|
||||||
|
// center = feature.center.toOffset(),
|
||||||
|
// alpha = alpha
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// is LineFeature -> drawLine(
|
||||||
|
// color,
|
||||||
|
// feature.a.toOffset(),
|
||||||
|
// feature.b.toOffset(),
|
||||||
|
// alpha = alpha
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// is PointsFeature -> {
|
||||||
|
// val points = feature.points.map { it.toOffset() }
|
||||||
|
// drawPoints(
|
||||||
|
// points = points,
|
||||||
|
// color = color,
|
||||||
|
// strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
||||||
|
// pointMode = PointMode.Points,
|
||||||
|
// pathEffect = feature.attributes[PathEffectAttribute],
|
||||||
|
// alpha = alpha
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// is MultiLineFeature -> {
|
||||||
|
// val points = feature.points.map { it.toOffset() }
|
||||||
|
// drawPoints(
|
||||||
|
// points = points,
|
||||||
|
// color = color,
|
||||||
|
// strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
||||||
|
// pointMode = PointMode.Polygon,
|
||||||
|
// pathEffect = feature.attributes[PathEffectAttribute],
|
||||||
|
// alpha = alpha
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// is ArcFeature -> {
|
||||||
|
// val topLeft = feature.oval.leftTop.toOffset()
|
||||||
|
// val bottomRight = feature.oval.rightBottom.toOffset()
|
||||||
|
//
|
||||||
|
// val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
|
||||||
|
//
|
||||||
|
// drawArc(
|
||||||
|
// color = color,
|
||||||
|
// startAngle = feature.startAngle.degrees.toFloat(),
|
||||||
|
// sweepAngle = feature.arcLength.degrees.toFloat(),
|
||||||
|
// useCenter = false,
|
||||||
|
// topLeft = topLeft,
|
||||||
|
// size = size,
|
||||||
|
// style = Stroke(),
|
||||||
|
// alpha = alpha
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset())
|
||||||
|
//
|
||||||
|
// is VectorIconFeature -> {
|
||||||
|
// val offset = feature.center.toOffset()
|
||||||
|
// val imageSize = feature.size.toSize()
|
||||||
|
// translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
|
||||||
|
// with(painterCache[feature]!!) {
|
||||||
|
// draw(imageSize, alpha = alpha)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// is TextFeature -> drawIntoCanvas { _ ->
|
||||||
|
// val offset = feature.position.toOffset()
|
||||||
|
// drawText(
|
||||||
|
// feature.text,
|
||||||
|
// offset.x + 5,
|
||||||
|
// offset.y - 5,
|
||||||
|
// java.awt.Font(null, PLAIN, 16),
|
||||||
|
// color
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// is DrawFeature -> {
|
||||||
|
// val offset = feature.position.toOffset()
|
||||||
|
// translate(offset.x, offset.y) {
|
||||||
|
// feature.drawFeature(this)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// is FeatureGroup -> {
|
||||||
|
// feature.featureMap.values.forEach {
|
||||||
|
// drawFeature(scale, it)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(width, height)
|
||||||
|
val svgCanvasState: XYCanvasState = XYCanvasState(ViewConfig()).apply {
|
||||||
|
this.viewPoint = viewPoint
|
||||||
|
this.canvasSize = DpSize(width.dp, height.dp)
|
||||||
|
}
|
||||||
|
val svgScope = SvgDrawScope(svgCanvasState, svgGraphics2D, painterCache)
|
||||||
|
|
||||||
|
svgScope.apply {
|
||||||
|
features.values.sortedBy { it.z }
|
||||||
|
.filter { state.viewPoint.zoom in it.zoomRange }
|
||||||
|
.forEach { feature ->
|
||||||
|
this@apply.drawFeature(feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return svgGraphics2D.getSVGElement(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun FeatureStateSnapshot<XY>.exportToSvg(
|
||||||
|
viewPoint: ViewPoint<XY>,
|
||||||
|
width: Double,
|
||||||
|
height: Double,
|
||||||
|
path: java.nio.file.Path,
|
||||||
|
) {
|
||||||
|
val svgString: String = generateSvg(viewPoint, width, height)
|
||||||
|
SVGUtils.writeToSVG(path.toFile(), svgString)
|
||||||
|
}
|
@ -16,13 +16,11 @@ pluginManagement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application").version(extra["agp.version"] as String)
|
// id("com.android.application").version(extra["agp.version"] as String)
|
||||||
id("com.android.library").version(extra["agp.version"] as String)
|
// id("com.android.library").version(extra["agp.version"] as String)
|
||||||
id("org.jetbrains.compose").version(extra["compose.version"] as String)
|
|
||||||
id("space.kscience.gradle.project") version toolsVersion
|
id("space.kscience.gradle.project") version toolsVersion
|
||||||
id("space.kscience.gradle.mpp") version toolsVersion
|
id("space.kscience.gradle.mpp") version toolsVersion
|
||||||
id("space.kscience.gradle.jvm") version toolsVersion
|
id("space.kscience.gradle.jvm") version toolsVersion
|
||||||
id("space.kscience.gradle.js") version toolsVersion
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,9 +50,11 @@ include(
|
|||||||
":maps-kt-features",
|
":maps-kt-features",
|
||||||
":maps-kt-compose",
|
":maps-kt-compose",
|
||||||
":maps-kt-scheme",
|
":maps-kt-scheme",
|
||||||
|
// ":maps-kt-leaflet",
|
||||||
":demo:maps",
|
":demo:maps",
|
||||||
":demo:scheme",
|
":demo:scheme",
|
||||||
":demo:polygon-editor",
|
":demo:polygon-editor",
|
||||||
":demo:trajectory-playground"
|
":demo:trajectory-playground",
|
||||||
|
":demo:maps-wasm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,19 +5,8 @@
|
|||||||
|
|
||||||
## Artifact:
|
## Artifact:
|
||||||
|
|
||||||
The Maven coordinates of this project are `space.kscience:trajectory-kt:0.2.2`.
|
The Maven coordinates of this project are `space.kscience:trajectory-kt:0.3.0`.
|
||||||
|
|
||||||
**Gradle Groovy:**
|
|
||||||
```groovy
|
|
||||||
repositories {
|
|
||||||
maven { url 'https://repo.kotlin.link' }
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'space.kscience:trajectory-kt:0.2.2'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Gradle Kotlin DSL:**
|
**Gradle Kotlin DSL:**
|
||||||
```kotlin
|
```kotlin
|
||||||
repositories {
|
repositories {
|
||||||
@ -26,7 +15,7 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("space.kscience:trajectory-kt:0.2.2")
|
implementation("space.kscience:trajectory-kt:0.3.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -11,9 +11,12 @@ kscience{
|
|||||||
jvm()
|
jvm()
|
||||||
js()
|
js()
|
||||||
native()
|
native()
|
||||||
|
wasm()
|
||||||
|
|
||||||
useContextReceivers()
|
useContextReceivers()
|
||||||
useSerialization()
|
useSerialization{
|
||||||
|
json()
|
||||||
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
api("space.kscience:kmath-geometry:$kmathVersion")
|
api("space.kscience:kmath-geometry:$kmathVersion")
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
@file:Suppress("UNCHECKED_CAST")
|
@file:Suppress("UNCHECKED_CAST")
|
||||||
|
|
||||||
package center.sciprog.attributes
|
package space.kscience
|
||||||
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.builtins.serializer
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
|
import space.kscience.attributes.Attribute
|
||||||
|
import space.kscience.attributes.Attributes
|
||||||
|
|
||||||
public class AttributesSerializer(
|
public class AttributesSerializer(
|
||||||
private val serializableAttributes: Set<SerializableAttribute<*>>,
|
private val serializableAttributes: Set<SerializableAttribute<*>>,
|
||||||
@ -29,12 +32,17 @@ public class AttributesSerializer(
|
|||||||
|
|
||||||
attr to value
|
attr to value
|
||||||
}
|
}
|
||||||
return Attributes(attributeMap)
|
return object : Attributes {
|
||||||
|
override val content: Map<out Attribute<*>, Any?> = attributeMap
|
||||||
|
override fun toString(): String = "Attributes(value=${content.entries})"
|
||||||
|
override fun equals(other: Any?): Boolean = other is Attributes && Attributes.equals(this, other)
|
||||||
|
override fun hashCode(): Int = content.hashCode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: Attributes) {
|
override fun serialize(encoder: Encoder, value: Attributes) {
|
||||||
val json = buildJsonObject {
|
val json = buildJsonObject {
|
||||||
value.content.forEach { (key: Attribute<*>, value: Any) ->
|
value.content.forEach { (key: Attribute<*>, value: Any?) ->
|
||||||
if (key !in serializableAttributes) error("An attribute key '$key' is not in the list of allowed attributes for this serializer")
|
if (key !in serializableAttributes) error("An attribute key '$key' is not in the list of allowed attributes for this serializer")
|
||||||
val serializableKey = key as SerializableAttribute
|
val serializableKey = key as SerializableAttribute
|
||||||
|
|
||||||
@ -46,10 +54,19 @@ public class AttributesSerializer(
|
|||||||
|
|
||||||
put(
|
put(
|
||||||
serializableKey.serialId,
|
serializableKey.serialId,
|
||||||
json.encodeToJsonElement(serializableKey.serializer as KSerializer<Any>, value)
|
json.encodeToJsonElement(serializableKey.serializer as KSerializer<Any?>, value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsonSerializer.serialize(encoder, json)
|
jsonSerializer.serialize(encoder, json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract class SerializableAttribute<T>(
|
||||||
|
public val serialId: String,
|
||||||
|
public val serializer: KSerializer<T>,
|
||||||
|
) : Attribute<T> {
|
||||||
|
override fun toString(): String = serialId
|
||||||
|
}
|
||||||
|
|
||||||
|
public object NameAttribute : SerializableAttribute<String>("name", String.serializer())
|
@ -1,21 +1,25 @@
|
|||||||
package space.kscience.kmath.geometry
|
package space.kscience
|
||||||
|
|
||||||
|
import space.kscience.kmath.geometry.*
|
||||||
|
import space.kscience.kmath.geometry.euclidean2d.Circle2D
|
||||||
|
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
|
||||||
|
import space.kscience.kmath.structures.Float64
|
||||||
import space.kscience.trajectory.*
|
import space.kscience.trajectory.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.sign
|
import kotlin.math.sign
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
public fun Euclidean2DSpace.circle(x: Number, y: Number, radius: Number): Circle2D =
|
public fun Float64Space2D.circle(x: Number, y: Number, radius: Number): Circle2D<Float64> =
|
||||||
Circle2D(vector(x, y), radius = radius.toDouble())
|
Circle2D(vector(x, y), radius = radius.toDouble())
|
||||||
|
|
||||||
public fun Euclidean2DSpace.segment(begin: DoubleVector2D, end: DoubleVector2D): LineSegment2D =
|
public fun Float64Space2D.segment(begin: Vector2D<Float64>, end: Vector2D<Float64>): LineSegment2D =
|
||||||
LineSegment(begin, end)
|
LineSegment(begin, end)
|
||||||
|
|
||||||
public fun Euclidean2DSpace.segment(x1: Number, y1: Number, x2: Number, y2: Number): LineSegment2D =
|
public fun Float64Space2D.segment(x1: Number, y1: Number, x2: Number, y2: Number): LineSegment2D =
|
||||||
LineSegment(vector(x1, y1), vector(x2, y2))
|
LineSegment(vector(x1, y1), vector(x2, y2))
|
||||||
|
|
||||||
public fun Euclidean2DSpace.intersectsOrInside(circle1: Circle2D, circle2: Circle2D): Boolean {
|
public fun Float64Space2D.intersectsOrInside(circle1: Circle2D<Float64>, circle2: Circle2D<Float64>): Boolean {
|
||||||
val distance = norm(circle2.center - circle1.center)
|
val distance = norm(circle2.center - circle1.center)
|
||||||
return distance <= circle1.radius + circle2.radius
|
return distance <= circle1.radius + circle2.radius
|
||||||
}
|
}
|
||||||
@ -23,7 +27,7 @@ public fun Euclidean2DSpace.intersectsOrInside(circle1: Circle2D, circle2: Circl
|
|||||||
/**
|
/**
|
||||||
* https://mathworld.wolfram.com/Circle-LineIntersection.html
|
* https://mathworld.wolfram.com/Circle-LineIntersection.html
|
||||||
*/
|
*/
|
||||||
public fun Euclidean2DSpace.intersects(segment: LineSegment2D, circle: Circle2D): Boolean {
|
public fun Float64Space2D.intersects(segment: LineSegment2D, circle: Circle2D<Float64>): Boolean {
|
||||||
val direction = segment.end - segment.begin
|
val direction = segment.end - segment.begin
|
||||||
val radiusVector = segment.begin - circle.center
|
val radiusVector = segment.begin - circle.center
|
||||||
|
|
||||||
@ -43,14 +47,14 @@ public fun Euclidean2DSpace.intersects(segment: LineSegment2D, circle: Circle2D)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public fun Euclidean2DSpace.intersects(circle: Circle2D, segment: LineSegment2D): Boolean =
|
public fun Float64Space2D.intersects(circle: Circle2D<Float64>, segment: LineSegment2D): Boolean =
|
||||||
intersects(segment, circle)
|
intersects(segment, circle)
|
||||||
|
|
||||||
public fun Euclidean2DSpace.intersects(segment1: LineSegment2D, segment2: LineSegment2D): Boolean {
|
public fun Float64Space2D.intersects(segment1: LineSegment2D, segment2: LineSegment2D): Boolean {
|
||||||
infix fun DoubleVector2D.cross(v2: DoubleVector2D): Double = x * v2.y - y * v2.x
|
infix fun Vector2D<Float64>.cross(v2: Vector2D<Float64>): Double = x * v2.y - y * v2.x
|
||||||
infix fun DoubleVector2D.crossSign(v2: DoubleVector2D) = cross(v2).sign
|
infix fun Vector2D<Float64>.crossSign(v2: Vector2D<Float64>) = cross(v2).sign
|
||||||
|
|
||||||
return with(Euclidean2DSpace) {
|
return with(Float64Space2D) {
|
||||||
(segment2.begin - segment1.begin) crossSign (segment2.end - segment1.begin) !=
|
(segment2.begin - segment1.begin) crossSign (segment2.end - segment1.begin) !=
|
||||||
(segment2.begin - segment1.end) crossSign (segment2.end - segment1.end) &&
|
(segment2.begin - segment1.end) crossSign (segment2.end - segment1.end) &&
|
||||||
(segment1.begin - segment2.begin) crossSign (segment1.end - segment2.begin) !=
|
(segment1.begin - segment2.begin) crossSign (segment1.end - segment2.begin) !=
|
||||||
@ -58,7 +62,7 @@ public fun Euclidean2DSpace.intersects(segment1: LineSegment2D, segment2: LineSe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun Euclidean2DSpace.intersectsTrajectory(segment: LineSegment2D, trajectory: Trajectory2D): Boolean =
|
public fun Float64Space2D.intersectsTrajectory(segment: LineSegment2D, trajectory: Trajectory2D): Boolean =
|
||||||
when (trajectory) {
|
when (trajectory) {
|
||||||
is CircleTrajectory2D -> intersects(segment, trajectory.circle)
|
is CircleTrajectory2D -> intersects(segment, trajectory.circle)
|
||||||
is StraightTrajectory2D -> intersects(segment, trajectory)
|
is StraightTrajectory2D -> intersects(segment, trajectory)
|
||||||
@ -72,7 +76,7 @@ public fun Euclidean2DSpace.intersectsTrajectory(segment: LineSegment2D, traject
|
|||||||
*
|
*
|
||||||
* @param bearing is counted the same way as in [Pose2D], from positive y clockwise
|
* @param bearing is counted the same way as in [Pose2D], from positive y clockwise
|
||||||
*/
|
*/
|
||||||
public fun Circle2D.tangent(bearing: Angle, direction: Trajectory2D.Direction): Pose2D = with(Euclidean2DSpace) {
|
public fun Circle2D<Float64>.tangent(bearing: Angle, direction: Trajectory2D.Direction): Pose2D = with(Float64Space2D) {
|
||||||
val coordinates: Vector2D<Double> = vector(center.x + radius * sin(bearing), center.y + radius * cos(bearing))
|
val coordinates: Vector2D<Double> = vector(center.x + radius * sin(bearing), center.y + radius * cos(bearing))
|
||||||
val tangentAngle = when (direction) {
|
val tangentAngle = when (direction) {
|
||||||
Trajectory2D.R -> bearing + Angle.piDiv2
|
Trajectory2D.R -> bearing + Angle.piDiv2
|
||||||
@ -82,7 +86,7 @@ public fun Circle2D.tangent(bearing: Angle, direction: Trajectory2D.Direction):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public fun CircleTrajectory2D.containsPoint(point: DoubleVector2D): Boolean = with(Euclidean2DSpace) {
|
public fun CircleTrajectory2D.containsPoint(point: Vector2D<Float64>): Boolean = with(Float64Space2D) {
|
||||||
val radiusVector = point - center
|
val radiusVector = point - center
|
||||||
if (abs(norm(radiusVector) - circle.radius) > 1e-4 * circle.radius) error("Wrong radius")
|
if (abs(norm(radiusVector) - circle.radius) > 1e-4 * circle.radius) error("Wrong radius")
|
||||||
val radiusVectorBearing = radiusVector.bearing
|
val radiusVectorBearing = radiusVector.bearing
|
@ -1,21 +0,0 @@
|
|||||||
package space.kscience.kmath.geometry
|
|
||||||
|
|
||||||
import space.kscience.kmath.misc.zipWithNextCircular
|
|
||||||
import space.kscience.trajectory.Trajectory2D
|
|
||||||
|
|
||||||
public fun Euclidean2DSpace.polygon(points: List<DoubleVector2D>): Polygon<Double> = object : Polygon<Double> {
|
|
||||||
override val points: List<Vector2D<Double>> get() = points
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun Euclidean2DSpace.intersects(polygon: Polygon<Double>, segment: LineSegment2D): Boolean =
|
|
||||||
polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, segment) }
|
|
||||||
|
|
||||||
public fun Euclidean2DSpace.intersects(polygon: Polygon<Double>, circle: Circle2D): Boolean =
|
|
||||||
polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, circle) }
|
|
||||||
|
|
||||||
public fun Euclidean2DSpace.intersectsTrajectory(polygon: Polygon<Double>, trajectory: Trajectory2D): Boolean =
|
|
||||||
polygon.points.zipWithNextCircular { l, r ->
|
|
||||||
segment(l, r)
|
|
||||||
}.any { edge ->
|
|
||||||
intersectsTrajectory(edge, trajectory)
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user