Compare commits
12 Commits
main
...
feature/ge
| Author | SHA1 | Date | |
|---|---|---|---|
| 237b9ec1c5 | |||
| 3ae45dbc93 | |||
| e6dab6071e | |||
| 585d174fa4 | |||
| 67469b3d62 | |||
| 558c217987 | |||
| 35c459d658 | |||
| e1a2ba06e1 | |||
| a63490a3d5 | |||
| 8b50045c6e | |||
| 621ab06361 | |||
| c1bb150dd8 |
@@ -7,17 +7,21 @@
|
||||
- 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
|
||||
|
||||
30
README.md
30
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -14,7 +32,7 @@ This repository is a work-in-progress implementation of Map-with-markers compone
|
||||
### [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.
|
||||
@@ -36,12 +54,17 @@ This repository is a work-in-progress implementation of Map-with-markers compone
|
||||
> **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
|
||||
@@ -67,3 +90,4 @@ This repository is a work-in-progress implementation of Map-with-markers compone
|
||||
### [demo/trajectory-playground](demo/trajectory-playground)
|
||||
>
|
||||
> **Maturity**: EXPERIMENTAL
|
||||
|
||||
|
||||
@@ -5,11 +5,9 @@ plugins {
|
||||
id("space.kscience.gradle.project")
|
||||
}
|
||||
|
||||
val kmathVersion: String by extra("0.4.0")
|
||||
|
||||
allprojects {
|
||||
group = "space.kscience"
|
||||
version = "0.4.0-dev-3"
|
||||
version = "0.4.0-dev-7"
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
|
||||
@file:OptIn(ExperimentalWasmDsl::class)
|
||||
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
@@ -9,7 +11,6 @@ plugins {
|
||||
//val ktorVersion: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
@OptIn(ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
binaries.executable()
|
||||
|
||||
@@ -6,17 +6,29 @@ plugins {
|
||||
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(spclibs.logback.classic)
|
||||
}
|
||||
|
||||
@@ -13,6 +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.maps.geotools.geoTiff
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -75,6 +76,10 @@ fun App() {
|
||||
.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))
|
||||
@@ -158,6 +163,7 @@ fun App() {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
centerCoordinates.filterNotNull().onEach {
|
||||
group(id = "center") {
|
||||
circle(center = it, id = "circle", size = 1.dp).color(Color.Blue)
|
||||
|
||||
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.
@@ -10,7 +10,7 @@ val ktorVersion: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvmToolchain(11)
|
||||
jvmToolchain(17)
|
||||
sourceSets {
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
|
||||
@@ -6,24 +6,27 @@ plugins {
|
||||
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"
|
||||
@@ -35,4 +38,8 @@ compose{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resources {
|
||||
generateResClass = always
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 968 KiB After Width: | Height: | Size: 968 KiB |
|
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 469 KiB |
@@ -6,18 +6,20 @@ 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 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
|
||||
|
||||
@@ -28,7 +30,7 @@ fun App() {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val features = FeatureStore.remember(XYCoordinateSpace) {
|
||||
background(1600f, 1200f) { painterResource("middle-earth.jpg") }
|
||||
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)
|
||||
|
||||
@@ -2,16 +2,23 @@ import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Face
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import 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("SPC-logo.png")) {
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Joker2023 demo",
|
||||
icon = painterResource(Res.drawable.SPC_logo)
|
||||
) {
|
||||
MaterialTheme {
|
||||
|
||||
SchemeView(
|
||||
@@ -22,7 +29,7 @@ fun main() = application {
|
||||
}
|
||||
)
|
||||
) {
|
||||
background(1734f, 724f, id = "background") { painterResource("joker2023.png") }
|
||||
background(1734f, 724f, id = "background") { painterResource(Res.drawable.joker2023) }
|
||||
group(id = "hall_1") {
|
||||
polygon(
|
||||
listOf(
|
||||
@@ -66,9 +73,9 @@ fun main() = application {
|
||||
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(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)
|
||||
icon(XY(293.5827, 319.21915), Icons.Default.Face).color(Color.Red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ val ktorVersion: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvmToolchain(11)
|
||||
jvmToolchain(17)
|
||||
sourceSets {
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
|
||||
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}
|
||||
|
||||
@@ -2,4 +2,4 @@ kotlin.code.style=official
|
||||
|
||||
org.gradle.jvmargs=-Xmx4096m
|
||||
|
||||
toolsVersion=0.15.4-kotlin-2.0.0
|
||||
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.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -7,7 +7,7 @@ The core interfaces of KMath.
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-compose:0.3.0`.
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-compose:0.4.0-dev-7`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
@@ -17,6 +17,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("space.kscience:maps-kt-compose:0.3.0")
|
||||
implementation("space.kscience:maps-kt-compose:0.4.0-dev-7")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -15,31 +15,26 @@ kscience {
|
||||
commonMain{
|
||||
api(projects.mapsKtCore)
|
||||
api(projects.mapsKtFeatures)
|
||||
api(dependencies.platform(spclibs.ktor.bom))
|
||||
api(compose.foundation)
|
||||
api(project.dependencies.platform(spclibs.ktor.bom))
|
||||
}
|
||||
jvmMain{
|
||||
api("io.ktor:ktor-client-cio")
|
||||
}
|
||||
jvmTest{
|
||||
implementation("io.ktor:ktor-client-cio")
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(spclibs.kotlinx.coroutines.test)
|
||||
|
||||
implementation(spclibs.logback.classic)
|
||||
|
||||
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." }
|
||||
}
|
||||
|
||||
//tasks.getByName<Copy>("downloadWix"){
|
||||
// duplicatesStrategy = DuplicatesStrategy.WARN
|
||||
//}
|
||||
|
||||
@@ -2,16 +2,13 @@
|
||||
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import kotlin.jvm.Synchronized
|
||||
|
||||
|
||||
internal class LruCache<K, V>(
|
||||
private var capacity: Int,
|
||||
) {
|
||||
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)
|
||||
}
|
||||
@@ -27,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
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpRect
|
||||
import androidx.compose.ui.unit.dp
|
||||
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
|
||||
@@ -15,9 +16,28 @@ import space.kscience.maps.features.*
|
||||
import kotlin.math.*
|
||||
|
||||
|
||||
/**
|
||||
* 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(
|
||||
public val mapTileProvider: MapTileProvider,
|
||||
config: ViewConfig<Gmc>,
|
||||
public val tileSize: Int = MapTileProvider.DEFAULT_TILE_SIZE
|
||||
) : CanvasState<Gmc>(config) {
|
||||
override val space: CoordinateSpace<Gmc> get() = WebMercatorSpace
|
||||
|
||||
@@ -63,7 +83,7 @@ public class MapCanvasState internal constructor(
|
||||
min(
|
||||
canvasSize.width.value / rectangle.longitudeDelta.toRadians().value,
|
||||
canvasSize.height.value / rectangle.latitudeDelta.toRadians().value
|
||||
) * 2 * PI / mapTileProvider.tileSize
|
||||
) * 2 * PI / tileSize
|
||||
).coerceIn(0.0..22.0)
|
||||
return space.ViewPoint(rectangle.center, zoom.toFloat())
|
||||
}
|
||||
@@ -84,12 +104,12 @@ public class MapCanvasState internal constructor(
|
||||
public companion object {
|
||||
@Composable
|
||||
public fun remember(
|
||||
mapTileProvider: MapTileProvider,
|
||||
config: ViewConfig<Gmc> = ViewConfig(),
|
||||
initialViewPoint: ViewPoint<Gmc>? = null,
|
||||
initialRectangle: Rectangle<Gmc>? = null,
|
||||
tileSize: Int = MapTileProvider.DEFAULT_TILE_SIZE,
|
||||
): MapCanvasState = remember {
|
||||
MapCanvasState(mapTileProvider, config).apply {
|
||||
MapCanvasState(config, tileSize).apply {
|
||||
if (initialViewPoint != null) {
|
||||
viewPoint = initialViewPoint
|
||||
} else if (initialRectangle != null) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.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
|
||||
@@ -13,38 +11,56 @@ 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 kotlinx.coroutines.supervisorScope
|
||||
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) = kotlin.math.max(first, other.first)..kotlin.math.min(last, other.last)
|
||||
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,
|
||||
mapTileProvider: MapTileProvider,
|
||||
featureStore: FeatureStore<Gmc>,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val mapTiles = remember(mapTileProvider) {
|
||||
mutableStateMapOf<TileId, Image>()
|
||||
): 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()
|
||||
}
|
||||
}
|
||||
|
||||
with(mapState) {
|
||||
|
||||
// Load tiles asynchronously
|
||||
LaunchedEffect(viewPoint, canvasSize) {
|
||||
with(mapTileProvider) {
|
||||
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
|
||||
@@ -58,59 +74,70 @@ public fun MapView(
|
||||
for (j in verticalIndices) {
|
||||
for (i in horizontalIndices) {
|
||||
val id = TileId(intZoom, i, j)
|
||||
//ensure that failed tiles do not fail the application
|
||||
supervisorScope {
|
||||
//start all
|
||||
val deferred = loadTileAsync(id)
|
||||
//wait asynchronously for it to finish
|
||||
launch {
|
||||
try {
|
||||
val tile = deferred.await()
|
||||
mapTiles[tile.id] = tile.image
|
||||
} catch (ex: Exception) {
|
||||
//displaying the error is maps responsibility
|
||||
if (ex !is CancellationException) {
|
||||
logger.error(ex) { "Failed to load tile with id=$id" }
|
||||
}
|
||||
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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
mapTiles.keys.filter {
|
||||
tiles.keys.filter {
|
||||
it.zoom != intZoom || it.j !in verticalIndices || it.i !in horizontalIndices
|
||||
}.forEach {
|
||||
mapTiles.remove(it)
|
||||
tiles.remove(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
FeatureCanvas(mapState, featureStore.featureFlow, modifier = modifier.canvasControls(mapState, featureStore)) {
|
||||
// draw custom features
|
||||
val tileScale = mapState.tileScale
|
||||
|
||||
clipRect {
|
||||
val tileSize = IntSize(
|
||||
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt(),
|
||||
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt()
|
||||
)
|
||||
mapTiles.forEach { (id, image) ->
|
||||
//converting back from tile index to screen offset
|
||||
val offset = IntOffset(
|
||||
(mapState.canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - mapState.centerCoordinates.x.dp) * tileScale).roundToPx(),
|
||||
(mapState.canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - mapState.centerCoordinates.y.dp) * tileScale).roundToPx()
|
||||
)
|
||||
drawImage(
|
||||
image = image.toComposeImageBitmap(),
|
||||
dstOffset = offset,
|
||||
dstSize = tileSize
|
||||
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.
|
||||
*/
|
||||
@@ -123,7 +150,7 @@ public fun MapView(
|
||||
initialRectangle: Rectangle<Gmc>? = null,
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val mapState = MapCanvasState.remember(mapTileProvider, config, initialViewPoint, initialRectangle)
|
||||
val mapState = MapCanvasState.remember(config, initialViewPoint, initialRectangle)
|
||||
MapView(mapState, mapTileProvider, featureStore, modifier)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -13,6 +13,13 @@ 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()
|
||||
@@ -126,6 +133,7 @@ public fun CoordinateSpace<Gmc>.Rectangle(
|
||||
/**
|
||||
* A quasi-square section.
|
||||
*/
|
||||
@Suppress("UnusedReceiverParameter")
|
||||
public fun CoordinateSpace<Gmc>.Rectangle(
|
||||
center: GeodeticMapCoordinates,
|
||||
height: Angle,
|
||||
|
||||
@@ -18,6 +18,14 @@ internal fun FeatureBuilder<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
|
||||
|
||||
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,
|
||||
@@ -26,6 +34,14 @@ public fun FeatureBuilder<Gmc>.circle(
|
||||
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),
|
||||
@@ -35,6 +51,14 @@ public fun FeatureBuilder<Gmc>.rectangle(
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -45,6 +69,13 @@ public fun FeatureBuilder<Gmc>.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,
|
||||
@@ -79,6 +110,18 @@ public fun FeatureBuilder<Gmc>.geodeticLine(
|
||||
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,
|
||||
@@ -87,6 +130,14 @@ public fun FeatureBuilder<Gmc>.geodeticLine(
|
||||
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>,
|
||||
@@ -96,6 +147,16 @@ public fun FeatureBuilder<Gmc>.line(
|
||||
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,
|
||||
@@ -112,16 +173,41 @@ public fun FeatureBuilder<Gmc>.arc(
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -137,6 +223,15 @@ public fun FeatureBuilder<Gmc>.icon(
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -147,6 +242,17 @@ public fun FeatureBuilder<Gmc>.text(
|
||||
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,
|
||||
|
||||
@@ -3,9 +3,11 @@ 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 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 org.jetbrains.skia.Image
|
||||
import java.net.URL
|
||||
@@ -22,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
|
||||
@@ -48,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" }
|
||||
@@ -65,18 +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 {
|
||||
if(it !is CancellationException) {
|
||||
if (it !is CancellationException) {
|
||||
logger.error(it) { "Failed to load tile image with id=$tileId" }
|
||||
}
|
||||
cache.remove(tileId)
|
||||
cacheMutex.withLock {
|
||||
cache.remove(tileId)
|
||||
}
|
||||
}.getOrThrow()
|
||||
|
||||
MapTile(tileId, image)
|
||||
|
||||
@@ -18,14 +18,22 @@ 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(
|
||||
mapTileProvider: MapTileProvider,
|
||||
viewPoint: ViewPoint<Gmc>,
|
||||
painterCache: Map<PainterFeature<Gmc>, Painter>,
|
||||
size: Size,
|
||||
path: Path,
|
||||
) {
|
||||
val mapCanvasState: MapCanvasState = MapCanvasState(mapTileProvider, ViewConfig()).apply {
|
||||
val mapCanvasState: MapCanvasState = MapCanvasState(ViewConfig()).apply {
|
||||
this.viewPoint = viewPoint
|
||||
this.canvasSize = DpSize(size.width.dp, size.height.dp)
|
||||
}
|
||||
@@ -35,14 +43,13 @@ public fun FeatureSet<Gmc>.exportToSvg(
|
||||
}
|
||||
|
||||
public fun FeatureSet<Gmc>.exportToPng(
|
||||
mapTileProvider: MapTileProvider,
|
||||
viewPoint: ViewPoint<Gmc>,
|
||||
painterCache: Map<PainterFeature<Gmc>, Painter>,
|
||||
textMeasurer: TextMeasurer,
|
||||
size: Size,
|
||||
path: Path,
|
||||
) {
|
||||
val mapCanvasState: MapCanvasState = MapCanvasState(mapTileProvider, ViewConfig()).apply {
|
||||
val mapCanvasState: MapCanvasState = MapCanvasState(ViewConfig()).apply {
|
||||
this.viewPoint = viewPoint
|
||||
this.canvasSize = DpSize(size.width.dp, size.height.dp)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ The core interfaces of KMath.
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-core:0.3.0`.
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-core:0.4.0-dev-7`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
@@ -19,6 +19,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("space.kscience:maps-kt-core:0.3.0")
|
||||
implementation("space.kscience:maps-kt-core:0.4.0-dev-7")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -3,8 +3,6 @@ plugins {
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
val kmathVersion: String by rootProject.extra
|
||||
|
||||
kscience{
|
||||
jvm()
|
||||
js()
|
||||
@@ -21,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",
|
||||
|
||||
@@ -4,7 +4,7 @@ import space.kscience.kmath.geometry.*
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
* A directed straight (geodetic) segment on a spheroid with given start, direction, end point and distance.
|
||||
* A directed straight (geodetic) segment on a spheroid with 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
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-features:0.3.0`.
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-features:0.4.0-dev-7`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
@@ -16,6 +16,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("space.kscience:maps-kt-features:0.3.0")
|
||||
implementation("space.kscience:maps-kt-features:0.4.0-dev-7")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -34,11 +34,12 @@ kscience {
|
||||
api(compose.foundation)
|
||||
api(compose.material)
|
||||
api(compose.ui)
|
||||
api("io.github.oshai:kotlin-logging:6.0.3")
|
||||
api("com.benasher44:uuid:0.8.4")
|
||||
|
||||
api(libs.attributes.serialization)
|
||||
api("io.github.oshai:kotlin-logging:7.0.7")
|
||||
}
|
||||
|
||||
jvmMain{
|
||||
api("org.jfree:org.jfree.svg:5.0.4")
|
||||
api("org.jfree:org.jfree.svg:5.0.6")
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,12 @@ import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.skia.Font
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.attributes.NameAttribute
|
||||
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>
|
||||
|
||||
@@ -36,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
|
||||
@@ -44,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
|
||||
}
|
||||
|
||||
@@ -129,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>(
|
||||
@@ -310,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>,
|
||||
|
||||
@@ -18,6 +18,7 @@ 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
|
||||
@@ -47,6 +48,10 @@ public abstract class FeatureDrawScope<T : Any>(
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,12 +64,14 @@ public class ComposeFeatureDrawScope<T : Any>(
|
||||
private val painterCache: Map<PainterFeature<T>, Painter>,
|
||||
private val textMeasurer: TextMeasurer?,
|
||||
) : FeatureDrawScope<T>(state), DrawScope by drawScope {
|
||||
|
||||
override fun drawText(text: String, position: Offset, attributes: Attributes) {
|
||||
try {
|
||||
//TODO don't draw text that is not on screen
|
||||
drawText(textMeasurer ?: error("Text measurer not defined"), text, position)
|
||||
} catch (ex: Exception) {
|
||||
logger.error(ex) { "Failed to measure text" }
|
||||
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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,13 +84,15 @@ public class ComposeFeatureDrawScope<T : Any>(
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun <T: Any> FeatureSet<T>.pointerCache(): Map<PainterFeature<T>, Painter> = key(features) {
|
||||
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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package space.kscience.maps.features
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -9,8 +11,6 @@ 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 com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.jetbrains.skia.Font
|
||||
@@ -20,6 +20,8 @@ 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)
|
||||
@@ -40,8 +42,6 @@ public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.resolve(): F =
|
||||
|
||||
public val <T : Any, F : Feature<T>> FeatureRef<T, F>.attributes: Attributes get() = resolve().attributes
|
||||
|
||||
public fun Uuid.toIndex(): String = leastSignificantBits.toString(16)
|
||||
|
||||
public interface FeatureBuilder<T : Any> {
|
||||
public val space: CoordinateSpace<T>
|
||||
|
||||
@@ -76,6 +76,15 @@ public interface FeatureSet<T : Any> {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
@@ -130,7 +139,7 @@ public class FeatureStore<T : Any>(
|
||||
public companion object {
|
||||
|
||||
internal fun generateFeatureId(prefix: String): String =
|
||||
"$prefix[${uuid4().toIndex()}]"
|
||||
"$prefix[${Uuid.random().toHexString()}]"
|
||||
|
||||
internal fun generateFeatureId(feature: Feature<*>): String =
|
||||
generateFeatureId(feature::class.simpleName ?: "undefined")
|
||||
@@ -162,9 +171,9 @@ public class FeatureStore<T : Any>(
|
||||
/**
|
||||
* A group of other features
|
||||
*/
|
||||
public data class FeatureGroup<T : Any> internal constructor(
|
||||
val store: FeatureStore<T>,
|
||||
val groupId: String,
|
||||
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> {
|
||||
|
||||
|
||||
@@ -23,11 +23,12 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
|
||||
feature: Feature<T>,
|
||||
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
|
||||
if (attributes[VisibleAttribute] == false) return
|
||||
|
||||
when (feature) {
|
||||
is FeatureSelector -> drawFeature(feature.selector(state.zoom), attributes)
|
||||
@@ -185,8 +186,9 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
//logger.error { "Unrecognized feature type: ${feature::class}" }
|
||||
is CustomFeature<*> -> {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,11 @@ 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 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>
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ import kotlinx.serialization.modules.contextual
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import kotlinx.serialization.serializer
|
||||
import space.kscience.attributes.Attributes
|
||||
import space.kscience.attributes.AttributesSerializer
|
||||
import space.kscience.attributes.NameAttribute
|
||||
import space.kscience.attributes.SerializableAttribute
|
||||
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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Module maps-kt-geojson
|
||||
|
||||
|
||||
GeoJson format support
|
||||
|
||||
## Usage
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-geojson:0.3.0`.
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-geojson:0.4.0-dev-7`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
@@ -16,6 +16,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("space.kscience:maps-kt-geojson:0.3.0")
|
||||
implementation("space.kscience:maps-kt-geojson:0.4.0-dev-7")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
description = "GeoJson format support"
|
||||
|
||||
kscience{
|
||||
jvm()
|
||||
@@ -17,4 +18,8 @@ kscience{
|
||||
api(projects.mapsKtFeatures)
|
||||
api(spclibs.kotlinx.serialization.json)
|
||||
}
|
||||
}
|
||||
|
||||
readme {
|
||||
maturity = space.kscience.gradle.Maturity.DEVELOPMENT
|
||||
}
|
||||
@@ -2,6 +2,6 @@ package space.kscience.maps.geojson
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.serializer
|
||||
import space.kscience.attributes.SerializableAttribute
|
||||
import space.kscience.attributes.serialization.SerializableAttribute
|
||||
|
||||
public object GeoJsonPropertiesAttribute : SerializableAttribute<JsonObject>("properties", serializer())
|
||||
@@ -4,7 +4,6 @@ import androidx.compose.ui.graphics.Color
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import space.kscience.attributes.NameAttribute
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.features.*
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.jvm")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven("https://repo.osgeo.org/repository/release/")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("org.geotools:gt-geotiff:27.2") {
|
||||
exclude(group = "javax.media", module = "jai_core")
|
||||
}
|
||||
|
||||
api(projects.mapsKtCore)
|
||||
api(projects.mapsKtFeatures)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package center.sciprog.maps.geotiff
|
||||
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.features.Feature
|
||||
import space.kscience.maps.features.FeatureGroup
|
||||
import space.kscience.maps.features.FeatureRef
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import org.geotools.gce.geotiff.GeoTiffReader
|
||||
import org.geotools.util.factory.Hints
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
|
||||
|
||||
public fun FeatureGroup<Gmc>.geoJson(
|
||||
geoTiffUrl: URL,
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, Feature<Gmc>> {
|
||||
val reader = GeoTiffReader
|
||||
val jsonString = geoJsonUrl.readText()
|
||||
val json = Json.parseToJsonElement(jsonString).jsonObject
|
||||
val geoJson = GeoJson(json)
|
||||
|
||||
return geoJson(geoJson, id)
|
||||
}
|
||||
|
||||
21
maps-kt-geotools/README.md
Normal file
21
maps-kt-geotools/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Module maps-kt-geotools
|
||||
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-geotools:0.4.0-dev-7`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
repositories {
|
||||
maven("https://repo.kotlin.link")
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("space.kscience:maps-kt-geotools:0.4.0-dev-7")
|
||||
}
|
||||
```
|
||||
32
maps-kt-geotools/build.gradle.kts
Normal file
32
maps-kt-geotools/build.gradle.kts
Normal file
@@ -0,0 +1,32 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
alias(spclibs.plugins.compose.compiler)
|
||||
alias(spclibs.plugins.compose.jb)
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven("https://repo.osgeo.org/repository/release/")
|
||||
exclusiveContent {
|
||||
forRepository {
|
||||
maven("https://repo.osgeo.org/repository/release/")
|
||||
}
|
||||
filter {
|
||||
includeGroup("javax.media")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kscience {
|
||||
jvm()
|
||||
useSerialization()
|
||||
commonMain {
|
||||
api(projects.mapsKtCore)
|
||||
api(projects.mapsKtFeatures)
|
||||
}
|
||||
jvmMain {
|
||||
api(libs.gt.geotiff)
|
||||
api(libs.gt.epsg.hsql)
|
||||
api(libs.gt.shapefile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package center.sciprog.maps.geotools
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.geotools.api.referencing.crs.CoordinateReferenceSystem
|
||||
import org.geotools.api.referencing.operation.MathTransform
|
||||
import org.geotools.geometry.Position2D
|
||||
import org.geotools.referencing.CRS
|
||||
import space.kscience.kmath.geometry.degrees
|
||||
import space.kscience.maps.coordinates.GeodeticMapCoordinates
|
||||
import space.kscience.maps.coordinates.MapProjection
|
||||
import space.kscience.maps.coordinates.ProjectionCoordinates
|
||||
import space.kscience.maps.coordinates.meters
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.readText
|
||||
|
||||
/**
|
||||
* Represents a map projection using the GeoTools library with a specified Coordinate Reference System (CRS).
|
||||
*
|
||||
* This class provides methods to convert between projection coordinates and geodetic map coordinates using the
|
||||
* specified CRS. The transformation is handled using `MathTransform` instances.
|
||||
*
|
||||
* @param crs The coordinate reference system used for the projection.
|
||||
*/
|
||||
@Serializable(with = GeoToolsMapProjection.Serializer::class)
|
||||
public class GeoToolsMapProjection(
|
||||
public val crs: CoordinateReferenceSystem
|
||||
) : MapProjection<ProjectionCoordinates> {
|
||||
|
||||
private val transform: MathTransform = CRS.findMathTransform(crs, crsEPSG4326, true)
|
||||
private val inverted: MathTransform = transform.inverse()
|
||||
override fun toGeodetic(pc: ProjectionCoordinates): GeodeticMapCoordinates {
|
||||
val input = Position2D(pc.x.meters, pc.y.meters)
|
||||
val output = Position2D()
|
||||
transform.transform(input, output)
|
||||
return GeodeticMapCoordinates(output.x.degrees, output.y.degrees)
|
||||
}
|
||||
|
||||
override fun toProjection(gmc: GeodeticMapCoordinates): ProjectionCoordinates {
|
||||
val input = Position2D(gmc.latitude.toDegrees().value, gmc.longitude.toDegrees().value)
|
||||
val output = Position2D()
|
||||
inverted.transform(input, output)
|
||||
return ProjectionCoordinates(output.x.meters, output.y.meters)
|
||||
}
|
||||
|
||||
override fun toString(): String = crs.name.toString()
|
||||
|
||||
public companion object {
|
||||
|
||||
internal val crsEPSG4326 by lazy { CRS.decode("EPSG:4326") }
|
||||
|
||||
public val EPSG4326: GeoToolsMapProjection = GeoToolsMapProjection(crsEPSG4326)
|
||||
|
||||
public fun decode(string: String): GeoToolsMapProjection {
|
||||
val crs = if (string.startsWith("file")) {
|
||||
val crsFile = Path.of(string)
|
||||
if (crsFile.extension == "wkt") {
|
||||
CRS.parseWKT(crsFile.readText())
|
||||
} else {
|
||||
error("Unknown CRS file: $crsFile")
|
||||
}
|
||||
} else {
|
||||
CRS.decode(string)
|
||||
}
|
||||
return GeoToolsMapProjection(crs)
|
||||
}
|
||||
}
|
||||
|
||||
public object Serializer : KSerializer<GeoToolsMapProjection> {
|
||||
|
||||
@Serializable
|
||||
@SerialName("geoTools")
|
||||
private class Proxy(val wkt: String)
|
||||
|
||||
private val serializer = Proxy.serializer()
|
||||
override val descriptor: SerialDescriptor get() = serializer.descriptor
|
||||
|
||||
override fun deserialize(
|
||||
decoder: Decoder,
|
||||
): GeoToolsMapProjection {
|
||||
val proxy = decoder.decodeSerializableValue(serializer)
|
||||
return GeoToolsMapProjection(CRS.parseWKT(proxy.wkt))
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: GeoToolsMapProjection) {
|
||||
val proxy = Proxy(value.crs.toWKT())
|
||||
encoder.encodeSerializableValue(serializer, proxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package center.sciprog.maps.geotools
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import org.geotools.api.geometry.Position
|
||||
import org.geotools.gce.geotiff.GeoTiffReader
|
||||
import org.geotools.util.factory.Hints
|
||||
import space.kscience.kmath.geometry.degrees
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.features.*
|
||||
import java.awt.geom.AffineTransform
|
||||
import java.awt.image.BufferedImage
|
||||
import java.awt.image.RenderedImage
|
||||
import java.io.InputStream
|
||||
import java.lang.Boolean
|
||||
import javax.media.jai.PlanarImage
|
||||
import kotlin.String
|
||||
|
||||
|
||||
private fun RenderedImage.toImageBitmap(transform: AffineTransform = AffineTransform()): ImageBitmap {
|
||||
val bufferedImage = when (this) {
|
||||
is BufferedImage -> this
|
||||
|
||||
is PlanarImage -> this.asBufferedImage
|
||||
|
||||
else -> {
|
||||
val bufferedImage = BufferedImage(
|
||||
this.width,
|
||||
this.height,
|
||||
BufferedImage.TYPE_INT_ARGB
|
||||
)
|
||||
val graphics = bufferedImage.createGraphics()
|
||||
graphics.drawRenderedImage(this, transform)
|
||||
graphics.dispose()
|
||||
bufferedImage
|
||||
}
|
||||
}
|
||||
|
||||
// Convert BufferedImage to Compose ImageBitmap
|
||||
return bufferedImage.toComposeImageBitmap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform position to geodetic coordinates assuming the position already uses geodetic cooridnates
|
||||
*/
|
||||
private fun Position.toGmc(): Gmc = Gmc(getOrdinate(0).degrees, getOrdinate(1).degrees)
|
||||
|
||||
public fun FeatureBuilder<Gmc>.geoTiff(
|
||||
geoTiffStream: () -> InputStream,
|
||||
hints: Hints = Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE),
|
||||
id: String? = null,
|
||||
): FeatureRef<Gmc, Feature<Gmc>> {
|
||||
geoTiffStream().use { stream ->
|
||||
val reader = GeoTiffReader(stream, hints)
|
||||
val coverage = reader.read(null)
|
||||
|
||||
val image = coverage.renderedImage.toImageBitmap()
|
||||
val envelope = coverage.envelope2D.transform(GeoToolsMapProjection.crsEPSG4326, true)
|
||||
|
||||
val rectangle: Rectangle<Gmc> = space.Rectangle(envelope.lowerCorner.toGmc(), envelope.upperCorner.toGmc())
|
||||
|
||||
return feature(
|
||||
id,
|
||||
ScalableImageFeature<Gmc>(space, rectangle) {
|
||||
BitmapPainter(image)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package center.sciprog.maps.geotools
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.geotools.api.data.FileDataStore
|
||||
import org.geotools.api.data.FileDataStoreFinder
|
||||
import org.geotools.api.feature.simple.SimpleFeature
|
||||
import org.geotools.api.referencing.crs.CoordinateReferenceSystem
|
||||
import org.geotools.api.referencing.operation.MathTransform
|
||||
import org.geotools.data.simple.SimpleFeatureCollection
|
||||
import org.geotools.geometry.jts.JTS
|
||||
import org.geotools.referencing.CRS
|
||||
import org.locationtech.jts.geom.Coordinate
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import org.locationtech.jts.geom.LineString
|
||||
import org.locationtech.jts.geom.MultiLineString
|
||||
import space.kscience.kmath.geometry.degrees
|
||||
import space.kscience.maps.coordinates.GeoEllipsoid
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.coordinates.GmcCurve
|
||||
import space.kscience.maps.coordinates.curveBetween
|
||||
import java.net.URL
|
||||
|
||||
|
||||
internal fun SimpleFeatureCollection.asSequence(): Sequence<SimpleFeature> = sequence {
|
||||
features().use { iterator ->
|
||||
while (iterator.hasNext()) {
|
||||
yield(iterator.next())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val MultiLineString.lines: Sequence<LineString>
|
||||
get() = sequence {
|
||||
for (n in 0 until numGeometries) {
|
||||
yield(getGeometryN(n) as LineString)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun readCrs(url: URL): CoordinateReferenceSystem {
|
||||
val store: FileDataStore = FileDataStoreFinder.getDataStore(url)
|
||||
val featureSource = store.featureSource
|
||||
val schema = featureSource.schema
|
||||
return schema.coordinateReferenceSystem
|
||||
}
|
||||
|
||||
private fun Double.checkFinite(): Double {
|
||||
if (!isFinite()) error("Not finite!")
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpret this [Coordinate] as geodetic coordinates
|
||||
*/
|
||||
public fun Coordinate.toGmc(): Gmc = Gmc(x.checkFinite().degrees, y.checkFinite().degrees)
|
||||
|
||||
//TODO add other shapes
|
||||
|
||||
/**
|
||||
* Loads and processes shape lines from a shapefile, transforming them into geodetic curves
|
||||
* compatible with the EPSG:4326 coordinate reference system.
|
||||
*
|
||||
* @param url the URL pointing to the shapefile to be processed
|
||||
* @param crsOverride an optional parameter to override the shapefile's coordinate reference system (CRS),
|
||||
* if null, the CRS from the shapefile schema will be used
|
||||
* @return a list of geodetic curves (GmcCurve) representing the transformed lines from the shapefile
|
||||
*/
|
||||
public fun GeoEllipsoid.loadShapeLines(
|
||||
url: URL,
|
||||
crsOverride: CoordinateReferenceSystem? = null
|
||||
): List<GmcCurve> {
|
||||
val store: FileDataStore = FileDataStoreFinder.getDataStore(url)
|
||||
val featureSource = store.featureSource
|
||||
val schema = featureSource.schema
|
||||
//https://gis.stackexchange.com/questions/359967/how-to-parse-crs-from-shapefile-using-geotools
|
||||
val crs: CoordinateReferenceSystem = crsOverride ?: schema.coordinateReferenceSystem
|
||||
val transform: MathTransform = CRS.findMathTransform(crs, GeoToolsMapProjection.EPSG4326.crs, true)
|
||||
|
||||
return featureSource.features.asSequence().mapNotNull {
|
||||
it.defaultGeometry as? Geometry
|
||||
}.filterIsInstance<MultiLineString>().flatMap { it.lines }.mapNotNull {
|
||||
val transformed: Geometry = JTS.transform(it, transform)
|
||||
val begin = transformed.coordinates[0].toGmc()
|
||||
val end = transformed.coordinates[1].toGmc()
|
||||
if (begin == end) {
|
||||
KotlinLogging.logger("PathPlanner")
|
||||
.error { "One of the lines has zero length: $begin == $end. Skipping it." }
|
||||
null
|
||||
} else {
|
||||
curveBetween(begin, end)
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-scheme:0.3.0`.
|
||||
The Maven coordinates of this project are `space.kscience:maps-kt-scheme:0.4.0-dev-7`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
@@ -16,6 +16,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("space.kscience:maps-kt-scheme:0.3.0")
|
||||
implementation("space.kscience:maps-kt-scheme:0.4.0-dev-7")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -19,6 +19,10 @@ kscience {
|
||||
}
|
||||
|
||||
|
||||
readme{
|
||||
maturity = space.kscience.gradle.Maturity.DEVELOPMENT
|
||||
}
|
||||
|
||||
//java {
|
||||
// targetCompatibility = JVM_TARGET
|
||||
//}
|
||||
@@ -12,6 +12,17 @@ import kotlin.math.min
|
||||
|
||||
private val logger = KotlinLogging.logger("SchemeView")
|
||||
|
||||
/**
|
||||
* A composable function that renders a view for visualizing and interacting with spatial features
|
||||
* within a two-dimensional coordinate system using a canvas.
|
||||
*
|
||||
* @param state The state object that manages the overall configuration and state of the canvas,
|
||||
* including zoom, focus, and coordinate transformations.
|
||||
* @param featureStore The store containing spatial features to be rendered on the canvas. It also
|
||||
* provides reactive updates for changes in the feature set.
|
||||
* @param modifier The Modifier applied to the composable. Defaults to filling the available space.
|
||||
* @return Unit
|
||||
*/
|
||||
@Composable
|
||||
public fun SchemeView(
|
||||
state: XYCanvasState,
|
||||
@@ -21,7 +32,17 @@ public fun SchemeView(
|
||||
FeatureCanvas(state, featureStore.featureFlow, modifier = modifier.canvasControls(state, featureStore))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Computes a view point for the canvas based on the current rectangle dimensions
|
||||
* and the specified canvas size.
|
||||
* The view point includes a calculated zoom value
|
||||
* to fit the rectangle within the canvas and its center as the focal point.
|
||||
*
|
||||
* @param canvasSize the size of the canvas on which the rectangle will be displayed.
|
||||
* Defaults to the internal `defaultCanvasSize` value if not provided.
|
||||
* @return a [ViewPoint] object with the rectangle's center as the focal point and
|
||||
* the computed zoom level required to fit the rectangle within the given canvas size.
|
||||
*/
|
||||
public fun Rectangle<XY>.computeViewPoint(
|
||||
canvasSize: DpSize = defaultCanvasSize,
|
||||
): ViewPoint<XY> {
|
||||
|
||||
@@ -10,8 +10,19 @@ import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* A data class representing a 2D vector in a coordinate space with `Float` precision.
|
||||
*
|
||||
* @property x The x-coordinate of the vector.
|
||||
* @property y The y-coordinate of the vector.
|
||||
*/
|
||||
public data class XY(override val x: Float, override val y: Float) : Vector2D<Float>
|
||||
|
||||
/**
|
||||
* Creates an instance of the `XY` data class representing a two-dimensional vector with `x` and `y` coordinates
|
||||
* converted to `Float` values.
|
||||
*
|
||||
* @param x The*/
|
||||
public fun XY(x: Number, y: Number): XY = XY(x.toFloat(), y.toFloat())
|
||||
|
||||
internal data class XYRectangle(
|
||||
@@ -45,11 +56,34 @@ public val Rectangle<XY>.rightBottom: XY get() = XY(right, bottom)
|
||||
|
||||
internal val defaultCanvasSize = DpSize(512.dp, 512.dp)
|
||||
|
||||
/**
|
||||
* A data class representing a viewpoint in a 2D coordinate space, defined by a focus point and
|
||||
* a zoom level.
|
||||
*
|
||||
* This class implements the [ViewPoint] interface using the [XY] coordinate system to define
|
||||
* spatial locations within a 2D space. The viewpoint is commonly used in visual representations
|
||||
* like maps or canvases to determine the focal area and zoom level.
|
||||
*
|
||||
* @property focus The central [XY] coordinate of the viewpoint.
|
||||
* @property zoom The magnification level of the viewpoint, where higher values indicate greater zoom-in.
|
||||
*/
|
||||
public data class XYViewPoint(
|
||||
override val focus: XY,
|
||||
override val zoom: Float = 1f,
|
||||
) : ViewPoint<XY>
|
||||
|
||||
/**
|
||||
* Constructs a rectangle in the coordinate space defined by the center point,
|
||||
* width, and height.
|
||||
* The rectangle is created by determining two diagonal corners
|
||||
* based on the given dimensions and center.
|
||||
*
|
||||
* @param center The center point of the rectangle in the coordinate space.
|
||||
* @param height The height of the rectangle.
|
||||
* @param width The width of the rectangle.
|
||||
* @return A rectangle defined in the coordinate space with the specified parameters.
|
||||
*/
|
||||
@Suppress("UnusedReceiverParameter")
|
||||
public fun CoordinateSpace<XY>.Rectangle(
|
||||
center: XY,
|
||||
height: Float,
|
||||
|
||||
@@ -15,6 +15,16 @@ import kotlin.math.ceil
|
||||
|
||||
internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat())
|
||||
|
||||
/**
|
||||
* Adds a scalable background image feature to the feature builder.
|
||||
*
|
||||
* @param width The width of the background in scheme units.
|
||||
* @param height The height of the background in scheme units.
|
||||
* @param offset The top-left corner offset of the background in the scheme space. Defaults to (0f, 0f).
|
||||
* @param id Optional unique identifier for the background feature. If null, a unique identifier will be generated.
|
||||
* @param painter A composable lambda function that returns the painter for the background image.
|
||||
* @return A reference to the added ScalableImageFeature.
|
||||
*/
|
||||
public fun FeatureBuilder<XY>.background(
|
||||
width: Float,
|
||||
height: Float,
|
||||
@@ -37,25 +47,58 @@ public fun FeatureBuilder<XY>.background(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a circular marker to the `FeatureBuilder` using the specified center coordinates.
|
||||
*
|
||||
* @param centerCoordinates The center position of the circle as a pair of numerical values (x, y).
|
||||
* @param size The diameter of the circle. Default value is 5.dp.
|
||||
* @param id The optional unique identifier for this circle. If not provided, a unique id is generated internally.
|
||||
* @return A reference to the created circle feature.
|
||||
*/
|
||||
public fun FeatureBuilder<XY>.circle(
|
||||
centerCoordinates: Pair<Number, Number>,
|
||||
size: Dp = 5.dp,
|
||||
id: String? = null,
|
||||
): FeatureRef<XY, CircleFeature<XY>> = circle(centerCoordinates.toCoordinates(), size, id = id)
|
||||
|
||||
/**
|
||||
* Adds a drawable feature to the feature builder at the specified position.
|
||||
*
|
||||
* @param position a pair of numbers representing the x and y coordinates of the feature in the coordinate space.
|
||||
* @param id an optional unique identifier for the feature. If null, a unique id will be generated automatically.
|
||||
* @param draw a lambda defining drawing logic, executed within the [DrawScope].
|
||||
* @return a reference to the created [DrawFeature] within the feature builder.
|
||||
*/
|
||||
public fun FeatureBuilder<XY>.draw(
|
||||
position: Pair<Number, Number>,
|
||||
id: String? = null,
|
||||
draw: DrawScope.() -> Unit,
|
||||
): FeatureRef<XY, DrawFeature<XY>> = draw(position.toCoordinates(), id = id, draw = draw)
|
||||
|
||||
/**
|
||||
* Creates a line feature between two specified coordinates within the `FeatureBuilder`.
|
||||
*
|
||||
* @param aCoordinates The starting coordinates of the line as a pair of numbers (x, y).
|
||||
* @param bCoordinates The ending coordinates of the line as a pair of numbers (x, y).
|
||||
* @param id The optional unique identifier for the line feature. If null, an ID is automatically generated.
|
||||
* @return A reference to the created line feature represented by `FeatureRef<XY, LineFeature<XY>>`.
|
||||
*/
|
||||
public fun FeatureBuilder<XY>.line(
|
||||
aCoordinates: Pair<Number, Number>,
|
||||
bCoordinates: Pair<Number, Number>,
|
||||
id: String? = null,
|
||||
): FeatureRef<XY, LineFeature<XY>> = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), id = id)
|
||||
|
||||
|
||||
/**
|
||||
* Adds an arc feature to the feature builder.
|
||||
*
|
||||
* @param center A pair representing the center of the arc in the coordinate space.
|
||||
* @param radius The radius of the arc.
|
||||
* @param startAngle The starting angle of the arc, measured in radians from the 3 o'clock position clockwise.
|
||||
* @param arcLength The length of the arc, measured in radians.
|
||||
* @param id An optional identifier for the arc. If null, a unique ID is generated.
|
||||
* @return A reference to the created arc feature.
|
||||
*/
|
||||
public fun FeatureBuilder<XY>.arc(
|
||||
center: Pair<Double, Double>,
|
||||
radius: Float,
|
||||
@@ -69,6 +112,15 @@ public fun FeatureBuilder<XY>.arc(
|
||||
id = id
|
||||
)
|
||||
|
||||
/**
|
||||
* Adds an image feature to the `FeatureBuilder`. The feature is represented as a fixed-size vector icon.
|
||||
*
|
||||
* @param position the position of the image as a pair of numbers representing X and Y coordinates.
|
||||
* @param image the vector image to display as the feature.
|
||||
* @param size the size of the image feature using Dp units, defaulting to the size specified in the `ImageVector`.
|
||||
* @param id an optional unique identifier for the feature. If null, an ID will be generated.
|
||||
* @return a reference to the created feature.
|
||||
*/
|
||||
public fun FeatureBuilder<XY>.image(
|
||||
position: Pair<Number, Number>,
|
||||
image: ImageVector,
|
||||
@@ -77,12 +129,31 @@ public fun FeatureBuilder<XY>.image(
|
||||
): FeatureRef<XY, VectorIconFeature<XY>> =
|
||||
icon(position.toCoordinates(), image, size = size, id = id)
|
||||
|
||||
/**
|
||||
* Adds a text feature to the feature builder at the specified position.
|
||||
*
|
||||
* @param position The position of the text as a pair of numbers, which will be converted to coordinates.
|
||||
* @param text The text content to be displayed.
|
||||
* @param id An optional identifier for the feature. If not provided, a unique ID will be generated.
|
||||
* @return A reference to the created text feature.
|
||||
*/
|
||||
public fun FeatureBuilder<XY>.text(
|
||||
position: Pair<Number, Number>,
|
||||
text: String,
|
||||
id: String? = null,
|
||||
): FeatureRef<XY, TextFeature<XY>> = text(position.toCoordinates(), text, id = id)
|
||||
|
||||
/**
|
||||
* Creates a pixel map feature within the specified rectangular boundaries and dimensions,
|
||||
* using a builder function to define the color of each pixel.
|
||||
*
|
||||
* @param rectangle The rectangular boundary of the pixel map.
|
||||
* @param xSize The width of each pixel in the map.
|
||||
* @param ySize The height of each pixel in the map.
|
||||
* @param id An optional ID for the pixel map feature. If null, a unique ID will be generated.
|
||||
* @param builder A function that determines the color of each pixel based on its coordinates.
|
||||
* @return A reference to the created pixel map feature.
|
||||
*/
|
||||
public fun FeatureBuilder<XY>.pixelMap(
|
||||
rectangle: Rectangle<XY>,
|
||||
xSize: Float,
|
||||
@@ -108,6 +179,17 @@ public fun FeatureBuilder<XY>.pixelMap(
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a rectangular polygon defined by its bounding edges in a 2D coordinate space.
|
||||
*
|
||||
* @param left The x-coordinate of the left edge of the rectangle.
|
||||
* @param right The x-coordinate of the right edge of the rectangle.
|
||||
* @param bottom The y-coordinate of the bottom edge of the rectangle.
|
||||
* @param top The y-coordinate of the top edge of the rectangle.
|
||||
* @param attributes The attributes to associate with the polygon feature. Defaults to an empty set of attributes.
|
||||
* @param id An optional identifier for the feature. If null, a unique identifier will be generated.
|
||||
* @return A reference to the created polygon feature within the feature store.
|
||||
*/
|
||||
public fun FeatureBuilder<XY>.rectanglePolygon(
|
||||
left: Number, right: Number,
|
||||
bottom: Number, top: Number,
|
||||
@@ -123,6 +205,14 @@ public fun FeatureBuilder<XY>.rectanglePolygon(
|
||||
attributes, id
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a polygon feature representing a rectangle in the coordinate space.
|
||||
*
|
||||
* @param rectangle The rectangle defined by two opposing corners.
|
||||
* @param attributes Attributes to associate with the polygon feature. Defaults to an empty attribute set.
|
||||
* @param id An optional unique identifier for the feature. If null, a unique identifier will be generated.
|
||||
* @return A reference to the created polygon feature.
|
||||
*/
|
||||
public fun FeatureBuilder<XY>.rectanglePolygon(
|
||||
rectangle: Rectangle<XY>,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
|
||||
@@ -46,7 +46,7 @@ include(
|
||||
":trajectory-kt",
|
||||
":maps-kt-core",
|
||||
":maps-kt-geojson",
|
||||
// ":maps-kt-geotiff",
|
||||
":maps-kt-geotools",
|
||||
":maps-kt-features",
|
||||
":maps-kt-compose",
|
||||
":maps-kt-scheme",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `space.kscience:trajectory-kt:0.3.0`.
|
||||
The Maven coordinates of this project are `space.kscience:trajectory-kt:0.4.0-dev-7`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
@@ -15,7 +15,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("space.kscience:trajectory-kt:0.3.0")
|
||||
implementation("space.kscience:trajectory-kt:0.4.0-dev-7")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -18,12 +18,11 @@ kscience{
|
||||
json()
|
||||
}
|
||||
dependencies {
|
||||
api("space.kscience:kmath-geometry:$kmathVersion")
|
||||
api(libs.kmath.geometry)
|
||||
}
|
||||
}
|
||||
|
||||
readme {
|
||||
description = "Path and trajectory optimization"
|
||||
maturity = space.kscience.gradle.Maturity.EXPERIMENTAL
|
||||
propertyByTemplate("artifact", rootProject.file("docs/templates/ARTIFACT-TEMPLATE.md"))
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package space.kscience.attributes
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
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 object : Attributes {
|
||||
override val content: Map<out Attribute<*>, Any?> = attributeMap
|
||||
override fun toString(): String = "Attributes(value=${content.entries})"
|
||||
override fun equals(other: Any?): Boolean = other is Attributes && Attributes.equals(this, other)
|
||||
override fun hashCode(): Int = content.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Attributes) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class SerializableAttribute<T>(
|
||||
public val serialId: String,
|
||||
public val serializer: KSerializer<T>,
|
||||
) : Attribute<T> {
|
||||
override fun toString(): String = serialId
|
||||
}
|
||||
|
||||
public object NameAttribute : SerializableAttribute<String>("name", String.serializer())
|
||||
Reference in New Issue
Block a user