Compare commits
52 Commits
v0.2.2
...
feature/ge
| Author | SHA1 | Date | |
|---|---|---|---|
| 237b9ec1c5 | |||
| 3ae45dbc93 | |||
| e6dab6071e | |||
| 585d174fa4 | |||
| 67469b3d62 | |||
| 558c217987 | |||
| 35c459d658 | |||
| e1a2ba06e1 | |||
| a63490a3d5 | |||
| 8b50045c6e | |||
| 621ab06361 | |||
| c1bb150dd8 | |||
| 4e08680b22 | |||
| bd2804d772 | |||
| e3b5ad0df4 | |||
| 3ddced5c4a | |||
| 2d46a0ad98 | |||
| 29a0fb743c | |||
| df800f05f0 | |||
|
|
498db37a7c | ||
| e913874ace | |||
| 4e76a25a15 | |||
| 5da7ee7944 | |||
| 1119d593a2 | |||
| bf128a3eb9 | |||
| 3a4c9133c6 | |||
| 29074a9624 | |||
| 62196fc6f5 | |||
| 0f5dcf9979 | |||
| 601a16e420 | |||
|
|
7d0dcd1b91 | ||
|
|
b4b3ecc8d7 | ||
|
|
79f4d0eba5 | ||
|
|
390a896e0a | ||
| 234d4715b6 | |||
| 6eafb5ec26 | |||
|
|
bd6d8e2f8e | ||
| f62f8181ce | |||
| c30f586120 | |||
| 07ea73a87a | |||
| 7ca4bba1b7 | |||
| 7c7a788d2e | |||
| 327cef9ea9 | |||
| ea7869e39d | |||
| d21d6ebb2a | |||
| f05f6e137c | |||
| 7d3b219d70 | |||
| aebf4af24f | |||
| 75b5a69a27 | |||
| 921aff4685 | |||
| 1caf141d27 | |||
| 2bc595d97d |
22
.github/workflows/build.yml
vendored
Normal file
22
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Gradle build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ dev ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4.2.1
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: liberica
|
||||
- name: execute build
|
||||
uses: gradle/gradle-build-action@v3.4.2
|
||||
with:
|
||||
arguments: build
|
||||
39
.github/workflows/pages.yml
vendored
Normal file
39
.github/workflows/pages.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Dokka publication
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: liberica
|
||||
- name: execute build
|
||||
uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
arguments: dokkaHtmlMultiModule
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: 'build/dokka/htmlMultiModule'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
27
.github/workflows/publish.yml
vendored
Normal file
27
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Gradle publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [ created ]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
environment:
|
||||
name: publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4.2.1
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: liberica
|
||||
- name: execute build
|
||||
uses: gradle/gradle-build-action@v3.4.2
|
||||
- name: Publish
|
||||
shell: bash
|
||||
run: >
|
||||
./gradlew release --no-daemon --build-cache -Ppublishing.enabled=true
|
||||
-Ppublishing.space.user=${{ secrets.SPACE_APP_ID }}
|
||||
-Ppublishing.space.token=${{ secrets.SPACE_APP_SECRET }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
build/
|
||||
.gradle/
|
||||
.idea/
|
||||
.kotlin
|
||||
/*.iml
|
||||
|
||||
mapCache/
|
||||
10
.space.kts
10
.space.kts
@@ -9,17 +9,17 @@ job("Publish") {
|
||||
gitPush { enabled = false }
|
||||
}
|
||||
container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") {
|
||||
env["SPACE_USER"] = Secrets("space_user")
|
||||
env["SPACE_TOKEN"] = Secrets("space_token")
|
||||
env["SPACE_USER"] = "{{ project:space_user }}"
|
||||
env["SPACE_TOKEN"] = "{{ project:space_token }}"
|
||||
kotlinScript { api ->
|
||||
|
||||
val spaceUser = System.getenv("SPACE_USER")
|
||||
val spaceToken = System.getenv("SPACE_TOKEN")
|
||||
|
||||
// write version to the build directory
|
||||
// write the version to the build directory
|
||||
api.gradlew("version")
|
||||
|
||||
//read version from build file
|
||||
//read the version from build file
|
||||
val version = java.nio.file.Path.of("build/project-version.txt").readText()
|
||||
|
||||
val revisionSuffix = if (version.endsWith("SNAPSHOT")) {
|
||||
@@ -32,7 +32,7 @@ job("Publish") {
|
||||
project = api.projectIdentifier(),
|
||||
targetIdentifier = TargetIdentifier.Key("maps-kt"),
|
||||
version = version+revisionSuffix,
|
||||
// automatically update deployment status based on a status of a job
|
||||
// automatically update deployment status based on the status of a job
|
||||
syncWithAutomationJob = true
|
||||
)
|
||||
api.gradlew(
|
||||
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -3,13 +3,34 @@
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- `alpha` extension for feature attribute builder
|
||||
- PNG export
|
||||
|
||||
### Changed
|
||||
- Features are now sealed. New `CustomFeature` is not drawn by default draw.
|
||||
- avoid drawing features with VisibleAttribute false
|
||||
- Move SVG export to `features` and make it usable for maps as well
|
||||
- Kotlin 2.1
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
- Text measurement outside of screen errors
|
||||
- Add alpha attribute comprehension for all standard features.
|
||||
- Package name for SerializeableAttribute
|
||||
|
||||
|
||||
### Security
|
||||
|
||||
## 0.3.0 - 2024-06-04
|
||||
|
||||
### Changed
|
||||
|
||||
- Package changed to `space.kscience`
|
||||
- Kotlin 2.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Use of generated resources for Wasm
|
||||
|
||||
42
README.md
42
README.md
@@ -1,21 +1,38 @@
|
||||
# Maps-kt
|
||||
|
||||
This repository is a work-in-progress implementation of Map-with-markers component for Compose-Multiplatform
|
||||
A Kotlin Multiplatform library for interactive maps and geospatial data visualization using Compose Multiplatform.
|
||||
|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
Maps-kt provides a comprehensive set of tools for working with maps, geospatial data, and cartographic projections in Kotlin. It offers a UI-agnostic core with Compose Multiplatform implementations, allowing you to create interactive maps with markers, layers, and custom visualizations across multiple platforms.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiplatform Support**: Works on JVM, JavaScript, Native, and WebAssembly platforms
|
||||
- **Compose Integration**: Seamless integration with Compose Multiplatform for modern UI development
|
||||
- **Map Projections**: Support for Mercator, Web Mercator, and other cartographic projections
|
||||
- **Geospatial Data**: Tools for working with coordinates, distances, angles, and ellipsoid geometry
|
||||
- **Tile Providers**: Integration with OpenStreetMap and other tile providers
|
||||
- **GeoJSON Support**: Parse and visualize GeoJSON data
|
||||
- **Path Optimization**: Trajectory and path optimization capabilities
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Apache 2.0 License.
|
||||
|
||||
## Modules
|
||||
|
||||
|
||||
### [demo](demo)
|
||||
>
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [maps-kt-compose](maps-kt-compose)
|
||||
> Compose-multiplaform implementation for web-mercator tiled maps
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
> **Maturity**: DEVELOPMENT
|
||||
>
|
||||
> **Features:**
|
||||
> - [osm](maps-kt-compose/#) : OpenStreetMap tile provider.
|
||||
@@ -33,19 +50,21 @@ This repository is a work-in-progress implementation of Map-with-markers compone
|
||||
|
||||
|
||||
### [maps-kt-features](maps-kt-features)
|
||||
>
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [maps-kt-geojson](maps-kt-geojson)
|
||||
>
|
||||
> GeoJson format support
|
||||
>
|
||||
> **Maturity**: DEVELOPMENT
|
||||
|
||||
### [maps-kt-geotools](maps-kt-geotools)
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [maps-kt-scheme](maps-kt-scheme)
|
||||
>
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
> **Maturity**: DEVELOPMENT
|
||||
|
||||
### [trajectory-kt](trajectory-kt)
|
||||
> Path and trajectory optimization
|
||||
@@ -53,21 +72,22 @@ This repository is a work-in-progress implementation of Map-with-markers compone
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [demo/maps](demo/maps)
|
||||
>
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [demo/maps-wasm](demo/maps-wasm)
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [demo/polygon-editor](demo/polygon-editor)
|
||||
>
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [demo/scheme](demo/scheme)
|
||||
>
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
### [demo/trajectory-playground](demo/trajectory-playground)
|
||||
>
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import space.kscience.gradle.isInDevelopment
|
||||
import space.kscience.gradle.useApache2Licence
|
||||
import space.kscience.gradle.useSPCTeam
|
||||
|
||||
@@ -6,16 +5,13 @@ plugins {
|
||||
id("space.kscience.gradle.project")
|
||||
}
|
||||
|
||||
val kmathVersion: String by extra("0.3.1-dev-RC")
|
||||
|
||||
allprojects {
|
||||
group = "center.sciprog"
|
||||
version = "0.2.2"
|
||||
group = "space.kscience"
|
||||
version = "0.4.0-dev-7"
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
maven("https://repo.kotlin.link")
|
||||
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,20 +20,12 @@ ksciencePublish {
|
||||
useApache2Licence()
|
||||
useSPCTeam()
|
||||
}
|
||||
github("SciProgCentre", "maps-kt")
|
||||
space(
|
||||
if (isInDevelopment) {
|
||||
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
|
||||
} else {
|
||||
"https://maven.pkg.jetbrains.space/spc/p/sci/maven"
|
||||
}
|
||||
)
|
||||
sonatype()
|
||||
repository("spc","https://maven.sciprog.center/kscience")
|
||||
sonatype("https://oss.sonatype.org")
|
||||
}
|
||||
|
||||
subprojects {
|
||||
repositories {
|
||||
maven("https://maven.pkg.jetbrains.space/mipt-npm/p/sci/dev")
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://repo.kotlin.link")
|
||||
@@ -48,4 +36,3 @@ subprojects {
|
||||
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
|
||||
|
||||
|
||||
|
||||
38
demo/maps-wasm/build.gradle.kts
Normal file
38
demo/maps-wasm/build.gradle.kts
Normal file
@@ -0,0 +1,38 @@
|
||||
@file:OptIn(ExperimentalWasmDsl::class)
|
||||
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
alias(spclibs.plugins.compose.compiler)
|
||||
alias(spclibs.plugins.compose.jb)
|
||||
}
|
||||
|
||||
//val ktorVersion: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
wasmJs {
|
||||
browser()
|
||||
binaries.executable()
|
||||
}
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
|
||||
api(compose.components.resources)
|
||||
}
|
||||
}
|
||||
|
||||
wasmJsMain {
|
||||
dependencies {
|
||||
implementation(projects.mapsKtScheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compose {
|
||||
web {
|
||||
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 469 KiB 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(ExperimentalResourceApi::class, ExperimentalComposeUiApi::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.FeatureStore
|
||||
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 = FeatureStore.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,22 +2,35 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("org.jetbrains.compose")
|
||||
alias(spclibs.plugins.compose.compiler)
|
||||
alias(spclibs.plugins.compose.jb)
|
||||
}
|
||||
|
||||
val ktorVersion: String by rootProject.extra
|
||||
repositories {
|
||||
maven("https://repo.osgeo.org/repository/release/")
|
||||
exclusiveContent {
|
||||
forRepository {
|
||||
maven("https://repo.osgeo.org/repository/release/")
|
||||
}
|
||||
filter {
|
||||
includeGroup("javax.media")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(11)
|
||||
jvmToolchain(17)
|
||||
jvm()
|
||||
sourceSets {
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation(projects.mapsKtCompose)
|
||||
implementation(projects.mapsKtGeojson)
|
||||
implementation(projects.mapsKtGeotools)
|
||||
implementation(compose.desktop.currentOs)
|
||||
|
||||
implementation("io.ktor:ktor-client-cio")
|
||||
implementation("ch.qos.logback:logback-classic:1.2.11")
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
}
|
||||
val jvmTest by getting
|
||||
|
||||
@@ -13,11 +13,7 @@ import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import center.sciprog.attributes.Attributes
|
||||
import center.sciprog.maps.compose.*
|
||||
import center.sciprog.maps.coordinates.*
|
||||
import center.sciprog.maps.features.*
|
||||
import center.sciprog.maps.geojson.geoJson
|
||||
import center.sciprog.maps.geotools.geoTiff
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -27,15 +23,22 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.kmath.geometry.degrees
|
||||
import space.kscience.kmath.geometry.radians
|
||||
import space.kscience.maps.compose.*
|
||||
import space.kscience.maps.coordinates.GeodeticMapCoordinates
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.coordinates.kilometers
|
||||
import space.kscience.maps.features.*
|
||||
import space.kscience.maps.geojson.geoJson
|
||||
import java.nio.file.Path
|
||||
import kotlin.math.PI
|
||||
import kotlin.random.Random
|
||||
|
||||
public fun GeodeticMapCoordinates.toShortString(): String =
|
||||
"${(latitude.degrees).toString().take(6)}:${(longitude.degrees).toString().take(6)}"
|
||||
"${(latitude.toDegrees().value).toString().take(6)}:${(longitude.toDegrees().value).toString().take(6)}"
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -55,7 +58,6 @@ fun App() {
|
||||
|
||||
val centerCoordinates = MutableStateFlow<Gmc?>(null)
|
||||
|
||||
|
||||
val pointOne = 55.568548 to 37.568604
|
||||
val pointTwo = 55.929444 to 37.518434
|
||||
// val pointThree = 60.929444 to 37.518434
|
||||
@@ -71,14 +73,21 @@ fun App() {
|
||||
) {
|
||||
|
||||
geoJson(javaClass.getResource("/moscow.geo.json")!!)
|
||||
.modifyAttribute(ColorAttribute, Color.Blue)
|
||||
.modifyAttribute(AlphaAttribute, 0.4f)
|
||||
.color(Color.Blue)
|
||||
.alpha(0.4f)
|
||||
|
||||
|
||||
geoTiff(geoTiffStream = { javaClass.getResourceAsStream("/wind_direction.tif")!! })
|
||||
.alpha(0.2f)
|
||||
|
||||
icon(pointOne, Icons.Filled.Home)
|
||||
|
||||
val marker1 = rectangle(55.744 to 38.614, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
|
||||
val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
|
||||
val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
|
||||
val marker1 = rectangle(55.744 to 38.614, size = DpSize(10.dp, 10.dp))
|
||||
.color(Color.Magenta)
|
||||
val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp))
|
||||
.color(Color.Magenta)
|
||||
val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp))
|
||||
.color(Color.Magenta)
|
||||
|
||||
draggableLine(marker1, marker2, id = "line 1").color(Color.Red).onClick {
|
||||
println("line 1 clicked")
|
||||
@@ -90,6 +99,7 @@ fun App() {
|
||||
println("line 3 clicked")
|
||||
}
|
||||
|
||||
|
||||
multiLine(
|
||||
points = listOf(
|
||||
55.742465 to 37.615812,
|
||||
@@ -101,7 +111,19 @@ fun App() {
|
||||
),
|
||||
)
|
||||
|
||||
//remember feature ID
|
||||
// points(
|
||||
// points = listOf(
|
||||
// 55.744 to 38.614,
|
||||
// 55.8 to 38.5,
|
||||
// 56.0 to 38.5,
|
||||
// )
|
||||
// ).pointSize(5f)
|
||||
|
||||
// geodeticLine(Gmc.ofDegrees(40.7128, -74.0060), Gmc.ofDegrees(55.742465, 37.615812)).color(Color.Blue)
|
||||
// line(Gmc.ofDegrees(40.7128, -74.0060), Gmc.ofDegrees(55.742465, 37.615812))
|
||||
|
||||
|
||||
//remember feature ref
|
||||
val circleId = circle(
|
||||
centerCoordinates = pointTwo,
|
||||
)
|
||||
@@ -120,9 +142,11 @@ fun App() {
|
||||
|
||||
arc(pointOne, 10.0.kilometers, (PI / 4).radians, -Angle.pi / 2)
|
||||
|
||||
|
||||
line(pointOne, pointTwo, id = "line")
|
||||
text(pointOne, "Home", font = { size = 32f })
|
||||
|
||||
|
||||
pixelMap(
|
||||
space.Rectangle(
|
||||
Gmc(latitude = 55.58461879539754.degrees, longitude = 37.8746197303493.degrees),
|
||||
@@ -132,12 +156,14 @@ fun App() {
|
||||
0.005.degrees
|
||||
) { gmc ->
|
||||
Color(
|
||||
red = ((gmc.latitude + Angle.piDiv2).degrees*10 % 1f).toFloat(),
|
||||
green = ((gmc.longitude + Angle.pi).degrees*10 % 1f).toFloat(),
|
||||
blue = 0f
|
||||
).copy(alpha = 0.3f)
|
||||
red = ((gmc.latitude + Angle.piDiv2).toDegrees().value * 10 % 1f).toFloat(),
|
||||
green = ((gmc.longitude + Angle.pi).toDegrees().value * 10 % 1f).toFloat(),
|
||||
blue = 0f,
|
||||
alpha = 0.3f
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
centerCoordinates.filterNotNull().onEach {
|
||||
group(id = "center") {
|
||||
circle(center = it, id = "circle", size = 1.dp).color(Color.Blue)
|
||||
@@ -146,19 +172,20 @@ fun App() {
|
||||
}.launchIn(scope)
|
||||
|
||||
//Add click listeners for all polygons
|
||||
forEachWithType<Gmc, PolygonFeature<Gmc>> { ref ->
|
||||
forEachWithType<Gmc, PolygonFeature<Gmc>> { ref, polygon: PolygonFeature<Gmc> ->
|
||||
ref.onClick(PointerMatcher.Primary) {
|
||||
println("Click on ${ref.id}")
|
||||
println("Click on $ref")
|
||||
//draw in top-level scope
|
||||
with(this@MapView) {
|
||||
multiLine(
|
||||
ref.resolve().points,
|
||||
polygon.points,
|
||||
attributes = Attributes(ZAttribute, 10f),
|
||||
id = "selected",
|
||||
).modifyAttribute(StrokeAttribute, 4f).color(Color.Magenta)
|
||||
}
|
||||
}
|
||||
}
|
||||
// println(toPrettyString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
BIN
demo/maps/src/jvmMain/resources/wind_direction.tif
Normal file
BIN
demo/maps/src/jvmMain/resources/wind_direction.tif
Normal file
Binary file not shown.
@@ -2,14 +2,15 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("org.jetbrains.compose")
|
||||
alias(spclibs.plugins.compose.compiler)
|
||||
alias(spclibs.plugins.compose.jb)
|
||||
}
|
||||
|
||||
val ktorVersion: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvmToolchain(11)
|
||||
jvmToolchain(17)
|
||||
sourceSets {
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
|
||||
@@ -9,11 +9,11 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.input.pointer.isSecondaryPressed
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import center.sciprog.maps.features.*
|
||||
import center.sciprog.maps.scheme.SchemeView
|
||||
import center.sciprog.maps.scheme.XY
|
||||
import center.sciprog.maps.scheme.XYCoordinateSpace
|
||||
import center.sciprog.maps.scheme.XYViewScope
|
||||
import space.kscience.maps.features.*
|
||||
import space.kscience.maps.scheme.SchemeView
|
||||
import space.kscience.maps.scheme.XY
|
||||
import space.kscience.maps.scheme.XYCanvasState
|
||||
import space.kscience.maps.scheme.XYCoordinateSpace
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
@@ -24,14 +24,14 @@ fun App() {
|
||||
|
||||
val myPolygon: SnapshotStateList<XY> = remember { mutableStateListOf<XY>() }
|
||||
|
||||
val featureState: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) {
|
||||
val featureState = FeatureStore.remember(XYCoordinateSpace) {
|
||||
multiLine(
|
||||
listOf(XY(0f, 0f), XY(0f, 1f), XY(1f, 1f), XY(1f, 0f), XY(0f, 0f)),
|
||||
id = "frame"
|
||||
)
|
||||
}
|
||||
|
||||
val mapState: XYViewScope = XYViewScope.remember(
|
||||
val mapState: XYCanvasState = XYCanvasState.remember(
|
||||
config = ViewConfig<XY>(
|
||||
onClick = { event, point ->
|
||||
if (event.buttons.isSecondaryPressed) {
|
||||
@@ -55,6 +55,7 @@ fun App() {
|
||||
}
|
||||
draggableMultiLine(
|
||||
pointRefs + pointRefs.first(),
|
||||
"line"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,30 +2,35 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("org.jetbrains.compose")
|
||||
alias(spclibs.plugins.compose.compiler)
|
||||
alias(spclibs.plugins.compose.jb)
|
||||
}
|
||||
|
||||
val ktorVersion: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvmToolchain(11)
|
||||
jvmToolchain(17)
|
||||
sourceSets {
|
||||
val jvmMain by getting {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(projects.mapsKtScheme)
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation("ch.qos.logback:logback-classic:1.2.11")
|
||||
implementation(compose.components.resources)
|
||||
}
|
||||
}
|
||||
|
||||
jvmMain{
|
||||
dependencies {
|
||||
implementation(projects.mapsKtScheme)
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
}
|
||||
val jvmTest by getting
|
||||
}
|
||||
}
|
||||
|
||||
compose{
|
||||
compose {
|
||||
desktop {
|
||||
application {
|
||||
mainClass = "MainKt"
|
||||
//mainClass = "Joker2023Kt"
|
||||
nativeDistributions {
|
||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||
packageName = "scheme-compose-demo"
|
||||
@@ -33,4 +38,8 @@ compose{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resources {
|
||||
generateResClass = always
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 968 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 469 KiB |
@@ -4,22 +4,22 @@ import androidx.compose.foundation.ContextMenuArea
|
||||
import androidx.compose.foundation.ContextMenuItem
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import center.sciprog.maps.features.FeatureGroup
|
||||
import center.sciprog.maps.features.ViewConfig
|
||||
import center.sciprog.maps.features.ViewPoint
|
||||
import center.sciprog.maps.features.color
|
||||
import center.sciprog.maps.scheme.*
|
||||
import center.sciprog.maps.svg.FeatureStateSnapshot
|
||||
import center.sciprog.maps.svg.exportToSvg
|
||||
import center.sciprog.maps.svg.snapshot
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.maps.features.*
|
||||
import space.kscience.maps.scheme.*
|
||||
import space.kscience.maps.svg.exportToPng
|
||||
import space.kscience.maps.svg.exportToSvg
|
||||
import space.kscience.scheme.generated.resources.Res
|
||||
import space.kscience.scheme.generated.resources.middle_earth
|
||||
import java.awt.Desktop
|
||||
import java.nio.file.Files
|
||||
|
||||
@@ -29,8 +29,8 @@ fun App() {
|
||||
MaterialTheme {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val schemeFeaturesState: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) {
|
||||
background(1600f, 1200f) { painterResource("middle-earth.jpg") }
|
||||
val features = FeatureStore.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)
|
||||
@@ -53,32 +53,40 @@ fun App() {
|
||||
}
|
||||
|
||||
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 snapshot: FeatureStateSnapshot<XY>? by remember { mutableStateOf(null) }
|
||||
val painterCache = features.pointerCache()
|
||||
|
||||
if (snapshot == null) {
|
||||
snapshot = schemeFeaturesState.snapshot()
|
||||
}
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
|
||||
ContextMenuArea(
|
||||
items = {
|
||||
listOf(
|
||||
ContextMenuItem("Export to SVG") {
|
||||
snapshot?.let {
|
||||
val path = Files.createTempFile("scheme-kt-", ".svg")
|
||||
it.exportToSvg(viewPoint, 800.0, 800.0, path)
|
||||
println(path.toFile())
|
||||
Desktop.getDesktop().browse(path.toFile().toURI())
|
||||
}
|
||||
val path = Files.createTempFile("scheme-kt-", ".svg")
|
||||
features.exportToSvg(viewPoint, painterCache, Size(800f, 800f), path)
|
||||
println(path.toFile())
|
||||
Desktop.getDesktop().browse(path.toFile().toURI())
|
||||
},
|
||||
ContextMenuItem("Export to PNG") {
|
||||
val path = Files.createTempFile("scheme-kt-", ".png")
|
||||
features.exportToPng(
|
||||
viewPoint,
|
||||
painterCache,
|
||||
textMeasurer,
|
||||
Size(800f, 800f),
|
||||
path
|
||||
)
|
||||
println(path.toFile())
|
||||
Desktop.getDesktop().browse(path.toFile().toURI())
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
val mapState: XYViewScope = XYViewScope.remember(
|
||||
val mapState: XYCanvasState = XYCanvasState.remember(
|
||||
ViewConfig(
|
||||
onClick = { _, click ->
|
||||
println("${click.focus.x}, ${click.focus.y}")
|
||||
@@ -90,7 +98,7 @@ fun App() {
|
||||
|
||||
SchemeView(
|
||||
mapState,
|
||||
schemeFeaturesState,
|
||||
features,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
83
demo/scheme/src/jvmMain/kotlin/joker2023.kt
Normal file
83
demo/scheme/src/jvmMain/kotlin/joker2023.kt
Normal file
@@ -0,0 +1,83 @@
|
||||
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.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import space.kscience.maps.features.*
|
||||
import space.kscience.maps.scheme.*
|
||||
import space.kscience.maps.scheme.XYCoordinateSpace.Rectangle
|
||||
import space.kscience.scheme.generated.resources.Res
|
||||
import space.kscience.scheme.generated.resources.SPC_logo
|
||||
import space.kscience.scheme.generated.resources.joker2023
|
||||
|
||||
|
||||
fun main() = application {
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Joker2023 demo",
|
||||
icon = painterResource(Res.drawable.SPC_logo)
|
||||
) {
|
||||
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(Res.drawable.joker2023) }
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("org.jetbrains.compose")
|
||||
alias(spclibs.plugins.compose.compiler)
|
||||
alias(spclibs.plugins.compose.jb)
|
||||
}
|
||||
|
||||
val ktorVersion: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvmToolchain(11)
|
||||
jvmToolchain(17)
|
||||
sourceSets {
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
|
||||
@@ -8,21 +8,21 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import center.sciprog.maps.features.*
|
||||
import center.sciprog.maps.scheme.SchemeView
|
||||
import center.sciprog.maps.scheme.XY
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.kmath.geometry.Circle2D
|
||||
import space.kscience.kmath.geometry.DoubleVector2D
|
||||
import space.kscience.kmath.geometry.Euclidean2DSpace
|
||||
import space.kscience.kmath.geometry.Vector2D
|
||||
import space.kscience.kmath.geometry.euclidean2d.Circle2D
|
||||
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
|
||||
import space.kscience.maps.features.*
|
||||
import space.kscience.maps.scheme.SchemeView
|
||||
import space.kscience.maps.scheme.XY
|
||||
import space.kscience.trajectory.*
|
||||
import kotlin.random.Random
|
||||
|
||||
private fun DoubleVector2D.toXY() = XY(x.toFloat(), y.toFloat())
|
||||
private fun Vector2D<out Number>.toXY() = XY(x.toFloat(), y.toFloat())
|
||||
|
||||
private val random = Random(123)
|
||||
|
||||
fun FeatureGroup<XY>.trajectory(
|
||||
fun FeatureBuilder<XY>.trajectory(
|
||||
trajectory: Trajectory2D,
|
||||
colorPicker: (Trajectory2D) -> Color = { Color.Blue },
|
||||
): FeatureRef<XY, FeatureGroup<XY>> = group {
|
||||
@@ -32,7 +32,7 @@ fun FeatureGroup<XY>.trajectory(
|
||||
bCoordinates = trajectory.end.toXY(),
|
||||
).color(colorPicker(trajectory))
|
||||
|
||||
is CircleTrajectory2D -> with(Euclidean2DSpace) {
|
||||
is CircleTrajectory2D -> with(Float64Space2D) {
|
||||
val topLeft = trajectory.circle.center + vector(-trajectory.circle.radius, trajectory.circle.radius)
|
||||
val bottomRight = trajectory.circle.center + vector(trajectory.circle.radius, -trajectory.circle.radius)
|
||||
|
||||
@@ -54,25 +54,25 @@ fun FeatureGroup<XY>.trajectory(
|
||||
}
|
||||
}
|
||||
|
||||
fun FeatureGroup<XY>.obstacle(obstacle: Obstacle, colorPicker: (Trajectory2D) -> Color = { Color.Red }) {
|
||||
fun FeatureBuilder<XY>.obstacle(obstacle: Obstacle, colorPicker: (Trajectory2D) -> Color = { Color.Red }) {
|
||||
trajectory(obstacle.circumvention, colorPicker)
|
||||
polygon(obstacle.arcs.map { it.center.toXY() }).color(Color.Gray)
|
||||
}
|
||||
|
||||
fun FeatureGroup<XY>.pose(pose2D: Pose2D) = with(Euclidean2DSpace) {
|
||||
fun FeatureBuilder<XY>.pose(pose2D: Pose2D) = with(Float64Space2D) {
|
||||
line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY())
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun closePoints() {
|
||||
fun closePoints() = with(Float64Space2D){
|
||||
SchemeView {
|
||||
|
||||
val obstacle = Obstacle(
|
||||
Circle2D(Euclidean2DSpace.vector(0.0, 0.0), 1.0),
|
||||
Circle2D(Euclidean2DSpace.vector(0.0, 1.0), 1.0),
|
||||
Circle2D(Euclidean2DSpace.vector(1.0, 1.0), 1.0),
|
||||
Circle2D(Euclidean2DSpace.vector(1.0, 0.0), 1.0)
|
||||
Circle2D(vector(0.0, 0.0), 1.0),
|
||||
Circle2D(vector(0.0, 1.0), 1.0),
|
||||
Circle2D(vector(1.0, 1.0), 1.0),
|
||||
Circle2D(vector(1.0, 0.0), 1.0)
|
||||
)
|
||||
val enter = Pose2D(-0.8, -0.8, Angle.pi)
|
||||
val exit = Pose2D(-0.8, -0.8, Angle.piDiv2)
|
||||
@@ -101,7 +101,7 @@ fun closePoints() {
|
||||
@Preview
|
||||
fun singleObstacle() {
|
||||
SchemeView {
|
||||
val obstacle = Obstacle(Circle2D(Euclidean2DSpace.vector(7.0, 1.0), 5.0))
|
||||
val obstacle = Obstacle(Circle2D(Float64Space2D.vector(7.0, 1.0), 5.0))
|
||||
val enter = Pose2D(-5, -1, Angle.pi / 4)
|
||||
val exit = Pose2D(20, 4, Angle.pi * 3 / 4)
|
||||
|
||||
@@ -123,19 +123,19 @@ fun singleObstacle() {
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun doubleObstacle() {
|
||||
fun doubleObstacle() = with(Float64Space2D){
|
||||
SchemeView {
|
||||
val obstacles = arrayOf(
|
||||
Obstacle(
|
||||
Circle2D(Euclidean2DSpace.vector(1.0, 6.5), 0.5),
|
||||
Circle2D(Euclidean2DSpace.vector(2.0, 1.0), 0.5),
|
||||
Circle2D(Euclidean2DSpace.vector(6.0, 0.0), 0.5),
|
||||
Circle2D(Euclidean2DSpace.vector(5.0, 5.0), 0.5)
|
||||
Circle2D(vector(1.0, 6.5), 0.5),
|
||||
Circle2D(vector(2.0, 1.0), 0.5),
|
||||
Circle2D(vector(6.0, 0.0), 0.5),
|
||||
Circle2D(vector(5.0, 5.0), 0.5)
|
||||
), Obstacle(
|
||||
Circle2D(Euclidean2DSpace.vector(10.0, 1.0), 0.5),
|
||||
Circle2D(Euclidean2DSpace.vector(16.0, 0.0), 0.5),
|
||||
Circle2D(Euclidean2DSpace.vector(14.0, 6.0), 0.5),
|
||||
Circle2D(Euclidean2DSpace.vector(9.0, 4.0), 0.5)
|
||||
Circle2D(vector(10.0, 1.0), 0.5),
|
||||
Circle2D(vector(16.0, 0.0), 0.5),
|
||||
Circle2D(vector(14.0, 6.0), 0.5),
|
||||
Circle2D(vector(9.0, 4.0), 0.5)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -157,6 +157,14 @@ fun doubleObstacle() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun singleElement() {
|
||||
SchemeView {
|
||||
points(listOf(XY(1f,1f)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
@@ -165,6 +173,7 @@ fun playground() {
|
||||
"Close starting points",
|
||||
"Single obstacle",
|
||||
"Two obstacles",
|
||||
"Single element"
|
||||
)
|
||||
|
||||
var currentExample by remember { mutableStateOf(examples.first()) }
|
||||
@@ -182,6 +191,7 @@ fun playground() {
|
||||
examples[0] -> closePoints()
|
||||
examples[1] -> singleObstacle()
|
||||
examples[2] -> doubleObstacle()
|
||||
examples[3] -> singleElement()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
docs/templates/ARTIFACT-TEMPLATE.md
vendored
17
docs/templates/ARTIFACT-TEMPLATE.md
vendored
@@ -1,17 +0,0 @@
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `${group}:${name}:${version}`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
repositories {
|
||||
maven("https://repo.kotlin.link")
|
||||
mavenCentral()
|
||||
// development and snapshot versions
|
||||
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("${group}:${name}:${version}")
|
||||
}
|
||||
```
|
||||
22
docs/templates/README-TEMPLATE.md
vendored
22
docs/templates/README-TEMPLATE.md
vendored
@@ -1,9 +1,27 @@
|
||||
# Maps-kt
|
||||
|
||||
This repository is a work-in-progress implementation of Map-with-markers component for Compose-Multiplatform
|
||||
A Kotlin Multiplatform library for interactive maps and geospatial data visualization using Compose Multiplatform.
|
||||
|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
Maps-kt provides a comprehensive set of tools for working with maps, geospatial data, and cartographic projections in Kotlin. It offers a UI-agnostic core with Compose Multiplatform implementations, allowing you to create interactive maps with markers, layers, and custom visualizations across multiple platforms.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiplatform Support**: Works on JVM, JavaScript, Native, and WebAssembly platforms
|
||||
- **Compose Integration**: Seamless integration with Compose Multiplatform for modern UI development
|
||||
- **Map Projections**: Support for Mercator, Web Mercator, and other cartographic projections
|
||||
- **Geospatial Data**: Tools for working with coordinates, distances, angles, and ellipsoid geometry
|
||||
- **Tile Providers**: Integration with OpenStreetMap and other tile providers
|
||||
- **GeoJSON Support**: Parse and visualize GeoJSON data
|
||||
- **Path Optimization**: Trajectory and path optimization capabilities
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Apache 2.0 License.
|
||||
|
||||
## Modules
|
||||
|
||||
${modules}
|
||||
${modules}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
kotlin.code.style=official
|
||||
|
||||
compose.version=1.4.0
|
||||
agp.version=7.4.2
|
||||
android.useAndroidX=true
|
||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
||||
|
||||
org.gradle.jvmargs=-Xmx4096m
|
||||
|
||||
toolsVersion=0.14.6-kotlin-1.8.20
|
||||
toolsVersion=0.17.1-kotlin-2.1.20
|
||||
13
gradle/libs.versions.toml
Normal file
13
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[versions]
|
||||
|
||||
kmath = "0.4.2"
|
||||
geotools = "32.2"
|
||||
attributes = "0.3.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
kmath-geometry = { module = "space.kscience:kmath-geometry", version.ref = "kmath" }
|
||||
gt-geotiff = { module = "org.geotools:gt-geotiff", version.ref = "geotools" }
|
||||
gt-shapefile = { module = "org.geotools:gt-shapefile", version.ref = "geotools" }
|
||||
gt-epsg-hsql = { module = "org.geotools:gt-epsg-hsql", version.ref = "geotools" }
|
||||
attributes-serialization = { module = "space.kscience:attributes-kt-serialization", version.ref = "attributes" }
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -7,19 +7,8 @@ The core interfaces of KMath.
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `center.sciprog:maps-kt-compose:0.2.2`.
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-compose:0.4.0-dev-7`.
|
||||
|
||||
**Gradle Groovy:**
|
||||
```groovy
|
||||
repositories {
|
||||
maven { url 'https://repo.kotlin.link' }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'center.sciprog:maps-kt-compose:0.2.2'
|
||||
}
|
||||
```
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
repositories {
|
||||
@@ -28,6 +17,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("center.sciprog:maps-kt-compose:0.2.2")
|
||||
implementation("space.kscience:maps-kt-compose:0.4.0-dev-7")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,43 +1,40 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
id("org.jetbrains.compose")
|
||||
alias(spclibs.plugins.compose.compiler)
|
||||
alias(spclibs.plugins.compose.jb)
|
||||
// id("com.android.library")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
kscience{
|
||||
kscience {
|
||||
jvm()
|
||||
}
|
||||
wasm()
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api(projects.mapsKtCore)
|
||||
api(projects.mapsKtFeatures)
|
||||
api(compose.foundation)
|
||||
api(project.dependencies.platform(spclibs.ktor.bom))
|
||||
api("io.ktor:ktor-client-core")
|
||||
api("io.github.microutils:kotlin-logging:2.1.23")
|
||||
}
|
||||
}
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-client-cio")
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(spclibs.kotlinx.coroutines.test)
|
||||
useCoroutines()
|
||||
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
}
|
||||
commonMain{
|
||||
api(projects.mapsKtCore)
|
||||
api(projects.mapsKtFeatures)
|
||||
api(dependencies.platform(spclibs.ktor.bom))
|
||||
api(compose.foundation)
|
||||
}
|
||||
jvmMain{
|
||||
api("io.ktor:ktor-client-cio")
|
||||
}
|
||||
jvmTest{
|
||||
implementation(spclibs.kotlinx.coroutines.test)
|
||||
|
||||
implementation(spclibs.logback.classic)
|
||||
|
||||
implementation(compose.desktop.currentOs)
|
||||
}
|
||||
}
|
||||
|
||||
readme {
|
||||
description = "Compose-multiplaform implementation for web-mercator tiled maps"
|
||||
maturity = space.kscience.gradle.Maturity.EXPERIMENTAL
|
||||
propertyByTemplate("artifact", rootProject.file("docs/templates/ARTIFACT-TEMPLATE.md"))
|
||||
maturity = space.kscience.gradle.Maturity.DEVELOPMENT
|
||||
|
||||
feature(
|
||||
id = "osm",
|
||||
) { "OpenStreetMap tile provider." }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,143 +0,0 @@
|
||||
package center.sciprog.maps.compose
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.coordinates.Distance
|
||||
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.coordinates.GmcCurve
|
||||
import center.sciprog.maps.features.*
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import kotlin.math.ceil
|
||||
|
||||
|
||||
internal fun FeatureGroup<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
|
||||
GeodeticMapCoordinates.ofDegrees(pair.first.toDouble(), pair.second.toDouble())
|
||||
|
||||
public typealias MapFeature = Feature<Gmc>
|
||||
|
||||
public fun FeatureGroup<Gmc>.circle(
|
||||
centerCoordinates: Pair<Number, Number>,
|
||||
size: Dp = 5.dp,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, CircleFeature<Gmc>> = feature(
|
||||
id, CircleFeature(space, coordinatesOf(centerCoordinates), size)
|
||||
)
|
||||
|
||||
public fun FeatureGroup<Gmc>.rectangle(
|
||||
centerCoordinates: Pair<Number, Number>,
|
||||
size: DpSize = DpSize(5.dp, 5.dp),
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, RectangleFeature<Gmc>> = feature(
|
||||
id, RectangleFeature(space, coordinatesOf(centerCoordinates), size)
|
||||
)
|
||||
|
||||
|
||||
public fun FeatureGroup<Gmc>.draw(
|
||||
position: Pair<Number, Number>,
|
||||
id: String? = null,
|
||||
draw: DrawScope.() -> Unit,
|
||||
): FeatureRef<Gmc, DrawFeature<Gmc>> = feature(
|
||||
id,
|
||||
DrawFeature(space, coordinatesOf(position), drawFeature = draw)
|
||||
)
|
||||
|
||||
|
||||
public fun FeatureGroup<Gmc>.line(
|
||||
curve: GmcCurve,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, LineFeature<Gmc>> = feature(
|
||||
id,
|
||||
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates)
|
||||
)
|
||||
|
||||
|
||||
public fun FeatureGroup<Gmc>.line(
|
||||
aCoordinates: Pair<Double, Double>,
|
||||
bCoordinates: Pair<Double, Double>,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, LineFeature<Gmc>> = feature(
|
||||
id,
|
||||
LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates))
|
||||
)
|
||||
|
||||
|
||||
public fun FeatureGroup<Gmc>.arc(
|
||||
center: Pair<Double, Double>,
|
||||
radius: Distance,
|
||||
startAngle: Angle,
|
||||
arcLength: Angle,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, ArcFeature<Gmc>> = feature(
|
||||
id,
|
||||
ArcFeature(
|
||||
space,
|
||||
oval = space.Rectangle(coordinatesOf(center), radius, radius),
|
||||
startAngle = startAngle,
|
||||
arcLength = arcLength,
|
||||
)
|
||||
)
|
||||
|
||||
public fun FeatureGroup<Gmc>.points(
|
||||
points: List<Pair<Double, Double>>,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, PointsFeature<Gmc>> = feature(id, PointsFeature(space, points.map(::coordinatesOf)))
|
||||
|
||||
public fun FeatureGroup<Gmc>.multiLine(
|
||||
points: List<Pair<Double, Double>>,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, MultiLineFeature<Gmc>> = feature(id, MultiLineFeature(space, points.map(::coordinatesOf)))
|
||||
|
||||
public fun FeatureGroup<Gmc>.icon(
|
||||
position: Pair<Double, Double>,
|
||||
image: ImageVector,
|
||||
size: DpSize = DpSize(20.dp, 20.dp),
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, VectorIconFeature<Gmc>> = feature(
|
||||
id,
|
||||
VectorIconFeature(
|
||||
space,
|
||||
coordinatesOf(position),
|
||||
size,
|
||||
image,
|
||||
)
|
||||
)
|
||||
|
||||
public fun FeatureGroup<Gmc>.text(
|
||||
position: Pair<Double, Double>,
|
||||
text: String,
|
||||
font: FeatureFont.() -> Unit = { size = 16f },
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, TextFeature<Gmc>> = feature(
|
||||
id,
|
||||
TextFeature(space, coordinatesOf(position), text, fontConfig = font)
|
||||
)
|
||||
|
||||
public fun FeatureGroup<Gmc>.pixelMap(
|
||||
rectangle: Rectangle<Gmc>,
|
||||
latitudeDelta: Angle,
|
||||
longitudeDelta: Angle,
|
||||
id: String? = null,
|
||||
builder: (Gmc) -> Color?,
|
||||
): FeatureRef<Gmc, PixelMapFeature<Gmc>> = feature(
|
||||
id,
|
||||
PixelMapFeature(
|
||||
space,
|
||||
rectangle,
|
||||
Structure2D(
|
||||
ceil(rectangle.longitudeDelta / latitudeDelta).toInt(),
|
||||
ceil(rectangle.latitudeDelta / longitudeDelta).toInt()
|
||||
|
||||
) { (i, j) ->
|
||||
val longitude = rectangle.left + longitudeDelta * i
|
||||
val latitude = rectangle.bottom + latitudeDelta * j
|
||||
builder(
|
||||
Gmc(latitude, longitude)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
package center.sciprog.maps.compose
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.features.Rectangle
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.kmath.geometry.abs
|
||||
import space.kscience.maps.coordinates.GeodeticMapCoordinates
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.features.Rectangle
|
||||
|
||||
internal fun Angle.isBetween(a: Angle, b: Angle) = this in a..b || this in b..a
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package center.sciprog.maps.compose
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
import kotlin.jvm.Synchronized
|
||||
package space.kscience.maps.compose
|
||||
|
||||
|
||||
internal class LruCache<K, V>(
|
||||
@@ -8,8 +8,7 @@ internal class LruCache<K, V>(
|
||||
) {
|
||||
private val cache = linkedMapOf<K, V>()
|
||||
|
||||
@Synchronized
|
||||
fun put(key: K, value: V){
|
||||
fun put(key: K, value: V) {
|
||||
if (cache.size >= capacity) {
|
||||
cache.remove(cache.iterator().next().key)
|
||||
}
|
||||
@@ -25,15 +24,12 @@ internal class LruCache<K, V>(
|
||||
return value
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun remove(key: K) {
|
||||
cache.remove(key)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getOrPut(key: K, factory: () -> V): V = get(key) ?: factory().also { put(key, it) }
|
||||
|
||||
@Synchronized
|
||||
fun reset(newCapacity: Int? = null) {
|
||||
cache.clear()
|
||||
capacity = newCapacity ?: capacity
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.compose
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -6,18 +6,39 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpRect
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.coordinates.MercatorProjection
|
||||
import center.sciprog.maps.coordinates.WebMercatorCoordinates
|
||||
import center.sciprog.maps.coordinates.WebMercatorProjection
|
||||
import center.sciprog.maps.features.*
|
||||
import space.kscience.kmath.geometry.radians
|
||||
import space.kscience.maps.compose.MapCanvasState.Companion.remember
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.coordinates.MercatorProjection
|
||||
import space.kscience.maps.coordinates.WebMercatorCoordinates
|
||||
import space.kscience.maps.coordinates.WebMercatorProjection
|
||||
import space.kscience.maps.features.*
|
||||
import kotlin.math.*
|
||||
|
||||
public class MapViewScope internal constructor(
|
||||
public val mapTileProvider: MapTileProvider,
|
||||
|
||||
/**
|
||||
* Represents the state of a map canvas, extending the functionality of [CanvasState] to handle
|
||||
* map-specific operations.
|
||||
*
|
||||
* This class utilizes the Web Mercator projection for map rendering and provides utilities
|
||||
* to manage zoom levels, convert coordinates, and track view states.
|
||||
* It operates with
|
||||
* geodetic map coordinates (GMC) and simplifies interaction with the map's coordinate space.
|
||||
*
|
||||
* The class is internal to prevent direct instantiation; use the [remember] function to
|
||||
* create or get a [MapCanvasState] instance within a composable.
|
||||
*
|
||||
* @constructor
|
||||
* Creates an instance of [MapCanvasState] with the given configuration and default tile size
|
||||
* (used only for scale computation).
|
||||
*
|
||||
* @param config The configuration for view-related behaviors such as zoom, clicks, and canvas size changes.
|
||||
* @param tileSize The tile size used to compute scale, defaulting to [MapTileProvider.DEFAULT_TILE_SIZE].
|
||||
*/
|
||||
public class MapCanvasState internal constructor(
|
||||
config: ViewConfig<Gmc>,
|
||||
) : CoordinateViewScope<Gmc>(config) {
|
||||
public val tileSize: Int = MapTileProvider.DEFAULT_TILE_SIZE
|
||||
) : CanvasState<Gmc>(config) {
|
||||
override val space: CoordinateSpace<Gmc> get() = WebMercatorSpace
|
||||
|
||||
private val scaleFactor: Float
|
||||
@@ -60,10 +81,10 @@ public class MapViewScope internal constructor(
|
||||
override fun computeViewPoint(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> {
|
||||
val zoom = log2(
|
||||
min(
|
||||
canvasSize.width.value / rectangle.longitudeDelta.radians,
|
||||
canvasSize.height.value / rectangle.latitudeDelta.radians
|
||||
) * 2 * PI / mapTileProvider.tileSize
|
||||
)
|
||||
canvasSize.width.value / rectangle.longitudeDelta.toRadians().value,
|
||||
canvasSize.height.value / rectangle.latitudeDelta.toRadians().value
|
||||
) * 2 * PI / tileSize
|
||||
).coerceIn(0.0..22.0)
|
||||
return space.ViewPoint(rectangle.center, zoom.toFloat())
|
||||
}
|
||||
|
||||
@@ -83,18 +104,18 @@ public class MapViewScope internal constructor(
|
||||
public companion object {
|
||||
@Composable
|
||||
public fun remember(
|
||||
mapTileProvider: MapTileProvider,
|
||||
config: ViewConfig<Gmc> = ViewConfig(),
|
||||
initialViewPoint: ViewPoint<Gmc>? = null,
|
||||
initialRectangle: Rectangle<Gmc>? = null,
|
||||
): MapViewScope = remember {
|
||||
MapViewScope(mapTileProvider, config).also { mapState ->
|
||||
tileSize: Int = MapTileProvider.DEFAULT_TILE_SIZE,
|
||||
): MapCanvasState = remember {
|
||||
MapCanvasState(config, tileSize).apply {
|
||||
if (initialViewPoint != null) {
|
||||
mapState.viewPoint = initialViewPoint
|
||||
viewPoint = initialViewPoint
|
||||
} 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.Deferred
|
||||
@@ -16,6 +16,12 @@ public data class MapTile(
|
||||
val image: Image,
|
||||
)
|
||||
|
||||
/**
|
||||
* Interface representing a provider for map tiles.
|
||||
*
|
||||
* This interface defines the contract for asynchronous loading of map tiles and
|
||||
* utilities for mapping between tile indices and coordinates.
|
||||
*/
|
||||
public interface MapTileProvider {
|
||||
public fun CoroutineScope.loadTileAsync(tileId: TileId): Deferred<MapTile>
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
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.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.skia.Image
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.features.*
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
|
||||
private fun IntRange.intersect(other: IntRange) = max(first, other.first)..min(last, other.last)
|
||||
|
||||
private val logger = KotlinLogging.logger("MapView")
|
||||
|
||||
//private fun FeatureDrawScope<Gmc>.drawTiles(
|
||||
// tileProvider: MapTileProvider
|
||||
//) {
|
||||
//
|
||||
//}
|
||||
|
||||
|
||||
/**
|
||||
* A component that renders map and provides basic map manipulation capabilities
|
||||
*/
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Composable
|
||||
public fun MapView(
|
||||
mapState: MapCanvasState,
|
||||
featureStore: FeatureStore<Gmc>,
|
||||
modifier: Modifier,
|
||||
): Unit = with(mapState) {
|
||||
|
||||
|
||||
val tileFeatures by featureStore.featureFlow
|
||||
.map { it.values.filterIsInstance<TileFeature>() }
|
||||
.collectAsState(emptyList())
|
||||
|
||||
val allTiles: Map<TileFeature, SnapshotStateMap<TileId, Image>> = remember(tileFeatures) {
|
||||
tileFeatures.associateWith {
|
||||
mutableStateMapOf()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(viewPoint, canvasSize, tileFeatures) {
|
||||
allTiles.forEach { (tileFeature, tiles) ->
|
||||
// Load tiles asynchronously
|
||||
with(tileFeature.tileProvider) {
|
||||
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)
|
||||
launch {
|
||||
try {
|
||||
val tile = loadTileAsync(id).await()
|
||||
tiles[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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
tiles.keys.filter {
|
||||
it.zoom != intZoom || it.j !in verticalIndices || it.i !in horizontalIndices
|
||||
}.forEach {
|
||||
tiles.remove(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
FeatureCanvas(mapState, featureStore.featureFlow, modifier = modifier.canvasControls(mapState, featureStore)) {
|
||||
// draw custom features
|
||||
val tileScale = mapState.tileScale
|
||||
|
||||
allTiles.forEach { (feature, tiles) ->
|
||||
val tileProvider = feature.tileProvider
|
||||
clipRect {
|
||||
val tileSize = IntSize(
|
||||
ceil((tileProvider.tileSize.dp * tileScale).toPx()).toInt(),
|
||||
ceil((tileProvider.tileSize.dp * tileScale).toPx()).toInt()
|
||||
)
|
||||
tiles.forEach { (id, image) ->
|
||||
//converting back from tile index to screen offset
|
||||
val offset = IntOffset(
|
||||
(mapState.canvasSize.width / 2 + (tileProvider.toCoordinate(id.i).dp - mapState.centerCoordinates.x.dp) * tileScale).roundToPx(),
|
||||
(mapState.canvasSize.height / 2 + (tileProvider.toCoordinate(id.j).dp - mapState.centerCoordinates.y.dp) * tileScale).roundToPx()
|
||||
)
|
||||
drawImage(
|
||||
image = image.toComposeImageBitmap(),
|
||||
dstOffset = offset,
|
||||
dstSize = tileSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun MapView(
|
||||
mapState: MapCanvasState,
|
||||
mapTileProvider: MapTileProvider,
|
||||
featureStore: FeatureStore<Gmc>,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
//FIXME this function modifies arguments
|
||||
featureStore.feature("map", TileFeature(mapState.space, mapTileProvider, Attributes.EMPTY))
|
||||
MapView(mapState, featureStore, modifier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [MapView] with given [featureStore] group.
|
||||
*/
|
||||
@Composable
|
||||
public fun MapView(
|
||||
mapTileProvider: MapTileProvider,
|
||||
config: ViewConfig<Gmc>,
|
||||
featureStore: FeatureStore<Gmc>,
|
||||
initialViewPoint: ViewPoint<Gmc>? = null,
|
||||
initialRectangle: Rectangle<Gmc>? = null,
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val mapState = MapCanvasState.remember(config, initialViewPoint, initialRectangle)
|
||||
MapView(mapState, mapTileProvider, featureStore, 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: FeatureStore<Gmc>.() -> Unit = {},
|
||||
) {
|
||||
val featureState = FeatureStore.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 center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.coordinates.WebMercatorProjection
|
||||
import center.sciprog.maps.features.ViewPoint
|
||||
import space.kscience.maps.coordinates.GeodeticMapCoordinates
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.coordinates.WebMercatorProjection
|
||||
import space.kscience.maps.features.ViewPoint
|
||||
|
||||
/**
|
||||
* Observable position on the map. Includes observation coordinate and [zoom] factor
|
||||
@@ -0,0 +1,49 @@
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.features.*
|
||||
|
||||
/**
|
||||
* Represents a custom feature associated with a specific map tile provider.
|
||||
*
|
||||
* This feature is designed to handle map tiles using the specified [MapTileProvider]
|
||||
* and includes functionality for defining and modifying its attributes and spatial
|
||||
* characteristics.
|
||||
*
|
||||
* @property space Defines the coordinate space used for this feature, enabling
|
||||
* manipulation and operations in the map/scheme coordinate context.
|
||||
* @property tileProvider The provider responsible for asynchronous loading and
|
||||
* management of map tiles.
|
||||
* @property attributes A collection of attributes associated with this feature,
|
||||
* which can be modified through the `withAttributes` method.
|
||||
*/
|
||||
public data class TileFeature(
|
||||
override val space: CoordinateSpace<Gmc>,
|
||||
public val tileProvider: MapTileProvider,
|
||||
override val attributes: Attributes
|
||||
) : CustomFeature<Gmc> {
|
||||
override fun getBoundingBox(zoom: Float): Rectangle<Gmc>? = null
|
||||
|
||||
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<Gmc> =
|
||||
copy(attributes = modify(attributes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tile-based feature to the builder using the specified tile provider and optional identifier.
|
||||
*
|
||||
* @param tileProvider The provider responsible for asynchronous loading and management of map tiles.
|
||||
* @param id An optional string identifier for the feature. If null, a unique ID will be generated.
|
||||
* @return A reference to the created tile-based feature.
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.tiles(
|
||||
tileProvider: MapTileProvider,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, TileFeature> = feature(
|
||||
id,
|
||||
TileFeature(
|
||||
space,
|
||||
tileProvider,
|
||||
Attributes.EMPTY
|
||||
)
|
||||
)
|
||||
@@ -1,25 +1,33 @@
|
||||
package center.sciprog.maps.compose
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.coordinates.*
|
||||
import center.sciprog.maps.features.CoordinateSpace
|
||||
import center.sciprog.maps.features.Rectangle
|
||||
import center.sciprog.maps.features.ViewPoint
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.kmath.geometry.radians
|
||||
import space.kscience.maps.coordinates.*
|
||||
import space.kscience.maps.features.CoordinateSpace
|
||||
import space.kscience.maps.features.Rectangle
|
||||
import space.kscience.maps.features.ViewPoint
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* An implementation of the `CoordinateSpace` interface for the Web Mercator Projection.
|
||||
*
|
||||
* This object provides functionality for manipulating geodetic map coordinates (Gmc),
|
||||
* which includes operations like creating rectangles, computing offset and distance,
|
||||
* moving viewpoints, zooming, and polygon containment checks.
|
||||
*/
|
||||
public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
||||
|
||||
private fun intZoom(zoom: Float): Int = floor(zoom).toInt()
|
||||
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> {
|
||||
val scale = WebMercatorProjection.scaleFactor(zoom)
|
||||
@@ -62,7 +70,10 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
||||
val maxLat = maxOf { it.top }
|
||||
val minLong = minOf { it.left }
|
||||
val maxLong = maxOf { it.right }
|
||||
return GmcRectangle(Gmc.normalized(minLat, minLong), Gmc.normalized(maxLat, maxLong))
|
||||
return space.kscience.maps.compose.GmcRectangle(
|
||||
Gmc.normalized(minLat, minLong),
|
||||
Gmc.normalized(maxLat, maxLong)
|
||||
)
|
||||
}
|
||||
|
||||
override fun Collection<Gmc>.wrapPoints(): Rectangle<Gmc>? {
|
||||
@@ -72,7 +83,10 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
||||
val maxLat = maxOf { it.latitude }
|
||||
val minLong = minOf { it.longitude }
|
||||
val maxLong = maxOf { it.longitude }
|
||||
return GmcRectangle(Gmc.normalized(minLat, minLong), Gmc.normalized(maxLat, maxLong))
|
||||
return space.kscience.maps.compose.GmcRectangle(
|
||||
Gmc.normalized(minLat, minLong),
|
||||
Gmc.normalized(maxLat, maxLong)
|
||||
)
|
||||
}
|
||||
|
||||
override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset {
|
||||
@@ -119,6 +133,7 @@ public fun CoordinateSpace<Gmc>.Rectangle(
|
||||
/**
|
||||
* A quasi-square section.
|
||||
*/
|
||||
@Suppress("UnusedReceiverParameter")
|
||||
public fun CoordinateSpace<Gmc>.Rectangle(
|
||||
center: GeodeticMapCoordinates,
|
||||
height: Angle,
|
||||
@@ -132,5 +147,5 @@ public fun CoordinateSpace<Gmc>.Rectangle(
|
||||
center.latitude + (height / 2),
|
||||
center.longitude + (width / 2)
|
||||
)
|
||||
return GmcRectangle(a, b)
|
||||
return space.kscience.maps.compose.GmcRectangle(a, b)
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.skia.Font
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.maps.coordinates.*
|
||||
import space.kscience.maps.features.*
|
||||
import kotlin.math.ceil
|
||||
|
||||
|
||||
internal fun FeatureBuilder<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
|
||||
GeodeticMapCoordinates.ofDegrees(pair.first.toDouble(), pair.second.toDouble())
|
||||
|
||||
public typealias MapFeature = Feature<Gmc>
|
||||
|
||||
/**
|
||||
* Adds a circle feature to the feature builder.
|
||||
*
|
||||
* @param centerCoordinates The geodetic map coordinates (latitude, longitude) of the circle's center as a pair of numbers.
|
||||
* @param size The radius of the circle as a Dp value. Defaults to 5.dp.
|
||||
* @param id An optional unique identifier for the circle feature. If null, an ID is automatically generated.
|
||||
* @return A reference to the created circle feature within the feature store.
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.circle(
|
||||
centerCoordinates: Pair<Number, Number>,
|
||||
size: Dp = 5.dp,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, CircleFeature<Gmc>> = feature(
|
||||
id, CircleFeature(space, coordinatesOf(centerCoordinates), size)
|
||||
)
|
||||
|
||||
/**
|
||||
* Adds a rectangular feature to the map with the specified center coordinates and size.
|
||||
*
|
||||
* @param centerCoordinates The center coordinates of the rectangle as a pair of latitude and longitude values.
|
||||
* @param size The size of the rectangle, with a default value of 5.dp x 5.dp.
|
||||
* @param id An optional identifier for the rectangle. If null, a unique ID is generated automatically.
|
||||
* @return A reference to the created rectangle feature.
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.rectangle(
|
||||
centerCoordinates: Pair<Number, Number>,
|
||||
size: DpSize = DpSize(5.dp, 5.dp),
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, RectangleFeature<Gmc>> = feature(
|
||||
id, RectangleFeature(space, coordinatesOf(centerCoordinates), size)
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Draws a feature on the map at the specified position.
|
||||
*
|
||||
* @param position The geographical coordinates as a pair of numbers (latitude, longitude) where the feature will be drawn.
|
||||
* @param id Optional identifier for the feature. If null, a unique identifier will be generated.
|
||||
* @param draw The drawing logic defined within the scope of the [DrawScope].
|
||||
* @return A reference to the newly created draw feature.
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.draw(
|
||||
position: Pair<Number, Number>,
|
||||
id: String? = null,
|
||||
draw: DrawScope.() -> Unit,
|
||||
): FeatureRef<Gmc, DrawFeature<Gmc>> = feature(
|
||||
id,
|
||||
DrawFeature(space, coordinatesOf(position), drawFeature = draw)
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Adds a line feature to the feature builder using the specified curve and optional identifier.
|
||||
*
|
||||
* @param curve the geodetic curve defining the start and end points of the line
|
||||
* @param id the optional identifier for the line feature; if null, an identifier is automatically generated
|
||||
* @return a reference to the created line feature
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.line(
|
||||
curve: GmcCurve,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, LineFeature<Gmc>> = feature(
|
||||
id,
|
||||
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates)
|
||||
)
|
||||
|
||||
/**
|
||||
* A segmented geodetic curve
|
||||
*/
|
||||
public fun FeatureBuilder<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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a segmented geodetic line between two geodetic coordinates. The line is calculated
|
||||
* based on the given ellipsoid and segmented into smaller parts if its total length exceeds the
|
||||
* specified maximum line segment distance.
|
||||
*
|
||||
* @param from The starting geodetic coordinate for the line.
|
||||
* @param to The ending geodetic coordinate for the line.
|
||||
* @param ellipsoid The reference ellipsoid used for calculations. Defaults to `GeoEllipsoid.WGS84`.
|
||||
* @param maxLineDistance The maximum allowed distance for a single line segment. Defaults to 100 kilometers.
|
||||
* @param id An optional unique identifier for the resulting feature. If null, a unique ID is generated.
|
||||
* @return A reference to the created geodetic line feature.
|
||||
*/
|
||||
public fun FeatureBuilder<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)
|
||||
|
||||
/**
|
||||
* Creates a `LineFeature` with the specified start and end coordinates and adds it to the feature store.
|
||||
*
|
||||
* @param aCoordinates The coordinates of the starting point of the line as a pair of latitude and longitude.
|
||||
* @param bCoordinates The coordinates of the ending point of the line as a pair of latitude and longitude.
|
||||
* @param id An optional unique identifier for the line feature. If null, a unique id will be generated.
|
||||
* @return A reference to the created `LineFeature`.
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.line(
|
||||
aCoordinates: Pair<Double, Double>,
|
||||
bCoordinates: Pair<Double, Double>,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, LineFeature<Gmc>> = feature(
|
||||
id,
|
||||
LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates))
|
||||
)
|
||||
|
||||
/**
|
||||
* Adds an arc-shaped feature to the feature builder.
|
||||
*
|
||||
* @param center the geographical center of the arc represented as a pair of latitude and longitude in degrees
|
||||
* @param radius the radius of the arc in kilometers
|
||||
* @param startAngle the starting angle of the arc in radians, measured from the 3 o'clock position downwards
|
||||
* @param arcLength the angular extent of the arc in radians
|
||||
* @param id an optional identifier for the arc feature; if null, a unique identifier is generated
|
||||
* @return a reference to the added arc feature
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.arc(
|
||||
center: Pair<Double, Double>,
|
||||
radius: Distance,
|
||||
startAngle: Angle,
|
||||
arcLength: Angle,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, ArcFeature<Gmc>> = feature(
|
||||
id,
|
||||
ArcFeature(
|
||||
space,
|
||||
oval = space.Rectangle(coordinatesOf(center), radius, radius),
|
||||
startAngle = startAngle,
|
||||
arcLength = arcLength,
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Adds a points feature to the current feature builder.
|
||||
*
|
||||
* @param points a list of pairs where each pair represents the latitude and longitude of a point.
|
||||
* @param id an optional unique identifier for the feature. If null, a unique id is generated automatically.
|
||||
* @return a reference to the created points feature in the feature store.
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.points(
|
||||
points: List<Pair<Double, Double>>,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, PointsFeature<Gmc>> = feature(id, PointsFeature(space, points.map(::coordinatesOf)))
|
||||
|
||||
/**
|
||||
* Creates a MultiLineFeature composed of multiple line segments defined by the given points
|
||||
* and adds it to the feature store. If an ID is provided, the feature is associated with that ID;
|
||||
* otherwise, a unique ID is generated.
|
||||
*
|
||||
* @param points A list of pairs representing the points (latitude, longitude) that define the line segments.
|
||||
* @param id An optional string identifier for the feature. If null, a unique ID is automatically created.
|
||||
* @return A reference to the created MultiLineFeature.
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.multiLine(
|
||||
points: List<Pair<Double, Double>>,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, MultiLineFeature<Gmc>> = feature(id, MultiLineFeature(space, points.map(::coordinatesOf)))
|
||||
|
||||
/**
|
||||
* Adds a vector icon feature to the map with a specified position, size, and image.
|
||||
*
|
||||
* @param position The geographic coordinates (latitude, longitude) of the icon's center.
|
||||
* @param image The image to be displayed as the vector icon.
|
||||
* @param size The size of the icon, specified as a [DpSize]. Defaults to 20.dp x 20.dp.
|
||||
* @param id An optional unique identifier for the icon. If null, a unique ID is generated automatically.
|
||||
* @return A reference to the created vector icon feature as a [FeatureRef].
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.icon(
|
||||
position: Pair<Double, Double>,
|
||||
image: ImageVector,
|
||||
size: DpSize = DpSize(20.dp, 20.dp),
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, VectorIconFeature<Gmc>> = feature(
|
||||
id,
|
||||
VectorIconFeature(
|
||||
space,
|
||||
coordinatesOf(position),
|
||||
size,
|
||||
image,
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Adds a text feature to the FeatureBuilder with the specified position, content, and font configuration.
|
||||
*
|
||||
* @param position The geographic position of the text as a pair of latitude and longitude.
|
||||
* @param text The text content to be displayed.
|
||||
* @param font A lambda to configure text font properties. Default size is 16f.
|
||||
* @param id An optional identifier for the feature. If null, a unique ID is generated.
|
||||
* @return A reference to the added TextFeature within the FeatureBuilder.
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.text(
|
||||
position: Pair<Double, Double>,
|
||||
text: String,
|
||||
font: Font.() -> Unit = { size = 16f },
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, TextFeature<Gmc>> = feature(
|
||||
id,
|
||||
TextFeature(space, coordinatesOf(position), text, fontConfig = font)
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a pixel map feature within the specified geographic rectangle, using the provided latitude and longitude deltas to
|
||||
* define pixel granularity and a builder function to determine pixel colors.
|
||||
*
|
||||
* @param rectangle The geographic rectangle defining the bounds of the pixel map.
|
||||
* @param latitudeDelta The latitude increment between two rows of pixels.
|
||||
* @param longitudeDelta The longitude increment between two columns of pixels.
|
||||
* @param id The optional identifier for the feature. If null, a unique ID will be generated.
|
||||
* @param builder A function that takes [Gmc] coordinates and returns the color for the corresponding pixel, or null for transparency.
|
||||
* @return A reference to the pixel map feature, encapsulating its boundaries and content.
|
||||
*/
|
||||
public fun FeatureBuilder<Gmc>.pixelMap(
|
||||
rectangle: Rectangle<Gmc>,
|
||||
latitudeDelta: Angle,
|
||||
longitudeDelta: Angle,
|
||||
id: String? = null,
|
||||
builder: (Gmc) -> Color?,
|
||||
): FeatureRef<Gmc, PixelMapFeature<Gmc>> = feature(
|
||||
id,
|
||||
PixelMapFeature(
|
||||
space,
|
||||
rectangle,
|
||||
Structure2D(
|
||||
ceil(rectangle.longitudeDelta / latitudeDelta).toInt(),
|
||||
ceil(rectangle.latitudeDelta / longitudeDelta).toInt()
|
||||
|
||||
) { (i, j) ->
|
||||
val longitude = rectangle.left + longitudeDelta * i
|
||||
val latitude = rectangle.bottom + latitudeDelta * j
|
||||
builder(
|
||||
Gmc(latitude, longitude)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -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,14 @@
|
||||
package center.sciprog.maps.compose
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.readBytes
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import io.ktor.client.statement.readRawBytes
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import mu.KotlinLogging
|
||||
import org.jetbrains.skia.Image
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
@@ -25,12 +24,14 @@ public class OpenStreetMapTileProvider(
|
||||
cacheCapacity: Int = 200,
|
||||
private val osmBaseUrl: String = "https://tile.openstreetmap.org",
|
||||
) : MapTileProvider {
|
||||
|
||||
private val cacheMutex = Mutex()
|
||||
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.cacheFilePath() = cacheDirectory.resolve("${zoom}/${i}/${j}.png")
|
||||
private fun TileId.cacheFilePath() = cacheDirectory.resolve("${zoom}/${i}/${j}.png").takeIf { it.exists() }
|
||||
|
||||
/**
|
||||
* Download and cache the tile image
|
||||
@@ -51,7 +52,7 @@ public class OpenStreetMapTileProvider(
|
||||
//semaphore works only for actual download
|
||||
semaphore.withPermit {
|
||||
val url = id.osmUrl()
|
||||
val byteArray = client.get(url).readBytes()
|
||||
val byteArray = client.get(url).readRawBytes()
|
||||
logger.debug { "Finished downloading map tile with id $id from $url" }
|
||||
id.cacheFilePath()?.let { path ->
|
||||
logger.debug { "Caching map tile $id to $path" }
|
||||
@@ -68,16 +69,22 @@ public class OpenStreetMapTileProvider(
|
||||
tileId: TileId,
|
||||
): Deferred<MapTile> {
|
||||
|
||||
//start image download
|
||||
val imageDeferred: Deferred<Image> = cache.getOrPut(tileId) {
|
||||
downloadImageAsync(tileId)
|
||||
}
|
||||
|
||||
//collect the result asynchronously
|
||||
return async {
|
||||
//start image download
|
||||
val imageDeferred: Deferred<Image> = cacheMutex.withLock {
|
||||
cache.getOrPut(tileId) {
|
||||
downloadImageAsync(tileId)
|
||||
}
|
||||
}
|
||||
|
||||
val image: Image = runCatching { imageDeferred.await() }.onFailure {
|
||||
logger.error(it) { "Failed to load tile image with id=$tileId" }
|
||||
cache.remove(tileId)
|
||||
if (it !is CancellationException) {
|
||||
logger.error(it) { "Failed to load tile image with id=$tileId" }
|
||||
}
|
||||
cacheMutex.withLock {
|
||||
cache.remove(tileId)
|
||||
}
|
||||
}.getOrThrow()
|
||||
|
||||
MapTile(tileId, image)
|
||||
@@ -0,0 +1,62 @@
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.asSkiaBitmap
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.text.TextMeasurer
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.skia.Image
|
||||
import org.jfree.svg.SVGUtils
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.features.FeatureSet
|
||||
import space.kscience.maps.features.PainterFeature
|
||||
import space.kscience.maps.features.ViewConfig
|
||||
import space.kscience.maps.features.ViewPoint
|
||||
import space.kscience.maps.svg.generateBitmap
|
||||
import space.kscience.maps.svg.generateSvg
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.writeBytes
|
||||
|
||||
/**
|
||||
* Exports the features of a [FeatureSet] to an SVG file.
|
||||
*
|
||||
* @param viewPoint The `ViewPoint` providing the spatial context (focus and zoom level) for rendering the SVG.
|
||||
* @param painterCache A map associating `PainterFeature` instances with their respective `Painter` objects,
|
||||
* used for rendering the features in the SVG.
|
||||
* @param size The dimensions (`Size`) of the SVG file to generate.
|
||||
* @param path The file path where the generated SVG will be saved.
|
||||
*/
|
||||
public fun FeatureSet<Gmc>.exportToSvg(
|
||||
viewPoint: ViewPoint<Gmc>,
|
||||
painterCache: Map<PainterFeature<Gmc>, Painter>,
|
||||
size: Size,
|
||||
path: Path,
|
||||
) {
|
||||
val mapCanvasState: MapCanvasState = MapCanvasState(ViewConfig()).apply {
|
||||
this.viewPoint = viewPoint
|
||||
this.canvasSize = DpSize(size.width.dp, size.height.dp)
|
||||
}
|
||||
|
||||
val svgString: String = generateSvg(mapCanvasState, painterCache)
|
||||
SVGUtils.writeToSVG(path.toFile(), svgString)
|
||||
}
|
||||
|
||||
public fun FeatureSet<Gmc>.exportToPng(
|
||||
viewPoint: ViewPoint<Gmc>,
|
||||
painterCache: Map<PainterFeature<Gmc>, Painter>,
|
||||
textMeasurer: TextMeasurer,
|
||||
size: Size,
|
||||
path: Path,
|
||||
) {
|
||||
val mapCanvasState: MapCanvasState = MapCanvasState(ViewConfig()).apply {
|
||||
this.viewPoint = viewPoint
|
||||
this.canvasSize = DpSize(size.width.dp, size.height.dp)
|
||||
}
|
||||
|
||||
val bitmap = generateBitmap(mapCanvasState, painterCache, textMeasurer, size)
|
||||
|
||||
Image.makeFromBitmap(bitmap.asSkiaBitmap()).encodeToData()?.bytes?.let {
|
||||
path.writeBytes(it)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package center.sciprog.maps.compose
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.assertFails
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class OsmTileProviderTest {
|
||||
// @get:Rule
|
||||
// val rule = createComposeRule()
|
||||
@@ -9,19 +9,8 @@ The core interfaces of KMath.
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `center.sciprog:maps-kt-core:0.2.2`.
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-core:0.4.0-dev-7`.
|
||||
|
||||
**Gradle Groovy:**
|
||||
```groovy
|
||||
repositories {
|
||||
maven { url 'https://repo.kotlin.link' }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'center.sciprog:maps-kt-core:0.2.2'
|
||||
}
|
||||
```
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
repositories {
|
||||
@@ -30,6 +19,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("center.sciprog:maps-kt-core:0.2.2")
|
||||
implementation("space.kscience:maps-kt-core:0.4.0-dev-7")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -3,11 +3,12 @@ plugins {
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
val kmathVersion: String by rootProject.extra
|
||||
|
||||
kscience{
|
||||
jvm()
|
||||
js()
|
||||
native()
|
||||
wasm()
|
||||
|
||||
useSerialization()
|
||||
|
||||
dependencies{
|
||||
@@ -18,7 +19,6 @@ kscience{
|
||||
readme {
|
||||
description = "Core cartography, UI-agnostic"
|
||||
maturity = space.kscience.gradle.Maturity.DEVELOPMENT
|
||||
propertyByTemplate("artifact", rootProject.file("docs/templates/ARTIFACT-TEMPLATE.md"))
|
||||
|
||||
feature(
|
||||
id = "angles and distances",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
package space.kscience.maps.coordinates
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.jvm.JvmInline
|
||||
@@ -1,10 +1,8 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
package space.kscience.maps.coordinates
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.kmath.geometry.tan
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
import space.kscience.kmath.geometry.*
|
||||
import kotlin.math.*
|
||||
|
||||
@Serializable
|
||||
public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRadius: Distance) {
|
||||
@@ -43,12 +41,7 @@ public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRa
|
||||
polarRadius = Distance(6378.137)
|
||||
)
|
||||
|
||||
// /**
|
||||
// * https://en.wikipedia.org/wiki/Great-circle_distance
|
||||
// */
|
||||
// public fun greatCircleAngleBetween(r1: GMC, r2: GMC): Radians = acos(
|
||||
// sin(r1.latitude) * sin(r2.latitude) + cos(r1.latitude) * cos(r2.latitude) * cos(r1.longitude - r2.longitude)
|
||||
// ).radians
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,22 +52,35 @@ public fun GeoEllipsoid.reducedRadius(latitude: Angle): Distance {
|
||||
val reducedLatitudeTan = (1 - f) * tan(latitude)
|
||||
return equatorRadius / sqrt(1.0 + reducedLatitudeTan.pow(2))
|
||||
}
|
||||
//
|
||||
//
|
||||
///**
|
||||
// * Compute distance between two map points using giv
|
||||
// * https://en.wikipedia.org/wiki/Geographical_distance#Lambert's_formula_for_long_lines
|
||||
// */
|
||||
//public fun GeoEllipsoid.lambertDistanceBetween(r1: GMC, r2: GMC): Distance {
|
||||
// val s = greatCircleAngleBetween(r1, r2)
|
||||
//
|
||||
// val b1: Double = (1 - f) * tan(r1.latitude)
|
||||
// val b2: Double = (1 - f) * tan(r2.latitude)
|
||||
// val p = (b1 + b2) / 2
|
||||
// val q = (b2 - b1) / 2
|
||||
//
|
||||
// val x = (s.value - sin(s)) * sin(p).pow(2) * cos(q).pow(2) / cos(s / 2).pow(2)
|
||||
// val y = (s.value + sin(s)) * cos(p).pow(2) * sin(q).pow(2) / sin(s / 2).pow(2)
|
||||
//
|
||||
// return equatorRadius * (s.value - f / 2 * (x + y))
|
||||
//}
|
||||
|
||||
|
||||
/**
|
||||
* Compute distance between two map points using giv
|
||||
* https://en.wikipedia.org/wiki/Geographical_distance#Lambert's_formula_for_long_lines
|
||||
*/
|
||||
public fun GeoEllipsoid.lambertDistanceBetween(r1: Gmc, r2: Gmc): Distance {
|
||||
|
||||
/**
|
||||
* https://en.wikipedia.org/wiki/Great-circle_distance
|
||||
*/
|
||||
fun greatCircleAngleBetween(
|
||||
r1: Gmc,
|
||||
r2: Gmc,
|
||||
): Radians = acos(
|
||||
sin(r1.latitude) * sin(r2.latitude) +
|
||||
cos(r1.latitude) * cos(r2.latitude) *
|
||||
cos(r1.longitude - r2.longitude)
|
||||
).radians
|
||||
|
||||
val s = greatCircleAngleBetween(r1, r2)
|
||||
|
||||
val b1: Double = (1 - f) * tan(r1.latitude)
|
||||
val b2: Double = (1 - f) * tan(r2.latitude)
|
||||
val p = (b1 + b2) / 2
|
||||
val q = (b2 - b1) / 2
|
||||
|
||||
val x = (s.value - sin(s)) * sin(p).pow(2) * cos(q).pow(2) / cos(s / 2).pow(2)
|
||||
val y = (s.value + sin(s)) * cos(p).pow(2) * sin(q).pow(2) / sin(s / 2).pow(2)
|
||||
|
||||
return equatorRadius * (s.value - f / 2 * (x + y))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
package space.kscience.maps.coordinates
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import space.kscience.kmath.geometry.*
|
||||
@@ -22,7 +22,6 @@ public class GeodeticMapCoordinates(
|
||||
"Longitude $longitude is not in (-PI..PI) range"
|
||||
}
|
||||
}
|
||||
|
||||
override val x: Angle get() = longitude
|
||||
|
||||
override val y: Angle get() = latitude
|
||||
@@ -43,7 +42,7 @@ public class GeodeticMapCoordinates(
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "GMC(latitude=${latitude.degrees} deg, longitude=${longitude.degrees} deg)"
|
||||
return "GMC(latitude=${latitude.toDegrees().value} deg, longitude=${longitude.toDegrees().value} deg)"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
package space.kscience.maps.coordinates
|
||||
|
||||
import space.kscience.kmath.geometry.*
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
* A directed straight (geodetic) segment on a spheroid with given start, direction, end point and distance.
|
||||
* @param forward coordinate of a start point with forward direction
|
||||
* @param backward coordinate of an end point with backward direction
|
||||
* A directed straight (geodetic) segment on a spheroid with the given start, direction, end point and distance.
|
||||
* @param forward coordinate of a start point with the forward direction
|
||||
* @param backward coordinate of an end point with the backward direction
|
||||
*/
|
||||
public class GmcCurve(
|
||||
public class GmcCurve internal constructor(
|
||||
public val forward: GmcPose,
|
||||
public val backward: GmcPose,
|
||||
public val distance: Distance,
|
||||
@@ -64,8 +64,8 @@ public fun GeoEllipsoid.meridianCurve(
|
||||
}
|
||||
|
||||
return GmcCurve(
|
||||
forward = GmcPose(Gmc.normalized(fromLatitude, longitude), if (up) Angle.zero else Angle.pi),
|
||||
backward = GmcPose(Gmc.normalized(toLatitude, longitude), if (up) Angle.pi else Angle.zero),
|
||||
forward = GmcPose(GeodeticMapCoordinates.normalized(fromLatitude, longitude), if (up) Angle.zero else Angle.pi),
|
||||
backward = GmcPose(GeodeticMapCoordinates.normalized(toLatitude, longitude), if (up) Angle.pi else Angle.zero),
|
||||
distance = s
|
||||
)
|
||||
}
|
||||
@@ -77,9 +77,9 @@ public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toL
|
||||
require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
|
||||
val right = toLongitude > fromLongitude
|
||||
return GmcCurve(
|
||||
forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) Angle.piDiv2 else -Angle.piDiv2),
|
||||
backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -Angle.piDiv2 else Angle.piDiv2),
|
||||
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians)
|
||||
forward = GmcPose(GeodeticMapCoordinates.normalized(latitude, fromLongitude), if (right) Angle.piDiv2 else -Angle.piDiv2),
|
||||
backward = GmcPose(GeodeticMapCoordinates.normalized(latitude, toLongitude), if (right) -Angle.piDiv2 else Angle.piDiv2),
|
||||
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).toRadians().value)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ public fun GeoEllipsoid.curveInDirection(
|
||||
val L = lambda - (1 - C) * f * sinAlpha *
|
||||
(sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)))
|
||||
|
||||
val endPoint = Gmc.normalized(phi2, start.longitude + L.radians)
|
||||
val endPoint = GeodeticMapCoordinates.normalized(phi2, start.longitude + L.radians)
|
||||
|
||||
// eq. 12
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
package space.kscience.maps.coordinates
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
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.
|
||||
*/
|
||||
|
||||
package center.sciprog.maps.coordinates
|
||||
package space.kscience.maps.coordinates
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import space.kscience.kmath.geometry.*
|
||||
@@ -57,12 +57,12 @@ public open class MercatorProjection(
|
||||
return if (ellipsoid === GeoEllipsoid.sphere) {
|
||||
GeodeticMapCoordinates.ofRadians(
|
||||
atan(sinh(pc.y / ellipsoid.equatorRadius)),
|
||||
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius),
|
||||
baseLongitude.toRadians().value + (pc.x / ellipsoid.equatorRadius),
|
||||
)
|
||||
} else {
|
||||
GeodeticMapCoordinates.ofRadians(
|
||||
cphi2(exp(-(pc.y / ellipsoid.equatorRadius))),
|
||||
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius)
|
||||
baseLongitude.toRadians().value + (pc.x / ellipsoid.equatorRadius)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -76,13 +76,13 @@ public open class MercatorProjection(
|
||||
|
||||
return if (ellipsoid === GeoEllipsoid.sphere) {
|
||||
ProjectionCoordinates(
|
||||
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians,
|
||||
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).toRadians().value,
|
||||
y = ellipsoid.equatorRadius * ln(tan(Angle.pi / 4 + gmc.latitude / 2))
|
||||
)
|
||||
} else {
|
||||
val sinPhi = sin(gmc.latitude)
|
||||
ProjectionCoordinates(
|
||||
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians,
|
||||
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).toRadians().value,
|
||||
y = ellipsoid.equatorRadius * ln(
|
||||
tan(Angle.pi / 4 + gmc.latitude / 2) * ((1 - e * sinPhi) / (1 + e * sinPhi)).pow(e / 2)
|
||||
)
|
||||
@@ -3,10 +3,9 @@
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package center.sciprog.maps.coordinates
|
||||
package space.kscience.maps.coordinates
|
||||
|
||||
import space.kscience.kmath.geometry.abs
|
||||
import space.kscience.kmath.geometry.radians
|
||||
import kotlin.math.*
|
||||
|
||||
public data class WebMercatorCoordinates(val zoom: Int, val x: Float, val y: Float)
|
||||
@@ -36,8 +35,8 @@ public object WebMercatorProjection {
|
||||
val scaleFactor = scaleFactor(zoom.toFloat())
|
||||
return WebMercatorCoordinates(
|
||||
zoom = zoom,
|
||||
x = scaleFactor * (gmc.longitude.radians + PI).toFloat(),
|
||||
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians / 2))).toFloat()
|
||||
x = scaleFactor * (gmc.longitude.toRadians().value + PI).toFloat(),
|
||||
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.toRadians().value / 2))).toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
package space.kscience.maps.coordinates
|
||||
|
||||
import space.kscience.kmath.geometry.radians
|
||||
import kotlin.test.Test
|
||||
@@ -6,8 +6,8 @@ import kotlin.test.assertEquals
|
||||
|
||||
internal class DistanceTest {
|
||||
companion object {
|
||||
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869)
|
||||
val spb = Gmc.ofDegrees(59.926686023580444, 30.36038109122013)
|
||||
val moscow = GeodeticMapCoordinates.ofDegrees(55.76058287719673, 37.60358622841869)
|
||||
val spb = GeodeticMapCoordinates.ofDegrees(59.926686023580444, 30.36038109122013)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -21,7 +21,7 @@ internal class DistanceTest {
|
||||
val distance = curve.distance
|
||||
|
||||
assertEquals(632.035426877, distance.kilometers, 0.0001)
|
||||
assertEquals(-0.6947937116552751, curve.forward.bearing.radians, 0.0001)
|
||||
assertEquals(-0.6947937116552751, curve.forward.bearing.toRadians().value, 0.0001)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -30,7 +30,7 @@ internal class DistanceTest {
|
||||
GmcPose(moscow, (-0.6947937116552751).radians), Distance(632.035426877)
|
||||
)
|
||||
|
||||
assertEquals(spb.latitude.radians, curve.backward.latitude.radians, 0.0001)
|
||||
assertEquals(spb.longitude.radians, curve.backward.longitude.radians, 0.0001)
|
||||
assertEquals(spb.latitude.toRadians().value, curve.backward.latitude.toRadians().value, 0.0001)
|
||||
assertEquals(spb.longitude.toRadians().value, curve.backward.longitude.toRadians().value, 0.0001)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,28 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
package space.kscience.maps.coordinates
|
||||
|
||||
import space.kscience.kmath.geometry.degrees
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class MercatorTest {
|
||||
@Test
|
||||
fun sphereForwardBackward(){
|
||||
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869)
|
||||
val moscow = GeodeticMapCoordinates.ofDegrees(55.76058287719673, 37.60358622841869)
|
||||
val mercator = MapProjection.epsg3857.toProjection(moscow)
|
||||
//https://epsg.io/transform#s_srs=4326&t_srs=3857&x=37.6035862&y=55.7605829
|
||||
assertEquals(4186.0120709, mercator.x.kilometers, 1e-4)
|
||||
assertEquals(7510.9013658, mercator.y.kilometers, 1e-4)
|
||||
val backwards = MapProjection.epsg3857.toGeodetic(mercator)
|
||||
assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 1e-6)
|
||||
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6)
|
||||
assertEquals(moscow.latitude.toDegrees().value, backwards.latitude.toDegrees().value, 1e-6)
|
||||
assertEquals(moscow.longitude.toDegrees().value, backwards.longitude.toDegrees().value, 1e-6)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ellipseForwardBackward(){
|
||||
val moscow = Gmc.ofDegrees(55.76058287719673, 37.60358622841869)
|
||||
val moscow = GeodeticMapCoordinates.ofDegrees(55.76058287719673, 37.60358622841869)
|
||||
val projection = MercatorProjection(ellipsoid = GeoEllipsoid.WGS84)
|
||||
val mercator = projection.toProjection(moscow)
|
||||
val backwards = projection.toGeodetic(mercator)
|
||||
assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 1e-6)
|
||||
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6)
|
||||
assertEquals(moscow.latitude.toDegrees().value, backwards.latitude.toDegrees().value, 1e-6)
|
||||
assertEquals(moscow.longitude.toDegrees().value, backwards.longitude.toDegrees().value, 1e-6)
|
||||
}
|
||||
}
|
||||
@@ -6,19 +6,8 @@
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `center.sciprog:maps-kt-features:0.2.2`.
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-features:0.4.0-dev-7`.
|
||||
|
||||
**Gradle Groovy:**
|
||||
```groovy
|
||||
repositories {
|
||||
maven { url 'https://repo.kotlin.link' }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'center.sciprog:maps-kt-features:0.2.2'
|
||||
}
|
||||
```
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
repositories {
|
||||
@@ -27,6 +16,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("center.sciprog:maps-kt-features:0.2.2")
|
||||
implementation("space.kscience:maps-kt-features:0.4.0-dev-7")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,30 +1,45 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
id("org.jetbrains.compose")
|
||||
alias(spclibs.plugins.compose.compiler)
|
||||
alias(spclibs.plugins.compose.jb)
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
val kmathVersion: String by rootProject.extra
|
||||
|
||||
kscience{
|
||||
kscience {
|
||||
jvm()
|
||||
js()
|
||||
useSerialization{
|
||||
json()
|
||||
}
|
||||
|
||||
useSerialization(sourceSet = space.kscience.gradle.DependencySourceSet.TEST){
|
||||
protobuf()
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api(projects.trajectoryKt)
|
||||
api(compose.foundation)
|
||||
// js()
|
||||
wasm{
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useCoroutines()
|
||||
|
||||
useSerialization {
|
||||
json()
|
||||
}
|
||||
|
||||
useSerialization(sourceSet = space.kscience.gradle.DependencySourceSet.TEST) {
|
||||
protobuf()
|
||||
}
|
||||
|
||||
commonMain{
|
||||
api(projects.trajectoryKt)
|
||||
api(compose.runtime)
|
||||
api(compose.foundation)
|
||||
api(compose.material)
|
||||
api(compose.ui)
|
||||
|
||||
api(libs.attributes.serialization)
|
||||
api("io.github.oshai:kotlin-logging:7.0.7")
|
||||
}
|
||||
|
||||
jvmMain{
|
||||
api("org.jfree:org.jfree.svg:5.0.6")
|
||||
}
|
||||
}
|
||||
@@ -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,55 +0,0 @@
|
||||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package center.sciprog.attributes
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
public class AttributesSerializer(
|
||||
private val serializableAttributes: Set<SerializableAttribute<*>>,
|
||||
) : KSerializer<Attributes> {
|
||||
private val jsonSerializer = JsonObject.serializer()
|
||||
override val descriptor: SerialDescriptor get() = jsonSerializer.descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): Attributes {
|
||||
val jsonElement = jsonSerializer.deserialize(decoder)
|
||||
val attributeMap: Map<SerializableAttribute<*>, Any> = jsonElement.entries.associate { (key, element) ->
|
||||
val attr = serializableAttributes.find { it.serialId == key }
|
||||
?: error("Attribute serializer for key $key not found")
|
||||
|
||||
val json = if (decoder is JsonDecoder) {
|
||||
decoder.json
|
||||
} else {
|
||||
Json { serializersModule = decoder.serializersModule }
|
||||
}
|
||||
val value = json.decodeFromJsonElement(attr.serializer, element) ?: error("Null values are not allowed")
|
||||
|
||||
attr to value
|
||||
}
|
||||
return Attributes(attributeMap)
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Attributes) {
|
||||
val json = buildJsonObject {
|
||||
value.content.forEach { (key: Attribute<*>, value: Any) ->
|
||||
if (key !in serializableAttributes) error("An attribute key '$key' is not in the list of allowed attributes for this serializer")
|
||||
val serializableKey = key as SerializableAttribute
|
||||
|
||||
val json = if (encoder is JsonEncoder) {
|
||||
encoder.json
|
||||
} else {
|
||||
Json { serializersModule = encoder.serializersModule }
|
||||
}
|
||||
|
||||
put(
|
||||
serializableKey.serialId,
|
||||
json.encodeToJsonElement(serializableKey.serializer as KSerializer<Any>, value)
|
||||
)
|
||||
}
|
||||
}
|
||||
jsonSerializer.serialize(encoder, json)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package center.sciprog.maps.features
|
||||
|
||||
public expect class FeatureFont {
|
||||
public var size: Float
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
package center.sciprog.maps.features
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.attributes.*
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.kmath.nd.*
|
||||
import space.kscience.kmath.structures.Buffer
|
||||
|
||||
//@JvmInline
|
||||
//public value class FeatureId<out F : Feature<*>>(public val id: String)
|
||||
|
||||
public class FeatureRef<T : Any, out F : Feature<T>>(public val id: String, public val parent: FeatureGroup<T>)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.resolve(): F =
|
||||
parent.featureMap[id]?.let { it as F } ?: error("Feature with id=$id not found")
|
||||
|
||||
public val <T : Any, F : Feature<T>> FeatureRef<T, F>.attributes: Attributes get() = resolve().attributes
|
||||
|
||||
/**
|
||||
* A group of other features
|
||||
*/
|
||||
public data class FeatureGroup<T : Any>(
|
||||
override val space: CoordinateSpace<T>,
|
||||
public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(),
|
||||
override val attributes: Attributes = Attributes.EMPTY,
|
||||
) : CoordinateSpace<T> by space, Feature<T> {
|
||||
//
|
||||
// @Suppress("UNCHECKED_CAST")
|
||||
// public operator fun <F : Feature<T>> get(id: FeatureId<F>): F =
|
||||
// featureMap[id.id]?.let { it as F } ?: error("Feature with id=$id not found")
|
||||
|
||||
private var uidCounter = 0
|
||||
|
||||
private fun generateUID(feature: Feature<T>?): String = if (feature == null) {
|
||||
"@group[${uidCounter++}]"
|
||||
} else {
|
||||
"@${feature::class.simpleName}[${uidCounter++}]"
|
||||
}
|
||||
|
||||
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> {
|
||||
val safeId = id ?: generateUID(feature)
|
||||
featureMap[safeId] = feature
|
||||
return FeatureRef(safeId, this)
|
||||
}
|
||||
|
||||
public fun removeFeature(id: String) {
|
||||
featureMap.remove(id)
|
||||
}
|
||||
|
||||
// public fun <F : Feature<T>> feature(id: FeatureId<F>, feature: F): FeatureId<F> = feature(id.id, feature)
|
||||
|
||||
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")
|
||||
// public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
|
||||
// get(id).attributes[key]
|
||||
|
||||
|
||||
override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) {
|
||||
featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
|
||||
}
|
||||
|
||||
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes))
|
||||
|
||||
|
||||
public companion object {
|
||||
|
||||
/**
|
||||
* Build, but do not remember map feature state
|
||||
*/
|
||||
public fun <T : Any> build(
|
||||
coordinateSpace: CoordinateSpace<T>,
|
||||
builder: FeatureGroup<T>.() -> Unit = {},
|
||||
): FeatureGroup<T> = FeatureGroup(coordinateSpace).apply(builder)
|
||||
|
||||
/**
|
||||
* Build and remember map feature state
|
||||
*/
|
||||
@Composable
|
||||
public fun <T : Any> remember(
|
||||
coordinateSpace: CoordinateSpace<T>,
|
||||
builder: FeatureGroup<T>.() -> Unit = {},
|
||||
): FeatureGroup<T> = remember {
|
||||
build(coordinateSpace, builder)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all features with a given attribute from the one with highest [z] to lowest
|
||||
*/
|
||||
public fun <T : Any, A> FeatureGroup<T>.forEachWithAttribute(
|
||||
key: Attribute<A>,
|
||||
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Unit,
|
||||
) {
|
||||
visit { id, feature ->
|
||||
feature.attributes[key]?.let {
|
||||
block(id, feature, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun <T : Any, A> FeatureGroup<T>.forEachWithAttributeUntil(
|
||||
key: Attribute<A>,
|
||||
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Boolean,
|
||||
) {
|
||||
visitUntil { id, feature ->
|
||||
feature.attributes[key]?.let {
|
||||
block(id, feature, it)
|
||||
} ?: true
|
||||
}
|
||||
}
|
||||
|
||||
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithType(
|
||||
crossinline block: (FeatureRef<T, F>) -> Unit,
|
||||
) {
|
||||
visit { id, feature ->
|
||||
if (feature is F) block(FeatureRef(id, this))
|
||||
}
|
||||
}
|
||||
|
||||
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithTypeUntil(
|
||||
crossinline block: (FeatureRef<T, F>) -> Boolean,
|
||||
) {
|
||||
visitUntil { id, feature ->
|
||||
if (feature is F) block(FeatureRef(id, this)) else true
|
||||
}
|
||||
}
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.circle(
|
||||
center: T,
|
||||
size: Dp = 5.dp,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, CircleFeature<T>> = feature(
|
||||
id, CircleFeature(space, center, size, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.rectangle(
|
||||
centerCoordinates: T,
|
||||
size: DpSize = DpSize(5.dp, 5.dp),
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, RectangleFeature<T>> = feature(
|
||||
id, RectangleFeature(space, centerCoordinates, size, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.draw(
|
||||
position: T,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
draw: DrawScope.() -> Unit,
|
||||
): FeatureRef<T, DrawFeature<T>> = feature(
|
||||
id,
|
||||
DrawFeature(space, position, drawFeature = draw, attributes = attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.line(
|
||||
aCoordinates: T,
|
||||
bCoordinates: T,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, LineFeature<T>> = feature(
|
||||
id,
|
||||
LineFeature(space, aCoordinates, bCoordinates, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.arc(
|
||||
oval: Rectangle<T>,
|
||||
startAngle: Angle,
|
||||
arcLength: Angle,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, ArcFeature<T>> = feature(
|
||||
id,
|
||||
ArcFeature(space, oval, startAngle, arcLength, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.points(
|
||||
points: List<T>,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, PointsFeature<T>> = feature(
|
||||
id,
|
||||
PointsFeature(space, points, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.multiLine(
|
||||
points: List<T>,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, MultiLineFeature<T>> = feature(
|
||||
id,
|
||||
MultiLineFeature(space, points, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.polygon(
|
||||
points: List<T>,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, PolygonFeature<T>> = feature(
|
||||
id,
|
||||
PolygonFeature(space, points, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.icon(
|
||||
position: T,
|
||||
image: ImageVector,
|
||||
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, VectorIconFeature<T>> =
|
||||
feature(
|
||||
id,
|
||||
VectorIconFeature(
|
||||
space,
|
||||
position,
|
||||
size,
|
||||
image,
|
||||
attributes
|
||||
)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.group(
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
builder: FeatureGroup<T>.() -> Unit,
|
||||
): FeatureRef<T, FeatureGroup<T>> {
|
||||
val collection = FeatureGroup(space).apply(builder)
|
||||
val feature = FeatureGroup(space, collection.featureMap, attributes)
|
||||
return feature(id, feature)
|
||||
}
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.scalableImage(
|
||||
box: Rectangle<T>,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
painter: @Composable () -> Painter,
|
||||
): FeatureRef<T, ScalableImageFeature<T>> = feature(
|
||||
id,
|
||||
ScalableImageFeature<T>(space, box, painter = painter, attributes = attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.text(
|
||||
position: T,
|
||||
text: String,
|
||||
font: FeatureFont.() -> Unit = { size = 16f },
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, TextFeature<T>> = feature(
|
||||
id,
|
||||
TextFeature(space, position, text, fontConfig = font, attributes = attributes)
|
||||
)
|
||||
|
||||
public fun <T> StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND<T> {
|
||||
val strides = Strides(shape)
|
||||
return BufferND(strides, Buffer.boxing(strides.linearSize) { initializer(strides.index(it)) })
|
||||
}
|
||||
|
||||
public fun <T> Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D<T> {
|
||||
val strides = Strides(ShapeND(rows, columns))
|
||||
return BufferND(strides, Buffer.boxing(strides.linearSize) { initializer(strides.index(it)) }).as2D()
|
||||
}
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.pixelMap(
|
||||
rectangle: Rectangle<T>,
|
||||
pixelMap: Structure2D<Color?>,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, PixelMapFeature<T>> = feature(
|
||||
id,
|
||||
PixelMapFeature(space, rectangle, pixelMap, attributes = attributes)
|
||||
)
|
||||
@@ -1,82 +0,0 @@
|
||||
package center.sciprog.maps.features
|
||||
|
||||
import center.sciprog.attributes.Attributes
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.draggableLine(
|
||||
aId: FeatureRef<T, MarkerFeature<T>>,
|
||||
bId: FeatureRef<T, MarkerFeature<T>>,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, LineFeature<T>> {
|
||||
var lineId: FeatureRef<T, LineFeature<T>>? = null
|
||||
|
||||
fun drawLine(): FeatureRef<T, LineFeature<T>> {
|
||||
val currentId = feature(
|
||||
lineId?.id ?: id,
|
||||
LineFeature(
|
||||
space,
|
||||
aId.resolve().center,
|
||||
bId.resolve().center,
|
||||
Attributes {
|
||||
ZAttribute(-10f)
|
||||
lineId?.attributes?.let { from(it) }
|
||||
}
|
||||
)
|
||||
)
|
||||
lineId = currentId
|
||||
return currentId
|
||||
}
|
||||
|
||||
aId.draggable { _, _ ->
|
||||
drawLine()
|
||||
}
|
||||
|
||||
bId.draggable { _, _ ->
|
||||
drawLine()
|
||||
}
|
||||
|
||||
return drawLine()
|
||||
}
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.draggableMultiLine(
|
||||
points: List<FeatureRef<T, MarkerFeature<T>>>,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, MultiLineFeature<T>> {
|
||||
var polygonId: FeatureRef<T, MultiLineFeature<T>>? = null
|
||||
|
||||
fun drawLines(): FeatureRef<T, MultiLineFeature<T>> {
|
||||
val currentId = feature(
|
||||
polygonId?.id ?: id,
|
||||
MultiLineFeature(
|
||||
space,
|
||||
points.map { it.resolve().center },
|
||||
Attributes {
|
||||
ZAttribute(-10f)
|
||||
polygonId?.attributes?.let { from(it) }
|
||||
}
|
||||
)
|
||||
)
|
||||
polygonId = currentId
|
||||
return currentId
|
||||
}
|
||||
|
||||
points.forEach {
|
||||
it.draggable { _, _ ->
|
||||
drawLines()
|
||||
}
|
||||
}
|
||||
|
||||
return drawLines()
|
||||
}
|
||||
|
||||
@JvmName("draggableMultiLineFromPoints")
|
||||
public fun <T : Any> FeatureGroup<T>.draggableMultiLine(
|
||||
points: List<T>,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, MultiLineFeature<T>> {
|
||||
val pointRefs = points.map {
|
||||
circle(it)
|
||||
}
|
||||
return draggableMultiLine(pointRefs, id)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.compose
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import androidx.compose.foundation.gestures.drag
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -7,7 +7,7 @@ import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpRect
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.features.*
|
||||
import space.kscience.maps.features.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -16,9 +16,9 @@ import kotlin.math.min
|
||||
* Create a modifier for Map/Scheme canvas controls on desktop
|
||||
* @param features a collection of features to be rendered in descending [ZAttribute] order
|
||||
*/
|
||||
public fun <T : Any> Modifier.mapControls(
|
||||
state: CoordinateViewScope<T>,
|
||||
features: FeatureGroup<T>,
|
||||
public fun <T : Any> Modifier.canvasControls(
|
||||
state: CanvasState<T>,
|
||||
features: FeatureStore<T>,
|
||||
): Modifier = with(state) {
|
||||
|
||||
// //selecting all tapabales ahead of time
|
||||
@@ -32,8 +32,8 @@ public fun <T : Any> Modifier.mapControls(
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val event = awaitPointerEvent()
|
||||
val coordinates = event.changes.first().position.toCoordinates(this)
|
||||
val point = space.ViewPoint(coordinates, zoom)
|
||||
val coordinates = toCoordinates(event.changes.first().position, this)
|
||||
val point = state.space.ViewPoint(coordinates, zoom)
|
||||
|
||||
if (event.type == PointerEventType.Move) {
|
||||
features.forEachWithAttribute(HoverListenerAttribute) { _, feature, listeners ->
|
||||
@@ -47,9 +47,9 @@ public fun <T : Any> Modifier.mapControls(
|
||||
}
|
||||
}.pointerInput(Unit) {
|
||||
detectClicks(
|
||||
onDoubleClick = if (state.config.zoomOnDoubleClick) {
|
||||
onDoubleClick = if (viewConfig.zoomOnDoubleClick) {
|
||||
{ event ->
|
||||
val invariant = event.position.toCoordinates(this)
|
||||
val invariant = toCoordinates(event.position, this)
|
||||
viewPoint = with(space) {
|
||||
viewPoint.zoomBy(
|
||||
if (event.buttons.isPrimaryPressed) 1f else if (event.buttons.isSecondaryPressed) -1f else 0f,
|
||||
@@ -59,15 +59,15 @@ public fun <T : Any> Modifier.mapControls(
|
||||
}
|
||||
} else null,
|
||||
onClick = { event ->
|
||||
val coordinates = event.position.toCoordinates(this)
|
||||
val coordinates = toCoordinates(event.position, this)
|
||||
val point = space.ViewPoint(coordinates, zoom)
|
||||
|
||||
config.onClick?.handle(
|
||||
viewConfig.onClick?.handle(
|
||||
event,
|
||||
point
|
||||
)
|
||||
|
||||
features.forEachWithAttributeUntil(ClickListenerAttribute) { _, feature, listeners ->
|
||||
features.forEachWithAttributeUntil(ClickListenerAttribute) {_, feature, listeners ->
|
||||
if (point in (feature as DomainFeature)) {
|
||||
listeners.forEach { it.handle(event, point) }
|
||||
false
|
||||
@@ -88,7 +88,7 @@ public fun <T : Any> Modifier.mapControls(
|
||||
//compute invariant point of translation
|
||||
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
|
||||
viewPoint = with(space) {
|
||||
viewPoint.zoomBy(-change.scrollDelta.y * config.zoomSpeed, invariant)
|
||||
viewPoint.zoomBy(-change.scrollDelta.y * viewConfig.zoomSpeed, invariant)
|
||||
}
|
||||
change.consume()
|
||||
}
|
||||
@@ -110,14 +110,14 @@ public fun <T : Any> Modifier.mapControls(
|
||||
//apply drag handle and check if it prohibits the drag even propagation
|
||||
if (selectionStart == null) {
|
||||
val dragStart = space.ViewPoint(
|
||||
dragChange.previousPosition.toCoordinates(this),
|
||||
toCoordinates(dragChange.previousPosition, this),
|
||||
zoom
|
||||
)
|
||||
val dragEnd = space.ViewPoint(
|
||||
dragChange.position.toCoordinates(this),
|
||||
toCoordinates(dragChange.position, this),
|
||||
zoom
|
||||
)
|
||||
val dragResult = config.dragHandle?.handle(event, dragStart, dragEnd)
|
||||
val dragResult = viewConfig.dragHandle?.handle(event, dragStart, dragEnd)
|
||||
if (dragResult?.handleNext == false) return@drag
|
||||
|
||||
var continueAfter = true
|
||||
@@ -132,7 +132,7 @@ public fun <T : Any> Modifier.mapControls(
|
||||
}
|
||||
|
||||
if (event.buttons.isPrimaryPressed) {
|
||||
//If selection process is started, modify the frame
|
||||
//If the selection process is started, modify the frame
|
||||
selectionStart?.let { start ->
|
||||
val offset = dragChange.position
|
||||
selectRect = DpRect(
|
||||
@@ -161,8 +161,8 @@ public fun <T : Any> Modifier.mapControls(
|
||||
rect.topLeft.toCoordinates(),
|
||||
rect.bottomRight.toCoordinates()
|
||||
)
|
||||
config.onSelect(coordinateRect)
|
||||
if (config.zoomOnSelect) {
|
||||
viewConfig.onSelect(coordinateRect)
|
||||
if (viewConfig.zoomOnSelect) {
|
||||
viewPoint = computeViewPoint(coordinateRect)
|
||||
}
|
||||
selectRect = null
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.compose
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import androidx.compose.foundation.gestures.GestureCancellationException
|
||||
import androidx.compose.foundation.gestures.PressGestureScope
|
||||
@@ -26,32 +26,9 @@ public val PointerEvent.position: Offset get() = firstChange.position
|
||||
|
||||
|
||||
/**
|
||||
* Detects tap, double-tap, and long press gestures and calls [onClick], [onDoubleClick], and
|
||||
* [onLongClick], respectively, when detected. [onPress] is called when the press is detected
|
||||
* and the [PressGestureScope.tryAwaitRelease] and [PressGestureScope.awaitRelease] can be
|
||||
* used to detect when pointers have released or the gesture was canceled.
|
||||
* The first pointer down and final pointer up are consumed, and in the
|
||||
* case of long press, all changes after the long press is detected are consumed.
|
||||
*
|
||||
* Each function parameter receives an [Offset] representing the position relative to the containing
|
||||
* element. The [Offset] can be outside the actual bounds of the element itself meaning the numbers
|
||||
* can be negative or larger than the element bounds if the touch target is smaller than the
|
||||
* [ViewConfiguration.minimumTouchTargetSize].
|
||||
*
|
||||
* When [onDoubleClick] is provided, the tap gesture is detected only after
|
||||
* the [ViewConfiguration.doubleTapMinTimeMillis] has passed and [onDoubleClick] is called if the
|
||||
* second tap is started before [ViewConfiguration.doubleTapTimeoutMillis]. If [onDoubleClick] is not
|
||||
* provided, then [onClick] is called when the pointer up has been received.
|
||||
*
|
||||
* After the initial [onPress], if the pointer moves out of the input area, the position change
|
||||
* is consumed, or another gesture consumes the down or up events, the gestures are considered
|
||||
* canceled. That means [onDoubleClick], [onLongClick], and [onClick] will not be called after a
|
||||
* gesture has been canceled.
|
||||
*
|
||||
* If the first down event is consumed somewhere else, the entire gesture will be skipped,
|
||||
* including [onPress].
|
||||
* An alternative to [detectTapGestures] with reimplementation of internal logic
|
||||
*/
|
||||
public suspend fun PointerInputScope.detectClicks(
|
||||
internal suspend fun PointerInputScope.detectClicks(
|
||||
onDoubleClick: (Density.(PointerEvent) -> Unit)? = null,
|
||||
onLongClick: (Density.(PointerEvent) -> Unit)? = null,
|
||||
onPress: suspend PressGestureScope.(event: PointerEvent) -> Unit = NoPressGesture,
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.features
|
||||
package space.kscience.maps.features
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -7,10 +7,12 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.unit.*
|
||||
|
||||
public abstract class CoordinateViewScope<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>
|
||||
|
||||
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)
|
||||
set(value) {
|
||||
canvasSizeState.value = value
|
||||
viewConfig.onCanvasSizeChange(value)
|
||||
}
|
||||
|
||||
public var viewPoint: ViewPoint<T>
|
||||
get() = viewPointState.value ?: space.defaultViewPoint
|
||||
set(value) {
|
||||
viewPointState.value = value
|
||||
config.onViewChange(viewPoint)
|
||||
viewConfig.onViewChange(viewPoint)
|
||||
}
|
||||
|
||||
public val zoom: Float get() = viewPoint.zoom
|
||||
@@ -35,28 +38,28 @@ public abstract class CoordinateViewScope<T : Any>(
|
||||
// Selection rectangle. If null - no selection
|
||||
public var selectRect: DpRect? by mutableStateOf(null)
|
||||
|
||||
public abstract fun DpOffset.toCoordinates(): T
|
||||
|
||||
|
||||
public abstract fun T.toDpOffset(): DpOffset
|
||||
|
||||
public fun T.toOffset(density: Density): Offset = with(density) {
|
||||
val dpOffset = this@toOffset.toDpOffset()
|
||||
Offset(dpOffset.x.toPx(), dpOffset.y.toPx())
|
||||
}
|
||||
|
||||
public fun Offset.toCoordinates(density: Density): T = with(density) {
|
||||
val dpOffset = DpOffset(x.toDp(), y.toDp())
|
||||
dpOffset.toCoordinates()
|
||||
}
|
||||
|
||||
public abstract fun Rectangle<T>.toDpRect(): DpRect
|
||||
|
||||
public abstract fun ViewPoint<T>.moveBy(x: Dp, y: Dp): 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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.features
|
||||
package space.kscience.maps.features
|
||||
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.features
|
||||
package space.kscience.maps.features
|
||||
|
||||
import androidx.compose.ui.input.pointer.PointerEvent
|
||||
import androidx.compose.ui.input.pointer.isPrimaryPressed
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.features
|
||||
package space.kscience.maps.features
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
@@ -17,14 +17,14 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.attributes.Attributes
|
||||
import center.sciprog.attributes.NameAttribute
|
||||
import org.jetbrains.skia.Font
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.kmath.nd.Structure2D
|
||||
|
||||
public typealias FloatRange = ClosedFloatingPointRange<Float>
|
||||
|
||||
public interface Feature<T : Any> {
|
||||
public sealed interface Feature<T : Any> {
|
||||
|
||||
public val space: CoordinateSpace<T>
|
||||
|
||||
@@ -35,6 +35,11 @@ public interface Feature<T : Any> {
|
||||
public fun withAttributes(modify: Attributes.() -> Attributes): Feature<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* A feature that is not processed by default
|
||||
*/
|
||||
public interface CustomFeature<T : Any> : Feature<T>
|
||||
|
||||
public val Feature<*>.color: Color? get() = attributes[ColorAttribute]
|
||||
|
||||
public val Feature<*>.zoomRange: FloatRange
|
||||
@@ -43,25 +48,25 @@ public val Feature<*>.zoomRange: FloatRange
|
||||
public val Feature<*>.name: String?
|
||||
get() = attributes[NameAttribute]
|
||||
|
||||
public interface PainterFeature<T : Any> : Feature<T> {
|
||||
public sealed interface PainterFeature<T : Any> : Feature<T> {
|
||||
@Composable
|
||||
public fun getPainter(): Painter
|
||||
}
|
||||
|
||||
public interface DomainFeature<T : Any> : Feature<T> {
|
||||
public sealed interface DomainFeature<T : Any> : Feature<T> {
|
||||
public operator fun contains(viewPoint: ViewPoint<T>): Boolean = getBoundingBox(viewPoint.zoom)?.let {
|
||||
viewPoint.focus in it
|
||||
} ?: false
|
||||
}
|
||||
|
||||
public interface DraggableFeature<T : Any> : DomainFeature<T> {
|
||||
public sealed interface DraggableFeature<T : Any> : DomainFeature<T> {
|
||||
public fun withCoordinates(newCoordinates: T): Feature<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* A draggable marker feature. Other features could be bound to this one.
|
||||
*/
|
||||
public interface MarkerFeature<T : Any> : DraggableFeature<T> {
|
||||
public sealed interface MarkerFeature<T : Any> : DraggableFeature<T> {
|
||||
public val center: T
|
||||
}
|
||||
|
||||
@@ -128,7 +133,7 @@ public data class PointsFeature<T : Any>(
|
||||
}
|
||||
|
||||
|
||||
public interface LineSegmentFeature<T : Any> : Feature<T>
|
||||
public sealed interface LineSegmentFeature<T : Any> : Feature<T>
|
||||
|
||||
@Stable
|
||||
public data class LineFeature<T : Any>(
|
||||
@@ -309,7 +314,7 @@ public data class VectorIconFeature<T : Any>(
|
||||
/**
|
||||
* An image that is bound to coordinates and is scaled (and possibly warped) together with them
|
||||
*
|
||||
* @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale.
|
||||
* @param rectangle the size of the image in scheme size units. The screen units to scheme units ratio equals scale.
|
||||
*/
|
||||
public data class ScalableImageFeature<T : Any>(
|
||||
override val space: CoordinateSpace<T>,
|
||||
@@ -331,7 +336,7 @@ public data class TextFeature<T : Any>(
|
||||
public val position: T,
|
||||
public val text: String,
|
||||
override val attributes: Attributes = Attributes.EMPTY,
|
||||
public val fontConfig: FeatureFont.() -> Unit,
|
||||
public val fontConfig: Font.() -> Unit,
|
||||
) : DraggableFeature<T> {
|
||||
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position)
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package space.kscience.maps.features
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
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.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.attributes.plus
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
public companion object {
|
||||
public val logger: KLogger = KotlinLogging.logger("FeatureDrawScope")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (position.x in 0f..size.width && position.y in 0f..size.height) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun <T : Any> FeatureSet<T>.pointerCache(): Map<PainterFeature<T>, Painter> = key(features) {
|
||||
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a canvas with extended functionality (e.g., drawing text)
|
||||
*
|
||||
* @param draw additional custom content drawn underneath features
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
public fun <T : Any> FeatureCanvas(
|
||||
state: CanvasState<T>,
|
||||
featureFlow: StateFlow<Map<String, Feature<T>>>,
|
||||
modifier: Modifier = Modifier,
|
||||
sampleDuration: Duration = 20.milliseconds,
|
||||
draw: FeatureDrawScope<T>.() -> Unit = {},
|
||||
) {
|
||||
val textMeasurer = rememberTextMeasurer(0)
|
||||
|
||||
val features: Map<String, Feature<T>> by featureFlow.sample(sampleDuration).collectAsState(featureFlow.value)
|
||||
|
||||
val painterCache = features.values
|
||||
.filterIsInstance<PainterFeature<T>>()
|
||||
.associateWith { it.getPainter() }
|
||||
|
||||
|
||||
Canvas(modifier) {
|
||||
if (state.canvasSize != size.toDpSize()) {
|
||||
state.canvasSize = size.toDpSize()
|
||||
}
|
||||
clipRect {
|
||||
ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply {
|
||||
|
||||
val attributesCache = mutableMapOf<List<String>, Attributes>()
|
||||
|
||||
fun computeGroupAttributes(path: List<String>): Attributes = attributesCache.getOrPut(path) {
|
||||
if (path.isEmpty()) return Attributes.EMPTY
|
||||
else if (path.size == 1) {
|
||||
features[path.first()]?.attributes ?: Attributes.EMPTY
|
||||
} else {
|
||||
computeGroupAttributes(path.dropLast(1)) + (features[path.first()]?.attributes
|
||||
?: Attributes.EMPTY)
|
||||
}
|
||||
}
|
||||
|
||||
features.entries.sortedBy { it.value.z }
|
||||
.filter { state.viewPoint.zoom in it.value.zoomRange }
|
||||
.forEach { (id, feature) ->
|
||||
val path = id.split("/")
|
||||
drawFeature(feature, computeGroupAttributes(path.dropLast(1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package space.kscience.maps.features
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.jetbrains.skia.Font
|
||||
import space.kscience.attributes.Attribute
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.kmath.nd.*
|
||||
import space.kscience.kmath.structures.Buffer
|
||||
import space.kscience.maps.features.FeatureStore.Companion.generateFeatureId
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
//@JvmInline
|
||||
//public value class FeatureId<out F : Feature<*>>(public val id: String)
|
||||
|
||||
/**
|
||||
* A reference to a feature inside a [FeatureStore]
|
||||
*/
|
||||
public class FeatureRef<T : Any, out F : Feature<T>> internal constructor(
|
||||
internal val store: FeatureStore<T>,
|
||||
internal val id: String,
|
||||
) {
|
||||
override fun toString(): String = "FeatureRef($id)"
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.resolve(): F =
|
||||
store.features[id]?.let { it as F } ?: error("Feature with ref $this not found")
|
||||
|
||||
public val <T : Any, F : Feature<T>> FeatureRef<T, F>.attributes: Attributes get() = resolve().attributes
|
||||
|
||||
public interface FeatureBuilder<T : Any> {
|
||||
public val space: CoordinateSpace<T>
|
||||
|
||||
/**
|
||||
* Add or replace feature. If [id] is null, then a unique id is generated
|
||||
*/
|
||||
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F>
|
||||
|
||||
public fun putFeatures(features: Map<String, Feature<T>?>)
|
||||
|
||||
/**
|
||||
* Update existing feature if it is present and is of type [F]
|
||||
*/
|
||||
public fun <F : Feature<T>> updateFeature(id: String, block: (F?) -> F): FeatureRef<T, F>
|
||||
|
||||
public fun group(
|
||||
id: String? = null,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
builder: FeatureGroup<T>.() -> Unit,
|
||||
): FeatureRef<T, FeatureGroup<T>>
|
||||
|
||||
public fun removeFeature(id: String)
|
||||
}
|
||||
|
||||
public interface FeatureSet<T : Any> {
|
||||
public val features: Map<String, Feature<T>>
|
||||
|
||||
/**
|
||||
* Create a reference
|
||||
*/
|
||||
public fun <F : Feature<T>> ref(id: String): FeatureRef<T, F>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A class representing a store for managing spatial features with the ability to add, update,
|
||||
* remove, and group features within a specific coordinate space.
|
||||
* It also provides reactive flow support to observe changes in the feature set.
|
||||
*
|
||||
* @param T The type parameter representing the coordinate points handled by the store.
|
||||
* @property space The coordinate space associated with this feature store, enabling operations
|
||||
* on map coordinates.
|
||||
*/
|
||||
public class FeatureStore<T : Any>(
|
||||
override val space: CoordinateSpace<T>,
|
||||
) : CoordinateSpace<T> by space, FeatureBuilder<T>, FeatureSet<T> {
|
||||
private val _featureFlow = MutableStateFlow<Map<String, Feature<T>>>(emptyMap())
|
||||
|
||||
public val featureFlow: StateFlow<Map<String, Feature<T>>> get() = _featureFlow
|
||||
|
||||
override val features: Map<String, Feature<T>> get() = featureFlow.value
|
||||
|
||||
override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> {
|
||||
val safeId = id ?: generateFeatureId(feature)
|
||||
_featureFlow.value += (safeId to feature)
|
||||
return FeatureRef(this, safeId)
|
||||
}
|
||||
|
||||
public override fun putFeatures(features: Map<String, Feature<T>?>) {
|
||||
_featureFlow.value = _featureFlow.value.toMutableMap().apply {
|
||||
features.forEach { (key, value) ->
|
||||
if (value == null) {
|
||||
remove(key)
|
||||
} else {
|
||||
put(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <F : Feature<T>> updateFeature(id: String, block: (F?) -> F): FeatureRef<T, F> =
|
||||
feature(id, block(features[id] as? F))
|
||||
|
||||
override fun group(
|
||||
id: String?,
|
||||
attributes: Attributes,
|
||||
builder: FeatureGroup<T>.() -> Unit,
|
||||
): FeatureRef<T, FeatureGroup<T>> {
|
||||
val safeId: String = id ?: generateFeatureId<FeatureGroup<*>>()
|
||||
return feature(safeId, FeatureGroup(this, safeId, attributes).apply(builder))
|
||||
}
|
||||
|
||||
override fun removeFeature(id: String) {
|
||||
_featureFlow.value -= id
|
||||
}
|
||||
|
||||
override fun <F : Feature<T>> ref(id: String): FeatureRef<T, F> = FeatureRef(this, id)
|
||||
|
||||
public fun getBoundingBox(zoom: Float = Float.MAX_VALUE): Rectangle<T>? = with(space) {
|
||||
features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
|
||||
}
|
||||
|
||||
|
||||
public companion object {
|
||||
|
||||
internal fun generateFeatureId(prefix: String): String =
|
||||
"$prefix[${Uuid.random().toHexString()}]"
|
||||
|
||||
internal fun generateFeatureId(feature: Feature<*>): String =
|
||||
generateFeatureId(feature::class.simpleName ?: "undefined")
|
||||
|
||||
internal inline fun <reified F : Feature<*>> generateFeatureId(): String =
|
||||
generateFeatureId(F::class.simpleName ?: "undefined")
|
||||
|
||||
/**
|
||||
* Build, but do not remember map feature state
|
||||
*/
|
||||
public fun <T : Any> build(
|
||||
coordinateSpace: CoordinateSpace<T>,
|
||||
builder: FeatureStore<T>.() -> Unit = {},
|
||||
): FeatureStore<T> = FeatureStore(coordinateSpace).apply(builder)
|
||||
|
||||
/**
|
||||
* Build and remember map feature state
|
||||
*/
|
||||
@Composable
|
||||
public fun <T : Any> remember(
|
||||
coordinateSpace: CoordinateSpace<T>,
|
||||
builder: FeatureStore<T>.() -> Unit = {},
|
||||
): FeatureStore<T> = remember {
|
||||
build(coordinateSpace, builder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A group of other features
|
||||
*/
|
||||
public class FeatureGroup<T : Any> internal constructor(
|
||||
public val store: FeatureStore<T>,
|
||||
public val groupId: String,
|
||||
override val attributes: Attributes,
|
||||
) : CoordinateSpace<T> by store.space, Feature<T>, FeatureBuilder<T>, FeatureSet<T> {
|
||||
|
||||
override val space: CoordinateSpace<T> get() = store.space
|
||||
|
||||
override fun withAttributes(modify: Attributes.() -> Attributes): FeatureGroup<T> =
|
||||
FeatureGroup(store, groupId, modify(attributes))
|
||||
|
||||
override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> =
|
||||
store.feature("$groupId/${id ?: generateFeatureId(feature)}", feature)
|
||||
|
||||
public override fun putFeatures(features: Map<String, Feature<T>?>) {
|
||||
store.putFeatures(features.mapKeys { "$groupId/${it.key}" })
|
||||
}
|
||||
|
||||
override fun <F : Feature<T>> updateFeature(id: String, block: (F?) -> F): FeatureRef<T, F> =
|
||||
store.updateFeature("$groupId/$id", block)
|
||||
|
||||
|
||||
override fun group(
|
||||
id: String?,
|
||||
attributes: Attributes,
|
||||
builder: FeatureGroup<T>.() -> Unit,
|
||||
): FeatureRef<T, FeatureGroup<T>> {
|
||||
val safeId = id ?: generateFeatureId<FeatureGroup<*>>()
|
||||
return feature(safeId, FeatureGroup(store, "$groupId/$safeId", attributes).apply(builder))
|
||||
}
|
||||
|
||||
override fun removeFeature(id: String) {
|
||||
store.removeFeature("$groupId/$id")
|
||||
}
|
||||
|
||||
override val features: Map<String, Feature<T>>
|
||||
get() = store.featureFlow.value
|
||||
.filterKeys { it.startsWith("$groupId/") }
|
||||
.mapKeys { it.key.removePrefix("$groupId/") }
|
||||
.toMap()
|
||||
|
||||
override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) {
|
||||
features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
|
||||
}
|
||||
|
||||
override fun <F : Feature<T>> ref(id: String): FeatureRef<T, F> = FeatureRef(store, "$groupId/$id")
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively search for feature until function returns true
|
||||
*/
|
||||
public fun <T : Any> FeatureSet<T>.forEachUntil(block: FeatureSet<T>.(ref: FeatureRef<T, *>, feature: Feature<T>) -> Boolean) {
|
||||
features.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
|
||||
if (!block(ref<Feature<T>>(key), feature)) return@forEachUntil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all features with a given attribute from the one with highest [z] to lowest
|
||||
*/
|
||||
public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttribute(
|
||||
key: Attribute<A>,
|
||||
block: FeatureSet<T>.(ref: FeatureRef<T, *>, feature: Feature<T>, attribute: A) -> Unit,
|
||||
) {
|
||||
features.forEach { (id, feature) ->
|
||||
feature.attributes[key]?.let {
|
||||
block(ref<Feature<T>>(id), feature, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttributeUntil(
|
||||
key: Attribute<A>,
|
||||
block: FeatureSet<T>.(ref: FeatureRef<T, *>, feature: Feature<T>, attribute: A) -> Boolean,
|
||||
) {
|
||||
features.forEach { (id, feature) ->
|
||||
feature.attributes[key]?.let {
|
||||
if (!block(ref<Feature<T>>(id), feature, it)) return@forEachWithAttributeUntil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public inline fun <T : Any, reified F : Feature<T>> FeatureSet<T>.forEachWithType(
|
||||
crossinline block: FeatureSet<T>.(ref: FeatureRef<T, F>, feature: F) -> Unit,
|
||||
) {
|
||||
features.forEach { (id, feature) ->
|
||||
if (feature is F) block(ref(id), feature)
|
||||
}
|
||||
}
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.circle(
|
||||
center: T,
|
||||
size: Dp = 5.dp,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, CircleFeature<T>> = feature(
|
||||
id, CircleFeature(space, center, size, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.rectangle(
|
||||
centerCoordinates: T,
|
||||
size: DpSize = DpSize(5.dp, 5.dp),
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, RectangleFeature<T>> = feature(
|
||||
id, RectangleFeature(space, centerCoordinates, size, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.draw(
|
||||
position: T,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
draw: DrawScope.() -> Unit,
|
||||
): FeatureRef<T, DrawFeature<T>> = feature(
|
||||
id,
|
||||
DrawFeature(space, position, drawFeature = draw, attributes = attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.line(
|
||||
aCoordinates: T,
|
||||
bCoordinates: T,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, LineFeature<T>> = feature(
|
||||
id,
|
||||
LineFeature(space, aCoordinates, bCoordinates, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.arc(
|
||||
oval: Rectangle<T>,
|
||||
startAngle: Angle,
|
||||
arcLength: Angle,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, ArcFeature<T>> = feature(
|
||||
id,
|
||||
ArcFeature(space, oval, startAngle, arcLength, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.points(
|
||||
points: List<T>,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, PointsFeature<T>> = feature(
|
||||
id,
|
||||
PointsFeature(space, points, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.multiLine(
|
||||
points: List<T>,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, MultiLineFeature<T>> = feature(
|
||||
id,
|
||||
MultiLineFeature(space, points, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.polygon(
|
||||
points: List<T>,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, PolygonFeature<T>> = feature(
|
||||
id,
|
||||
PolygonFeature(space, points, attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.icon(
|
||||
position: T,
|
||||
image: ImageVector,
|
||||
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, VectorIconFeature<T>> = feature(
|
||||
id,
|
||||
VectorIconFeature(
|
||||
space,
|
||||
position,
|
||||
size,
|
||||
image,
|
||||
attributes
|
||||
)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.scalableImage(
|
||||
box: Rectangle<T>,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
painter: @Composable () -> Painter,
|
||||
): FeatureRef<T, ScalableImageFeature<T>> = feature(
|
||||
id,
|
||||
ScalableImageFeature<T>(space, box, painter = painter, attributes = attributes)
|
||||
)
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.text(
|
||||
position: T,
|
||||
text: String,
|
||||
font: Font.() -> Unit = { size = 16f },
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, TextFeature<T>> = feature(
|
||||
id,
|
||||
TextFeature(space, position, text, fontConfig = font, attributes = attributes)
|
||||
)
|
||||
|
||||
//public fun <T> StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND<T> {
|
||||
// val strides = Strides(shape)
|
||||
// return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) })
|
||||
//}
|
||||
|
||||
public inline fun <reified T> Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D<T> {
|
||||
val strides = Strides(ShapeND(rows, columns))
|
||||
return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }).as2D()
|
||||
}
|
||||
|
||||
public fun <T : Any> FeatureStore<T>.pixelMap(
|
||||
rectangle: Rectangle<T>,
|
||||
pixelMap: Structure2D<Color?>,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, PixelMapFeature<T>> = feature(
|
||||
id,
|
||||
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.features.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.isPrimaryPressed
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.features
|
||||
package space.kscience.maps.features
|
||||
|
||||
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
|
||||
@@ -0,0 +1,66 @@
|
||||
package space.kscience.maps.features
|
||||
|
||||
import space.kscience.attributes.Attributes
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.draggableLine(
|
||||
aId: FeatureRef<T, MarkerFeature<T>>,
|
||||
bId: FeatureRef<T, MarkerFeature<T>>,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, LineFeature<T>> {
|
||||
val lineId = id ?: FeatureStore.generateFeatureId<LineFeature<*>>()
|
||||
|
||||
fun drawLine(): FeatureRef<T, LineFeature<T>> = updateFeature(lineId) { old ->
|
||||
LineFeature(
|
||||
space,
|
||||
aId.resolve().center,
|
||||
bId.resolve().center,
|
||||
old?.attributes ?: Attributes(ZAttribute, -10f)
|
||||
)
|
||||
}
|
||||
|
||||
aId.draggable { _, _ ->
|
||||
drawLine()
|
||||
}
|
||||
|
||||
bId.draggable { _, _ ->
|
||||
drawLine()
|
||||
}
|
||||
|
||||
return drawLine()
|
||||
}
|
||||
|
||||
public fun <T : Any> FeatureBuilder<T>.draggableMultiLine(
|
||||
points: List<FeatureRef<T, MarkerFeature<T>>>,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, MultiLineFeature<T>> {
|
||||
val polygonId = id ?: FeatureStore.generateFeatureId("multiline")
|
||||
|
||||
fun drawLines(): FeatureRef<T, MultiLineFeature<T>> = updateFeature(polygonId) { old ->
|
||||
MultiLineFeature(
|
||||
space,
|
||||
points.map { it.resolve().center },
|
||||
old?.attributes ?: Attributes(ZAttribute, -10f)
|
||||
)
|
||||
}
|
||||
|
||||
points.forEach {
|
||||
it.draggable { _, _ ->
|
||||
drawLines()
|
||||
}
|
||||
}
|
||||
|
||||
return drawLines()
|
||||
}
|
||||
|
||||
@JvmName("draggableMultiLineFromPoints")
|
||||
public fun <T : Any> FeatureBuilder<T>.draggableMultiLine(
|
||||
points: List<T>,
|
||||
id: String? = null,
|
||||
): FeatureRef<T, MultiLineFeature<T>> {
|
||||
val pointRefs = points.map {
|
||||
circle(it)
|
||||
}
|
||||
return draggableMultiLine(pointRefs, id)
|
||||
}
|
||||
@@ -1,37 +1,42 @@
|
||||
package center.sciprog.maps.features
|
||||
package space.kscience.maps.features
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import center.sciprog.attributes.plus
|
||||
import org.jetbrains.skia.Font
|
||||
import org.jetbrains.skia.Paint
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PointMode
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.attributes.plus
|
||||
import space.kscience.kmath.PerformancePitfall
|
||||
import space.kscience.kmath.geometry.degrees
|
||||
|
||||
|
||||
internal fun Color.toPaint(): Paint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = toArgb()
|
||||
}
|
||||
//internal fun Color.toPaint(): Paint = Paint().apply {
|
||||
// isAntiAlias = true
|
||||
// color = toArgb()
|
||||
//}
|
||||
|
||||
public fun <T : Any> DrawScope.drawFeature(
|
||||
state: CoordinateViewScope<T>,
|
||||
painterCache: Map<PainterFeature<T>, Painter>,
|
||||
|
||||
public fun <T : Any> FeatureDrawScope<T>.drawFeature(
|
||||
feature: Feature<T>,
|
||||
): Unit = with(state) {
|
||||
val color = feature.color ?: Color.Red
|
||||
val alpha = feature.attributes[AlphaAttribute] ?: 1f
|
||||
fun T.toOffset(): Offset = toOffset(this@drawFeature)
|
||||
baseAttributes: Attributes,
|
||||
): Unit {
|
||||
|
||||
val attributes = baseAttributes + feature.attributes
|
||||
val color = attributes[ColorAttribute] ?: Color.Red
|
||||
val alpha = attributes[AlphaAttribute] ?: 1f
|
||||
//avoid drawing invisible features
|
||||
if (attributes[VisibleAttribute] == false) return
|
||||
|
||||
when (feature) {
|
||||
is FeatureSelector -> drawFeature(state, painterCache, feature.selector(state.zoom))
|
||||
is FeatureSelector -> drawFeature(feature.selector(state.zoom), attributes)
|
||||
is CircleFeature -> drawCircle(
|
||||
color,
|
||||
feature.radius.toPx(),
|
||||
center = feature.center.toOffset()
|
||||
center = feature.center.toOffset(),
|
||||
alpha = alpha
|
||||
)
|
||||
|
||||
is RectangleFeature -> drawRect(
|
||||
@@ -40,15 +45,17 @@ public fun <T : Any> DrawScope.drawFeature(
|
||||
feature.size.width.toPx() / 2,
|
||||
feature.size.height.toPx() / 2
|
||||
),
|
||||
size = feature.size.toSize()
|
||||
size = feature.size.toSize(),
|
||||
alpha = alpha
|
||||
)
|
||||
|
||||
is LineFeature -> drawLine(
|
||||
color,
|
||||
feature.a.toOffset(),
|
||||
feature.b.toOffset(),
|
||||
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
||||
pathEffect = feature.attributes[PathEffectAttribute]
|
||||
strokeWidth = attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
||||
pathEffect = attributes[PathEffectAttribute],
|
||||
alpha = alpha
|
||||
)
|
||||
|
||||
is ArcFeature -> {
|
||||
@@ -58,8 +65,8 @@ public fun <T : Any> DrawScope.drawFeature(
|
||||
|
||||
drawArc(
|
||||
color = color,
|
||||
startAngle = (feature.startAngle.degrees).toFloat(),
|
||||
sweepAngle = (feature.arcLength.degrees).toFloat(),
|
||||
startAngle = (feature.startAngle.toDegrees().value).toFloat(),
|
||||
sweepAngle = (feature.arcLength.toDegrees().value).toFloat(),
|
||||
useCenter = false,
|
||||
topLeft = dpRect.topLeft,
|
||||
size = size,
|
||||
@@ -69,28 +76,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 -> {
|
||||
val offset = feature.center.toOffset()
|
||||
val size = feature.size.toSize()
|
||||
translate(offset.x - size.width / 2, offset.y - size.height / 2) {
|
||||
with(painterCache[feature]!!) {
|
||||
draw(size)
|
||||
with(this@drawFeature.painterFor(feature)) {
|
||||
draw(size, colorFilter = feature.color?.let { ColorFilter.tint(it) }, alpha = alpha)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TextFeature -> drawIntoCanvas { canvas ->
|
||||
val offset = feature.position.toOffset()
|
||||
canvas.nativeCanvas.drawString(
|
||||
feature.text,
|
||||
offset.x + 5,
|
||||
offset.y - 5,
|
||||
Font().apply(feature.fontConfig),
|
||||
(feature.color ?: Color.Black).toPaint()
|
||||
)
|
||||
}
|
||||
is TextFeature -> drawText(feature.text, feature.position.toOffset(), attributes)
|
||||
|
||||
is DrawFeature -> {
|
||||
val offset = feature.position.toOffset()
|
||||
@@ -100,11 +98,7 @@ public fun <T : Any> DrawScope.drawFeature(
|
||||
}
|
||||
|
||||
is FeatureGroup -> {
|
||||
feature.featureMap.values.forEach {
|
||||
drawFeature(state, painterCache, it.withAttributes {
|
||||
feature.attributes + this
|
||||
})
|
||||
}
|
||||
//ignore groups
|
||||
}
|
||||
|
||||
is PathFeature -> {
|
||||
@@ -121,9 +115,9 @@ public fun <T : Any> DrawScope.drawFeature(
|
||||
drawPoints(
|
||||
points = points,
|
||||
color = color,
|
||||
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
||||
strokeWidth = attributes[StrokeAttribute] ?: 5f,
|
||||
pointMode = PointMode.Points,
|
||||
pathEffect = feature.attributes[PathEffectAttribute],
|
||||
pathEffect = attributes[PathEffectAttribute],
|
||||
alpha = alpha
|
||||
)
|
||||
}
|
||||
@@ -133,9 +127,9 @@ public fun <T : Any> DrawScope.drawFeature(
|
||||
drawPoints(
|
||||
points = points,
|
||||
color = color,
|
||||
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
||||
strokeWidth = attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
|
||||
pointMode = PointMode.Polygon,
|
||||
pathEffect = feature.attributes[PathEffectAttribute],
|
||||
pathEffect = attributes[PathEffectAttribute],
|
||||
alpha = alpha
|
||||
)
|
||||
}
|
||||
@@ -160,8 +154,8 @@ public fun <T : Any> DrawScope.drawFeature(
|
||||
val offset = rect.topLeft
|
||||
|
||||
translate(offset.x, offset.y) {
|
||||
with(painterCache[feature]!!) {
|
||||
draw(rect.size)
|
||||
with(this@drawFeature.painterFor(feature)) {
|
||||
draw(rect.size, alpha = alpha)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,15 +178,17 @@ public fun <T : Any> DrawScope.drawFeature(
|
||||
x = i * xStep,
|
||||
y = rect.height - j * yStep
|
||||
),
|
||||
size = pixelSize
|
||||
size = pixelSize,
|
||||
alpha = alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
//logger.error { "Unrecognized feature type: ${feature::class}" }
|
||||
is CustomFeature<*> -> {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.features
|
||||
package space.kscience.maps.features
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.PointerMatcher
|
||||
@@ -6,13 +6,17 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.input.pointer.PointerEvent
|
||||
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
|
||||
import center.sciprog.attributes.Attribute
|
||||
import center.sciprog.attributes.AttributesBuilder
|
||||
import center.sciprog.attributes.SetAttribute
|
||||
import center.sciprog.attributes.withAttribute
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import space.kscience.attributes.*
|
||||
import space.kscience.attributes.serialization.SerializableAttribute
|
||||
|
||||
public object NameAttribute : SerializableAttribute<String>("name", String.serializer())
|
||||
|
||||
public object ZAttribute : Attribute<Float>
|
||||
|
||||
public val Feature<*>.z: Float
|
||||
get() = attributes[ZAttribute] ?: 0f
|
||||
|
||||
public object DraggableAttribute : Attribute<DragHandle<Any>>
|
||||
|
||||
public object DragListenerAttribute : SetAttribute<DragListener<Any>>
|
||||
@@ -46,23 +50,28 @@ public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.zoomRange(range: FloatRang
|
||||
|
||||
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")
|
||||
parent.feature(
|
||||
store.feature(
|
||||
id,
|
||||
resolve().withAttributes {
|
||||
AttributesBuilder(this).apply(modify).build()
|
||||
} as F
|
||||
resolve().withAttributes { modified(modification) } as F
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(
|
||||
key: Attribute<V>,
|
||||
value: V?,
|
||||
value: V,
|
||||
): FeatureRef<T, F> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F)
|
||||
store.feature(id, resolve().withAttributes { withAttribute(key, value) } as F)
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -75,10 +84,10 @@ public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(
|
||||
public fun <T : Any, F : DraggableFeature<T>> FeatureRef<T, F>.draggable(
|
||||
constraint: ((T) -> T)? = null,
|
||||
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null,
|
||||
): FeatureRef<T, F> = with(parent) {
|
||||
): FeatureRef<T, F> = with(store) {
|
||||
if (attributes[DraggableAttribute] == null) {
|
||||
val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
|
||||
val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
|
||||
val feature = features[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
|
||||
start as ViewPoint<T>
|
||||
end as ViewPoint<T>
|
||||
if (start in feature) {
|
||||
@@ -169,4 +178,7 @@ public fun <T : Any> FeatureRef<T, LineSegmentFeature<T>>.pathEffect(effect: Pat
|
||||
public object StrokeAttribute : Attribute<Float>
|
||||
|
||||
public fun <T : Any, F : LineSegmentFeature<T>> FeatureRef<T, F>.stroke(width: Float): FeatureRef<T, F> =
|
||||
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,22 @@
|
||||
package center.sciprog.attributes
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.contextual
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import kotlinx.serialization.serializer
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.attributes.serialization.AttributesSerializer
|
||||
import space.kscience.attributes.serialization.SerializableAttribute
|
||||
import space.kscience.maps.features.NameAttribute
|
||||
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class AttributesSerializationTest {
|
||||
|
||||
@@ -28,32 +37,36 @@ internal class AttributesSerializationTest {
|
||||
override fun toString(): String = "test"
|
||||
}
|
||||
|
||||
val serializer = AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute))
|
||||
|
||||
@Test
|
||||
fun restoreFromJson() {
|
||||
|
||||
val json = Json {
|
||||
serializersModule = SerializersModule {
|
||||
contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute)))
|
||||
contextual(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
val attributes = Attributes {
|
||||
val attributes = Attributes<Any> {
|
||||
NameAttribute("myTest")
|
||||
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
|
||||
ContainerAttribute(
|
||||
Container(
|
||||
Attributes {
|
||||
Attributes<Any> {
|
||||
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val serialized: String = json.encodeToString(attributes)
|
||||
|
||||
val serialized: String = json.encodeToString(serializer, attributes)
|
||||
println(serialized)
|
||||
|
||||
val restored: Attributes = json.decodeFromString(serialized)
|
||||
val restored: Attributes = json.decodeFromString(serializer, serialized)
|
||||
|
||||
assertEquals(attributes, restored)
|
||||
assertTrue { Attributes.equals(attributes, restored) }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@@ -62,25 +75,25 @@ internal class AttributesSerializationTest {
|
||||
fun restoreFromProtoBuf() {
|
||||
val protoBuf = ProtoBuf {
|
||||
serializersModule = SerializersModule {
|
||||
contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute)))
|
||||
contextual(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
val attributes = Attributes {
|
||||
val attributes = Attributes<Any> {
|
||||
NameAttribute("myTest")
|
||||
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
|
||||
ContainerAttribute(
|
||||
Container(
|
||||
Attributes {
|
||||
Attributes<Any> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
package center.sciprog.maps.svg
|
||||
package space.kscience.maps.svg
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
@@ -301,7 +301,7 @@ internal class SvgCanvas(val graphics: SVGGraphics2D) : Canvas {
|
||||
}
|
||||
|
||||
internal class SvgDrawContext(val graphics: SVGGraphics2D, override var size: Size) : DrawContext {
|
||||
override val canvas: Canvas = SvgCanvas(graphics)
|
||||
override var canvas: Canvas = SvgCanvas(graphics)
|
||||
|
||||
override val transform: DrawTransform = asDrawTransform()
|
||||
}
|
||||
@@ -1,25 +1,34 @@
|
||||
package center.sciprog.maps.svg
|
||||
package space.kscience.maps.svg
|
||||
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.*
|
||||
import androidx.compose.ui.graphics.drawscope.DrawContext
|
||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import org.jfree.svg.SVGGraphics2D
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.maps.features.CanvasState
|
||||
import space.kscience.maps.features.ColorAttribute
|
||||
import space.kscience.maps.features.FeatureDrawScope
|
||||
import space.kscience.maps.features.PainterFeature
|
||||
import java.awt.BasicStroke
|
||||
import java.awt.Font
|
||||
import java.awt.geom.*
|
||||
import java.awt.image.AffineTransformOp
|
||||
import java.awt.Color as AWTColor
|
||||
|
||||
public class SvgDrawScope(
|
||||
public class SvgDrawScope<T: Any>(
|
||||
state: CanvasState<T>,
|
||||
private val graphics: SVGGraphics2D,
|
||||
size: Size,
|
||||
private val painterCache: Map<PainterFeature<T>, Painter>,
|
||||
private val defaultStrokeWidth: Float = 1f,
|
||||
) : DrawScope {
|
||||
) : FeatureDrawScope<T>(state) {
|
||||
|
||||
override val layoutDirection: LayoutDirection
|
||||
get() = LayoutDirection.Ltr
|
||||
@@ -459,18 +468,22 @@ public class SvgDrawScope(
|
||||
}
|
||||
}
|
||||
|
||||
public fun drawText(
|
||||
text: String,
|
||||
x: Float,
|
||||
y: Float,
|
||||
font: Font,
|
||||
color: Color,
|
||||
) {
|
||||
setupColor(color)
|
||||
graphics.font = font
|
||||
graphics.drawString(text, x, y)
|
||||
// public fun renderText(
|
||||
// textFeature: TextFeature<T>,
|
||||
// ) {
|
||||
// textFeature.color?.let { setupColor(it) }
|
||||
// graphics.drawString(textFeature.text, textFeature.position.x, textFeature.position.y)
|
||||
// }
|
||||
|
||||
override fun painterFor(feature: PainterFeature<T>): Painter {
|
||||
return painterCache[feature]!!
|
||||
}
|
||||
|
||||
override val drawContext: DrawContext = SvgDrawContext(graphics, size)
|
||||
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,34 @@
|
||||
package space.kscience.maps.svg
|
||||
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.text.TextMeasurer
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import space.kscience.maps.features.CanvasState
|
||||
import space.kscience.maps.features.ComposeFeatureDrawScope
|
||||
import space.kscience.maps.features.FeatureSet
|
||||
import space.kscience.maps.features.PainterFeature
|
||||
|
||||
public fun <T : Any> FeatureSet<T>.generateBitmap(
|
||||
canvasState: CanvasState<T>,
|
||||
painterCache: Map<PainterFeature<T>, Painter>,
|
||||
textMeasurer: TextMeasurer,
|
||||
size: Size
|
||||
): ImageBitmap {
|
||||
|
||||
val bitmap = ImageBitmap(size.width.toInt(), size.height.toInt())
|
||||
|
||||
CanvasDrawScope().draw(
|
||||
density = Density(1f),
|
||||
layoutDirection = LayoutDirection.Ltr,
|
||||
canvas = Canvas(bitmap),
|
||||
size = size,
|
||||
) {
|
||||
ComposeFeatureDrawScope(this, canvasState, painterCache, textMeasurer).features(this@generateBitmap)
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package space.kscience.maps.svg
|
||||
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import org.jfree.svg.SVGGraphics2D
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.attributes.plus
|
||||
import space.kscience.maps.features.*
|
||||
|
||||
|
||||
public fun <T : Any> FeatureDrawScope<T>.features(featureSet: FeatureSet<T>) {
|
||||
featureSet.features.entries.sortedBy { it.value.z }
|
||||
.filter { state.viewPoint.zoom in it.value.zoomRange }
|
||||
.forEach { (id, feature) ->
|
||||
val attributesCache = mutableMapOf<List<String>, Attributes>()
|
||||
|
||||
fun computeGroupAttributes(path: List<String>): Attributes = attributesCache.getOrPut(path) {
|
||||
if (path.isEmpty()) return Attributes.EMPTY
|
||||
else if (path.size == 1) {
|
||||
featureSet.features[path.first()]?.attributes ?: Attributes.EMPTY
|
||||
} else {
|
||||
computeGroupAttributes(path.dropLast(1)) +
|
||||
(featureSet.features[path.first()]?.attributes ?: Attributes.EMPTY)
|
||||
}
|
||||
}
|
||||
|
||||
val path = id.split("/")
|
||||
drawFeature(feature, computeGroupAttributes(path.dropLast(1)))
|
||||
}
|
||||
}
|
||||
|
||||
public fun <T : Any> FeatureSet<T>.generateSvg(
|
||||
canvasState: CanvasState<T>,
|
||||
painterCache: Map<PainterFeature<T>, Painter>,
|
||||
id: String? = null,
|
||||
): String {
|
||||
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(
|
||||
canvasState.canvasSize.width.value.toDouble(),
|
||||
canvasState.canvasSize.height.value.toDouble()
|
||||
)
|
||||
val svgScope = SvgDrawScope(canvasState, svgGraphics2D, painterCache)
|
||||
|
||||
svgScope.features(this)
|
||||
|
||||
return svgGraphics2D.getSVGElement(id)
|
||||
}
|
||||
@@ -1,24 +1,13 @@
|
||||
# Module maps-kt-geojson
|
||||
|
||||
|
||||
GeoJson format support
|
||||
|
||||
## Usage
|
||||
|
||||
## 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.4.0-dev-7`.
|
||||
|
||||
**Gradle Groovy:**
|
||||
```groovy
|
||||
repositories {
|
||||
maven { url 'https://repo.kotlin.link' }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'center.sciprog:maps-kt-geojson:0.2.2'
|
||||
}
|
||||
```
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
repositories {
|
||||
@@ -27,6 +16,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("center.sciprog:maps-kt-geojson:0.2.2")
|
||||
implementation("space.kscience:maps-kt-geojson:0.4.0-dev-7")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -3,10 +3,13 @@ plugins {
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
description = "GeoJson format support"
|
||||
|
||||
kscience{
|
||||
jvm()
|
||||
js()
|
||||
// js()
|
||||
wasm()
|
||||
|
||||
useSerialization {
|
||||
json()
|
||||
}
|
||||
@@ -15,4 +18,8 @@ kscience{
|
||||
api(projects.mapsKtFeatures)
|
||||
api(spclibs.kotlinx.serialization.json)
|
||||
}
|
||||
}
|
||||
|
||||
readme {
|
||||
maturity = space.kscience.gradle.Maturity.DEVELOPMENT
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package center.sciprog.maps.geojson
|
||||
package space.kscience.maps.geojson
|
||||
|
||||
import center.sciprog.maps.geojson.GeoJson.Companion.PROPERTIES_KEY
|
||||
import center.sciprog.maps.geojson.GeoJson.Companion.TYPE_KEY
|
||||
import center.sciprog.maps.geojson.GeoJsonFeatureCollection.Companion.FEATURES_KEY
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.*
|
||||
import space.kscience.maps.geojson.GeoJson.Companion.PROPERTIES_KEY
|
||||
import space.kscience.maps.geojson.GeoJson.Companion.TYPE_KEY
|
||||
import space.kscience.maps.geojson.GeoJsonFeatureCollection.Companion.FEATURES_KEY
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
/**
|
||||
@@ -1,10 +1,9 @@
|
||||
package center.sciprog.maps.geojson
|
||||
package space.kscience.maps.geojson
|
||||
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.coordinates.meters
|
||||
import center.sciprog.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY
|
||||
import kotlinx.serialization.json.*
|
||||
import space.kscience.kmath.geometry.degrees
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.coordinates.meters
|
||||
import space.kscience.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
public sealed interface GeoJsonGeometry : GeoJson {
|
||||
@@ -35,8 +34,8 @@ internal fun JsonElement.toGmc() = jsonArray.run {
|
||||
}
|
||||
|
||||
internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray {
|
||||
add(longitude.degrees)
|
||||
add(latitude.degrees)
|
||||
add(longitude.toDegrees().value)
|
||||
add(latitude.toDegrees().value)
|
||||
elevation?.let {
|
||||
add(it.meters)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package center.sciprog.maps.geojson
|
||||
package space.kscience.maps.geojson
|
||||
|
||||
import center.sciprog.attributes.SerializableAttribute
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.serializer
|
||||
import space.kscience.attributes.serialization.SerializableAttribute
|
||||
|
||||
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.descriptors.SerialDescriptor
|
||||
@@ -1,18 +1,17 @@
|
||||
package center.sciprog.maps.geojson
|
||||
package space.kscience.maps.geojson
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import center.sciprog.attributes.NameAttribute
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.features.*
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.features.*
|
||||
|
||||
|
||||
/**
|
||||
* Add a single Json geometry to a feature builder
|
||||
*/
|
||||
public fun FeatureGroup<Gmc>.geoJsonGeometry(
|
||||
public fun FeatureBuilder<Gmc>.geoJsonGeometry(
|
||||
geometry: GeoJsonGeometry,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, Feature<Gmc>> = when (geometry) {
|
||||
@@ -50,11 +49,11 @@ public fun FeatureGroup<Gmc>.geoJsonGeometry(
|
||||
}
|
||||
}
|
||||
|
||||
public fun FeatureGroup<Gmc>.geoJsonFeature(
|
||||
public fun FeatureBuilder<Gmc>.geoJsonFeature(
|
||||
geoJson: GeoJsonFeature,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, Feature<Gmc>> {
|
||||
val geometry = geoJson.geometry ?: return group {}
|
||||
val geometry = geoJson.geometry ?: return group(null) {}
|
||||
val idOverride = id ?: geoJson.getProperty("id")?.jsonPrimitive?.contentOrNull
|
||||
|
||||
return geoJsonGeometry(geometry, idOverride).modifyAttributes {
|
||||
@@ -72,7 +71,7 @@ public fun FeatureGroup<Gmc>.geoJsonFeature(
|
||||
}
|
||||
}
|
||||
|
||||
public fun FeatureGroup<Gmc>.geoJson(
|
||||
public fun FeatureBuilder<Gmc>.geoJson(
|
||||
geoJson: GeoJson,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, Feature<Gmc>> = when (geoJson) {
|
||||
@@ -1,17 +1,17 @@
|
||||
package center.sciprog.maps.geojson
|
||||
package space.kscience.maps.geojson
|
||||
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.features.Feature
|
||||
import center.sciprog.maps.features.FeatureGroup
|
||||
import center.sciprog.maps.features.FeatureRef
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.features.Feature
|
||||
import space.kscience.maps.features.FeatureBuilder
|
||||
import space.kscience.maps.features.FeatureRef
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* Add geojson features from url
|
||||
*/
|
||||
public fun FeatureGroup<Gmc>.geoJson(
|
||||
public fun FeatureBuilder<Gmc>.geoJson(
|
||||
geoJsonUrl: URL,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, Feature<Gmc>> {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user