Compare commits

...

122 Commits
layers ... main

Author SHA1 Message Date
a059697f5c Merge remote-tracking branch 'space/dev' into dev 2023-05-06 15:46:16 +03:00
1ccd35f152 Prepare for 0.2.2 release 2023-05-06 15:39:58 +03:00
Alexander Nozik
7981422f94
change container version for CI 2023-05-06 11:26:57 +00:00
d8548ab162 Differentiate obstacles 2023-05-06 14:22:09 +03:00
5d0e0f054e Merge remote-tracking branch 'space/main' into dev 2023-05-06 13:57:23 +03:00
Alexander Nozik
869aee887a
update container image reference 2023-05-06 10:56:28 +00:00
11b11118fa Fix circle line intersection and add a special case for a single-point obstacle 2023-05-06 12:50:44 +03:00
05ef3aa4dd Fix segment-circle intersection 2023-05-02 16:17:04 +03:00
8947998e0c Fix propagation of a finished route 2023-05-02 13:52:06 +03:00
74b86dbc59 Add possibility to skip some paths if target is close to the obstacle 2023-05-02 13:30:41 +03:00
35efce087e remove unnecessary elvis. He lives, but not needed here. 2023-05-01 21:29:37 +03:00
fcf0600d0c Seems to be working... finally 2023-05-01 21:25:38 +03:00
f0da3efd27 [WIP] A lot of bugfixes 2023-05-01 16:56:42 +03:00
614ca8d6f3 [WIP] refactor CircleTrajectory2D logic to be more correct 2023-05-01 09:14:26 +03:00
8bc1987acf [WIP] full rewrite of obstacle avoidance 2023-04-30 21:20:10 +03:00
b06fc5c87a Merge remote-tracking branch 'space/dev' into dev 2023-04-28 20:33:01 +03:00
Artyom Degtyarev
dde5b2c9f7 intersection of lineSegment and Circle fixed 2023-04-28 13:07:18 +03:00
5e35ce8b5f Rename ObstacleNode to ObstacleConnection 2023-04-26 10:44:13 +03:00
a6a5baa352 Obstacle avoidance refactoring 2023-04-23 21:20:32 +03:00
4e7ef35280 Obstacle avoidance refactoring 2023-04-23 13:44:07 +03:00
cac5841401 Obstacle avoidance refactoring 2023-04-23 11:01:08 +03:00
cdaa17a3b9 Change trajectory package 2023-04-16 20:50:51 +03:00
3fc52dd60f Fixed zero tangents 2023-04-16 20:28:58 +03:00
375de71ca6 Fixed zero tangents 2023-04-16 20:08:04 +03:00
df675c8d45 Fix Trajectory2D type name 2023-04-12 20:46:45 +03:00
e553e33d4c [WIP] disentangling obstacles phase 2 2023-04-11 17:08:28 +03:00
dbfe61b949 Update kmath version and gradle 2023-04-10 11:33:13 +03:00
3ab875d87c Add trajectory-kt 2023-04-06 10:03:38 +03:00
dcee205b7c Remove tile flicker on move 2023-03-16 10:54:40 +03:00
4658418517 Add builder for pixelMap to scheme 2023-03-16 10:10:53 +03:00
3b43446f82 Add pixel map implementation 2023-03-15 15:24:23 +03:00
2ed9d72029 Fix circle clickable radius 2023-02-22 14:57:25 +03:00
1bcae1f362 Fix autoscale for maps 2023-02-22 14:00:41 +03:00
94a91e9ff9 Add remaining attributes arguments to feature builders 2023-02-22 12:20:32 +03:00
1ca1b3cd43 Fix line distance 2023-02-22 11:48:44 +03:00
a89a5cf36a up version 2023-02-22 11:10:37 +03:00
1a7646f311 Add ellipsoid mercator backward transformation 2023-02-22 10:34:59 +03:00
8f489ea0f9 Fix line clickability region 2023-02-22 09:48:18 +03:00
7623e5f622 Add ability to remove a feature by key 2023-02-13 17:32:48 +03:00
dab9784089 Optional zoom on double click 2023-02-13 17:01:16 +03:00
90eb7b4575 Split points and multiline 2023-02-13 16:49:36 +03:00
3219e13fa7 Add double click navigation 2023-02-13 13:32:25 +03:00
ce25690dfb fix arc rendering 2023-02-13 11:13:00 +03:00
e7784c2960 Fix attribute serialization tests 2023-02-12 12:49:55 +03:00
e80c9406c9 Fix attribute serialization tests 2023-02-11 19:09:27 +03:00
2343f37655 Fix attribute serializer 2023-02-11 17:14:00 +03:00
50ccfeab70 Type safe angles 2023-02-06 17:19:51 +03:00
a23b9954cd FeatureId -> FeatureRef 2023-02-06 10:37:22 +03:00
82a1260e3f Make elevation optional for Gmc 2023-01-14 20:09:58 +03:00
9a7e086591 GeoJson builders 2023-01-12 22:58:33 +03:00
b6a3ce0fe7 GeoJson builders 2023-01-12 14:50:10 +03:00
0f00abb1b2 Attribute serialization 2023-01-12 11:52:54 +03:00
190834634f API fixes 2023-01-06 22:16:46 +03:00
6278235b51 Polygon editor demo 2023-01-06 14:15:06 +03:00
5a3a4b059e Detect taps manually 2023-01-06 12:36:02 +03:00
f288a17243 fix featureGroup remember 2023-01-06 10:47:24 +03:00
ad938a614a Attributes cleanup 2023-01-06 10:40:21 +03:00
ffc77dc611 Introduce Attribute builders 2023-01-06 10:26:29 +03:00
69e7b058d2 Fix scheme 2023-01-05 17:24:27 +03:00
70fe9cf97a Update title and logo 2023-01-05 17:12:37 +03:00
cb160110e7 Optimize attributes working 2023-01-05 12:15:52 +03:00
ea8c5571bb Proper polygon contains check 2023-01-04 22:43:51 +03:00
ba962acf5c Change the working of groups 2023-01-02 20:28:47 +03:00
a8a3da7e70 Change the working of groups 2023-01-02 14:08:20 +03:00
26c3e589da Immutable attributes 2023-01-01 09:10:01 +03:00
7643968d39 Fix view rectangle computation 2022-12-28 21:02:23 +03:00
d34c5099f6 Fix view change listener 2022-12-28 20:03:08 +03:00
15ad690129 Refactor drag. Again. 2022-12-28 19:49:09 +03:00
9e3eec9533 Full refacor of map state and arguments 2022-12-28 14:41:46 +03:00
56ccd66db5 Make attributeMap val 2022-12-27 12:46:27 +03:00
33cadb1d15 Add js support for features 2022-12-27 12:34:24 +03:00
8969b2b094 Add geojson demo 2022-12-27 11:51:14 +03:00
5da23b83c1 Add GeoJson bindings 2022-12-26 23:22:21 +03:00
7e7cb0a260 Multi-listener drag 2022-12-26 17:44:01 +03:00
572adf041f
Cleanup drag 2022-12-26 11:19:08 +03:00
42dbbeea58
Add depth attribute 2022-12-25 14:51:34 +03:00
5ed46b278c
Generalize feature draw logic for schemes 2022-12-25 14:33:31 +03:00
8a94438dfd
Generalize feature draw logic 2022-12-25 11:29:31 +03:00
5b95adc649
Generalize feature draw logic 2022-12-25 11:07:45 +03:00
fb13fa1431
Generalize map control logic 2022-12-24 22:59:33 +03:00
9b8ba884e1
Generic features 2022-12-23 22:16:16 +03:00
c2157c8351 [WIP] generic features 2022-12-23 11:47:34 +03:00
7735d667bc
[WIP] Generic features 2022-12-17 23:12:00 +03:00
0d9efadcb8
New attributes 2022-12-17 21:36:05 +03:00
8451cc3aa1
[WIP] attributes refactoring 2022-12-10 13:41:54 +03:00
edeb422335
Typed features for Map 2022-12-09 22:21:24 +03:00
5448929d31 Fixed multi-drag issue. Added demo for bounded drag. 2022-12-07 18:02:20 +03:00
5561a4188c More robust drag API and implementation 2022-11-28 07:58:22 +03:00
1ebcb02f0a More robust drag API and implementation 2022-11-28 07:56:15 +03:00
e7cb006a3c
0.1.0 release with Kotlin 1.7.20 2022-10-12 15:03:02 +03:00
5ab7862660
Use exact shapes for SVG 2022-10-09 20:25:21 +03:00
737bbbde6a
Fix SVG rendering for scheme 2022-10-09 17:11:03 +03:00
77e27c7eb6
Add string export 2022-10-09 13:48:19 +03:00
28663a010d
Add SVG render for Scheme 2022-10-09 13:03:36 +03:00
c679680cf0
Fix curve reverse 2022-10-08 14:16:12 +03:00
6a4531fe1d
Fix repos 2022-10-08 13:44:21 +03:00
c8c6aef24e
Fix curveInDirection 2022-10-08 13:39:52 +03:00
3e31effa80
Fix failing test 2022-10-04 15:06:00 +03:00
e106ceef9a
Fix the problem with failed downloads... again 2022-09-20 13:57:41 +03:00
c82f47a786
Fix scheme rectangle bug 2022-09-17 13:11:49 +03:00
b68fa02fa4
Add key to SchemeView 2022-09-17 12:44:44 +03:00
9c5c4b5747
line builder for scheme 2022-09-17 11:49:39 +03:00
354cd8c9a2
Add arc builders to scheme 2022-09-17 11:44:34 +03:00
409d749054
Update schemes to match Maps features 2022-09-17 10:32:28 +03:00
6decba8e83
Add toString to curves and correct rounding normalization for angles 2022-09-16 12:59:58 +03:00
2b01b8e316
Add separate builders for feature state 2022-09-14 15:09:02 +03:00
8c79c913e6
Fix draggable 2022-09-14 12:11:15 +03:00
693f608b14
Reorder some API 2022-09-13 22:53:56 +03:00
4c71b8e7bb
Change state propagation to better support dynamic features. Drag not working for now 2022-09-13 20:30:49 +03:00
fa51fc03db
Change state propagation and rectangle selection 2022-09-13 19:01:23 +03:00
6b0a2566bd
Change state propagation and rectangle selection 2022-09-13 18:27:57 +03:00
4cabfcf16f
Add rectangle override for map component builder 2022-09-13 13:44:38 +03:00
1808a9b7d3
Change notation for arc angles. WARNING meaning changed without API change 2022-09-12 14:05:01 +03:00
890a1bceca
Fix arc builder 2022-09-11 22:24:53 +03:00
8e143eb81c
Fix Angle inversion 2022-09-11 21:16:48 +03:00
8429577fd8
Add extension builders for distance 2022-09-10 18:47:03 +03:00
25e9eed88d
Correct mercator projection. Use epsg naming 2022-09-10 18:04:42 +03:00
42e0a4c46d
Add abstraction for coordinate projections 2022-09-10 16:14:30 +03:00
cda8d8e76f
Update feature builder AP 2022-09-10 15:20:01 +03:00
7939677e5a
Remove unnecessary state duplication for viewPoint 2022-09-09 21:23:38 +03:00
48e425bbae
Use null viewPoint to infer it from features 2022-09-09 21:16:54 +03:00
74e878e7e5
Switch to kscience project configuration. 2022-09-06 13:32:13 +03:00
135 changed files with 87960 additions and 2014 deletions

View File

@ -1,14 +1,14 @@
import kotlin.io.path.readText import kotlin.io.path.readText
job("Build") { job("Build") {
gradlew("openjdk:11", "build") gradlew("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3", "build")
} }
job("Publish") { job("Publish") {
startOn { startOn {
gitPush { enabled = false } gitPush { enabled = false }
} }
container("openjdk:11") { container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") {
env["SPACE_USER"] = Secrets("space_user") env["SPACE_USER"] = Secrets("space_user")
env["SPACE_TOKEN"] = Secrets("space_token") env["SPACE_TOKEN"] = Secrets("space_token")
kotlinScript { api -> kotlinScript { api ->

15
CHANGELOG.md Normal file
View File

@ -0,0 +1,15 @@
# Changelog
## Unreleased
### Added
### Changed
### Deprecated
### Removed
### Fixed
### Security

View File

@ -1,13 +1,73 @@
# Maps-kt
This repository is a work-in-progress implementation of Map-with-markers component for Compose-Multiplatform This repository is a work-in-progress implementation of Map-with-markers component for Compose-Multiplatform
## [maps-kt-core](maps-kt-core) ![](docs/images/Screenshot%202023-01-12%20110429.png)
A multiplatform coordinates representation and conversion.
## [maps-kt-compose](maps-kt-compose) ## Modules
A compose multiplatform (currently desktop only, contributions of android target are welcome) implementation of a map component, features and builder.
## [maps-kt-scheme](maps-kt-scheme)
An alternative component used for the same functionality on 2D schemes. Not all features from maps could be ported because it requires some code duplication (ideas for common API are welcome).
## [demo](demo) ### [demo](demo)
Demonstration projects for different features >
>
> **Maturity**: EXPERIMENTAL
### [maps-kt-compose](maps-kt-compose)
> Compose-multiplaform implementation for web-mercator tiled maps
>
> **Maturity**: EXPERIMENTAL
>
> **Features:**
> - [osm](maps-kt-compose/#) : OpenStreetMap tile provider.
### [maps-kt-core](maps-kt-core)
> Core cartography, UI-agnostic
>
> **Maturity**: DEVELOPMENT
>
> **Features:**
> - [angles and distances](maps-kt-core/#) : Type-safe angle and distance measurements.
> - [ellipsoid](maps-kt-core/#) : Ellipsoid geometry and distances
> - [mercator](maps-kt-core/#) : Mercator and web-mercator projections
### [maps-kt-features](maps-kt-features)
>
>
> **Maturity**: EXPERIMENTAL
### [maps-kt-geojson](maps-kt-geojson)
>
>
> **Maturity**: EXPERIMENTAL
### [maps-kt-scheme](maps-kt-scheme)
>
>
> **Maturity**: EXPERIMENTAL
### [trajectory-kt](trajectory-kt)
> Path and trajectory optimization
>
> **Maturity**: EXPERIMENTAL
### [demo/maps](demo/maps)
>
>
> **Maturity**: EXPERIMENTAL
### [demo/polygon-editor](demo/polygon-editor)
>
>
> **Maturity**: EXPERIMENTAL
### [demo/scheme](demo/scheme)
>
>
> **Maturity**: EXPERIMENTAL
### [demo/trajectory-playground](demo/trajectory-playground)
>
>
> **Maturity**: EXPERIMENTAL

View File

@ -1,90 +1,51 @@
import space.kscience.gradle.isInDevelopment
import space.kscience.gradle.useApache2Licence
import space.kscience.gradle.useSPCTeam
plugins { plugins {
base id("space.kscience.gradle.project")
} }
val ktorVersion by extra("2.0.3") val kmathVersion: String by extra("0.3.1-dev-RC")
allprojects { allprojects {
group = "center.sciprog" group = "center.sciprog"
version = "0.1.0-SNAPSHOT" version = "0.2.2"
repositories {
mavenLocal()
maven("https://repo.kotlin.link")
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
}
} }
tasks.create("version") { ksciencePublish {
group = "publishing" pom("https://github.com/SciProgCentre/maps-kt") {
val versionFile = project.buildDir.resolve("project-version.txt") useApache2Licence()
outputs.file(versionFile) useSPCTeam()
doLast {
versionFile.createNewFile()
versionFile.writeText(project.version.toString())
println(project.version)
} }
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()
} }
subprojects { subprojects {
repositories { repositories {
maven("https://maven.pkg.jetbrains.space/mipt-npm/p/sci/dev")
google() google()
mavenCentral() mavenCentral()
maven("https://repo.kotlin.link")
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
} }
plugins.withId("maven-publish") {
configure<PublishingExtension> {
val vcs = "https://github.com/mipt-npm/maps-kt"
// Process each publication we have in this project
publications {
withType<MavenPublication> {
pom {
name.set(project.name)
description.set(project.description)
url.set(vcs)
licenses {
license {
name.set("The Apache Software License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
distribution.set("repo")
}
} }
developers { readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
developer {
id.set("SPC")
name.set("Scientific programming centre")
organization.set("MIPT")
organizationUrl.set("https://sciprog.center/")
}
}
scm {
url.set(vcs)
tag.set(project.version.toString())
}
}
}
}
val spaceRepo = "https://maven.pkg.jetbrains.space/mipt-npm/p/sci/maven"
val spaceUser: String? = project.findProperty("publishing.space.user") as? String
val spaceToken: String? = project.findProperty("publishing.space.token") as? String
if (spaceUser != null && spaceToken != null) {
project.logger.info("Adding mipt-npm Space publishing to project [${project.name}]")
repositories.maven {
name = "space"
url = uri(spaceRepo)
credentials {
username = spaceUser
password = spaceToken
}
}
}
}
}
}

4
demo/README.md Normal file
View File

@ -0,0 +1,4 @@
# Module demo

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

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

18
demo/maps/api/maps.api Normal file
View File

@ -0,0 +1,18 @@
public final class ComposableSingletons$MainKt {
public static final field INSTANCE LComposableSingletons$MainKt;
public static field lambda-1 Lkotlin/jvm/functions/Function2;
public static field lambda-2 Lkotlin/jvm/functions/Function3;
public static field lambda-3 Lkotlin/jvm/functions/Function3;
public fun <init> ()V
public final fun getLambda-1$maps ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-2$maps ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-3$maps ()Lkotlin/jvm/functions/Function3;
}
public final class MainKt {
public static final fun App (Landroidx/compose/runtime/Composer;I)V
public static final fun main ()V
public static synthetic fun main ([Ljava/lang/String;)V
public static final fun toShortString (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;)Ljava/lang/String;
}

View File

@ -1,4 +1,3 @@
import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins { plugins {
@ -9,18 +8,15 @@ plugins {
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra
kotlin { kotlin {
jvm { jvmToolchain(11)
compilations.all { jvm()
kotlinOptions.jvmTarget = "11"
}
withJava()
}
sourceSets { sourceSets {
val jvmMain by getting { val jvmMain by getting {
dependencies { dependencies {
implementation(projects.mapsKtCompose) implementation(projects.mapsKtCompose)
implementation(projects.mapsKtGeojson)
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("io.ktor:ktor-client-cio")
implementation("ch.qos.logback:logback-classic:1.2.11") implementation("ch.qos.logback:logback-classic:1.2.11")
} }
} }
@ -28,7 +24,8 @@ kotlin {
} }
} }
compose.desktop { compose {
desktop {
application { application {
mainClass = "MainKt" mainClass = "MainKt"
nativeDistributions { nativeDistributions {
@ -38,3 +35,4 @@ compose.desktop {
} }
} }
} }
}

View File

@ -1,46 +1,51 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Home
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Offset import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.attributes.Attributes
import center.sciprog.maps.compose.* import center.sciprog.maps.compose.*
import center.sciprog.maps.coordinates.Distance import center.sciprog.maps.coordinates.*
import center.sciprog.maps.coordinates.GeodeticMapCoordinates import center.sciprog.maps.features.*
import center.sciprog.maps.coordinates.MapViewPoint import center.sciprog.maps.geojson.geoJson
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.degrees
import space.kscience.kmath.geometry.radians
import java.nio.file.Path import java.nio.file.Path
import kotlin.math.PI import kotlin.math.PI
import kotlin.random.Random import kotlin.random.Random
private fun GeodeticMapCoordinates.toShortString(): String = public fun GeodeticMapCoordinates.toShortString(): String =
"${(latitude.degrees.value).toString().take(6)}:${(longitude.degrees.value).toString().take(6)}" "${(latitude.degrees).toString().take(6)}:${(longitude.degrees).toString().take(6)}"
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@Preview @Preview
fun App() { fun App() {
MaterialTheme { MaterialTheme {
//create a view point
val viewPoint = remember {
MapViewPoint(
GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
8.0
)
}
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val mapTileProvider = remember { val mapTileProvider = remember {
OpenStreetMapTileProvider( OpenStreetMapTileProvider(
client = HttpClient(CIO), client = HttpClient(CIO),
@ -48,29 +53,44 @@ fun App() {
) )
} }
var centerCoordinates by remember { mutableStateOf<GeodeticMapCoordinates?>(null) } val centerCoordinates = MutableStateFlow<Gmc?>(null)
val pointOne = 55.568548 to 37.568604 val pointOne = 55.568548 to 37.568604
val pointTwo = 55.929444 to 37.518434 val pointTwo = 55.929444 to 37.518434
val pointThree = 60.929444 to 37.518434 // val pointThree = 60.929444 to 37.518434
val dragPoint = 55.744 to 37.614
MapView( MapView(
mapTileProvider = mapTileProvider, mapTileProvider = mapTileProvider,
initialViewPoint = viewPoint, config = ViewConfig(
config = MapViewConfig( onViewChange = { centerCoordinates.value = focus },
inferViewBoxFromFeatures = true, onClick = { _, viewPoint ->
onViewChange = { centerCoordinates = focus }, println(viewPoint)
}
) )
) { ) {
image(pointOne, Icons.Filled.Home) geoJson(javaClass.getResource("/moscow.geo.json")!!)
.modifyAttribute(ColorAttribute, Color.Blue)
.modifyAttribute(AlphaAttribute, 0.4f)
rectangle(dragPoint, id = "dragMe", size = DpSize(10.dp, 10.dp)).draggable() icon(pointOne, Icons.Filled.Home)
points( 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")
}
draggableLine(marker2, marker3, id = "line 2").color(Color.DarkGray).onClick {
println("line 2 clicked")
}
draggableLine(marker3, marker1, id = "line 3").color(Color.Blue).onClick {
println("line 3 clicked")
}
multiLine(
points = listOf( points = listOf(
55.742465 to 37.615812, 55.742465 to 37.615812,
55.742713 to 37.616370, 55.742713 to 37.616370,
@ -79,48 +99,73 @@ fun App() {
55.742086 to 37.616566, 55.742086 to 37.616566,
55.741715 to 37.616716 55.741715 to 37.616716
), ),
pointMode = PointMode.Polygon
) )
//remember feature ID //remember feature ID
val circleId: FeatureId = circle( val circleId = circle(
centerCoordinates = pointTwo, centerCoordinates = pointTwo,
) )
scope.launch {
draw(position = pointThree) { while (isActive) {
drawLine(start = Offset(-10f, -10f), end = Offset(10f, 10f), color = Color.Red) delay(200)
drawLine(start = Offset(-10f, 10f), end = Offset(10f, -10f), color = Color.Red) circleId.color(Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))
}
} }
arc(pointOne, Distance(10.0), 0f, PI)
// draw(position = pointThree) {
// drawLine(start = Offset(-10f, -10f), end = Offset(10f, 10f), color = Color.Red)
// drawLine(start = Offset(-10f, 10f), end = Offset(10f, -10f), color = Color.Red)
// }
arc(pointOne, 10.0.kilometers, (PI / 4).radians, -Angle.pi / 2)
line(pointOne, pointTwo, id = "line") line(pointOne, pointTwo, id = "line")
text(pointOne, "Home", font = { size = 32f }) text(pointOne, "Home", font = { size = 32f })
centerCoordinates?.let { pixelMap(
space.Rectangle(
Gmc(latitude = 55.58461879539754.degrees, longitude = 37.8746197303493.degrees),
Gmc(latitude = 55.442792937592415.degrees, longitude = 38.132240805463844.degrees)
),
0.005.degrees,
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)
}
centerCoordinates.filterNotNull().onEach {
group(id = "center") { group(id = "center") {
circle(center = it, color = Color.Blue, size = 1f) circle(center = it, id = "circle", size = 1.dp).color(Color.Blue)
text(position = it, it.toShortString(), color = Color.Blue) text(position = it, it.toShortString(), id = "text").color(Color.Blue)
}
}.launchIn(scope)
//Add click listeners for all polygons
forEachWithType<Gmc, PolygonFeature<Gmc>> { ref ->
ref.onClick(PointerMatcher.Primary) {
println("Click on ${ref.id}")
//draw in top-level scope
with(this@MapView) {
multiLine(
ref.resolve().points,
attributes = Attributes(ZAttribute, 10f),
id = "selected",
).modifyAttribute(StrokeAttribute, 4f).color(Color.Magenta)
}
}
}
}
} }
} }
scope.launch {
while (isActive) {
delay(200)
//Overwrite a feature with new color
circle(
pointTwo,
id = circleId,
color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat())
)
}
}
}
}
}
fun main() = application { fun main() = application {
Window(onCloseRequest = ::exitApplication) { Window(onCloseRequest = ::exitApplication, title = "Maps-kt demo", icon = painterResource("SPC-logo.png")) {
App() App()
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
# Module polygon-editor

View File

@ -0,0 +1,19 @@
public final class ComposableSingletons$MainKt {
public static final field INSTANCE LComposableSingletons$MainKt;
public static field lambda-1 Lkotlin/jvm/functions/Function3;
public static field lambda-2 Lkotlin/jvm/functions/Function2;
public static field lambda-3 Lkotlin/jvm/functions/Function3;
public static field lambda-4 Lkotlin/jvm/functions/Function3;
public fun <init> ()V
public final fun getLambda-1$polygon_editor ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-2$polygon_editor ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-3$polygon_editor ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-4$polygon_editor ()Lkotlin/jvm/functions/Function3;
}
public final class MainKt {
public static final fun App (Landroidx/compose/runtime/Composer;I)V
public static final fun main ()V
public static synthetic fun main ([Ljava/lang/String;)V
}

View File

@ -0,0 +1,36 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
val ktorVersion: String by rootProject.extra
kotlin {
jvm()
jvmToolchain(11)
sourceSets {
val jvmMain by getting {
dependencies {
implementation(projects.mapsKtScheme)
implementation(compose.desktop.currentOs)
implementation("ch.qos.logback:logback-classic:1.2.11")
}
}
val jvmTest by getting
}
}
compose{
desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "polygon-editor-demo"
packageVersion = "1.0.0"
}
}
}
}

View File

@ -0,0 +1,80 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.material.CursorDropdownMenu
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.*
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
@Composable
@Preview
fun App() {
MaterialTheme {
var clickPoint by remember { mutableStateOf<XY?>(null) }
val myPolygon: SnapshotStateList<XY> = remember { mutableStateListOf<XY>() }
val featureState: FeatureGroup<XY> = FeatureGroup.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(
config = ViewConfig<XY>(
onClick = { event, point ->
if (event.buttons.isSecondaryPressed) {
clickPoint = point.focus
}
}
),
initialRectangle = featureState.getBoundingBox(1f),
)
CursorDropdownMenu(clickPoint != null, { clickPoint = null }) {
clickPoint?.let { point ->
TextButton({
myPolygon.add(point)
if (myPolygon.isNotEmpty()) {
featureState.group(id = "polygon") {
val pointRefs = myPolygon.mapIndexed { index, xy ->
circle(xy, id = "point[$index]").draggable { _, to ->
myPolygon[index] = to.focus
}
}
draggableMultiLine(
pointRefs + pointRefs.first(),
)
}
}
clickPoint = null
}) {
Text("Create node")
}
}
}
SchemeView(
mapState,
featureState,
)
}
}
fun main() = application {
Window(title = "Polygon editor demo", onCloseRequest = ::exitApplication) {
App()
}
}

4
demo/scheme/README.md Normal file
View File

@ -0,0 +1,4 @@
# Module scheme

View File

@ -0,0 +1,17 @@
public final class ComposableSingletons$MainKt {
public static final field INSTANCE LComposableSingletons$MainKt;
public static field lambda-1 Lkotlin/jvm/functions/Function2;
public static field lambda-2 Lkotlin/jvm/functions/Function3;
public static field lambda-3 Lkotlin/jvm/functions/Function3;
public fun <init> ()V
public final fun getLambda-1$scheme ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-2$scheme ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-3$scheme ()Lkotlin/jvm/functions/Function3;
}
public final class MainKt {
public static final fun App (Landroidx/compose/runtime/Composer;I)V
public static final fun main ()V
public static synthetic fun main ([Ljava/lang/String;)V
}

View File

@ -1,4 +1,3 @@
import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins { plugins {
@ -9,12 +8,8 @@ plugins {
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra
kotlin { kotlin {
jvm { jvm()
compilations.all { jvmToolchain(11)
kotlinOptions.jvmTarget = "11"
}
withJava()
}
sourceSets { sourceSets {
val jvmMain by getting { val jvmMain by getting {
dependencies { dependencies {
@ -27,7 +22,8 @@ kotlin {
} }
} }
compose.desktop { compose{
desktop {
application { application {
mainClass = "MainKt" mainClass = "MainKt"
nativeDistributions { nativeDistributions {
@ -37,3 +33,4 @@ compose.desktop {
} }
} }
} }
}

View File

@ -1,67 +1,104 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.ContextMenuArea
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.maps.features.FeatureGroup
import center.sciprog.maps.features.ViewConfig
import center.sciprog.maps.features.ViewPoint
import center.sciprog.maps.features.color
import center.sciprog.maps.scheme.* import center.sciprog.maps.scheme.*
import center.sciprog.maps.svg.FeatureStateSnapshot
import center.sciprog.maps.svg.exportToSvg
import center.sciprog.maps.svg.snapshot
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.kmath.geometry.Angle
import java.awt.Desktop
import java.nio.file.Files
@Composable @Composable
@Preview @Preview
fun App() { fun App() {
MaterialTheme { MaterialTheme {
//create a view point
val viewPoint = remember {
SchemeViewPoint(
SchemeCoordinates(0f, 0f),
1f
)
}
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val schemeFeaturesState: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) {
background(1600f, 1200f) { painterResource("middle-earth.jpg") }
circle(410.52737 to 868.7676).color(Color.Blue)
text(410.52737 to 868.7676, "Shire").color(Color.Blue)
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)
SchemeView( //circle(410.52737 to 868.7676, id = "hobbit")
viewPoint,
config = SchemeViewConfig(
inferViewBoxFromFeatures = true,
onClick = {
println("${focus.x}, ${focus.y}")
}
)
) {
background(painterResource("middle-earth.jpg"))
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)
val hobbitId = circle(410.52737 to 868.7676)
scope.launch { scope.launch {
var t = 0.0 var t = 0.0
while (isActive) { while (isActive) {
val x = 410.52737 + t * (1132.0881 - 410.52737) val x = 410.52737 + t * (1132.0881 - 410.52737)
val y = 868.7676 + t * (394.99127 - 868.7676) val y = 868.7676 + t * (394.99127 - 868.7676)
circle(x to y, color = Color.Green, id = hobbitId) circle(x to y, id = "hobbit").color(Color.Green)
delay(100) delay(100)
t += 0.005 t += 0.005
if (t >= 1.0) t = 0.0 if (t >= 1.0) t = 0.0
} }
} }
} }
val initialViewPoint: ViewPoint<XY> = remember {
schemeFeaturesState.getBoundingBox(1f)?.computeViewPoint() ?: XYViewPoint(XY(0f, 0f))
}
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
var snapshot: FeatureStateSnapshot<XY>? by remember { mutableStateOf(null) }
if (snapshot == null) {
snapshot = schemeFeaturesState.snapshot()
}
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 mapState: XYViewScope = XYViewScope.remember(
ViewConfig(
onClick = { _, click ->
println("${click.focus.x}, ${click.focus.y}")
},
onViewChange = { viewPoint = this }
),
initialViewPoint = initialViewPoint,
)
SchemeView(
mapState,
schemeFeaturesState,
)
}
} }
} }
fun main() = application { fun main() = application {
Window(onCloseRequest = ::exitApplication) { Window(title = "Scheme demo", onCloseRequest = ::exitApplication) {
App() App()
} }
} }

View File

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 469 KiB

View File

@ -0,0 +1,4 @@
# Module trajectory-playground

View File

@ -0,0 +1,25 @@
public final class ComposableSingletons$MainKt {
public static final field INSTANCE LComposableSingletons$MainKt;
public static field lambda-1 Lkotlin/jvm/functions/Function2;
public static field lambda-2 Lkotlin/jvm/functions/Function3;
public static field lambda-3 Lkotlin/jvm/functions/Function3;
public fun <init> ()V
public final fun getLambda-1$trajectory_playground ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-2$trajectory_playground ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-3$trajectory_playground ()Lkotlin/jvm/functions/Function3;
}
public final class MainKt {
public static final fun closePoints (Landroidx/compose/runtime/Composer;I)V
public static final fun doubleObstacle (Landroidx/compose/runtime/Composer;I)V
public static final fun main ()V
public static synthetic fun main ([Ljava/lang/String;)V
public static final fun obstacle (Lcenter/sciprog/maps/features/FeatureGroup;Lspace/kscience/trajectory/Obstacle;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun obstacle$default (Lcenter/sciprog/maps/features/FeatureGroup;Lspace/kscience/trajectory/Obstacle;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun playground (Landroidx/compose/runtime/Composer;I)V
public static final fun pose (Lcenter/sciprog/maps/features/FeatureGroup;Lspace/kscience/trajectory/Pose2D;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun singleObstacle (Landroidx/compose/runtime/Composer;I)V
public static final fun trajectory (Lcenter/sciprog/maps/features/FeatureGroup;Lspace/kscience/trajectory/Trajectory2D;Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun trajectory$default (Lcenter/sciprog/maps/features/FeatureGroup;Lspace/kscience/trajectory/Trajectory2D;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
}

View File

@ -0,0 +1,30 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
val ktorVersion: String by rootProject.extra
kotlin {
jvm()
jvmToolchain(11)
sourceSets {
val jvmMain by getting {
dependencies {
implementation(projects.mapsKtScheme)
implementation(projects.trajectoryKt)
implementation(compose.desktop.currentOs)
implementation(spclibs.logback.classic)
}
}
val jvmTest by getting
}
}
compose {
desktop {
application {
mainClass = "MainKt"
}
}
}

View File

@ -0,0 +1,195 @@
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
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.trajectory.*
import kotlin.random.Random
private fun DoubleVector2D.toXY() = XY(x.toFloat(), y.toFloat())
private val random = Random(123)
fun FeatureGroup<XY>.trajectory(
trajectory: Trajectory2D,
colorPicker: (Trajectory2D) -> Color = { Color.Blue },
): FeatureRef<XY, FeatureGroup<XY>> = group {
when (trajectory) {
is StraightTrajectory2D -> line(
aCoordinates = trajectory.begin.toXY(),
bCoordinates = trajectory.end.toXY(),
).color(colorPicker(trajectory))
is CircleTrajectory2D -> with(Euclidean2DSpace) {
val topLeft = trajectory.circle.center + vector(-trajectory.circle.radius, trajectory.circle.radius)
val bottomRight = trajectory.circle.center + vector(trajectory.circle.radius, -trajectory.circle.radius)
val rectangle = Rectangle(
topLeft.toXY(),
bottomRight.toXY()
)
arc(
oval = rectangle,
startAngle = trajectory.arcStart - Angle.piDiv2,
arcLength = trajectory.arcAngle,
).color(colorPicker(trajectory))
}
is CompositeTrajectory2D -> trajectory.segments.forEach {
trajectory(it, colorPicker)
}
}
}
fun FeatureGroup<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) {
line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY())
}
@Composable
@Preview
fun closePoints() {
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)
)
val enter = Pose2D(-0.8, -0.8, Angle.pi)
val exit = Pose2D(-0.8, -0.8, Angle.piDiv2)
pose(enter)
pose(exit)
val paths: List<Trajectory2D> = Obstacles.avoidObstacles(
enter,
exit,
1.0,
obstacle
)
obstacle(obstacle)
paths.forEach {
val color = Color(random.nextInt())
trajectory(it) { color }
}
}
}
@Composable
@Preview
fun singleObstacle() {
SchemeView {
val obstacle = Obstacle(Circle2D(Euclidean2DSpace.vector(7.0, 1.0), 5.0))
val enter = Pose2D(-5, -1, Angle.pi / 4)
val exit = Pose2D(20, 4, Angle.pi * 3 / 4)
pose(enter)
pose(exit)
obstacle(obstacle)
Obstacles.avoidObstacles(
enter,
exit,
0.5,
obstacle
).forEach {
val color = Color(random.nextInt())
trajectory(it) { color }
}
}
}
@Composable
@Preview
fun doubleObstacle() {
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)
), 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)
)
)
obstacles.forEach { obstacle(it) }
val enter = Pose2D(-5, -1, Angle.pi / 4)
val exit = Pose2D(20, 4, Angle.pi * 3 / 4)
pose(enter)
pose(exit)
Obstacles.avoidObstacles(
enter,
exit,
0.5,
*obstacles
).forEach {
val color = Color(random.nextInt())
trajectory(it) { color }
}
}
}
@Composable
@Preview
fun playground() {
val examples = listOf(
"Close starting points",
"Single obstacle",
"Two obstacles",
)
var currentExample by remember { mutableStateOf(examples.first()) }
Scaffold(floatingActionButton = {
Column {
examples.forEach {
Button(onClick = { currentExample = it }) {
Text(it)
}
}
}
}) {
when (currentExample) {
examples[0] -> closePoints()
examples[1] -> singleObstacle()
examples[2] -> doubleObstacle()
}
}
}
fun main() = application {
Window(title = "Trajectory-playground", onCloseRequest = ::exitApplication) {
MaterialTheme {
playground()
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

17
docs/templates/ARTIFACT-TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,17 @@
## 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}")
}
```

9
docs/templates/README-TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,9 @@
# Maps-kt
This repository is a work-in-progress implementation of Map-with-markers component for Compose-Multiplatform
![](docs/images/Screenshot%202023-01-12%20110429.png)
## Modules
${modules}

View File

@ -1,5 +1,10 @@
kotlin.code.style=official kotlin.code.style=official
kotlin.version=1.6.10
compose.version=1.1.1 compose.version=1.4.0
agp.version=4.2.2 agp.version=7.4.2
android.useAndroidX=true android.useAndroidX=true
org.jetbrains.compose.experimental.jscanvas.enabled=true
org.gradle.jvmargs=-Xmx4096m
toolsVersion=0.14.6-kotlin-1.8.20

View File

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

2213
gradle/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

33
maps-kt-compose/README.md Normal file
View File

@ -0,0 +1,33 @@
# Module kmath-core
The core interfaces of KMath.
- [osm](#) : OpenStreetMap tile provider.
## Artifact:
The Maven coordinates of this project are `center.sciprog:maps-kt-compose:0.2.2`.
**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 {
maven("https://repo.kotlin.link")
mavenCentral()
}
dependencies {
implementation("center.sciprog:maps-kt-compose:0.2.2")
}
```

View File

@ -1,30 +1,43 @@
import org.jetbrains.compose.compose
plugins { plugins {
kotlin("multiplatform") id("space.kscience.gradle.mpp")
id("org.jetbrains.compose") id("org.jetbrains.compose")
`maven-publish` `maven-publish`
} }
val ktorVersion: String by rootProject.extra kscience{
jvm()
}
kotlin { kotlin {
explicitApi = org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode.Warning
jvm {
compilations.all {
kotlinOptions.jvmTarget = "11"
}
}
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
api(projects.mapsKtCore) api(projects.mapsKtCore)
api(projects.mapsKtFeatures)
api(compose.foundation) api(compose.foundation)
api("io.ktor:ktor-client-core:$ktorVersion") api(project.dependencies.platform(spclibs.ktor.bom))
api("io.ktor:ktor-client-core")
api("io.github.microutils:kotlin-logging:2.1.23") api("io.github.microutils:kotlin-logging:2.1.23")
} }
} }
val jvmMain by getting val jvmTest by getting {
val jvmTest by getting dependencies {
implementation("io.ktor:ktor-client-cio")
implementation(compose.desktop.currentOs)
implementation(spclibs.kotlinx.coroutines.test)
implementation(spclibs.logback.classic)
} }
} }
}
}
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"))
feature(
id = "osm",
) { "OpenStreetMap tile provider." }
}

View File

@ -0,0 +1,7 @@
# Module kmath-core
The core interfaces of KMath.
${features}
${artifact}

View File

@ -0,0 +1,74 @@
package center.sciprog.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
internal fun Angle.isBetween(a: Angle, b: Angle) = this in a..b || this in b..a
/**
* A section of the map between two parallels and two meridians. The figure represents a square in a Mercator projection.
* Params are two opposing "corners" of quasi-square.
*
* Note that this is a rectangle only on a Mercator projection.
*/
internal data class GmcRectangle(
override val a: GeodeticMapCoordinates,
override val b: GeodeticMapCoordinates,
) : Rectangle<Gmc> {
override val center: GeodeticMapCoordinates
get() = GeodeticMapCoordinates.normalized(
(a.latitude + b.latitude) / 2,
(a.longitude + b.longitude) / 2
)
override fun contains(point: Gmc): Boolean =
point.latitude.isBetween(a.latitude, b.latitude) && point.longitude.isBetween(a.longitude, b.longitude)
}
/**
* Minimum longitude
*/
public val Rectangle<Gmc>.left: Angle get() = minOf(a.longitude, b.longitude)
/**
* maximum longitude
*/
public val Rectangle<Gmc>.right: Angle get() = maxOf(a.longitude, b.longitude)
/**
* Maximum latitude
*/
public val Rectangle<Gmc>.top: Angle get() = maxOf(a.latitude, b.latitude)
/**
* Minimum latitude
*/
public val Rectangle<Gmc>.bottom: Angle get() = minOf(a.latitude, b.latitude)
public val Rectangle<Gmc>.longitudeDelta: Angle get() = abs(a.longitude - b.longitude)
public val Rectangle<Gmc>.latitudeDelta: Angle get() = abs(a.latitude - b.latitude)
public val Rectangle<Gmc>.topLeft: Gmc get() = Gmc.normalized(top, left)
public val Rectangle<Gmc>.bottomRight: Gmc get() = Gmc.normalized(bottom, right)
//public fun GmcRectangle.enlarge(
// top: Distance,
// bottom: Distance = top,
// left: Distance = top,
// right: Distance = left,
//): GmcRectangle {
//
//}
//
//public fun GmcRectangle.enlarge(
// top: Angle,
// bottom: Angle = top,
// left: Angle = top,
// right: Angle = left,
//): GmcRectangle {
//
//}

View File

@ -1,174 +0,0 @@
package center.sciprog.maps.compose
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.PointMode
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.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import center.sciprog.maps.coordinates.*
import kotlin.math.floor
public interface MapFeature {
public val zoomRange: IntRange
public fun getBoundingBox(zoom: Double): GmcRectangle?
}
public interface DraggableMapFeature : MapFeature {
public fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature
}
public fun Iterable<MapFeature>.computeBoundingBox(zoom: Double): GmcRectangle? =
mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
internal fun Pair<Double, Double>.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second)
internal val defaultZoomRange = 1..18
/**
* A feature that decides what to show depending on the zoom value (it could change size of shape)
*/
public class MapFeatureSelector(
public val selector: (zoom: Int) -> MapFeature,
) : MapFeature {
override val zoomRange: IntRange get() = defaultZoomRange
override fun getBoundingBox(zoom: Double): GmcRectangle? = selector(floor(zoom).toInt()).getBoundingBox(zoom)
}
public class MapDrawFeature(
public val position: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange,
public val drawFeature: DrawScope.() -> Unit,
) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle {
//TODO add box computation
return GmcRectangle(position, position)
}
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapDrawFeature(newCoordinates, zoomRange, drawFeature)
}
public class MapPointsFeature(
public val points: List<GeodeticMapCoordinates>,
override val zoomRange: IntRange = defaultZoomRange,
public val stroke: Float = 2f,
public val color: Color = Color.Red,
public val pointMode: PointMode = PointMode.Points,
) : MapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle {
return GmcRectangle(points.first(), points.last())
}
}
public class MapCircleFeature(
public val center: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange,
public val size: Float = 5f,
public val color: Color = Color.Red,
) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle {
val scale = WebMercatorProjection.scaleFactor(zoom)
return GmcRectangle.square(center, (size/scale).radians, (size/scale).radians)
}
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapCircleFeature(newCoordinates, zoomRange, size, color)
}
public class MapRectangleFeature(
public val center: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange,
public val size: DpSize = DpSize(5.dp, 5.dp),
public val color: Color = Color.Red,
) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle {
val scale = WebMercatorProjection.scaleFactor(zoom)
return GmcRectangle.square(center, (size.height.value/scale).radians, (size.width.value/scale).radians)
}
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapRectangleFeature(newCoordinates, zoomRange, size, color)
}
public class MapLineFeature(
public val a: GeodeticMapCoordinates,
public val b: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange,
public val color: Color = Color.Red,
) : MapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(a, b)
}
public class MapArcFeature(
public val oval: GmcRectangle,
public val startAngle: Float,
public val endAngle: Float,
override val zoomRange: IntRange = defaultZoomRange,
public val color: Color = Color.Red,
) : MapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = oval
}
public class MapBitmapImageFeature(
public val position: GeodeticMapCoordinates,
public val image: ImageBitmap,
public val size: IntSize = IntSize(15, 15),
override val zoomRange: IntRange = defaultZoomRange,
) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapBitmapImageFeature(newCoordinates, image, size, zoomRange)
}
public class MapVectorImageFeature(
public val position: GeodeticMapCoordinates,
public val painter: Painter,
public val size: DpSize,
override val zoomRange: IntRange = defaultZoomRange,
) : DraggableMapFeature{
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapVectorImageFeature(newCoordinates,painter, size, zoomRange)
}
@Composable
public fun MapVectorImageFeature(
position: GeodeticMapCoordinates,
image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp),
zoomRange: IntRange = defaultZoomRange,
): MapVectorImageFeature = MapVectorImageFeature(position, rememberVectorPainter(image), size, zoomRange)
/**
* A group of other features
*/
public class MapFeatureGroup(
public val children: Map<FeatureId, MapFeature>,
override val zoomRange: IntRange = defaultZoomRange,
) : MapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle? =
children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
}
public class MapTextFeature(
public val position: GeodeticMapCoordinates,
public val text: String,
override val zoomRange: IntRange = defaultZoomRange,
public val color: Color,
public val fontConfig: MapTextFeatureFont.() -> Unit,
) : DraggableMapFeature{
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapTextFeature(newCoordinates, text, zoomRange, color, fontConfig)
}

View File

@ -1,190 +0,0 @@
package center.sciprog.maps.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.vector.ImageVector
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.GmcRectangle
public typealias FeatureId = String
public interface MapFeatureAttributeKey<T>
public class MapFeatureAttributeSet(private val map: Map<MapFeatureAttributeKey<*>, *>) {
public operator fun <T> get(key: MapFeatureAttributeKey<*>): T? = map[key]?.let {
@Suppress("UNCHECKED_CAST")
it as T
}
}
public interface MapFeatureBuilder {
public fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId
public fun <T> setAttribute(id: FeatureId, key: MapFeatureAttributeKey<T>, value: T)
public val features: MutableMap<FeatureId, MapFeature>
public fun attributes(): Map<FeatureId, MapFeatureAttributeSet>
//TODO use context receiver for that
public fun FeatureId.draggable(enabled: Boolean = true) {
setAttribute(this, DraggableAttribute, enabled)
}
}
internal class MapFeatureBuilderImpl(
override val features: SnapshotStateMap<FeatureId, MapFeature>,
) : MapFeatureBuilder {
private val attributes = SnapshotStateMap<FeatureId, SnapshotStateMap<MapFeatureAttributeKey<out Any?>, in Any?>>()
private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]"
override fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId {
val safeId = id ?: generateID(feature)
features[id ?: generateID(feature)] = feature
return safeId
}
override fun <T> setAttribute(id: FeatureId, key: MapFeatureAttributeKey<T>, value: T) {
attributes.getOrPut(id) { SnapshotStateMap() }[key] = value
}
override fun attributes(): Map<FeatureId, MapFeatureAttributeSet> =
attributes.mapValues { MapFeatureAttributeSet(it.value) }
}
public fun MapFeatureBuilder.circle(
center: GeodeticMapCoordinates,
zoomRange: IntRange = defaultZoomRange,
size: Float = 5f,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id, MapCircleFeature(center, zoomRange, size, color)
)
public fun MapFeatureBuilder.circle(
centerCoordinates: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange,
size: Float = 5f,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id, MapCircleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color)
)
public fun MapFeatureBuilder.rectangle(
centerCoordinates: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp),
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id, MapRectangleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color)
)
public fun MapFeatureBuilder.draw(
position: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange,
id: FeatureId? = null,
drawFeature: DrawScope.() -> Unit,
): FeatureId = addFeature(id, MapDrawFeature(position.toCoordinates(), zoomRange, drawFeature))
public fun MapFeatureBuilder.line(
aCoordinates: Pair<Double, Double>,
bCoordinates: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id,
MapLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), zoomRange, color)
)
public fun MapFeatureBuilder.arc(
oval: GmcRectangle,
startAngle: Number,
endAngle: Number,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id,
MapArcFeature(oval, startAngle.toFloat(), endAngle.toFloat(), zoomRange, color)
)
public fun MapFeatureBuilder.arc(
center: Pair<Double, Double>,
radius: Distance,
startAngle: Number,
endAngle: Number,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: FeatureId? = null,
): FeatureId = addFeature(
id,
MapArcFeature(
GmcRectangle.square(center.toCoordinates(), radius, radius),
startAngle.toFloat(),
endAngle.toFloat(),
zoomRange,
color
)
)
public fun MapFeatureBuilder.points(
points: List<Pair<Double, Double>>,
zoomRange: IntRange = defaultZoomRange,
stroke: Float = 2f,
color: Color = Color.Red,
pointMode: PointMode = PointMode.Points,
id: FeatureId? = null,
): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode))
@Composable
public fun MapFeatureBuilder.image(
position: Pair<Double, Double>,
image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp),
zoomRange: IntRange = defaultZoomRange,
id: FeatureId? = null,
): FeatureId = addFeature(id, MapVectorImageFeature(position.toCoordinates(), image, size, zoomRange))
public fun MapFeatureBuilder.group(
zoomRange: IntRange = defaultZoomRange,
id: FeatureId? = null,
builder: MapFeatureBuilder.() -> Unit,
): FeatureId {
val map = MapFeatureBuilderImpl(mutableStateMapOf()).apply(builder).features
val feature = MapFeatureGroup(map, zoomRange)
return addFeature(id, feature)
}
public fun MapFeatureBuilder.text(
position: GeodeticMapCoordinates,
text: String,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
font: MapTextFeatureFont.() -> Unit = { size = 16f },
id: FeatureId? = null,
): FeatureId = addFeature(id, MapTextFeature(position, text, zoomRange, color, font))
public fun MapFeatureBuilder.text(
position: Pair<Double, Double>,
text: String,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
font: MapTextFeatureFont.() -> Unit = { size = 16f },
id: FeatureId? = null,
): FeatureId = addFeature(id, MapTextFeature(position.toCoordinates(), text, zoomRange, color, font))

View File

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

View File

@ -1,8 +1,8 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import androidx.compose.ui.graphics.ImageBitmap
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import org.jetbrains.skia.Image
import kotlin.math.floor import kotlin.math.floor
public data class TileId( public data class TileId(
@ -13,7 +13,7 @@ public data class TileId(
public data class MapTile( public data class MapTile(
val id: TileId, val id: TileId,
val image: ImageBitmap, val image: Image,
) )
public interface MapTileProvider { public interface MapTileProvider {
@ -21,9 +21,9 @@ public interface MapTileProvider {
public val tileSize: Int get() = DEFAULT_TILE_SIZE public val tileSize: Int get() = DEFAULT_TILE_SIZE
public fun toIndex(d: Double): Int = floor(d / tileSize).toInt() public fun toIndex(d: Float): Int = floor(d / tileSize).toInt()
public fun toCoordinate(i: Int): Double = (i * tileSize).toDouble() public fun toCoordinate(i: Int): Float = (i * tileSize).toFloat()
public companion object { public companion object {
public const val DEFAULT_TILE_SIZE: Int = 256 public const val DEFAULT_TILE_SIZE: Int = 256

View File

@ -1,155 +1,70 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEvent import center.sciprog.maps.coordinates.Gmc
import androidx.compose.ui.input.pointer.isPrimaryPressed import center.sciprog.maps.features.*
import androidx.compose.ui.unit.DpSize
import center.sciprog.maps.coordinates.*
import kotlin.math.PI
import kotlin.math.log2
import kotlin.math.min
public fun interface DragHandle {
/**
* @param event - qualifiers of the event used for drag
* @param start - is a point where drag begins, end is a point where drag ends
* @param end - end point of the drag
*
* @return true if default event processors should be used after this one
*/
public fun handle(event: PointerEvent, start: MapViewPoint, end: MapViewPoint): Boolean
public companion object {
public val BYPASS: DragHandle = DragHandle { _, _, _ -> true }
/**
* Process only events with primary button pressed
*/
public fun withPrimaryButton(
block: (event: PointerEvent, start: MapViewPoint, end: MapViewPoint) -> Boolean,
): DragHandle = DragHandle { event, start, end ->
if (event.buttons.isPrimaryPressed) {
block(event, start, end)
} else {
false
}
}
/**
* Combine several handles into one
*/
public fun combine(vararg handles: DragHandle): DragHandle = DragHandle { event, start, end ->
handles.forEach {
if (!it.handle(event, start, end)) return@DragHandle false
}
return@DragHandle true
}
}
}
//TODO consider replacing by modifier
/**
*/
public data class MapViewConfig(
val zoomSpeed: Double = 1.0 / 3.0,
val inferViewBoxFromFeatures: Boolean = false,
val onClick: MapViewPoint.(PointerEvent) -> Unit = {},
val dragHandle: DragHandle = DragHandle.BYPASS,
val onViewChange: MapViewPoint.() -> Unit = {},
val onSelect: (GmcRectangle) -> Unit = {},
val zoomOnSelect: Boolean = true,
val resetViewPoint: Boolean = false,
)
@Composable @Composable
public expect fun MapView( public expect fun MapView(
mapTileProvider: MapTileProvider, viewScope: MapViewScope,
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, features: FeatureGroup<Gmc>,
features: Map<FeatureId, MapFeature>,
config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
) )
private fun prepareConfig(initialConfig: MapViewConfig, featureBuilder: MapFeatureBuilder): MapViewConfig { /**
val draggableFeatureIds: Set<FeatureId> = featureBuilder.attributes().filterValues { * A builder for a Map with static features.
it[DraggableAttribute] ?: false */
}.keys
val features = featureBuilder.features
val featureDrag = DragHandle.withPrimaryButton { _, start, end ->
val zoom = start.zoom
draggableFeatureIds.forEach { id ->
val feature = features[id] as? DraggableMapFeature ?: return@forEach
//val border = WebMercatorProjection.scaleFactor(zoom)
val boundingBox = feature.getBoundingBox(zoom) ?: return@forEach
if (start.focus in boundingBox) {
features[id] = feature.withCoordinates(end.focus)
return@withPrimaryButton false
}
}
return@withPrimaryButton true
}
return initialConfig.copy(
dragHandle = DragHandle.combine(featureDrag, initialConfig.dragHandle),
)
}
@Composable @Composable
public fun MapView( public fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
initialViewPoint: MapViewPoint, features: FeatureGroup<Gmc>,
config: MapViewConfig = MapViewConfig(), initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null,
config: ViewConfig<Gmc> = ViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
) { ) {
val featuresBuilder = MapFeatureBuilderImpl(mutableStateMapOf())
featuresBuilder.buildFeatures()
val features = remember { featuresBuilder.features }
val newConfig = remember(features) { val mapState: MapViewScope = MapViewScope.remember(
prepareConfig(config, featuresBuilder)
}
MapView(
mapTileProvider, mapTileProvider,
{ initialViewPoint }, config,
features, initialViewPoint = initialViewPoint,
newConfig, initialRectangle = initialRectangle ?: features.getBoundingBox(Float.MAX_VALUE),
modifier
) )
MapView(mapState, features, modifier)
} }
internal fun GmcRectangle.computeViewPoint( /**
* 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, mapTileProvider: MapTileProvider,
): (canvasSize: DpSize) -> MapViewPoint = { canvasSize -> initialViewPoint: ViewPoint<Gmc>? = null,
val zoom = log2( initialRectangle: Rectangle<Gmc>? = null,
min( config: ViewConfig<Gmc> = ViewConfig(),
canvasSize.width.value / longitudeDelta.radians.value, modifier: Modifier = Modifier.fillMaxSize(),
canvasSize.height.value / latitudeDelta.radians.value buildFeatures: FeatureGroup<Gmc>.() -> Unit = {},
) * PI / mapTileProvider.tileSize ) {
val featureState = FeatureGroup.remember(WebMercatorSpace, buildFeatures)
val mapState: MapViewScope = MapViewScope.remember(
mapTileProvider,
config,
initialViewPoint = initialViewPoint,
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox(
WebMercatorSpace,
Float.MAX_VALUE
),
) )
MapViewPoint(center, zoom)
MapView(mapState, featureState, modifier)
} }
//
//@Composable
//public fun MapView(
// mapTileProvider: MapTileProvider,
// box: GmcRectangle,
// config: MapViewConfig = MapViewConfig(),
// modifier: Modifier = Modifier.fillMaxSize(),
// buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
//) {
// val builder by derivedStateOf { MapFeatureBuilderImpl().apply(buildFeatures) }
//
// MapView(
// mapTileProvider,
// box.computeViewPoint(mapTileProvider),
// builder.features,
// prepareConfig(config, builder),
// modifier
// )
//}

View File

@ -0,0 +1,20 @@
package center.sciprog.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
/**
* Observable position on the map. Includes observation coordinate and [zoom] factor
*/
internal data class MapViewPoint(
override val focus: GeodeticMapCoordinates,
override val zoom: Float,
) : ViewPoint<Gmc>{
val scaleFactor: Float by lazy { WebMercatorProjection.scaleFactor(zoom) }
public companion object{
public val globe: MapViewPoint = MapViewPoint(Gmc.ofRadians(0.0, 0.0), 1f)
}
}

View File

@ -0,0 +1,100 @@
package center.sciprog.maps.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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 kotlin.math.*
public class MapViewScope internal constructor(
public val mapTileProvider: MapTileProvider,
config: ViewConfig<Gmc>,
) : CoordinateViewScope<Gmc>(config) {
override val space: CoordinateSpace<Gmc> get() = WebMercatorSpace
private val scaleFactor: Float
get() = WebMercatorProjection.scaleFactor(zoom)
public val intZoom: Int get() = floor(zoom).toInt()
public val centerCoordinates: WebMercatorCoordinates
get() = WebMercatorProjection.toMercator(viewPoint.focus, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f)
public val tileScale: Float
get() = 2f.pow(zoom - floor(zoom))
/*
* Convert screen independent offset to GMC, adjusting for fractional zoom
*/
override fun DpOffset.toCoordinates(): Gmc {
val mercator = WebMercatorCoordinates(
intZoom,
(x - canvasSize.width / 2).value / tileScale + centerCoordinates.x,
(y - canvasSize.height / 2).value / tileScale + centerCoordinates.y,
)
return WebMercatorProjection.toGeodetic(mercator)
}
override fun Gmc.toDpOffset(): DpOffset {
val mercator = WebMercatorProjection.toMercator(this, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f)
return DpOffset(
(canvasSize.width / 2 + (mercator.x.dp - centerCoordinates.x.dp) * tileScale),
(canvasSize.height / 2 + (mercator.y.dp - centerCoordinates.y.dp) * tileScale)
)
}
override fun Rectangle<Gmc>.toDpRect(): DpRect {
val topLeft = topLeft.toDpOffset()
val bottomRight = bottomRight.toDpOffset()
return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
}
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
)
return space.ViewPoint(rectangle.center, zoom.toFloat())
}
override fun ViewPoint<Gmc>.moveBy(x: Dp, y: Dp): ViewPoint<Gmc> {
val deltaX = x.value / tileScale
val deltaY = y.value / tileScale
val newCoordinates = Gmc.normalized(
(focus.latitude + (deltaY / scaleFactor).radians).coerceIn(
-MercatorProjection.MAXIMUM_LATITUDE,
MercatorProjection.MAXIMUM_LATITUDE
),
focus.longitude + (deltaX / scaleFactor).radians
)
return space.ViewPoint(newCoordinates, zoom)
}
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 ->
if (initialViewPoint != null) {
mapState.viewPoint = initialViewPoint
} else if (initialRectangle != null) {
mapState.viewPoint = mapState.computeViewPoint(initialRectangle)
}
}
}
}
}

View File

@ -0,0 +1,136 @@
package center.sciprog.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 kotlin.math.abs
import kotlin.math.floor
import kotlin.math.pow
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(center: Gmc, zoom: Float, size: DpSize): Rectangle<Gmc> {
val scale = WebMercatorProjection.scaleFactor(zoom)
return Rectangle(center, (size.width.value / scale).radians, (size.height.value / scale).radians)
}
override val defaultViewPoint: ViewPoint<Gmc> = MapViewPoint.globe
override fun ViewPoint(center: Gmc, zoom: Float): ViewPoint<Gmc> = MapViewPoint(center, zoom)
override fun ViewPoint<Gmc>.moveBy(delta: Gmc): ViewPoint<Gmc> {
val newCoordinates = Gmc.normalized(
(focus.latitude + delta.latitude).coerceIn(
-MercatorProjection.MAXIMUM_LATITUDE,
MercatorProjection.MAXIMUM_LATITUDE
),
focus.longitude + delta.longitude
)
return MapViewPoint(newCoordinates, zoom)
}
override fun ViewPoint<Gmc>.zoomBy(zoomDelta: Float, invariant: Gmc): ViewPoint<Gmc> = if (invariant == focus) {
ViewPoint(focus, (zoom + zoomDelta).coerceIn(2f, 18f))
} else {
val difScale = (1 - 2f.pow(-zoomDelta))
val newCenter = Gmc.normalized(
focus.latitude + (invariant.latitude - focus.latitude) * difScale,
focus.longitude + (invariant.longitude - focus.longitude) * difScale
)
MapViewPoint(newCenter, (zoom + zoomDelta).coerceIn(2f, 18f))
}
override fun Rectangle<Gmc>.withCenter(center: Gmc): Rectangle<Gmc> =
Rectangle(center, height = latitudeDelta, width = longitudeDelta)
override fun Collection<Rectangle<Gmc>>.wrapRectangles(): Rectangle<Gmc>? {
if (isEmpty()) return null
//TODO optimize computation
val minLat = minOf { it.bottom }
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))
}
override fun Collection<Gmc>.wrapPoints(): Rectangle<Gmc>? {
if (isEmpty()) return null
//TODO optimize computation
val minLat = minOf { it.latitude }
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))
}
override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset {
val intZoom = intZoom(zoom)
val mercatorA = WebMercatorProjection.toMercator(this, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f)
val mercatorB = WebMercatorProjection.toMercator(b, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f)
val tileScale = tileScale(zoom)
return DpOffset(
(mercatorA.x - mercatorB.x).dp * tileScale,
(mercatorA.y - mercatorB.y).dp * tileScale
)
}
override fun Gmc.isInsidePolygon(points: List<Gmc>): Boolean = points.zipWithNext().count { (left, right) ->
//using raytracing algorithm with the ray pointing "up"
val longitudeRange = if (right.longitude >= left.longitude) {
left.longitude..right.longitude
} else {
right.longitude..left.longitude
}
if (longitude !in longitudeRange) return@count false
val longitudeDelta = right.longitude - left.longitude
left.latitude * abs((right.longitude - longitude) / longitudeDelta) +
right.latitude * abs((longitude - left.longitude) / longitudeDelta) >= latitude
} % 2 == 1
}
/**
* A quasi-square section. Note that latitudinal distance could be imprecise for large distances
*/
public fun CoordinateSpace<Gmc>.Rectangle(
center: Gmc,
height: Distance,
width: Distance,
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
): Rectangle<Gmc> {
val reducedRadius = ellipsoid.reducedRadius(center.latitude)
return Rectangle(center, (height / ellipsoid.polarRadius).radians, (width / reducedRadius).radians)
}
/**
* A quasi-square section.
*/
public fun CoordinateSpace<Gmc>.Rectangle(
center: GeodeticMapCoordinates,
height: Angle,
width: Angle,
): Rectangle<Gmc> {
val a = Gmc.normalized(
center.latitude - (height / 2),
center.longitude - (width / 2)
)
val b = Gmc.normalized(
center.latitude + (height / 2),
center.longitude + (width / 2)
)
return GmcRectangle(a, b)
}

View File

@ -1,3 +0,0 @@
package center.sciprog.maps.compose
public object DraggableAttribute: MapFeatureAttributeKey<Boolean>

View File

@ -0,0 +1,143 @@
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)
)
}
)
)

View File

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

View File

@ -1,371 +0,0 @@
package center.sciprog.maps.compose
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.unit.*
import center.sciprog.maps.coordinates.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import mu.KotlinLogging
import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint
import kotlin.math.*
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)
internal fun MapViewPoint.move(deltaX: Double, deltaY: Double): MapViewPoint {
val newCoordinates = GeodeticMapCoordinates(
(focus.latitude + (deltaY / scaleFactor).radians).coerceIn(
-MercatorProjection.MAXIMUM_LATITUDE,
MercatorProjection.MAXIMUM_LATITUDE
),
focus.longitude + (deltaX / scaleFactor).radians
)
return MapViewPoint(newCoordinates, zoom)
}
private val logger = KotlinLogging.logger("MapView")
/**
* A component that renders map and provides basic map manipulation capabilities
*/
@Composable
public actual fun MapView(
mapTileProvider: MapTileProvider,
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint,
features: Map<FeatureId, MapFeature>,
config: MapViewConfig,
modifier: Modifier,
) {
var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) }
var viewPointInternal: MapViewPoint? by remember {
mutableStateOf(null)
}
if (config.resetViewPoint) {
viewPointInternal = null
}
val viewPoint: MapViewPoint by derivedStateOf {
viewPointInternal ?: if (config.inferViewBoxFromFeatures) {
features.values.computeBoundingBox(1.0)?.let { box ->
val zoom = log2(
min(
canvasSize.width.value / box.longitudeDelta.radians.value,
canvasSize.height.value / box.latitudeDelta.radians.value
) * PI / mapTileProvider.tileSize
)
MapViewPoint(box.center, zoom)
} ?: computeViewPoint(canvasSize)
} else {
computeViewPoint(canvasSize)
}
}
val zoom: Int by derivedStateOf {
require(viewPoint.zoom in 1.0..18.0) { "Zoom value of ${viewPoint.zoom} is not valid" }
floor(viewPoint.zoom).toInt()
}
val tileScale: Double by derivedStateOf {
2.0.pow(viewPoint.zoom - zoom)
}
val mapTiles = remember { mutableStateListOf<MapTile>() }
val centerCoordinates by derivedStateOf { WebMercatorProjection.toMercator(viewPoint.focus, zoom) }
fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates(
zoom,
(x - canvasSize.width / 2).value / tileScale + centerCoordinates.x,
(y - canvasSize.height / 2).value / tileScale + centerCoordinates.y,
)
/*
* Convert screen independent offset to GMC, adjusting for fractional zoom
*/
fun DpOffset.toGeodetic() = WebMercatorProjection.toGeodetic(toMercator())
// Selection rectangle. If null - no selection
var selectRect by remember { mutableStateOf<Rect?>(null) }
@OptIn(ExperimentalComposeUiApi::class)
val canvasModifier = modifier.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
val event: PointerEvent = awaitPointerEvent()
event.changes.forEach { change ->
val dragStart = change.position
val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
//start selection
if (event.buttons.isPrimaryPressed && event.keyboardModifiers.isShiftPressed) {
selectRect = Rect(change.position, change.position)
}
drag(change.id) { dragChange ->
val dragAmount = dragChange.position - dragChange.previousPosition
val dpStart = DpOffset(
dragChange.previousPosition.x.toDp(),
dragChange.previousPosition.y.toDp()
)
val dpEnd = DpOffset(dragChange.position.x.toDp(), dragChange.position.y.toDp())
//apply drag handle and check if it prohibits the drag even propagation
if (
!config.dragHandle.handle(
event,
MapViewPoint(dpStart.toGeodetic(), viewPoint.zoom),
MapViewPoint(dpEnd.toGeodetic(), viewPoint.zoom)
)
) {
//clear selection just in case
selectRect = null
return@drag
}
if (event.buttons.isPrimaryPressed) {
//Evaluating selection frame
selectRect?.let { rect ->
val offset = dragChange.position
selectRect = Rect(
min(offset.x, rect.left),
min(offset.y, rect.top),
max(offset.x, rect.right),
max(offset.y, rect.bottom)
)
return@drag
}
config.onClick(MapViewPoint(dpPos.toGeodetic(), viewPoint.zoom), event)
val newViewPoint = viewPoint.move(
-dragAmount.x.toDp().value / tileScale,
+dragAmount.y.toDp().value / tileScale
)
config.onViewChange(newViewPoint)
viewPointInternal = newViewPoint
}
}
// evaluate selection
selectRect?.let { rect ->
//Use selection override if it is defined
val gmcBox = GmcRectangle(
rect.topLeft.toDpOffset().toGeodetic(),
rect.bottomRight.toDpOffset().toGeodetic()
)
config.onSelect(gmcBox)
if (config.zoomOnSelect) {
val newViewPoint = gmcBox.computeViewPoint(mapTileProvider).invoke(canvasSize)
config.onViewChange(newViewPoint)
viewPointInternal = newViewPoint
}
selectRect = null
}
}
}
}
}.onPointerEvent(PointerEventType.Scroll) {
val change = it.changes.first()
val (xPos, yPos) = change.position
//compute invariant point of translation
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic()
val newViewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant)
config.onViewChange(newViewPoint)
viewPointInternal = newViewPoint
}.fillMaxSize()
// Load tiles asynchronously
LaunchedEffect(viewPoint, canvasSize) {
with(mapTileProvider) {
val indexRange = 0 until 2.0.pow(zoom).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)
mapTiles.clear()
for (j in verticalIndices) {
for (i in horizontalIndices) {
val id = TileId(zoom, i, j)
try {
//start all
val deferred = loadTileAsync(id)
//wait asynchronously for it to finish
launch {
mapTiles += deferred.await()
}
} catch (ex: Exception) {
if (ex !is CancellationException) {
//displaying the error is maps responsibility
logger.error(ex) { "Failed to load tile with id=$id" }
}
}
}
}
}
}
Canvas(canvasModifier) {
fun WebMercatorCoordinates.toOffset(): Offset = Offset(
(canvasSize.width / 2 + (x.dp - centerCoordinates.x.dp) * tileScale.toFloat()).toPx(),
(canvasSize.height / 2 + (y.dp - centerCoordinates.y.dp) * tileScale.toFloat()).toPx()
)
//Convert GMC to offset in pixels (not DP), adjusting for zoom
fun GeodeticMapCoordinates.toOffset(): Offset = WebMercatorProjection.toMercator(this, zoom).toOffset()
fun DrawScope.drawFeature(zoom: Int, feature: MapFeature) {
when (feature) {
is MapFeatureSelector -> drawFeature(zoom, feature.selector(zoom))
is MapCircleFeature -> drawCircle(
feature.color,
feature.size,
center = feature.center.toOffset()
)
is MapRectangleFeature -> drawRect(
feature.color,
topLeft = feature.center.toOffset() - Offset(
feature.size.width.toPx() / 2,
feature.size.height.toPx() / 2
),
size = feature.size.toSize()
)
is MapLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
is MapArcFeature -> {
val topLeft = feature.oval.topLeft.toOffset()
val bottomRight = feature.oval.bottomRight.toOffset()
val path = Path().apply {
addArcRad(Rect(topLeft, bottomRight), feature.startAngle, feature.endAngle - feature.startAngle)
}
drawPath(path, color = feature.color, style = Stroke())
}
is MapBitmapImageFeature -> drawImage(feature.image, feature.position.toOffset())
is MapVectorImageFeature -> {
val offset = feature.position.toOffset()
val size = feature.size.toSize()
translate(offset.x - size.width / 2, offset.y - size.height / 2) {
with(feature.painter) {
draw(size)
}
}
}
is MapTextFeature -> drawIntoCanvas { canvas ->
val offset = feature.position.toOffset()
canvas.nativeCanvas.drawString(
feature.text,
offset.x + 5,
offset.y - 5,
Font().apply(feature.fontConfig),
feature.color.toPaint()
)
}
is MapDrawFeature -> {
val offset = feature.position.toOffset()
translate(offset.x, offset.y) {
feature.drawFeature(this)
}
}
is MapFeatureGroup -> {
feature.children.values.forEach {
drawFeature(zoom, it)
}
}
is MapPointsFeature -> {
val points = feature.points.map { it.toOffset() }
drawPoints(
points = points,
color = feature.color,
strokeWidth = feature.stroke,
pointMode = feature.pointMode
)
}
else -> {
logger.error { "Unrecognized feature type: ${feature::class}" }
}
}
}
if (canvasSize != size.toDpSize()) {
canvasSize = size.toDpSize()
logger.debug { "Recalculate canvas. Size: $size" }
}
clipRect {
val tileSize = IntSize(
ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).toPx()).toInt(),
ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).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.toFloat()).roundToPx(),
(canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale.toFloat()).roundToPx()
)
drawImage(
image = image,
dstOffset = offset,
dstSize = tileSize
)
}
features.values.filter { zoom in it.zoomRange }.forEach { feature ->
drawFeature(zoom, feature)
}
}
selectRect?.let { rect ->
drawRect(
color = Color.Blue,
topLeft = rect.topLeft,
size = rect.size,
alpha = 0.5f,
style = Stroke(
width = 2f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
)
}
}
}

View File

@ -1,11 +1,8 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.readBytes import io.ktor.client.statement.readBytes
import io.ktor.utils.io.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -26,23 +23,24 @@ public class OpenStreetMapTileProvider(
private val cacheDirectory: Path, private val cacheDirectory: Path,
parallelism: Int = 4, parallelism: Int = 4,
cacheCapacity: Int = 200, cacheCapacity: Int = 200,
private val osmBaseUrl: String = "https://tile.openstreetmap.org",
) : MapTileProvider { ) : MapTileProvider {
private val semaphore = Semaphore(parallelism) private val semaphore = Semaphore(parallelism)
private val cache = LruCache<TileId, Deferred<ImageBitmap>>(cacheCapacity) private val cache = LruCache<TileId, Deferred<Image>>(cacheCapacity)
private fun TileId.osmUrl() = URL("https://tile.openstreetmap.org/${zoom}/${i}/${j}.png") 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")
/** /**
* Download and cache the tile image * Download and cache the tile image
*/ */
private fun CoroutineScope.downloadImageAsync(id: TileId): Deferred<ImageBitmap> = async(Dispatchers.IO) { private fun CoroutineScope.downloadImageAsync(id: TileId): Deferred<Image> = async(Dispatchers.IO) {
id.cacheFilePath()?.let { path -> id.cacheFilePath()?.let { path ->
if (path.exists()) { if (path.exists()) {
try { try {
return@async Image.makeFromEncoded(path.readBytes()).toComposeImageBitmap() return@async Image.makeFromEncoded(path.readBytes())
} catch (ex: Exception) { } catch (ex: Exception) {
logger.debug { "Failed to load image from $path" } logger.debug { "Failed to load image from $path" }
path.deleteIfExists() path.deleteIfExists()
@ -62,7 +60,7 @@ public class OpenStreetMapTileProvider(
path.writeBytes(byteArray) path.writeBytes(byteArray)
} }
Image.makeFromEncoded(byteArray).toComposeImageBitmap() Image.makeFromEncoded(byteArray)
} }
} }
@ -71,21 +69,17 @@ public class OpenStreetMapTileProvider(
): Deferred<MapTile> { ): Deferred<MapTile> {
//start image download //start image download
val imageDeferred = cache.getOrPut(tileId) { val imageDeferred: Deferred<Image> = cache.getOrPut(tileId) {
downloadImageAsync(tileId) downloadImageAsync(tileId)
} }
//collect the result asynchronously //collect the result asynchronously
return async { return async {
val image: ImageBitmap = try { val image: Image = runCatching { imageDeferred.await() }.onFailure {
imageDeferred.await() logger.error(it) { "Failed to load tile image with id=$tileId" }
} catch (ex: Exception) {
cache.remove(tileId) cache.remove(tileId)
if (ex !is CancellationException) { }.getOrThrow()
logger.error(ex) { "Failed to load tile image with id=$tileId" }
}
throw ex
}
MapTile(tileId, image) MapTile(tileId, image)
} }
} }

View File

@ -0,0 +1,146 @@
package center.sciprog.maps.compose
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import center.sciprog.attributes.z
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.FeatureGroup
import center.sciprog.maps.features.PainterFeature
import center.sciprog.maps.features.drawFeature
import center.sciprog.maps.features.zoomRange
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import mu.KotlinLogging
import org.jetbrains.skia.Image
import org.jetbrains.skia.Paint
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
private fun Color.toPaint(): Paint = Paint().apply {
isAntiAlias = true
color = toArgb()
}
private fun IntRange.intersect(other: IntRange) = max(first, other.first)..min(last, other.last)
private val logger = KotlinLogging.logger("MapView")
/**
* A component that renders map and provides basic map manipulation capabilities
*/
@Composable
public actual fun MapView(
viewScope: MapViewScope,
features: FeatureGroup<Gmc>,
modifier: Modifier,
): Unit = with(viewScope) {
val mapTiles = remember(mapTileProvider) { mutableStateMapOf<TileId, Image>() }
// Load tiles asynchronously
LaunchedEffect(viewPoint, canvasSize) {
with(mapTileProvider) {
val indexRange = 0 until 2.0.pow(intZoom).toInt()
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
for (j in verticalIndices) {
for (i in horizontalIndices) {
val id = TileId(intZoom, i, j)
//ensure that failed tiles do not fail the application
supervisorScope {
//start all
val deferred = loadTileAsync(id)
//wait asynchronously for it to finish
launch {
try {
val tile = deferred.await()
mapTiles[tile.id] = tile.image
} catch (ex: Exception) {
//displaying the error is maps responsibility
logger.error(ex) { "Failed to load tile with id=$id" }
}
}
}
mapTiles.keys.filter {
it.zoom != intZoom || it.j !in verticalIndices || it.i !in horizontalIndices
}.forEach {
mapTiles.remove(it)
}
}
}
}
}
key(viewScope, features) {
val painterCache: Map<PainterFeature<Gmc>, Painter> =
features.features.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.getPainter() }
Canvas(modifier = modifier.mapControls(viewScope, features)) {
if (canvasSize != size.toDpSize()) {
logger.debug { "Recalculate canvas. Size: $size" }
config.onCanvasSizeChange(canvasSize)
canvasSize = size.toDpSize()
}
clipRect {
val tileSize = IntSize(
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt(),
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt()
)
mapTiles.forEach { (id, image) ->
//converting back from tile index to screen offset
val offset = IntOffset(
(canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale).roundToPx(),
(canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale).roundToPx()
)
drawImage(
image = image.toComposeImageBitmap(),
dstOffset = offset,
dstSize = tileSize
)
}
features.featureMap.values.sortedBy { it.z }
.filter { viewPoint.zoom in it.zoomRange }
.forEach { feature ->
drawFeature(viewScope, painterCache, feature)
}
}
selectRect?.let { dpRect ->
val rect = dpRect.toRect()
drawRect(
color = Color.Blue,
topLeft = rect.topLeft,
size = rect.size,
alpha = 0.5f,
style = Stroke(
width = 2f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
)
}
}
}
}

View File

@ -0,0 +1,45 @@
package center.sciprog.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()
@Test
fun testCorrectOsm() = runTest {
val provider = OpenStreetMapTileProvider(HttpClient(CIO), Files.createTempDirectory("mapCache"))
val tileId = TileId(3, 1, 1)
with(provider) {
loadTileAsync(tileId).await()
}
}
@Test
fun testFailedOsm() = runTest {
val provider = OpenStreetMapTileProvider(
HttpClient(CIO),
Files.createTempDirectory("mapCache"),
osmBaseUrl = "https://tile.openstreetmap1.org"
)
val tileId = TileId(3, 1, 1)
supervisorScope {
with(provider) {
val deferred = loadTileAsync(tileId)
assertFails {
deferred.await()
}
}
}
}
}

35
maps-kt-core/README.md Normal file
View File

@ -0,0 +1,35 @@
# Module kmath-core
The core interfaces of KMath.
- [angles and distances](#) : Type-safe angle and distance measurements.
- [ellipsoid](#) : Ellipsoid geometry and distances
- [mercator](#) : Mercator and web-mercator projections
## Artifact:
The Maven coordinates of this project are `center.sciprog:maps-kt-core:0.2.2`.
**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 {
maven("https://repo.kotlin.link")
mavenCentral()
}
dependencies {
implementation("center.sciprog:maps-kt-core:0.2.2")
}
```

View File

@ -0,0 +1,250 @@
public final class center/sciprog/maps/coordinates/Distance : java/lang/Comparable {
public static final field Companion Lcenter/sciprog/maps/coordinates/Distance$Companion;
public static final synthetic fun box-impl (D)Lcenter/sciprog/maps/coordinates/Distance;
public synthetic fun compareTo (Ljava/lang/Object;)I
public fun compareTo-LPoRJxU (D)I
public static fun compareTo-LPoRJxU (DD)I
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (DLjava/lang/Object;)Z
public static final fun equals-impl0 (DD)Z
public final fun getKilometers ()D
public fun hashCode ()I
public static fun hashCode-impl (D)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (D)Ljava/lang/String;
public final synthetic fun unbox-impl ()D
}
public final class center/sciprog/maps/coordinates/Distance$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lcenter/sciprog/maps/coordinates/Distance$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize-h1Ihu8M (Lkotlinx/serialization/encoding/Decoder;)D
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize-xeUJnDc (Lkotlinx/serialization/encoding/Encoder;D)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class center/sciprog/maps/coordinates/Distance$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class center/sciprog/maps/coordinates/DistanceKt {
public static final fun div-XO6jDg8 (DLjava/lang/Number;)D
public static final fun div-yHr8yDM (DD)D
public static final fun getKilometers (Ljava/lang/Number;)D
public static final fun getMeters (Ljava/lang/Number;)D
public static final fun getMeters-LPoRJxU (D)D
public static final fun minus-yHr8yDM (DD)D
public static final fun plus-yHr8yDM (DD)D
public static final fun times-XO6jDg8 (DLjava/lang/Number;)D
}
public final class center/sciprog/maps/coordinates/GeoEllipsoid {
public static final field Companion Lcenter/sciprog/maps/coordinates/GeoEllipsoid$Companion;
public synthetic fun <init> (DDLkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (ILcenter/sciprog/maps/coordinates/Distance;Lcenter/sciprog/maps/coordinates/Distance;DDDDLkotlinx/serialization/internal/SerializationConstructorMarker;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getESquared ()D
public final fun getEccentricity ()D
public final fun getEquatorRadius-zipobaw ()D
public final fun getF ()D
public final fun getInverseF ()D
public final fun getPolarRadius-zipobaw ()D
public static final synthetic fun write$Self (Lcenter/sciprog/maps/coordinates/GeoEllipsoid;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class center/sciprog/maps/coordinates/GeoEllipsoid$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lcenter/sciprog/maps/coordinates/GeoEllipsoid$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcenter/sciprog/maps/coordinates/GeoEllipsoid;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcenter/sciprog/maps/coordinates/GeoEllipsoid;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class center/sciprog/maps/coordinates/GeoEllipsoid$Companion {
public final fun getGRS80 ()Lcenter/sciprog/maps/coordinates/GeoEllipsoid;
public final fun getSphere ()Lcenter/sciprog/maps/coordinates/GeoEllipsoid;
public final fun getWGS84 ()Lcenter/sciprog/maps/coordinates/GeoEllipsoid;
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class center/sciprog/maps/coordinates/GeoEllipsoidKt {
public static final fun reducedRadius (Lcenter/sciprog/maps/coordinates/GeoEllipsoid;Lspace/kscience/kmath/geometry/Angle;)D
}
public final class center/sciprog/maps/coordinates/GeodeticMapCoordinates : space/kscience/kmath/geometry/Vector2D {
public static final field Companion Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates$Companion;
public synthetic fun <init> (ILspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Lcenter/sciprog/maps/coordinates/Distance;Lkotlinx/serialization/internal/SerializationConstructorMarker;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Lcenter/sciprog/maps/coordinates/Distance;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Lcenter/sciprog/maps/coordinates/Distance;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getElevation-irfbOC8 ()Lcenter/sciprog/maps/coordinates/Distance;
public final fun getLatitude ()Lspace/kscience/kmath/geometry/Angle;
public final fun getLongitude ()Lspace/kscience/kmath/geometry/Angle;
public synthetic fun getX ()Ljava/lang/Object;
public fun getX ()Lspace/kscience/kmath/geometry/Angle;
public synthetic fun getY ()Ljava/lang/Object;
public fun getY ()Lspace/kscience/kmath/geometry/Angle;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class center/sciprog/maps/coordinates/GeodeticMapCoordinates$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class center/sciprog/maps/coordinates/GeodeticMapCoordinates$Companion {
public final fun normalized-8x94Bck (Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Lcenter/sciprog/maps/coordinates/Distance;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public static synthetic fun normalized-8x94Bck$default (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates$Companion;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Lcenter/sciprog/maps/coordinates/Distance;ILjava/lang/Object;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public final fun ofDegrees-8x94Bck (DDLcenter/sciprog/maps/coordinates/Distance;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public static synthetic fun ofDegrees-8x94Bck$default (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates$Companion;DDLcenter/sciprog/maps/coordinates/Distance;ILjava/lang/Object;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public final fun ofRadians-8x94Bck (DDLcenter/sciprog/maps/coordinates/Distance;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public static synthetic fun ofRadians-8x94Bck$default (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates$Companion;DDLcenter/sciprog/maps/coordinates/Distance;ILjava/lang/Object;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class center/sciprog/maps/coordinates/GmcCurve {
public synthetic fun <init> (Lcenter/sciprog/maps/coordinates/GmcPose;Lcenter/sciprog/maps/coordinates/GmcPose;DLkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getBackward ()Lcenter/sciprog/maps/coordinates/GmcPose;
public final fun getDistance-zipobaw ()D
public final fun getForward ()Lcenter/sciprog/maps/coordinates/GmcPose;
public fun toString ()Ljava/lang/String;
}
public final class center/sciprog/maps/coordinates/GmcCurveKt {
public static final fun contains (Lkotlin/ranges/ClosedRange;Lspace/kscience/kmath/geometry/Angle;)Z
public static final fun curveBetween (Lcenter/sciprog/maps/coordinates/GeoEllipsoid;Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;D)Lcenter/sciprog/maps/coordinates/GmcCurve;
public static synthetic fun curveBetween$default (Lcenter/sciprog/maps/coordinates/GeoEllipsoid;Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;DILjava/lang/Object;)Lcenter/sciprog/maps/coordinates/GmcCurve;
public static final fun curveInDirection-EeP4nfk (Lcenter/sciprog/maps/coordinates/GeoEllipsoid;Lcenter/sciprog/maps/coordinates/GmcPose;DD)Lcenter/sciprog/maps/coordinates/GmcCurve;
public static synthetic fun curveInDirection-EeP4nfk$default (Lcenter/sciprog/maps/coordinates/GeoEllipsoid;Lcenter/sciprog/maps/coordinates/GmcPose;DDILjava/lang/Object;)Lcenter/sciprog/maps/coordinates/GmcCurve;
public static final fun meridianCurve-L4GImx8 (Lcenter/sciprog/maps/coordinates/GeoEllipsoid;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;D)Lcenter/sciprog/maps/coordinates/GmcCurve;
public static synthetic fun meridianCurve-L4GImx8$default (Lcenter/sciprog/maps/coordinates/GeoEllipsoid;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;DILjava/lang/Object;)Lcenter/sciprog/maps/coordinates/GmcCurve;
public static final fun parallelCurve (Lcenter/sciprog/maps/coordinates/GeoEllipsoid;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;)Lcenter/sciprog/maps/coordinates/GmcCurve;
public static final fun reversed (Lcenter/sciprog/maps/coordinates/GmcCurve;)Lcenter/sciprog/maps/coordinates/GmcCurve;
}
public final class center/sciprog/maps/coordinates/GmcPose {
public static final field Companion Lcenter/sciprog/maps/coordinates/GmcPose$Companion;
public synthetic fun <init> (ILcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;Lspace/kscience/kmath/geometry/Angle;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;Lspace/kscience/kmath/geometry/Angle;)V
public final fun component1 ()Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public final fun component2 ()Lspace/kscience/kmath/geometry/Angle;
public final fun copy (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;Lspace/kscience/kmath/geometry/Angle;)Lcenter/sciprog/maps/coordinates/GmcPose;
public static synthetic fun copy$default (Lcenter/sciprog/maps/coordinates/GmcPose;Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;Lspace/kscience/kmath/geometry/Angle;ILjava/lang/Object;)Lcenter/sciprog/maps/coordinates/GmcPose;
public fun equals (Ljava/lang/Object;)Z
public final fun getBearing ()Lspace/kscience/kmath/geometry/Angle;
public final fun getCoordinates ()Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public final fun getLatitude ()Lspace/kscience/kmath/geometry/Angle;
public final fun getLongitude ()Lspace/kscience/kmath/geometry/Angle;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lcenter/sciprog/maps/coordinates/GmcPose;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class center/sciprog/maps/coordinates/GmcPose$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lcenter/sciprog/maps/coordinates/GmcPose$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcenter/sciprog/maps/coordinates/GmcPose;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcenter/sciprog/maps/coordinates/GmcPose;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class center/sciprog/maps/coordinates/GmcPose$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class center/sciprog/maps/coordinates/GmcPoseKt {
public static final fun reversed (Lcenter/sciprog/maps/coordinates/GmcPose;)Lcenter/sciprog/maps/coordinates/GmcPose;
}
public abstract interface class center/sciprog/maps/coordinates/MapProjection {
public static final field Companion Lcenter/sciprog/maps/coordinates/MapProjection$Companion;
public abstract fun toGeodetic (Ljava/lang/Object;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public abstract fun toProjection (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;)Ljava/lang/Object;
}
public final class center/sciprog/maps/coordinates/MapProjection$Companion {
public final fun getEpsg3857 ()Lcenter/sciprog/maps/coordinates/MercatorProjection;
}
public class center/sciprog/maps/coordinates/MercatorProjection : center/sciprog/maps/coordinates/MapProjection {
public static final field Companion Lcenter/sciprog/maps/coordinates/MercatorProjection$Companion;
public fun <init> ()V
public synthetic fun <init> (ILspace/kscience/kmath/geometry/Angle;Lcenter/sciprog/maps/coordinates/GeoEllipsoid;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Lspace/kscience/kmath/geometry/Angle;Lcenter/sciprog/maps/coordinates/GeoEllipsoid;)V
public synthetic fun <init> (Lspace/kscience/kmath/geometry/Angle;Lcenter/sciprog/maps/coordinates/GeoEllipsoid;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getBaseLongitude ()Lspace/kscience/kmath/geometry/Angle;
public final fun getEllipsoid ()Lcenter/sciprog/maps/coordinates/GeoEllipsoid;
public fun toGeodetic (Lcenter/sciprog/maps/coordinates/ProjectionCoordinates;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public synthetic fun toGeodetic (Ljava/lang/Object;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public fun toProjection (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;)Lcenter/sciprog/maps/coordinates/ProjectionCoordinates;
public synthetic fun toProjection (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;)Ljava/lang/Object;
public static final synthetic fun write$Self (Lcenter/sciprog/maps/coordinates/MercatorProjection;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class center/sciprog/maps/coordinates/MercatorProjection$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lcenter/sciprog/maps/coordinates/MercatorProjection$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcenter/sciprog/maps/coordinates/MercatorProjection;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcenter/sciprog/maps/coordinates/MercatorProjection;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class center/sciprog/maps/coordinates/MercatorProjection$Companion {
public final fun getMAXIMUM_LATITUDE ()Lspace/kscience/kmath/geometry/Angle;
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class center/sciprog/maps/coordinates/ProjectionCoordinates {
public synthetic fun <init> (DDLkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1-zipobaw ()D
public final fun component2-zipobaw ()D
public final fun copy-yHr8yDM (DD)Lcenter/sciprog/maps/coordinates/ProjectionCoordinates;
public static synthetic fun copy-yHr8yDM$default (Lcenter/sciprog/maps/coordinates/ProjectionCoordinates;DDILjava/lang/Object;)Lcenter/sciprog/maps/coordinates/ProjectionCoordinates;
public fun equals (Ljava/lang/Object;)Z
public final fun getX-zipobaw ()D
public final fun getY-zipobaw ()D
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class center/sciprog/maps/coordinates/WebMercatorCoordinates {
public fun <init> (IFF)V
public final fun component1 ()I
public final fun component2 ()F
public final fun component3 ()F
public final fun copy (IFF)Lcenter/sciprog/maps/coordinates/WebMercatorCoordinates;
public static synthetic fun copy$default (Lcenter/sciprog/maps/coordinates/WebMercatorCoordinates;IFFILjava/lang/Object;)Lcenter/sciprog/maps/coordinates/WebMercatorCoordinates;
public fun equals (Ljava/lang/Object;)Z
public final fun getX ()F
public final fun getY ()F
public final fun getZoom ()I
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class center/sciprog/maps/coordinates/WebMercatorProjection {
public static final field INSTANCE Lcenter/sciprog/maps/coordinates/WebMercatorProjection;
public final fun scaleFactor (F)F
public final fun toGeodetic (Lcenter/sciprog/maps/coordinates/WebMercatorCoordinates;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public final fun toMercator (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;I)Lcenter/sciprog/maps/coordinates/WebMercatorCoordinates;
}

View File

@ -1,26 +1,34 @@
plugins { plugins {
kotlin("multiplatform") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
val ktorVersion: String by rootProject.extra val kmathVersion: String by rootProject.extra
kotlin { kscience{
explicitApi = org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode.Warning jvm()
jvm { js()
compilations.all { useSerialization()
kotlinOptions.jvmTarget = "11"
}
}
js(IR) {
browser()
}
sourceSets{
commonTest{
dependencies{ dependencies{
implementation(kotlin("test")) api(projects.trajectoryKt)
}
} }
} }
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",
) { "Type-safe angle and distance measurements." }
feature(
id = "ellipsoid",
) { "Ellipsoid geometry and distances" }
feature(
id = "mercator",
) { "Mercator and web-mercator projections" }
} }

View File

@ -0,0 +1,7 @@
# Module kmath-core
The core interfaces of KMath.
${features}
${artifact}

View File

@ -1,12 +1,17 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
@Serializable
@JvmInline @JvmInline
public value class Distance(public val kilometers: Double) : Comparable<Distance> { public value class Distance internal constructor(public val kilometers: Double) : Comparable<Distance> {
override fun compareTo(other: Distance): Int = this.kilometers.compareTo(other.kilometers) override fun compareTo(other: Distance): Int = this.kilometers.compareTo(other.kilometers)
} }
public val Number.kilometers: Distance get() = Distance(toDouble())
public val Number.meters: Distance get() = Distance(toDouble() / 1000)
public operator fun Distance.div(other: Distance): Double = kilometers / other.kilometers public operator fun Distance.div(other: Distance): Double = kilometers / other.kilometers
public operator fun Distance.plus(other: Distance): Distance = Distance(kilometers + other.kilometers) public operator fun Distance.plus(other: Distance): Distance = Distance(kilometers + other.kilometers)

View File

@ -1,10 +1,12 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import kotlin.math.acos import kotlinx.serialization.Serializable
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.tan
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt import kotlin.math.sqrt
@Serializable
public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRadius: Distance) { public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRadius: Distance) {
/** /**
@ -17,8 +19,13 @@ public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRa
*/ */
public val inverseF: Double = equatorRadius.kilometers / (equatorRadius.kilometers - polarRadius.kilometers) public val inverseF: Double = equatorRadius.kilometers / (equatorRadius.kilometers - polarRadius.kilometers)
/**
* Eccentricity squared
*/
public val eSquared: Double = 2 * f - f * f public val eSquared: Double = 2 * f - f * f
public val eccentricity: Double = sqrt(eSquared)
public companion object { public companion object {
public val WGS84: GeoEllipsoid = GeoEllipsoid( public val WGS84: GeoEllipsoid = GeoEllipsoid(
@ -48,7 +55,7 @@ public class GeoEllipsoid(public val equatorRadius: Distance, public val polarRa
/** /**
* A radius of circle normal to the axis of the ellipsoid at given latitude * A radius of circle normal to the axis of the ellipsoid at given latitude
*/ */
internal fun GeoEllipsoid.reducedRadius(latitude: Angle): Distance { public fun GeoEllipsoid.reducedRadius(latitude: Angle): Distance {
val reducedLatitudeTan = (1 - f) * tan(latitude) val reducedLatitudeTan = (1 - f) * tan(latitude)
return equatorRadius / sqrt(1.0 + reducedLatitudeTan.pow(2)) return equatorRadius / sqrt(1.0 + reducedLatitudeTan.pow(2))
} }

View File

@ -1,19 +1,31 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import kotlin.math.PI import kotlinx.serialization.Serializable
import space.kscience.kmath.geometry.*
/** /**
* Geodetic coordinated * Geodetic coordinated
*
* @param elevation is optional
*/ */
@Serializable
public class GeodeticMapCoordinates( public class GeodeticMapCoordinates(
public val latitude: Angle, public val latitude: Angle,
longitude: Angle, public val longitude: Angle,
) { public val elevation: Distance? = null,
public val longitude: Radians = longitude.radians.value.rem(PI / 2).radians ) : Vector2D<Angle> {
init { init {
require(latitude.radians.value in (-PI / 2)..(PI / 2)) { "Latitude $latitude is not in (-PI/2)..(PI/2)" } require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) {
"Latitude $latitude is not in (-PI/2)..(PI/2)"
} }
require(longitude in (-Angle.pi..Angle.pi)) {
"Longitude $longitude is not in (-PI..PI) range"
}
}
override val x: Angle get() = longitude
override val y: Angle get() = latitude
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -21,10 +33,7 @@ public class GeodeticMapCoordinates(
other as GeodeticMapCoordinates other as GeodeticMapCoordinates
if (latitude != other.latitude) return false return latitude == other.latitude && longitude == other.longitude
if (longitude != other.longitude) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -34,19 +43,34 @@ public class GeodeticMapCoordinates(
} }
override fun toString(): String { override fun toString(): String {
return "GMC(latitude=${latitude.degrees.value} deg, longitude=${longitude.degrees.value} deg)" return "GMC(latitude=${latitude.degrees} deg, longitude=${longitude.degrees} deg)"
} }
public companion object { public companion object {
public fun ofRadians(latitude: Double, longitude: Double): GeodeticMapCoordinates = public fun normalized(
GeodeticMapCoordinates(latitude.radians, longitude.radians) latitude: Angle,
longitude: Angle,
elevation: Distance? = null,
): GeodeticMapCoordinates = GeodeticMapCoordinates(
latitude.coerceIn(-Angle.piDiv2..Angle.piDiv2), longitude.normalized(Angle.zero), elevation
)
public fun ofDegrees(latitude: Double, longitude: Double): GeodeticMapCoordinates = public fun ofRadians(
GeodeticMapCoordinates(latitude.degrees.radians, longitude.degrees.radians) latitude: Double,
longitude: Double,
elevation: Distance? = null,
): GeodeticMapCoordinates = normalized(latitude.radians, longitude.radians, elevation)
public fun ofDegrees(
latitude: Double,
longitude: Double,
elevation: Distance? = null,
): GeodeticMapCoordinates = normalized(latitude.degrees, longitude.degrees, elevation)
} }
} }
/** /**
* Short name for GeodeticMapCoordinates * Short name for GeodeticMapCoordinates
*/ */

View File

@ -1,8 +1,6 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import center.sciprog.maps.coordinates.Angle.Companion.pi import space.kscience.kmath.geometry.*
import center.sciprog.maps.coordinates.Angle.Companion.piDiv2
import center.sciprog.maps.coordinates.Angle.Companion.zero
import kotlin.math.* import kotlin.math.*
/** /**
@ -10,13 +8,22 @@ import kotlin.math.*
* @param forward coordinate of a start point with forward direction * @param forward coordinate of a start point with forward direction
* @param backward coordinate of an end point with backward direction * @param backward coordinate of an end point with backward direction
*/ */
public class GmcCurve internal constructor( public class GmcCurve(
public val forward: GmcPose, public val forward: GmcPose,
public val backward: GmcPose, public val backward: GmcPose,
public val distance: Distance, public val distance: Distance,
) ){
override fun toString(): String {
return "GmcCurve(from: ${forward.coordinates}, to: ${backward.coordinates})"
}
}
public fun GmcCurve.reversed(): GmcCurve = GmcCurve(backward, forward, distance) public operator fun ClosedRange<Radians>.contains(angle: Angle): Boolean = contains(angle.toRadians())
/**
* Reverse direction and order of ends
*/
public fun GmcCurve.reversed(): GmcCurve = GmcCurve(backward.reversed(), forward.reversed(), distance)
/** /**
* Compute a curve alongside a meridian * Compute a curve alongside a meridian
@ -27,8 +34,8 @@ public fun GeoEllipsoid.meridianCurve(
toLatitude: Angle, toLatitude: Angle,
step: Radians = 0.015.radians, step: Radians = 0.015.radians,
): GmcCurve { ): GmcCurve {
require(fromLatitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" } require(fromLatitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
require(toLatitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" } require(toLatitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
fun smallDistance(from: Radians, to: Radians): Distance = equatorRadius * fun smallDistance(from: Radians, to: Radians): Distance = equatorRadius *
(1 - eSquared) * (1 - eSquared) *
@ -41,14 +48,14 @@ public fun GeoEllipsoid.meridianCurve(
val integrateTo: Radians val integrateTo: Radians
if (up) { if (up) {
integrateFrom = fromLatitude.radians integrateFrom = fromLatitude.toRadians()
integrateTo = toLatitude.radians integrateTo = toLatitude.toRadians()
} else { } else {
integrateTo = fromLatitude.radians integrateTo = fromLatitude.toRadians()
integrateFrom = toLatitude.radians integrateFrom = toLatitude.toRadians()
} }
var current = integrateFrom var current: Radians = integrateFrom
var s = Distance(0.0) var s = Distance(0.0)
while (current < integrateTo) { while (current < integrateTo) {
val next = minOf(current + step, integrateTo) val next = minOf(current + step, integrateTo)
@ -57,8 +64,8 @@ public fun GeoEllipsoid.meridianCurve(
} }
return GmcCurve( return GmcCurve(
forward = GmcPose(Gmc(fromLatitude, longitude), if (up) zero else pi), forward = GmcPose(Gmc.normalized(fromLatitude, longitude), if (up) Angle.zero else Angle.pi),
backward = GmcPose(Gmc(toLatitude, longitude), if (up) pi else zero), backward = GmcPose(Gmc.normalized(toLatitude, longitude), if (up) Angle.pi else Angle.zero),
distance = s distance = s
) )
} }
@ -67,12 +74,12 @@ public fun GeoEllipsoid.meridianCurve(
* Compute a curve alongside a parallel * Compute a curve alongside a parallel
*/ */
public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toLongitude: Angle): GmcCurve { public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toLongitude: Angle): GmcCurve {
require(latitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" } require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
val right = toLongitude > fromLongitude val right = toLongitude > fromLongitude
return GmcCurve( return GmcCurve(
forward = GmcPose(Gmc(latitude, fromLongitude), if (right) piDiv2.radians else -piDiv2.radians), forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) Angle.piDiv2 else -Angle.piDiv2),
backward = GmcPose(Gmc(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians), backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -Angle.piDiv2 else Angle.piDiv2),
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians.value) distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians)
) )
} }
@ -186,7 +193,7 @@ public fun GeoEllipsoid.curveInDirection(
val L = lambda - (1 - C) * f * sinAlpha * val L = lambda - (1 - C) * f * sinAlpha *
(sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2))) (sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)))
val endPoint = Gmc(phi2, L.radians) val endPoint = Gmc.normalized(phi2, start.longitude + L.radians)
// eq. 12 // eq. 12
@ -222,6 +229,8 @@ public fun GeoEllipsoid.curveBetween(start: Gmc, end: Gmc, precision: Double = 1
val a = equatorRadius val a = equatorRadius
val b = polarRadius val b = polarRadius
if(start == end) error("Can't compute a curve because start and end coincide at $start")
// get parameters as radians // get parameters as radians
val phi1 = start.latitude val phi1 = start.latitude
val lambda1 = start.longitude val lambda1 = start.longitude
@ -232,7 +241,7 @@ public fun GeoEllipsoid.curveBetween(start: Gmc, end: Gmc, precision: Double = 1
val a2 = a.kilometers * a.kilometers val a2 = a.kilometers * a.kilometers
val b2 = b.kilometers * b.kilometers val b2 = b.kilometers * b.kilometers
val a2b2b2 = (a2 - b2) / b2 val a2b2b2 = (a2 - b2) / b2
val omega: Radians = lambda2 - lambda1 val omega: Angle = lambda2 - lambda1
val tanphi1: Double = tan(phi1) val tanphi1: Double = tan(phi1)
val tanU1 = (1.0 - f) * tanphi1 val tanU1 = (1.0 - f) * tanphi1
val U1: Double = atan(tanU1) val U1: Double = atan(tanU1)
@ -249,14 +258,14 @@ public fun GeoEllipsoid.curveBetween(start: Gmc, end: Gmc, precision: Double = 1
val cosU1cosU2 = cosU1 * cosU2 val cosU1cosU2 = cosU1 * cosU2
// eq. 13 // eq. 13
var lambda = omega var lambda: Angle = omega
// intermediates we'll need to compute 's' // intermediates we'll need to compute 's'
var A = 0.0 var A = 0.0
var sigma = 0.0 var sigma = 0.0
var deltasigma = 0.0 var deltasigma = 0.0
var lambda0: Radians var lambda0: Angle
var converged = false var converged = false
for (i in 0..19) { for (i in 0..19) {
lambda0 = lambda lambda0 = lambda
@ -320,14 +329,13 @@ public fun GeoEllipsoid.curveBetween(start: Gmc, end: Gmc, precision: Double = 1
// didn't converge? must be N/S // didn't converge? must be N/S
if (!converged) { if (!converged) {
if (phi1 > phi2) { if (phi1 > phi2) {
alpha1 = pi.radians alpha1 = Angle.pi.toRadians()
alpha2 = 0.0.radians alpha2 = 0.0.radians
} else if (phi1 < phi2) { } else if (phi1 < phi2) {
alpha1 = 0.0.radians alpha1 = 0.0.radians
alpha2 = pi.radians alpha2 = Angle.pi.toRadians()
} else { } else {
alpha1 = Double.NaN.radians error("Start and end point coinside.")
alpha2 = Double.NaN.radians
} }
} else { } else {
// eq. 20 // eq. 20
@ -340,7 +348,7 @@ public fun GeoEllipsoid.curveBetween(start: Gmc, end: Gmc, precision: Double = 1
alpha2 = atan2( alpha2 = atan2(
cosU1 * sin(lambda), cosU1 * sin(lambda),
-sinU1cosU2 + cosU1sinU2 * cos(lambda) -sinU1cosU2 + cosU1sinU2 * cos(lambda)
).radians + pi ).radians + Angle.pi
} }
return GmcCurve( return GmcCurve(
GmcPose(start, alpha1), GmcPose(start, alpha1),

View File

@ -1,8 +1,13 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import kotlinx.serialization.Serializable
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.normalized
/** /**
* A coordinate-bearing pair * A coordinate-bearing pair
*/ */
@Serializable
public data class GmcPose(val coordinates: GeodeticMapCoordinates, val bearing: Angle) { public data class GmcPose(val coordinates: GeodeticMapCoordinates, val bearing: Angle) {
val latitude: Angle get() = coordinates.latitude val latitude: Angle get() = coordinates.latitude
val longitude: Angle get() = coordinates.longitude val longitude: Angle get() = coordinates.longitude

View File

@ -1,116 +0,0 @@
package center.sciprog.maps.coordinates
/**
* A section of the map between two parallels and two meridians. The figure represents a square in a Mercator projection.
* Params are two opposing "corners" of quasi-square.
*
* Note that this is a rectangle only on a Mercator projection.
*/
public data class GmcRectangle(
public val a: GeodeticMapCoordinates,
public val b: GeodeticMapCoordinates,
) {
public companion object {
/**
* A quasi-square section.
*/
public fun square(
center: GeodeticMapCoordinates,
height: Angle,
width: Angle,
): GmcRectangle {
val a = GeodeticMapCoordinates(
center.latitude - (height / 2),
center.longitude - (width / 2)
)
val b = GeodeticMapCoordinates(
center.latitude + (height / 2),
center.longitude + (width / 2)
)
return GmcRectangle(a, b)
}
/**
* A quasi-square section. Note that latitudinal distance could be imprecise for large distances
*/
public fun square(
center: GeodeticMapCoordinates,
height: Distance,
width: Distance,
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
): GmcRectangle {
val reducedRadius = ellipsoid.reducedRadius(center.latitude)
return square(center, (height / ellipsoid.polarRadius).radians, (width / reducedRadius).radians)
}
}
}
public val GmcRectangle.center: GeodeticMapCoordinates
get() = GeodeticMapCoordinates(
(a.latitude + b.latitude) / 2,
(a.longitude + b.longitude) / 2
)
/**
* Minimum longitude
*/
public val GmcRectangle.left: Angle get() = minOf(a.longitude, b.longitude)
/**
* maximum longitude
*/
public val GmcRectangle.right: Angle get() = maxOf(a.longitude, b.longitude)
/**
* Maximum latitude
*/
public val GmcRectangle.top: Angle get() = maxOf(a.latitude, b.latitude)
/**
* Minimum latitude
*/
public val GmcRectangle.bottom: Angle get() = minOf(a.latitude, b.latitude)
public val GmcRectangle.longitudeDelta: Angle get() = abs(a.longitude - b.longitude)
public val GmcRectangle.latitudeDelta: Angle get() = abs(a.latitude - b.latitude)
public val GmcRectangle.topLeft: GeodeticMapCoordinates get() = GeodeticMapCoordinates(top, left)
public val GmcRectangle.bottomRight: GeodeticMapCoordinates get() = GeodeticMapCoordinates(bottom, right)
//public fun GmcRectangle.enlarge(
// top: Distance,
// bottom: Distance = top,
// left: Distance = top,
// right: Distance = left,
//): GmcRectangle {
//
//}
//
//public fun GmcRectangle.enlarge(
// top: Angle,
// bottom: Angle = top,
// left: Angle = top,
// right: Angle = left,
//): GmcRectangle {
//
//}
/**
* Check if coordinate is inside the box
*/
public operator fun GmcRectangle.contains(coordinate: Gmc): Boolean =
coordinate.latitude in (bottom..top) && coordinate.longitude in (left..right)
/**
* Compute a minimal bounding box including all given boxes. Return null if collection is empty
*/
public fun Collection<GmcRectangle>.wrapAll(): GmcRectangle? {
if (isEmpty()) return null
//TODO optimize computation
val minLat = minOf { it.bottom }
val maxLat = maxOf { it.top }
val minLong = minOf { it.left }
val maxLong = maxOf { it.right }
return GmcRectangle(GeodeticMapCoordinates(minLat, minLong), GeodeticMapCoordinates(maxLat, maxLong))
}

View File

@ -1,38 +0,0 @@
package center.sciprog.maps.coordinates
import kotlin.math.pow
/**
* Observable position on the map. Includes observation coordinate and [zoom] factor
*/
public data class MapViewPoint(
val focus: GeodeticMapCoordinates,
val zoom: Double,
) {
val scaleFactor: Double by lazy { WebMercatorProjection.scaleFactor(zoom) }
}
public fun MapViewPoint.move(delta: GeodeticMapCoordinates): MapViewPoint {
val newCoordinates = GeodeticMapCoordinates(
(focus.latitude + delta.latitude).coerceIn(
-MercatorProjection.MAXIMUM_LATITUDE,
MercatorProjection.MAXIMUM_LATITUDE
),
focus.longitude + delta.longitude
)
return MapViewPoint(newCoordinates, zoom)
}
public fun MapViewPoint.zoom(
zoomDelta: Double,
invariant: GeodeticMapCoordinates = focus,
): MapViewPoint = if (invariant == focus) {
copy(zoom = (zoom + zoomDelta).coerceIn(2.0, 18.0))
} else {
val difScale = (1 - 2.0.pow(-zoomDelta))
val newCenter = GeodeticMapCoordinates(
focus.latitude + (invariant.latitude - focus.latitude) * difScale,
focus.longitude + (invariant.longitude - focus.longitude) * difScale
)
MapViewPoint(newCenter, (zoom + zoomDelta).coerceIn(2.0, 18.0))
}

View File

@ -5,54 +5,92 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import center.sciprog.maps.coordinates.Angle.Companion.pi import kotlinx.serialization.Serializable
import kotlin.math.atan import space.kscience.kmath.geometry.*
import kotlin.math.ln import kotlin.math.*
import kotlin.math.sinh
public data class ProjectionCoordinates(val x: Distance, val y: Distance)
/**
* @param T the type of projection coordinates
*/
public interface MapProjection<T : Any> {
public fun toGeodetic(pc: T): GeodeticMapCoordinates
public fun toProjection(gmc: GeodeticMapCoordinates): T
public companion object {
public val epsg3857: MercatorProjection = MercatorProjection()
}
}
public data class MercatorCoordinates(val x: Distance, val y: Distance)
/** /**
* @param baseLongitude the longitude offset in radians * @param baseLongitude the longitude offset in radians
* @param radius the average radius of the Earth * @param ellipsoid - a [GeoEllipsoid] to be used for conversion
* @param correctedRadius optional radius correction to account for ellipsoid model
*/ */
@Serializable
public open class MercatorProjection( public open class MercatorProjection(
public val baseLongitude: Angle = Angle.zero, public val baseLongitude: Angle = Angle.zero,
protected val radius: Distance = DEFAULT_EARTH_RADIUS, public val ellipsoid: GeoEllipsoid = GeoEllipsoid.sphere,
private val correctedRadius: ((GeodeticMapCoordinates) -> Distance)? = null, ) : MapProjection<ProjectionCoordinates> {
) {
public fun toGeodetic(mc: MercatorCoordinates): GeodeticMapCoordinates {
val res = GeodeticMapCoordinates.ofRadians( /**
atan(sinh(mc.y / radius)), * Taken from https://github.com/geotools/geotools/blob/main/modules/library/referencing/src/main/java/org/geotools/referencing/operation/projection/Mercator.java#L164
baseLongitude.radians.value + (mc.x / radius), */
) private fun cphi2(ts: Double): Double {
return if (correctedRadius != null) { val eccnth: Double = 0.5 * ellipsoid.eccentricity
val r = correctedRadius.invoke(res) var phi: Double = PI / 2 - 2.0 * atan(ts)
for (i in 0 until 15) {
val con: Double = ellipsoid.eccentricity * sin(phi)
val dphi: Double = PI / 2 - 2.0 * atan(ts * ((1 - con) / (1 + con)).pow(eccnth)) - phi
phi += dphi
if (abs(dphi) <= 1e-10) {
return phi
}
}
error("Inverse mercator projection transformation failed to converge")
}
override fun toGeodetic(pc: ProjectionCoordinates): GeodeticMapCoordinates {
return if (ellipsoid === GeoEllipsoid.sphere) {
GeodeticMapCoordinates.ofRadians( GeodeticMapCoordinates.ofRadians(
atan(sinh(mc.y / r)), atan(sinh(pc.y / ellipsoid.equatorRadius)),
baseLongitude.radians.value + mc.x / r, baseLongitude.radians + (pc.x / ellipsoid.equatorRadius),
) )
} else { } else {
res GeodeticMapCoordinates.ofRadians(
cphi2(exp(-(pc.y / ellipsoid.equatorRadius))),
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius)
)
} }
} }
/** /**
* https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas * https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas
*/ */
public fun toMercator(gmc: GeodeticMapCoordinates): MercatorCoordinates { override fun toProjection(gmc: GeodeticMapCoordinates): ProjectionCoordinates {
require(abs(gmc.latitude) <= MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" } require(abs(gmc.latitude) <= MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" }
val r: Distance = correctedRadius?.invoke(gmc) ?: radius val e = sqrt(ellipsoid.eSquared)
return MercatorCoordinates(
x = r * (gmc.longitude - baseLongitude).radians.value, return if (ellipsoid === GeoEllipsoid.sphere) {
y = r * ln(tan(pi / 4 + gmc.latitude / 2)) ProjectionCoordinates(
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians,
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,
y = ellipsoid.equatorRadius * ln(
tan(Angle.pi / 4 + gmc.latitude / 2) * ((1 - e * sinPhi) / (1 + e * sinPhi)).pow(e / 2)
)
) )
} }
}
public companion object : MercatorProjection(Angle.zero, Distance(6378.137)) { public companion object {
public val MAXIMUM_LATITUDE: Angle = 85.05113.degrees public val MAXIMUM_LATITUDE: Angle = 85.05113.degrees
public val DEFAULT_EARTH_RADIUS: Distance = radius
} }
} }

View File

@ -5,19 +5,21 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import space.kscience.kmath.geometry.abs
import space.kscience.kmath.geometry.radians
import kotlin.math.* import kotlin.math.*
public data class WebMercatorCoordinates(val zoom: Int, val x: Double, val y: Double) public data class WebMercatorCoordinates(val zoom: Int, val x: Float, val y: Float)
public object WebMercatorProjection { public object WebMercatorProjection {
/** /**
* Compute radians to projection coordinates ratio for given [zoom] factor * Compute radians to projection coordinates ratio for given [zoom] factor
*/ */
public fun scaleFactor(zoom: Double): Double = 256.0 / 2 / PI * 2.0.pow(zoom) public fun scaleFactor(zoom: Float): Float = (256.0 / 2 / PI * 2f.pow(zoom)).toFloat()
public fun toGeodetic(mercator: WebMercatorCoordinates): GeodeticMapCoordinates { public fun toGeodetic(mercator: WebMercatorCoordinates): GeodeticMapCoordinates {
val scaleFactor = scaleFactor(mercator.zoom.toDouble()) val scaleFactor = scaleFactor(mercator.zoom.toFloat())
val longitude = mercator.x / scaleFactor - PI val longitude = mercator.x / scaleFactor - PI
val latitude = (atan(exp(PI - mercator.y / scaleFactor)) - PI / 4) * 2 val latitude = (atan(exp(PI - mercator.y / scaleFactor)) - PI / 4) * 2
return GeodeticMapCoordinates.ofRadians(latitude, longitude) return GeodeticMapCoordinates.ofRadians(latitude, longitude)
@ -25,15 +27,17 @@ public object WebMercatorProjection {
/** /**
* https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas * https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas
*
* return null if gmc is outside of possible coordinate scope for WebMercator
*/ */
public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Int): WebMercatorCoordinates { public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Int): WebMercatorCoordinates? {
require(abs(gmc.latitude) <= MercatorProjection.MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" } if (abs(gmc.latitude) > MercatorProjection.MAXIMUM_LATITUDE) return null
val scaleFactor = scaleFactor(zoom.toDouble()) val scaleFactor = scaleFactor(zoom.toFloat())
return WebMercatorCoordinates( return WebMercatorCoordinates(
zoom = zoom, zoom = zoom,
x = scaleFactor * (gmc.longitude.radians.value + PI), x = scaleFactor * (gmc.longitude.radians + PI).toFloat(),
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians.value / 2))) y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians / 2))).toFloat()
) )
} }

View File

@ -1,92 +0,0 @@
/*
* Copyright 2018-2021 KMath contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package center.sciprog.maps.coordinates
import kotlin.jvm.JvmInline
import kotlin.math.PI
// Taken from KMath dev version, to be used directly in the future
public sealed interface Angle : Comparable<Angle> {
public val radians: Radians
public val degrees: Degrees
public operator fun plus(other: Angle): Angle
public operator fun minus(other: Angle): Angle
public operator fun times(other: Number): Angle
public operator fun div(other: Number): Angle
public operator fun div(other: Angle): Double
public operator fun unaryMinus(): Angle
public companion object {
public val zero: Angle = 0.radians
public val pi: Angle = PI.radians
public val piTimes2: Angle = (2 * PI).radians
public val piDiv2: Angle = (PI / 2).radians
}
}
/**
* Type safe radians
*/
@JvmInline
public value class Radians(public val value: Double) : Angle {
override val radians: Radians
get() = this
override val degrees: Degrees
get() = Degrees(value * 180 / PI)
public override fun plus(other: Angle): Radians = Radians(value + other.radians.value)
public override fun minus(other: Angle): Radians = Radians(value - other.radians.value)
public override fun times(other: Number): Radians = Radians(value * other.toDouble())
public override fun div(other: Number): Radians = Radians(value / other.toDouble())
override fun div(other: Angle): Double = value / other.radians.value
public override fun unaryMinus(): Radians = Radians(-value)
override fun compareTo(other: Angle): Int = value.compareTo(other.radians.value)
}
public fun sin(angle: Angle): Double = kotlin.math.sin(angle.radians.value)
public fun cos(angle: Angle): Double = kotlin.math.cos(angle.radians.value)
public fun tan(angle: Angle): Double = kotlin.math.tan(angle.radians.value)
public val Number.radians: Radians get() = Radians(toDouble())
/**
* Type safe degrees
*/
@JvmInline
public value class Degrees(public val value: Double) : Angle {
override val radians: Radians
get() = Radians(value * PI / 180)
override val degrees: Degrees
get() = this
public override fun plus(other: Angle): Degrees = Degrees(value + other.degrees.value)
public override fun minus(other: Angle): Degrees = Degrees(value - other.degrees.value)
public override fun times(other: Number): Degrees = Degrees(value * other.toDouble())
public override fun div(other: Number): Degrees = Degrees(value / other.toDouble())
override fun div(other: Angle): Double = value / other.degrees.value
public override fun unaryMinus(): Degrees = Degrees(-value)
override fun compareTo(other: Angle): Int = value.compareTo(other.degrees.value)
}
public val Number.degrees: Degrees get() = Degrees(toDouble())
/**
* Normalized angle to (0, 2PI) for radians or (0, 360) for degrees.
*/
public fun Angle.normalized(): Angle = when (this) {
is Degrees -> (value + 180.0).rem(360.0).degrees
is Radians -> (value + PI).rem(PI * 2).radians
}
public fun abs(angle: Angle): Angle = if (angle < Angle.zero) -angle else angle

View File

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

View File

@ -0,0 +1,29 @@
package center.sciprog.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 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)
}
@Test
fun ellipseForwardBackward(){
val moscow = Gmc.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)
}
}

View File

@ -0,0 +1,32 @@
# Module maps-kt-features
## Usage
## Artifact:
The Maven coordinates of this project are `center.sciprog:maps-kt-features:0.2.2`.
**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 {
maven("https://repo.kotlin.link")
mavenCentral()
}
dependencies {
implementation("center.sciprog:maps-kt-features:0.2.2")
}
```

View File

@ -0,0 +1,765 @@
public abstract interface class center/sciprog/attributes/Attribute {
}
public abstract interface class center/sciprog/attributes/AttributeWithDefault : center/sciprog/attributes/Attribute {
public abstract fun getDefault ()Ljava/lang/Object;
}
public final class center/sciprog/attributes/Attributes {
public static final field Companion Lcenter/sciprog/attributes/Attributes$Companion;
public static final synthetic fun box-impl (Ljava/util/Map;)Lcenter/sciprog/attributes/Attributes;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Ljava/util/Map;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Ljava/util/Map;Ljava/util/Map;)Z
public static final fun get-impl (Ljava/util/Map;Lcenter/sciprog/attributes/Attribute;)Ljava/lang/Object;
public final fun getContent ()Ljava/util/Map;
public static final fun getKeys-impl (Ljava/util/Map;)Ljava/util/Set;
public fun hashCode ()I
public static fun hashCode-impl (Ljava/util/Map;)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Ljava/util/Map;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Ljava/util/Map;
}
public final class center/sciprog/attributes/Attributes$Companion {
public final fun getEMPTY-hlWAoAs ()Ljava/util/Map;
}
public final class center/sciprog/attributes/AttributesBuilder {
public static final field $stable I
public fun <init> ()V
public final fun add (Lcenter/sciprog/attributes/SetAttribute;Ljava/lang/Object;)V
public final fun build-hlWAoAs ()Ljava/util/Map;
public final fun from-xbryb2s (Ljava/util/Map;)V
public final fun get (Lcenter/sciprog/attributes/Attribute;)Ljava/lang/Object;
public final fun invoke (Lcenter/sciprog/attributes/Attribute;Ljava/lang/Object;)V
public final fun remove (Lcenter/sciprog/attributes/SetAttribute;Ljava/lang/Object;)V
}
public final class center/sciprog/attributes/AttributesBuilderKt {
public static final fun Attributes (Lkotlin/jvm/functions/Function1;)Ljava/util/Map;
public static final fun AttributesBuilder-xbryb2s (Ljava/util/Map;)Lcenter/sciprog/attributes/AttributesBuilder;
}
public final class center/sciprog/attributes/AttributesKt {
public static final fun Attributes (Lcenter/sciprog/attributes/Attribute;Ljava/lang/Object;)Ljava/util/Map;
public static final fun getOrDefault-hZ80nBk (Ljava/util/Map;Lcenter/sciprog/attributes/AttributeWithDefault;)Ljava/lang/Object;
public static final fun getZ (Lcenter/sciprog/maps/features/Feature;)F
public static final fun isEmpty-xbryb2s (Ljava/util/Map;)Z
public static final fun plus-0GKEh6s (Ljava/util/Map;Ljava/util/Map;)Ljava/util/Map;
public static final fun withAttribute-svNjpeE (Ljava/util/Map;Lcenter/sciprog/attributes/Attribute;Ljava/lang/Object;)Ljava/util/Map;
public static final fun withAttributeElement-svNjpeE (Ljava/util/Map;Lcenter/sciprog/attributes/SetAttribute;Ljava/lang/Object;)Ljava/util/Map;
public static final fun withoutAttributeElement-svNjpeE (Ljava/util/Map;Lcenter/sciprog/attributes/SetAttribute;Ljava/lang/Object;)Ljava/util/Map;
}
public final class center/sciprog/attributes/AttributesSerializer : kotlinx/serialization/KSerializer {
public static final field $stable I
public fun <init> (Ljava/util/Set;)V
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize-7icwqAM (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/Map;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize-izrVrxc (Lkotlinx/serialization/encoding/Encoder;Ljava/util/Map;)V
}
public final class center/sciprog/attributes/NameAttribute : center/sciprog/attributes/SerializableAttribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/attributes/NameAttribute;
}
public abstract class center/sciprog/attributes/SerializableAttribute : center/sciprog/attributes/Attribute {
public static final field $stable I
public fun <init> (Ljava/lang/String;Lkotlinx/serialization/KSerializer;)V
public final fun getSerialId ()Ljava/lang/String;
public final fun getSerializer ()Lkotlinx/serialization/KSerializer;
public fun toString ()Ljava/lang/String;
}
public abstract interface class center/sciprog/attributes/SetAttribute : center/sciprog/attributes/Attribute {
}
public final class center/sciprog/maps/compose/ClickGesturesKt {
public static final fun detectClicks (Landroidx/compose/ui/input/pointer/PointerInputScope;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun detectClicks$default (Landroidx/compose/ui/input/pointer/PointerInputScope;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun getPosition (Landroidx/compose/ui/input/pointer/PointerEvent;)J
}
public final class center/sciprog/maps/compose/MapControlsKt {
public static final fun mapControls (Landroidx/compose/ui/Modifier;Lcenter/sciprog/maps/features/CoordinateViewScope;Lcenter/sciprog/maps/features/FeatureGroup;)Landroidx/compose/ui/Modifier;
}
public final class center/sciprog/maps/features/AlphaAttribute : center/sciprog/attributes/Attribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/AlphaAttribute;
}
public final class center/sciprog/maps/features/ArcFeature : center/sciprog/maps/features/DraggableFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Lcenter/sciprog/maps/features/Rectangle;
public final fun component3 ()Lspace/kscience/kmath/geometry/Angle;
public final fun component4 ()Lspace/kscience/kmath/geometry/Angle;
public final fun component5-hlWAoAs ()Ljava/util/Map;
public final fun copy-CqjioLk (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Ljava/util/Map;)Lcenter/sciprog/maps/features/ArcFeature;
public static synthetic fun copy-CqjioLk$default (Lcenter/sciprog/maps/features/ArcFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Ljava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/ArcFeature;
public fun equals (Ljava/lang/Object;)Z
public final fun getArcLength ()Lspace/kscience/kmath/geometry/Angle;
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public final fun getOval ()Lcenter/sciprog/maps/features/Rectangle;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun getStartAngle ()Lspace/kscience/kmath/geometry/Angle;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
public fun withCoordinates (Ljava/lang/Object;)Lcenter/sciprog/maps/features/Feature;
}
public abstract interface class center/sciprog/maps/features/Area {
public abstract fun contains (Ljava/lang/Object;)Z
}
public final class center/sciprog/maps/features/BitmapIconFeature : center/sciprog/maps/features/MarkerFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLandroidx/compose/ui/graphics/ImageBitmap;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLandroidx/compose/ui/graphics/ImageBitmap;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Ljava/lang/Object;
public final fun component3-MYxV2XQ ()J
public final fun component4 ()Landroidx/compose/ui/graphics/ImageBitmap;
public final fun component5-hlWAoAs ()Ljava/util/Map;
public final fun copy-xcw5jm0 (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLandroidx/compose/ui/graphics/ImageBitmap;Ljava/util/Map;)Lcenter/sciprog/maps/features/BitmapIconFeature;
public static synthetic fun copy-xcw5jm0$default (Lcenter/sciprog/maps/features/BitmapIconFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLandroidx/compose/ui/graphics/ImageBitmap;Ljava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/BitmapIconFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public fun getCenter ()Ljava/lang/Object;
public final fun getImage ()Landroidx/compose/ui/graphics/ImageBitmap;
public final fun getSize-MYxV2XQ ()J
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
public fun withCoordinates (Ljava/lang/Object;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/CircleFeature : center/sciprog/maps/features/MarkerFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;FLjava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;FLjava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Ljava/lang/Object;
public final fun component3-D9Ej5fM ()F
public final fun component4-hlWAoAs ()Ljava/util/Map;
public fun contains (Lcenter/sciprog/maps/features/ViewPoint;)Z
public final fun copy-eji0nfc (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;FLjava/util/Map;)Lcenter/sciprog/maps/features/CircleFeature;
public static synthetic fun copy-eji0nfc$default (Lcenter/sciprog/maps/features/CircleFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;FLjava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/CircleFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public fun getCenter ()Ljava/lang/Object;
public final fun getRadius-D9Ej5fM ()F
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
public fun withCoordinates (Ljava/lang/Object;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/ClickListenerAttribute : center/sciprog/attributes/SetAttribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/ClickListenerAttribute;
}
public final class center/sciprog/maps/features/ClickRadius : center/sciprog/attributes/Attribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/ClickRadius;
}
public final class center/sciprog/maps/features/ColorAttribute : center/sciprog/attributes/Attribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/ColorAttribute;
}
public final class center/sciprog/maps/features/CompositeFeaturesKt {
public static final fun draggableLine (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/features/FeatureRef;Lcenter/sciprog/maps/features/FeatureRef;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun draggableLine$default (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/features/FeatureRef;Lcenter/sciprog/maps/features/FeatureRef;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun draggableMultiLine (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/List;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun draggableMultiLine$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun draggableMultiLineFromPoints (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/List;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun draggableMultiLineFromPoints$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
}
public abstract interface class center/sciprog/maps/features/CoordinateSpace {
public abstract fun Rectangle (Ljava/lang/Object;Ljava/lang/Object;)Lcenter/sciprog/maps/features/Rectangle;
public abstract fun Rectangle-Iw8sGQE (Ljava/lang/Object;FJ)Lcenter/sciprog/maps/features/Rectangle;
public abstract fun ViewPoint (Ljava/lang/Object;F)Lcenter/sciprog/maps/features/ViewPoint;
public fun distanceTo-ccRj1GA (Ljava/lang/Object;Ljava/lang/Object;F)F
public fun distanceToLine-DwT6o7Y (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;F)F
public abstract fun getDefaultViewPoint ()Lcenter/sciprog/maps/features/ViewPoint;
public abstract fun isInsidePolygon (Ljava/lang/Object;Ljava/util/List;)Z
public abstract fun moveBy (Lcenter/sciprog/maps/features/ViewPoint;Ljava/lang/Object;)Lcenter/sciprog/maps/features/ViewPoint;
public abstract fun offsetTo-LQHvzoY (Ljava/lang/Object;Ljava/lang/Object;F)J
public abstract fun withCenter (Lcenter/sciprog/maps/features/Rectangle;Ljava/lang/Object;)Lcenter/sciprog/maps/features/Rectangle;
public abstract fun wrapPoints (Ljava/util/Collection;)Lcenter/sciprog/maps/features/Rectangle;
public abstract fun wrapRectangles (Ljava/util/Collection;)Lcenter/sciprog/maps/features/Rectangle;
public abstract fun zoomBy (Lcenter/sciprog/maps/features/ViewPoint;FLjava/lang/Object;)Lcenter/sciprog/maps/features/ViewPoint;
public static synthetic fun zoomBy$default (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/ViewPoint;FLjava/lang/Object;ILjava/lang/Object;)Lcenter/sciprog/maps/features/ViewPoint;
}
public final class center/sciprog/maps/features/CoordinateSpaceKt {
public static final fun Rectangle-Iw8sGQE (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/ViewPoint;J)Lcenter/sciprog/maps/features/Rectangle;
}
public abstract class center/sciprog/maps/features/CoordinateViewScope {
public static final field $stable I
public fun <init> (Lcenter/sciprog/maps/features/ViewConfig;)V
public abstract fun computeViewPoint (Lcenter/sciprog/maps/features/Rectangle;)Lcenter/sciprog/maps/features/ViewPoint;
public final fun getCanvasSize-MYxV2XQ ()J
public final fun getConfig ()Lcenter/sciprog/maps/features/ViewConfig;
public final fun getSelectRect ()Landroidx/compose/ui/unit/DpRect;
public abstract fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun getViewPoint ()Lcenter/sciprog/maps/features/ViewPoint;
public final fun getZoom ()F
public abstract fun moveBy-VpY3zN4 (Lcenter/sciprog/maps/features/ViewPoint;FF)Lcenter/sciprog/maps/features/ViewPoint;
public final fun setCanvasSize-EaSLcWc (J)V
public final fun setSelectRect (Landroidx/compose/ui/unit/DpRect;)V
public final fun setViewPoint (Lcenter/sciprog/maps/features/ViewPoint;)V
public final fun toCoordinates-3MmeM6k (JLandroidx/compose/ui/unit/Density;)Ljava/lang/Object;
public abstract fun toCoordinates-jo-Fl9I (J)Ljava/lang/Object;
public abstract fun toDpOffset-gVRvYmI (Ljava/lang/Object;)J
public abstract fun toDpRect (Lcenter/sciprog/maps/features/Rectangle;)Landroidx/compose/ui/unit/DpRect;
public final fun toOffset-dBAh8RU (Ljava/lang/Object;Landroidx/compose/ui/unit/Density;)J
}
public final class center/sciprog/maps/features/CoordinateViewScopeKt {
public static final fun getBottomRight (Landroidx/compose/ui/unit/DpRect;)J
public static final fun getTopLeft (Landroidx/compose/ui/unit/DpRect;)J
}
public abstract interface class center/sciprog/maps/features/DomainFeature : center/sciprog/maps/features/Feature {
public fun contains (Lcenter/sciprog/maps/features/ViewPoint;)Z
}
public abstract interface class center/sciprog/maps/features/DragHandle {
public static final field Companion Lcenter/sciprog/maps/features/DragHandle$Companion;
public abstract fun handle (Landroidx/compose/ui/input/pointer/PointerEvent;Lcenter/sciprog/maps/features/ViewPoint;Lcenter/sciprog/maps/features/ViewPoint;)Lcenter/sciprog/maps/features/DragResult;
}
public final class center/sciprog/maps/features/DragHandle$Companion {
public final fun bypass ()Lcenter/sciprog/maps/features/DragHandle;
public final fun combine ([Lcenter/sciprog/maps/features/DragHandle;)Lcenter/sciprog/maps/features/DragHandle;
public final fun withPrimaryButton (Lkotlin/jvm/functions/Function3;)Lcenter/sciprog/maps/features/DragHandle;
}
public abstract interface class center/sciprog/maps/features/DragListener {
public abstract fun handle (Landroidx/compose/ui/input/pointer/PointerEvent;Lcenter/sciprog/maps/features/ViewPoint;Lcenter/sciprog/maps/features/ViewPoint;)V
}
public final class center/sciprog/maps/features/DragListenerAttribute : center/sciprog/attributes/SetAttribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/DragListenerAttribute;
}
public final class center/sciprog/maps/features/DragResult {
public static final field $stable I
public fun <init> (Lcenter/sciprog/maps/features/ViewPoint;Z)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/ViewPoint;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/ViewPoint;
public final fun component2 ()Z
public final fun copy (Lcenter/sciprog/maps/features/ViewPoint;Z)Lcenter/sciprog/maps/features/DragResult;
public static synthetic fun copy$default (Lcenter/sciprog/maps/features/DragResult;Lcenter/sciprog/maps/features/ViewPoint;ZILjava/lang/Object;)Lcenter/sciprog/maps/features/DragResult;
public fun equals (Ljava/lang/Object;)Z
public final fun getHandleNext ()Z
public final fun getResult ()Lcenter/sciprog/maps/features/ViewPoint;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class center/sciprog/maps/features/DraggableAttribute : center/sciprog/attributes/Attribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/DraggableAttribute;
}
public abstract interface class center/sciprog/maps/features/DraggableFeature : center/sciprog/maps/features/DomainFeature {
public abstract fun withCoordinates (Ljava/lang/Object;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/DrawFeature : center/sciprog/maps/features/DraggableFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Ljava/lang/Object;
public final fun component3-hlWAoAs ()Ljava/util/Map;
public final fun component4 ()Lkotlin/jvm/functions/Function1;
public final fun copy-4xdkkFs (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/util/Map;Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/DrawFeature;
public static synthetic fun copy-4xdkkFs$default (Lcenter/sciprog/maps/features/DrawFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcenter/sciprog/maps/features/DrawFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public final fun getDrawFeature ()Lkotlin/jvm/functions/Function1;
public final fun getPosition ()Ljava/lang/Object;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
public fun withCoordinates (Ljava/lang/Object;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/DrawFeatureKt {
public static final fun drawFeature (Landroidx/compose/ui/graphics/drawscope/DrawScope;Lcenter/sciprog/maps/features/CoordinateViewScope;Ljava/util/Map;Lcenter/sciprog/maps/features/Feature;)V
}
public abstract interface class center/sciprog/maps/features/Feature {
public abstract fun getAttributes-hlWAoAs ()Ljava/util/Map;
public abstract fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public static synthetic fun getBoundingBox$default (Lcenter/sciprog/maps/features/Feature;FILjava/lang/Object;)Lcenter/sciprog/maps/features/Rectangle;
public abstract fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public abstract fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/FeatureGroup : center/sciprog/maps/features/CoordinateSpace, center/sciprog/maps/features/Feature {
public static final field $stable I
public static final field Companion Lcenter/sciprog/maps/features/FeatureGroup$Companion;
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun Rectangle (Ljava/lang/Object;Ljava/lang/Object;)Lcenter/sciprog/maps/features/Rectangle;
public fun Rectangle-Iw8sGQE (Ljava/lang/Object;FJ)Lcenter/sciprog/maps/features/Rectangle;
public fun ViewPoint (Ljava/lang/Object;F)Lcenter/sciprog/maps/features/ViewPoint;
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Landroidx/compose/runtime/snapshots/SnapshotStateMap;
public final fun component3-hlWAoAs ()Ljava/util/Map;
public final fun copy-EHyjM0Q (Lcenter/sciprog/maps/features/CoordinateSpace;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Ljava/util/Map;)Lcenter/sciprog/maps/features/FeatureGroup;
public static synthetic fun copy-EHyjM0Q$default (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/features/CoordinateSpace;Landroidx/compose/runtime/snapshots/SnapshotStateMap;Ljava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureGroup;
public fun distanceTo-ccRj1GA (Ljava/lang/Object;Ljava/lang/Object;F)F
public fun distanceToLine-DwT6o7Y (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;F)F
public fun equals (Ljava/lang/Object;)Z
public final fun feature (Ljava/lang/String;Lcenter/sciprog/maps/features/Feature;)Lcenter/sciprog/maps/features/FeatureRef;
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public fun getDefaultViewPoint ()Lcenter/sciprog/maps/features/ViewPoint;
public final fun getFeatureMap ()Landroidx/compose/runtime/snapshots/SnapshotStateMap;
public final fun getFeatures ()Ljava/util/Collection;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun isInsidePolygon (Ljava/lang/Object;Ljava/util/List;)Z
public fun moveBy (Lcenter/sciprog/maps/features/ViewPoint;Ljava/lang/Object;)Lcenter/sciprog/maps/features/ViewPoint;
public fun offsetTo-LQHvzoY (Ljava/lang/Object;Ljava/lang/Object;F)J
public final fun removeFeature (Ljava/lang/String;)V
public fun toString ()Ljava/lang/String;
public final fun visit (Lkotlin/jvm/functions/Function3;)V
public final fun visitUntil (Lkotlin/jvm/functions/Function3;)V
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
public fun withCenter (Lcenter/sciprog/maps/features/Rectangle;Ljava/lang/Object;)Lcenter/sciprog/maps/features/Rectangle;
public fun wrapPoints (Ljava/util/Collection;)Lcenter/sciprog/maps/features/Rectangle;
public fun wrapRectangles (Ljava/util/Collection;)Lcenter/sciprog/maps/features/Rectangle;
public fun zoomBy (Lcenter/sciprog/maps/features/ViewPoint;FLjava/lang/Object;)Lcenter/sciprog/maps/features/ViewPoint;
}
public final class center/sciprog/maps/features/FeatureGroup$Companion {
public final fun build (Lcenter/sciprog/maps/features/CoordinateSpace;Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/FeatureGroup;
public static synthetic fun build$default (Lcenter/sciprog/maps/features/FeatureGroup$Companion;Lcenter/sciprog/maps/features/CoordinateSpace;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureGroup;
public final fun remember (Lcenter/sciprog/maps/features/CoordinateSpace;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Lcenter/sciprog/maps/features/FeatureGroup;
}
public final class center/sciprog/maps/features/FeatureGroupKt {
public static final fun Structure2D (IILkotlin/jvm/functions/Function1;)Lspace/kscience/kmath/nd/Structure2D;
public static final fun StructureND-qL90JFI ([ILkotlin/jvm/functions/Function1;)Lspace/kscience/kmath/nd/StructureND;
public static final fun arc-f9v9FWM (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Ljava/util/Map;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun arc-f9v9FWM$default (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/geometry/Angle;Lspace/kscience/kmath/geometry/Angle;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun circle-vJIlCZg (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;FLjava/util/Map;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun circle-vJIlCZg$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;FLjava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun draw-UqNt6FI (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun draw-UqNt6FI$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun forEachWithAttribute (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/attributes/Attribute;Lkotlin/jvm/functions/Function4;)V
public static final fun forEachWithAttributeUntil (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/attributes/Attribute;Lkotlin/jvm/functions/Function4;)V
public static final fun getAttributes (Lcenter/sciprog/maps/features/FeatureRef;)Ljava/util/Map;
public static final fun group-WhaQnWU (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/Map;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun group-WhaQnWU$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/Map;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun icon-Rwk-ljY (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;Landroidx/compose/ui/graphics/vector/ImageVector;JLjava/util/Map;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun icon-Rwk-ljY$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;Landroidx/compose/ui/graphics/vector/ImageVector;JLjava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun line-JCOR_Dk (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun line-JCOR_Dk$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun multiLine-4xdkkFs (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun multiLine-4xdkkFs$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun pixelMap-JCOR_Dk (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/nd/Structure2D;Ljava/util/Map;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun pixelMap-JCOR_Dk$default (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/nd/Structure2D;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun points-4xdkkFs (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun points-4xdkkFs$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun polygon-4xdkkFs (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun polygon-4xdkkFs$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun rectangle-H_bhbsI (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;JLjava/util/Map;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun rectangle-H_bhbsI$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;JLjava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun resolve (Lcenter/sciprog/maps/features/FeatureRef;)Lcenter/sciprog/maps/features/Feature;
public static final fun scalableImage-UqNt6FI (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/features/Rectangle;Ljava/util/Map;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun scalableImage-UqNt6FI$default (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/features/Rectangle;Ljava/util/Map;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun text-f9v9FWM (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/util/Map;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun text-f9v9FWM$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
}
public final class center/sciprog/maps/features/FeatureKt {
public static final fun computeBoundingBox (Ljava/lang/Iterable;Lcenter/sciprog/maps/features/CoordinateSpace;F)Lcenter/sciprog/maps/features/Rectangle;
public static final fun getColor (Lcenter/sciprog/maps/features/Feature;)Landroidx/compose/ui/graphics/Color;
public static final fun getName (Lcenter/sciprog/maps/features/Feature;)Ljava/lang/String;
public static final fun getZoomRange (Lcenter/sciprog/maps/features/Feature;)Lkotlin/ranges/ClosedFloatingPointRange;
}
public final class center/sciprog/maps/features/FeatureRef {
public static final field $stable I
public fun <init> (Ljava/lang/String;Lcenter/sciprog/maps/features/FeatureGroup;)V
public final fun getId ()Ljava/lang/String;
public final fun getParent ()Lcenter/sciprog/maps/features/FeatureGroup;
}
public final class center/sciprog/maps/features/FeatureSelector : center/sciprog/maps/features/Feature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2-hlWAoAs ()Ljava/util/Map;
public final fun component3 ()Lkotlin/jvm/functions/Function1;
public final fun copy-I8jStn0 (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/Map;Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/FeatureSelector;
public static synthetic fun copy-I8jStn0$default (Lcenter/sciprog/maps/features/FeatureSelector;Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureSelector;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public final fun getSelector ()Lkotlin/jvm/functions/Function1;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/HoverListenerAttribute : center/sciprog/attributes/SetAttribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/HoverListenerAttribute;
}
public final class center/sciprog/maps/features/LineFeature : center/sciprog/maps/features/DomainFeature, center/sciprog/maps/features/LineSegmentFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Ljava/lang/Object;
public final fun component3 ()Ljava/lang/Object;
public final fun component4-hlWAoAs ()Ljava/util/Map;
public fun contains (Lcenter/sciprog/maps/features/ViewPoint;)Z
public final fun copy-a7qfu0U (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/Map;)Lcenter/sciprog/maps/features/LineFeature;
public static synthetic fun copy-a7qfu0U$default (Lcenter/sciprog/maps/features/LineFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/LineFeature;
public fun equals (Ljava/lang/Object;)Z
public final fun getA ()Ljava/lang/Object;
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public final fun getB ()Ljava/lang/Object;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public final fun getCenter ()Ljava/lang/Object;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
}
public abstract interface class center/sciprog/maps/features/LineSegmentFeature : center/sciprog/maps/features/Feature {
}
public final class center/sciprog/maps/features/MapFeatureAttributesKt {
public static final fun color-4WTKRHQ (Lcenter/sciprog/maps/features/FeatureRef;J)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun draggable (Lcenter/sciprog/maps/features/FeatureRef;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun draggable$default (Lcenter/sciprog/maps/features/FeatureRef;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun modifyAttribute (Lcenter/sciprog/maps/features/FeatureRef;Lcenter/sciprog/attributes/Attribute;Ljava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun modifyAttributes (Lcenter/sciprog/maps/features/FeatureRef;Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun onClick (Lcenter/sciprog/maps/features/FeatureRef;Landroidx/compose/foundation/PointerMatcher;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun onClick (Lcenter/sciprog/maps/features/FeatureRef;Lkotlin/jvm/functions/Function2;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun onClick$default (Lcenter/sciprog/maps/features/FeatureRef;Landroidx/compose/foundation/PointerMatcher;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun onDrag (Lcenter/sciprog/maps/features/FeatureRef;Lkotlin/jvm/functions/Function3;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun onHover (Lcenter/sciprog/maps/features/FeatureRef;Lkotlin/jvm/functions/Function2;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun pathEffect (Lcenter/sciprog/maps/features/FeatureRef;Landroidx/compose/ui/graphics/PathEffect;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun stroke (Lcenter/sciprog/maps/features/FeatureRef;F)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun zoomRange (Lcenter/sciprog/maps/features/FeatureRef;Lkotlin/ranges/ClosedFloatingPointRange;)Lcenter/sciprog/maps/features/FeatureRef;
}
public abstract interface class center/sciprog/maps/features/MarkerFeature : center/sciprog/maps/features/DraggableFeature {
public abstract fun getCenter ()Ljava/lang/Object;
}
public abstract interface class center/sciprog/maps/features/MouseListener {
public static final field Companion Lcenter/sciprog/maps/features/MouseListener$Companion;
public abstract fun handle (Landroidx/compose/ui/input/pointer/PointerEvent;Lcenter/sciprog/maps/features/ViewPoint;)V
}
public final class center/sciprog/maps/features/MouseListener$Companion {
public final fun withPrimaryButton (Lkotlin/jvm/functions/Function2;)Lcenter/sciprog/maps/features/MouseListener;
}
public final class center/sciprog/maps/features/MultiLineFeature : center/sciprog/maps/features/DomainFeature, center/sciprog/maps/features/LineSegmentFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Ljava/util/List;
public final fun component3-hlWAoAs ()Ljava/util/Map;
public fun contains (Lcenter/sciprog/maps/features/ViewPoint;)Z
public final fun copy-EHyjM0Q (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;)Lcenter/sciprog/maps/features/MultiLineFeature;
public static synthetic fun copy-EHyjM0Q$default (Lcenter/sciprog/maps/features/MultiLineFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/MultiLineFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public final fun getPoints ()Ljava/util/List;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
}
public abstract interface class center/sciprog/maps/features/PainterFeature : center/sciprog/maps/features/Feature {
public abstract fun getPainter (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter;
}
public final class center/sciprog/maps/features/PathEffectAttribute : center/sciprog/attributes/Attribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/PathEffectAttribute;
}
public final class center/sciprog/maps/features/PathFeature : center/sciprog/maps/features/DraggableFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Landroidx/compose/ui/graphics/Path;Landroidx/compose/ui/graphics/Brush;Landroidx/compose/ui/graphics/drawscope/DrawStyle;Landroidx/compose/ui/geometry/Rect;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Landroidx/compose/ui/graphics/Path;Landroidx/compose/ui/graphics/Brush;Landroidx/compose/ui/graphics/drawscope/DrawStyle;Landroidx/compose/ui/geometry/Rect;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Lcenter/sciprog/maps/features/Rectangle;
public final fun component3 ()Landroidx/compose/ui/graphics/Path;
public final fun component4 ()Landroidx/compose/ui/graphics/Brush;
public final fun component5 ()Landroidx/compose/ui/graphics/drawscope/DrawStyle;
public final fun component6 ()Landroidx/compose/ui/geometry/Rect;
public final fun component7-hlWAoAs ()Ljava/util/Map;
public final fun copy-mDmFKLY (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Landroidx/compose/ui/graphics/Path;Landroidx/compose/ui/graphics/Brush;Landroidx/compose/ui/graphics/drawscope/DrawStyle;Landroidx/compose/ui/geometry/Rect;Ljava/util/Map;)Lcenter/sciprog/maps/features/PathFeature;
public static synthetic fun copy-mDmFKLY$default (Lcenter/sciprog/maps/features/PathFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Landroidx/compose/ui/graphics/Path;Landroidx/compose/ui/graphics/Brush;Landroidx/compose/ui/graphics/drawscope/DrawStyle;Landroidx/compose/ui/geometry/Rect;Ljava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/PathFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public final fun getBrush ()Landroidx/compose/ui/graphics/Brush;
public final fun getPath ()Landroidx/compose/ui/graphics/Path;
public final fun getRectangle ()Lcenter/sciprog/maps/features/Rectangle;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun getStyle ()Landroidx/compose/ui/graphics/drawscope/DrawStyle;
public final fun getTargetRect ()Landroidx/compose/ui/geometry/Rect;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
public fun withCoordinates (Ljava/lang/Object;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/PixelMapFeature : center/sciprog/maps/features/Feature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/nd/Structure2D;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/nd/Structure2D;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Lcenter/sciprog/maps/features/Rectangle;
public final fun component3 ()Lspace/kscience/kmath/nd/Structure2D;
public final fun component4-hlWAoAs ()Ljava/util/Map;
public final fun copy-a7qfu0U (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/nd/Structure2D;Ljava/util/Map;)Lcenter/sciprog/maps/features/PixelMapFeature;
public static synthetic fun copy-a7qfu0U$default (Lcenter/sciprog/maps/features/PixelMapFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Lspace/kscience/kmath/nd/Structure2D;Ljava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/PixelMapFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public final fun getPixelMap ()Lspace/kscience/kmath/nd/Structure2D;
public final fun getRectangle ()Lcenter/sciprog/maps/features/Rectangle;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/PointsFeature : center/sciprog/maps/features/Feature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Ljava/util/List;
public final fun component3-hlWAoAs ()Ljava/util/Map;
public final fun copy-EHyjM0Q (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;)Lcenter/sciprog/maps/features/PointsFeature;
public static synthetic fun copy-EHyjM0Q$default (Lcenter/sciprog/maps/features/PointsFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/PointsFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public final fun getPoints ()Ljava/util/List;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/PolygonFeature : center/sciprog/maps/features/DomainFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Ljava/util/List;
public final fun component3-hlWAoAs ()Ljava/util/Map;
public fun contains (Lcenter/sciprog/maps/features/ViewPoint;)Z
public final fun copy-EHyjM0Q (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;)Lcenter/sciprog/maps/features/PolygonFeature;
public static synthetic fun copy-EHyjM0Q$default (Lcenter/sciprog/maps/features/PolygonFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/util/List;Ljava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/PolygonFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public final fun getPoints ()Ljava/util/List;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
}
public abstract interface class center/sciprog/maps/features/Rectangle : center/sciprog/maps/features/Area {
public abstract fun getA ()Ljava/lang/Object;
public abstract fun getB ()Ljava/lang/Object;
public abstract fun getCenter ()Ljava/lang/Object;
}
public final class center/sciprog/maps/features/RectangleFeature : center/sciprog/maps/features/MarkerFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLjava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLjava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Ljava/lang/Object;
public final fun component3-MYxV2XQ ()J
public final fun component4-hlWAoAs ()Ljava/util/Map;
public final fun copy-tzWo0Eo (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLjava/util/Map;)Lcenter/sciprog/maps/features/RectangleFeature;
public static synthetic fun copy-tzWo0Eo$default (Lcenter/sciprog/maps/features/RectangleFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLjava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/RectangleFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public fun getCenter ()Ljava/lang/Object;
public final fun getSize-MYxV2XQ ()J
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
public fun withCoordinates (Ljava/lang/Object;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/ScalableImageFeature : center/sciprog/maps/features/Feature, center/sciprog/maps/features/PainterFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Ljava/util/Map;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Lcenter/sciprog/maps/features/Rectangle;
public final fun component3-hlWAoAs ()Ljava/util/Map;
public final fun component4 ()Lkotlin/jvm/functions/Function2;
public final fun copy-4xdkkFs (Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Ljava/util/Map;Lkotlin/jvm/functions/Function2;)Lcenter/sciprog/maps/features/ScalableImageFeature;
public static synthetic fun copy-4xdkkFs$default (Lcenter/sciprog/maps/features/ScalableImageFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Lcenter/sciprog/maps/features/Rectangle;Ljava/util/Map;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcenter/sciprog/maps/features/ScalableImageFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public final fun getPainter ()Lkotlin/jvm/functions/Function2;
public fun getPainter (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter;
public final fun getRectangle ()Lcenter/sciprog/maps/features/Rectangle;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/StrokeAttribute : center/sciprog/attributes/Attribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/StrokeAttribute;
}
public final class center/sciprog/maps/features/TextFeature : center/sciprog/maps/features/DraggableFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Ljava/lang/Object;
public final fun component3 ()Ljava/lang/String;
public final fun component4-hlWAoAs ()Ljava/util/Map;
public final fun component5 ()Lkotlin/jvm/functions/Function1;
public final fun copy-JCOR_Dk (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/TextFeature;
public static synthetic fun copy-JCOR_Dk$default (Lcenter/sciprog/maps/features/TextFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcenter/sciprog/maps/features/TextFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public final fun getFontConfig ()Lkotlin/jvm/functions/Function1;
public final fun getPosition ()Ljava/lang/Object;
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun getText ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
public fun withCoordinates (Ljava/lang/Object;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/VectorIconFeature : center/sciprog/maps/features/MarkerFeature, center/sciprog/maps/features/PainterFeature {
public static final field $stable I
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLandroidx/compose/ui/graphics/vector/ImageVector;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLandroidx/compose/ui/graphics/vector/ImageVector;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lcenter/sciprog/maps/features/CoordinateSpace;
public final fun component2 ()Ljava/lang/Object;
public final fun component3-MYxV2XQ ()J
public final fun component4 ()Landroidx/compose/ui/graphics/vector/ImageVector;
public final fun component5-hlWAoAs ()Ljava/util/Map;
public final fun copy-xcw5jm0 (Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLandroidx/compose/ui/graphics/vector/ImageVector;Ljava/util/Map;)Lcenter/sciprog/maps/features/VectorIconFeature;
public static synthetic fun copy-xcw5jm0$default (Lcenter/sciprog/maps/features/VectorIconFeature;Lcenter/sciprog/maps/features/CoordinateSpace;Ljava/lang/Object;JLandroidx/compose/ui/graphics/vector/ImageVector;Ljava/util/Map;ILjava/lang/Object;)Lcenter/sciprog/maps/features/VectorIconFeature;
public fun equals (Ljava/lang/Object;)Z
public fun getAttributes-hlWAoAs ()Ljava/util/Map;
public fun getBoundingBox (F)Lcenter/sciprog/maps/features/Rectangle;
public fun getCenter ()Ljava/lang/Object;
public final fun getImage ()Landroidx/compose/ui/graphics/vector/ImageVector;
public synthetic fun getPainter (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter;
public fun getPainter (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/vector/VectorPainter;
public final fun getSize-MYxV2XQ ()J
public fun getSpace ()Lcenter/sciprog/maps/features/CoordinateSpace;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public fun withAttributes (Lkotlin/jvm/functions/Function1;)Lcenter/sciprog/maps/features/Feature;
public fun withCoordinates (Ljava/lang/Object;)Lcenter/sciprog/maps/features/Feature;
}
public final class center/sciprog/maps/features/ViewConfig {
public static final field $stable I
public fun <init> ()V
public fun <init> (FLcenter/sciprog/maps/features/MouseListener;Lcenter/sciprog/maps/features/DragHandle;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZ)V
public synthetic fun <init> (FLcenter/sciprog/maps/features/MouseListener;Lcenter/sciprog/maps/features/DragHandle;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()F
public final fun component2 ()Lcenter/sciprog/maps/features/MouseListener;
public final fun component3 ()Lcenter/sciprog/maps/features/DragHandle;
public final fun component4 ()Lkotlin/jvm/functions/Function1;
public final fun component5 ()Lkotlin/jvm/functions/Function1;
public final fun component6 ()Lkotlin/jvm/functions/Function1;
public final fun component7 ()Z
public final fun component8 ()Z
public final fun copy (FLcenter/sciprog/maps/features/MouseListener;Lcenter/sciprog/maps/features/DragHandle;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZ)Lcenter/sciprog/maps/features/ViewConfig;
public static synthetic fun copy$default (Lcenter/sciprog/maps/features/ViewConfig;FLcenter/sciprog/maps/features/MouseListener;Lcenter/sciprog/maps/features/DragHandle;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZILjava/lang/Object;)Lcenter/sciprog/maps/features/ViewConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getDragHandle ()Lcenter/sciprog/maps/features/DragHandle;
public final fun getOnCanvasSizeChange ()Lkotlin/jvm/functions/Function1;
public final fun getOnClick ()Lcenter/sciprog/maps/features/MouseListener;
public final fun getOnSelect ()Lkotlin/jvm/functions/Function1;
public final fun getOnViewChange ()Lkotlin/jvm/functions/Function1;
public final fun getZoomOnDoubleClick ()Z
public final fun getZoomOnSelect ()Z
public final fun getZoomSpeed ()F
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public abstract interface class center/sciprog/maps/features/ViewPoint {
public abstract fun getFocus ()Ljava/lang/Object;
public abstract fun getZoom ()F
}
public final class center/sciprog/maps/features/VisibleAttribute : center/sciprog/attributes/Attribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/VisibleAttribute;
}
public final class center/sciprog/maps/features/ZAttribute : center/sciprog/attributes/Attribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/ZAttribute;
}
public final class center/sciprog/maps/features/ZoomRangeAttribute : center/sciprog/attributes/Attribute {
public static final field $stable I
public static final field INSTANCE Lcenter/sciprog/maps/features/ZoomRangeAttribute;
}

View File

@ -0,0 +1,30 @@
plugins {
id("space.kscience.gradle.mpp")
id("org.jetbrains.compose")
`maven-publish`
}
val kmathVersion: String by rootProject.extra
kscience{
jvm()
js()
useSerialization{
json()
}
useSerialization(sourceSet = space.kscience.gradle.DependencySourceSet.TEST){
protobuf()
}
}
kotlin {
sourceSets {
commonMain {
dependencies {
api(projects.trajectoryKt)
api(compose.foundation)
}
}
}
}

View File

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

View File

@ -0,0 +1,74 @@
package center.sciprog.attributes
import center.sciprog.maps.features.Feature
import center.sciprog.maps.features.ZAttribute
import kotlin.jvm.JvmInline
@JvmInline
public value class Attributes internal constructor(public val content: Map<out Attribute<*>, Any>) {
public val keys: Set<Attribute<*>> get() = content.keys
@Suppress("UNCHECKED_CAST")
public operator fun <T> get(attribute: Attribute<T>): T? = content[attribute] as? T
override fun toString(): String = "Attributes(value=${content.entries})"
public companion object {
public val EMPTY: Attributes = Attributes(emptyMap())
}
}
public fun Attributes.isEmpty(): Boolean = content.isEmpty()
public fun <T> Attributes.getOrDefault(attribute: AttributeWithDefault<T>): T = get(attribute) ?: attribute.default
public fun <T, A : Attribute<T>> Attributes.withAttribute(
attribute: A,
attrValue: T?,
): Attributes = Attributes(
if (attrValue == null) {
content - attribute
} else {
content + (attribute to attrValue)
}
)
/**
* Add an element to a [SetAttribute]
*/
public fun <T, A : SetAttribute<T>> Attributes.withAttributeElement(
attribute: A,
attrValue: T,
): Attributes {
val currentSet: Set<T> = get(attribute) ?: emptySet()
return Attributes(
content + (attribute to (currentSet + attrValue))
)
}
/**
* Remove an element from [SetAttribute]
*/
public fun <T, A : SetAttribute<T>> Attributes.withoutAttributeElement(
attribute: A,
attrValue: T,
): Attributes {
val currentSet: Set<T> = get(attribute) ?: emptySet()
return Attributes(
content + (attribute to (currentSet - attrValue))
)
}
public fun <T : Any, A : Attribute<T>> Attributes(
attribute: A,
attrValue: T,
): Attributes = Attributes(mapOf(attribute to attrValue))
public operator fun Attributes.plus(other: Attributes): Attributes = Attributes(content + other.content)
public val Feature<*>.z: Float
get() = attributes[ZAttribute] ?: 0f
// set(value) {
// attributes[ZAttribute] = value
// }

View File

@ -0,0 +1,47 @@
package center.sciprog.attributes
/**
* A safe builder for [Attributes]
*/
public class AttributesBuilder internal constructor(private val map: MutableMap<Attribute<*>, Any> = mutableMapOf()) {
@Suppress("UNCHECKED_CAST")
public operator fun <T> get(attribute: Attribute<T>): T? = map[attribute] as? T
public operator fun <V> Attribute<V>.invoke(value: V?) {
if (value == null) {
map.remove(this)
} else {
map[this] = value
}
}
public fun from(attributes: Attributes) {
map.putAll(attributes.content)
}
public fun <V> SetAttribute<V>.add(
attrValue: V,
) {
val currentSet: Set<V> = get(this) ?: emptySet()
map[this] = currentSet + attrValue
}
/**
* Remove an element from [SetAttribute]
*/
public fun <V> SetAttribute<V>.remove(
attrValue: V,
) {
val currentSet: Set<V> = get(this) ?: emptySet()
map[this] = currentSet - attrValue
}
public fun build(): Attributes = Attributes(map)
}
public fun AttributesBuilder(
attributes: Attributes,
): AttributesBuilder = AttributesBuilder(attributes.content.toMutableMap())
public fun Attributes(builder: AttributesBuilder.() -> Unit): Attributes = AttributesBuilder().apply(builder).build()

View File

@ -0,0 +1,55 @@
@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)
}
}

View File

@ -0,0 +1,86 @@
package center.sciprog.maps.features
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.sqrt
public interface Area<T : Any> {
public operator fun contains(point: T): Boolean
}
/**
* A map coordinates rectangle. [a] and [b] represent opposing angles
* of the rectangle without specifying which ones.
*/
public interface Rectangle<T : Any> : Area<T> {
public val a: T
public val b: T
public val center: T
}
/**
* A context for map/scheme coordinates manipulation
*/
public interface CoordinateSpace<T : Any> {
/**
* Build a rectangle by two opposing corners
*/
public fun Rectangle(first: T, second: T): Rectangle<T>
/**
* Build a rectangle of visual size [size]
*/
public fun Rectangle(center: T, zoom: Float, size: DpSize): Rectangle<T>
/**
* A view point used by default
*/
public val defaultViewPoint: ViewPoint<T>
/**
* Create a [ViewPoint] associated with this coordinate space.
*/
public fun ViewPoint(center: T, zoom: Float): ViewPoint<T>
public fun ViewPoint<T>.moveBy(delta: T): ViewPoint<T>
public fun ViewPoint<T>.zoomBy(
zoomDelta: Float,
invariant: T = focus,
): ViewPoint<T>
/**
* Move given rectangle to be centered at [center]
*/
public fun Rectangle<T>.withCenter(center: T): Rectangle<T>
public fun Collection<Rectangle<T>>.wrapRectangles(): Rectangle<T>?
public fun Collection<T>.wrapPoints(): Rectangle<T>?
public fun T.offsetTo(b: T, zoom: Float): DpOffset
public fun T.distanceTo(b: T, zoom: Float): Dp {
val offset: DpOffset = offsetTo(b, zoom)
return sqrt(offset.x.value * offset.x.value + offset.y.value * offset.y.value).dp
}
public fun T.distanceToLine(a: T, b: T, zoom: Float): Dp {
val d12 = a.offsetTo(b, zoom)
val d01 = offsetTo(a, zoom)
val distanceVale = abs(d12.x.value * d01.y.value - d12.y.value * d01.x.value) / a.distanceTo(b, zoom).value
return distanceVale.dp
}
public fun T.isInsidePolygon(points: List<T>): Boolean
}
public fun <T : Any> CoordinateSpace<T>.Rectangle(viewPoint: ViewPoint<T>, size: DpSize): Rectangle<T> =
Rectangle(viewPoint.focus, viewPoint.zoom, size)

View File

@ -0,0 +1,63 @@
package center.sciprog.maps.features
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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>,
) {
public abstract val space: CoordinateSpace<T>
private var canvasSizeState: MutableState<DpSize?> = mutableStateOf(null)
private var viewPointState: MutableState<ViewPoint<T>?> = mutableStateOf(null)
public var canvasSize: DpSize
get() = canvasSizeState.value ?: DpSize(512.dp, 512.dp)
set(value) {
canvasSizeState.value = value
}
public var viewPoint: ViewPoint<T>
get() = viewPointState.value ?: space.defaultViewPoint
set(value) {
viewPointState.value = value
config.onViewChange(viewPoint)
}
public val zoom: Float get() = viewPoint.zoom
// 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 val DpRect.topLeft: DpOffset get() = DpOffset(left, top)
public val DpRect.bottomRight: DpOffset get() = DpOffset(right, bottom)

View File

@ -0,0 +1,56 @@
package center.sciprog.maps.features
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.isPrimaryPressed
/**
* @param result - the endpoint of the drag to perform constrained drag
* @param handleNext - if false do not evaluate subsequent drag handles
*/
public data class DragResult<T : Any>(val result: ViewPoint<T>, val handleNext: Boolean = true)
public fun interface DragListener<in T : Any> {
public fun handle(event: PointerEvent, from: ViewPoint<T>, to: ViewPoint<T>)
}
public fun interface DragHandle<T : Any> {
/**
* @param event - qualifiers of the event used for drag
* @param start - is a point where drag begins, end is a point where drag ends
* @param end - end point of the drag
*
* @return true if default event processors should be used after this one
*/
public fun handle(event: PointerEvent, start: ViewPoint<T>, end: ViewPoint<T>): DragResult<T>
public companion object {
public fun <T : Any> bypass(): DragHandle<T> = DragHandle<T> { _, _, end -> DragResult(end) }
/**
* Process only events with primary button pressed
*/
public fun <T : Any> withPrimaryButton(
block: (event: PointerEvent, start: ViewPoint<T>, end: ViewPoint<T>) -> DragResult<T>,
): DragHandle<T> = DragHandle { event, start, end ->
if (event.buttons.isPrimaryPressed) {
block(event, start, end)
} else {
DragResult(end)
}
}
/**
* Combine several handles into one
*/
public fun <T : Any> combine(vararg handles: DragHandle<T>): DragHandle<T> = DragHandle { event, start, end ->
var current: ViewPoint<T> = end
handles.forEach {
val result = it.handle(event, start, current)
if (!result.handleNext) return@DragHandle result else {
current = result.result
}
}
return@DragHandle DragResult(current)
}
}
}

View File

@ -0,0 +1,364 @@
package center.sciprog.maps.features
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.VectorPainter
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 space.kscience.kmath.geometry.Angle
import space.kscience.kmath.nd.Structure2D
public typealias FloatRange = ClosedFloatingPointRange<Float>
public interface Feature<T : Any> {
public val space: CoordinateSpace<T>
public val attributes: Attributes
public fun getBoundingBox(zoom: Float = Float.MAX_VALUE): Rectangle<T>?
public fun withAttributes(modify: Attributes.() -> Attributes): Feature<T>
}
public val Feature<*>.color: Color? get() = attributes[ColorAttribute]
public val Feature<*>.zoomRange: FloatRange
get() = attributes[ZoomRangeAttribute] ?: Float.NEGATIVE_INFINITY..Float.POSITIVE_INFINITY
public val Feature<*>.name: String?
get() = attributes[NameAttribute]
public interface PainterFeature<T : Any> : Feature<T> {
@Composable
public fun getPainter(): Painter
}
public 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 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 val center: T
}
public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
space: CoordinateSpace<T>,
zoom: Float,
): Rectangle<T>? = with(space) {
mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
}
/**
* A feature that decides what to show depending on the zoom value (it could change size of shape)
*/
@Stable
public data class FeatureSelector<T : Any>(
override val space: CoordinateSpace<T>,
override val attributes: Attributes = Attributes.EMPTY,
public val selector: (zoom: Float) -> Feature<T>,
) : Feature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T>? = selector(zoom).getBoundingBox(zoom)
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
@Stable
public data class PathFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>,
public val path: Path,
public val brush: Brush,
public val style: DrawStyle = Fill,
public val targetRect: Rect = path.getBounds(),
override val attributes: Attributes = Attributes.EMPTY,
) : DraggableFeature<T> {
override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
PathFeature(
space = space,
rectangle = rectangle.withCenter(newCoordinates),
path = path,
brush = brush,
style = style,
targetRect = targetRect,
)
}
override fun getBoundingBox(zoom: Float): Rectangle<T> = rectangle
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
@Stable
public data class PointsFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val points: List<T>,
override val attributes: Attributes = Attributes.EMPTY,
) : Feature<T> {
private val boundingBox by lazy {
with(space) { points.wrapPoints() }
}
override fun getBoundingBox(zoom: Float): Rectangle<T>? = boundingBox
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
public interface LineSegmentFeature<T : Any> : Feature<T>
@Stable
public data class LineFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val a: T,
public val b: T,
override val attributes: Attributes = Attributes.EMPTY,
) : DomainFeature<T>, LineSegmentFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(a, b)
val center: T by lazy { getBoundingBox().center }
override fun contains(viewPoint: ViewPoint<T>): Boolean = with(space) {
val length = a.distanceTo(b, viewPoint.zoom)
viewPoint.focus.distanceTo(center, viewPoint.zoom) <= length / 2 &&
viewPoint.focus.distanceToLine(a, b, viewPoint.zoom).value <= clickRadius
}
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
public data class MultiLineFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val points: List<T>,
override val attributes: Attributes = Attributes.EMPTY,
) : DomainFeature<T>, LineSegmentFeature<T> {
private val boundingBox by lazy {
with(space) { points.wrapPoints() }
}
override fun getBoundingBox(zoom: Float): Rectangle<T>? = boundingBox
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
override fun contains(viewPoint: ViewPoint<T>): Boolean = with(space) {
val boundingBox = getBoundingBox(viewPoint.zoom) ?: return@with false
viewPoint.focus in boundingBox && points.zipWithNext().minOf { (a, b) ->
viewPoint.focus.distanceToLine(
a,
b,
viewPoint.zoom
).value
} < clickRadius
}
}
private val <T : Any, F : LineSegmentFeature<T>> F.clickRadius get() = attributes[ClickRadius] ?: 10f
@Stable
public data class PolygonFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val points: List<T>,
override val attributes: Attributes = Attributes.EMPTY,
) : DomainFeature<T> {
private val boundingBox: Rectangle<T>? by lazy {
with(space) { points.wrapPoints() }
}
override fun getBoundingBox(zoom: Float): Rectangle<T>? = boundingBox
override fun contains(viewPoint: ViewPoint<T>): Boolean {
val boundingBox = boundingBox ?: return false
return viewPoint.focus in boundingBox && with(space) { viewPoint.focus.isInsidePolygon(points) }
}
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
@Stable
public data class CircleFeature<T : Any>(
override val space: CoordinateSpace<T>,
override val center: T,
public val radius: Dp = 5.dp,
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(center, zoom, DpSize(radius * 2, radius * 2))
override fun contains(viewPoint: ViewPoint<T>): Boolean = with(space) {
viewPoint.focus.distanceTo(center, viewPoint.zoom) < radius
}
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(center = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
@Stable
public data class RectangleFeature<T : Any>(
override val space: CoordinateSpace<T>,
override val center: T,
public val size: DpSize = DpSize(5.dp, 5.dp),
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(center, zoom, size)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(center = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
/**
* @param startAngle the angle from 3 o'clock downwards for the start of the arc in radians
* @param arcLength arc length in radians
*/
@Stable
public data class ArcFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val oval: Rectangle<T>,
public val startAngle: Angle,
public val arcLength: Angle,
override val attributes: Attributes = Attributes.EMPTY,
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = oval
override fun withCoordinates(newCoordinates: T): Feature<T> =
copy(oval = with(space) { oval.withCenter(newCoordinates) })
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
public data class DrawFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val position: T,
override val attributes: Attributes = Attributes.EMPTY,
public val drawFeature: DrawScope.() -> Unit,
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
/**
* Fixed size bitmap icon
*/
@Stable
public data class BitmapIconFeature<T : Any>(
override val space: CoordinateSpace<T>,
override val center: T,
public val size: DpSize,
public val image: ImageBitmap,
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(center, zoom, size)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(center = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
/**
* Fixed size vector icon
*/
@Stable
public data class VectorIconFeature<T : Any>(
override val space: CoordinateSpace<T>,
override val center: T,
public val size: DpSize,
public val image: ImageVector,
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T>, PainterFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(center, zoom, size)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(center = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
@Composable
override fun getPainter(): VectorPainter = rememberVectorPainter(image)
}
/**
* 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.
*/
public data class ScalableImageFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>,
override val attributes: Attributes = Attributes.EMPTY,
public val painter: @Composable () -> Painter,
) : Feature<T>, PainterFeature<T> {
@Composable
override fun getPainter(): Painter = painter.invoke()
override fun getBoundingBox(zoom: Float): Rectangle<T> = rectangle
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
public data class TextFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val position: T,
public val text: String,
override val attributes: Attributes = Attributes.EMPTY,
public val fontConfig: FeatureFont.() -> Unit,
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
/**
* A pixel map representation on the map/scheme.
* [rectangle] describes the boundary of the pixel map.
* [pixelMap] contained indexed color of pixels. Minimal indices correspond to bottom-left corner of the rectangle.
* Maximal indices - top-right.
*/
public data class PixelMapFeature<T : Any>(
override val space: CoordinateSpace<T>,
val rectangle: Rectangle<T>,
val pixelMap: Structure2D<Color?>,
override val attributes: Attributes = Attributes.EMPTY,
) : Feature<T> {
init {
require(pixelMap.shape[0] > 0) { "Empty dimensions in pixel map are not allowed" }
require(pixelMap.shape[1] > 0) { "Empty dimensions in pixel map are not allowed" }
}
override fun getBoundingBox(zoom: Float): Rectangle<T> = rectangle
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}

View File

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

View File

@ -0,0 +1,305 @@
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)
)

View File

@ -0,0 +1,25 @@
package center.sciprog.maps.features
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.isPrimaryPressed
public fun interface MouseListener<in T : Any> {
public fun handle(event: PointerEvent, point: ViewPoint<T>): Unit
public companion object {
public fun <T : Any> withPrimaryButton(
block: (event: PointerEvent, click: ViewPoint<T>) -> Unit,
): MouseListener<T> = MouseListener { event, click ->
if (event.buttons.isPrimaryPressed) {
block(event, click)
}
}
}
}
//@OptIn(ExperimentalFoundationApi::class)
//public class TapListener<in T : Any>(
// public val pointerMatcher: PointerMatcher,
// public val keyboardFilter: PointerKeyboardModifiers.() -> Boolean = { true },
// public val onTap: (point: ViewPoint<T>) -> Unit,
//)

View File

@ -0,0 +1,14 @@
package center.sciprog.maps.features
import androidx.compose.ui.unit.DpSize
public data class ViewConfig<T : Any>(
val zoomSpeed: Float = 1f / 3f,
val onClick: MouseListener<T>? = null,
val dragHandle: DragHandle<T>? = null,
val onViewChange: ViewPoint<T>.() -> Unit = {},
val onSelect: (Rectangle<T>) -> Unit = {},
val onCanvasSizeChange: (DpSize) -> Unit = {},
val zoomOnSelect: Boolean = true,
val zoomOnDoubleClick: Boolean = true
)

View File

@ -0,0 +1,9 @@
package center.sciprog.maps.features
/**
* @param T type of coordinates used for the view point
*/
public interface ViewPoint<out T: Any> {
public val focus: T
public val zoom: Float
}

View File

@ -0,0 +1,82 @@
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)
}

View File

@ -0,0 +1,172 @@
package center.sciprog.maps.features
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher
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
public object ZAttribute : Attribute<Float>
public object DraggableAttribute : Attribute<DragHandle<Any>>
public object DragListenerAttribute : SetAttribute<DragListener<Any>>
/**
* Click radius for point-like and line objects
*/
public object ClickRadius : Attribute<Float>
public object ClickListenerAttribute : SetAttribute<MouseListener<Any>>
public object HoverListenerAttribute : SetAttribute<MouseListener<Any>>
//public object TapListenerAttribute : SetAttribute<TapListener<Any>>
public object VisibleAttribute : Attribute<Boolean>
public object ColorAttribute : Attribute<Color>
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.color(color: Color): FeatureRef<T, F> =
modifyAttribute(ColorAttribute, color)
public object ZoomRangeAttribute : Attribute<FloatRange>
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.zoomRange(range: FloatRange): FeatureRef<T, F> =
modifyAttribute(ZoomRangeAttribute, range)
public object AlphaAttribute : Attribute<Float>
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes(modify: AttributesBuilder.() -> Unit): FeatureRef<T, F> {
@Suppress("UNCHECKED_CAST")
parent.feature(
id,
resolve().withAttributes {
AttributesBuilder(this).apply(modify).build()
} as F
)
return this
}
public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(
key: Attribute<V>,
value: V?,
): FeatureRef<T, F> {
@Suppress("UNCHECKED_CAST")
parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F)
return this
}
/**
* Add drag to this feature
*
* @param constraint optional drag constraint
*/
@Suppress("UNCHECKED_CAST")
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) {
if (attributes[DraggableAttribute] == null) {
val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
start as ViewPoint<T>
end as ViewPoint<T>
if (start in feature) {
val finalPosition = constraint?.invoke(end.focus) ?: end.focus
feature(id, feature.withCoordinates(finalPosition))
feature.attributes[DragListenerAttribute]?.forEach {
it.handle(event, start, ViewPoint(finalPosition, end.zoom))
}
DragResult(ViewPoint(finalPosition, end.zoom), false)
} else {
DragResult(end, true)
}
}
modifyAttribute(DraggableAttribute, handle)
}
//Apply callback
if (listener != null) {
onDrag(listener)
}
return this@draggable
}
@Suppress("UNCHECKED_CAST")
public fun <T : Any, F : DraggableFeature<T>> FeatureRef<T, F>.onDrag(
listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit,
): FeatureRef<T, F> = modifyAttributes {
DragListenerAttribute.add(
DragListener { event, from, to -> event.listener(from as ViewPoint<T>, to as ViewPoint<T>) }
)
}
@Suppress("UNCHECKED_CAST")
public fun <T : Any, F : DomainFeature<T>> FeatureRef<T, F>.onClick(
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
): FeatureRef<T, F> = modifyAttributes {
ClickListenerAttribute.add(
MouseListener { event, point ->
event.onClick(point as ViewPoint<T>)
}
)
}
@OptIn(ExperimentalFoundationApi::class)
@Suppress("UNCHECKED_CAST")
public fun <T : Any, F : DomainFeature<T>> FeatureRef<T, F>.onClick(
pointerMatcher: PointerMatcher,
keyboardModifiers: PointerKeyboardModifiers.() -> Boolean = { true },
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
): FeatureRef<T, F> = modifyAttributes {
ClickListenerAttribute.add(
MouseListener { event, point ->
if (pointerMatcher.matches(event) && keyboardModifiers(event.keyboardModifiers)) {
event.onClick(point as ViewPoint<T>)
}
}
)
}
@Suppress("UNCHECKED_CAST")
public fun <T : Any, F : DomainFeature<T>> FeatureRef<T, F>.onHover(
onClick: PointerEvent.(move: ViewPoint<T>) -> Unit,
): FeatureRef<T, F> = modifyAttributes {
HoverListenerAttribute.add(
MouseListener { event, point -> event.onClick(point as ViewPoint<T>) }
)
}
// @Suppress("UNCHECKED_CAST")
// @OptIn(ExperimentalFoundationApi::class)
// public fun <F : DomainFeature<T>> FeatureId<F>.onTap(
// pointerMatcher: PointerMatcher = PointerMatcher.Primary,
// keyboardFilter: PointerKeyboardModifiers.() -> Boolean = { true },
// onTap: (point: ViewPoint<T>) -> Unit,
// ): FeatureId<F> = modifyAttributes {
// TapListenerAttribute.add(
// TapListener(pointerMatcher, keyboardFilter) { point -> onTap(point as ViewPoint<T>) }
// )
// }
public object PathEffectAttribute : Attribute<PathEffect>
public fun <T : Any> FeatureRef<T, LineSegmentFeature<T>>.pathEffect(effect: PathEffect): FeatureRef<T, LineSegmentFeature<T>> =
modifyAttribute(PathEffectAttribute, effect)
public object StrokeAttribute : Attribute<Float>
public fun <T : Any, F : LineSegmentFeature<T>> FeatureRef<T, F>.stroke(width: Float): FeatureRef<T, F> =
modifyAttribute(StrokeAttribute, width)

View File

@ -0,0 +1,87 @@
package center.sciprog.attributes
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import kotlinx.serialization.protobuf.ProtoBuf
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertEquals
internal class AttributesSerializationTest {
@Serializable
internal class Container(@Contextual val attributes: Attributes) {
override fun equals(other: Any?): Boolean = (other as? Container)?.attributes?.equals(attributes) ?: false
override fun hashCode(): Int = attributes.hashCode()
override fun toString(): String = attributes.toString()
}
internal object ContainerAttribute : SerializableAttribute<Container>("container", serializer()) {
override fun toString(): String = "container"
}
internal object TestAttribute : SerializableAttribute<Map<String, String>>("test", serializer()) {
override fun toString(): String = "test"
}
@Test
fun restoreFromJson() {
val json = Json {
serializersModule = SerializersModule {
contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute)))
}
}
val attributes = Attributes {
NameAttribute("myTest")
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
ContainerAttribute(
Container(
Attributes {
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
}
)
)
}
val serialized: String = json.encodeToString(attributes)
println(serialized)
val restored: Attributes = json.decodeFromString(serialized)
assertEquals(attributes, restored)
}
@OptIn(ExperimentalSerializationApi::class)
@Test
@Ignore
fun restoreFromProtoBuf() {
val protoBuf = ProtoBuf {
serializersModule = SerializersModule {
contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute)))
}
}
val attributes = Attributes {
NameAttribute("myTest")
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
ContainerAttribute(
Container(
Attributes {
TestAttribute(mapOf("a" to "aa", "b" to "bb"))
}
)
)
}
val serialized = protoBuf.encodeToByteArray(attributes)
val restored: Attributes = protoBuf.decodeFromByteArray(serialized)
assertEquals(attributes, restored)
}
}

View File

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

View File

@ -0,0 +1,306 @@
package center.sciprog.maps.compose
import androidx.compose.foundation.gestures.GestureCancellationException
import androidx.compose.foundation.gestures.PressGestureScope
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.unit.Density
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
/*
* Clone of tap gestures for mouse
*/
private val NoPressGesture: suspend PressGestureScope.(event: PointerEvent) -> Unit = { }
internal fun PointerEvent.consume() = changes.forEach { it.consume() }
internal val PointerEvent.firstChange get() = changes.first()
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].
*/
public suspend fun PointerInputScope.detectClicks(
onDoubleClick: (Density.(PointerEvent) -> Unit)? = null,
onLongClick: (Density.(PointerEvent) -> Unit)? = null,
onPress: suspend PressGestureScope.(event: PointerEvent) -> Unit = NoPressGesture,
onClick: (Density.(PointerEvent) -> Unit)? = null,
): Unit = coroutineScope {
// special signal to indicate to the sending side that it shouldn't intercept and consume
// cancel/up events as we're only require down events
val pressScope = PressGestureScopeImpl(this@detectClicks)
awaitEachGesture {
val down = awaitFirstDownEvent()
down.consume()
pressScope.reset()
if (onPress !== NoPressGesture) launch {
pressScope.onPress(down)
}
val longPressTimeout = onLongClick?.let {
viewConfiguration.longPressTimeoutMillis
} ?: (Long.MAX_VALUE / 2)
var upOrCancel: PointerEvent? = null
try {
// wait for first tap up or long press
upOrCancel = withTimeout(longPressTimeout) {
waitForUpOrCancellation()
}
if (upOrCancel == null) {
pressScope.cancel() // tap-up was canceled
} else {
upOrCancel.consume()
pressScope.release()
}
} catch (_: PointerEventTimeoutCancellationException) {
onLongClick?.invoke(this, down)
consumeUntilUp()
pressScope.release()
}
if (upOrCancel != null) {
// tap was successful.
if (onDoubleClick == null) {
onClick?.invoke(this, down) // no need to check for double-tap.
} else {
// check for second tap
val secondDown = awaitSecondDown(upOrCancel.firstChange)
if (secondDown == null) {
onClick?.invoke(this, down) // no valid second tap started
} else {
// Second tap down detected
pressScope.reset()
if (onPress !== NoPressGesture) {
launch { pressScope.onPress(secondDown) }
}
try {
// Might have a long second press as the second tap
withTimeout(longPressTimeout) {
val secondUp = waitForUpOrCancellation()
if (secondUp != null) {
secondUp.consume()
pressScope.release()
onDoubleClick(secondDown)
} else {
pressScope.cancel()
onClick?.invoke(this, down)
}
}
} catch (e: PointerEventTimeoutCancellationException) {
// The first tap was valid, but the second tap is a long press.
// notify for the first tap
onClick?.invoke(this, down)
// notify for the long press
onLongClick?.invoke(this, secondDown)
consumeUntilUp()
pressScope.release()
}
}
}
}
}
}
/**
* Consumes all pointer events until nothing is pressed and then returns. This method assumes
* that something is currently pressed.
*/
private suspend fun AwaitPointerEventScope.consumeUntilUp() {
do {
val event = awaitPointerEvent()
event.consume()
} while (event.changes.any { it.pressed })
}
/**
* Waits for [ViewConfiguration.doubleTapTimeoutMillis] for a second press event. If a
* second press event is received before the time-out, it is returned or `null` is returned
* if no second press is received.
*/
private suspend fun AwaitPointerEventScope.awaitSecondDown(
firstUp: PointerInputChange,
): PointerEvent? = withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) {
val minUptime = firstUp.uptimeMillis + viewConfiguration.doubleTapMinTimeMillis
var event: PointerEvent
// The second tap doesn't count if it happens before DoubleTapMinTime of the first tap
do {
event = awaitFirstDownEvent()
} while (event.firstChange.uptimeMillis < minUptime)
event
}
///**
// * Shortcut for cases when we only need to get press/click logic, as for cases without long press
// * and double click we don't require channelling or any other complications.
// *
// * 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].
// */
//internal suspend fun PointerInputScope.detectTapAndPress(
// onPress: suspend PressGestureScope.(event: PointerEvent, Offset) -> Unit = NoPressGesture,
// onTap: ((Offset) -> Unit)? = null,
//) {
// val pressScope = PressGestureScopeImpl(this)
// forEachGesture {
// coroutineScope {
// pressScope.reset()
// awaitPointerEventScope {
//
// val down = awaitFirstDownEvent()
//
// down.consume()
//
// if (onPress !== NoPressGesture) {
// launch { pressScope.onPress(down, down.position) }
// }
//
// val up = waitForUpOrCancellation()
// if (up == null) {
// pressScope.cancel() // tap-up was canceled
// } else {
// up.consume()
// pressScope.release()
// onTap?.invoke(up.position)
// }
// }
// }
// }
//}
/**
* Reads events until the first down is received. If [requireUnconsumed] is `true` and the first
* down is consumed in the [PointerEventPass.Main] pass, that gesture is ignored.
*/
internal suspend fun AwaitPointerEventScope.awaitFirstDownEvent(
requireUnconsumed: Boolean = true,
): PointerEvent = awaitFirstDownEventOnPass(pass = PointerEventPass.Main, requireUnconsumed = requireUnconsumed)
internal suspend fun AwaitPointerEventScope.awaitFirstDownEventOnPass(
pass: PointerEventPass,
requireUnconsumed: Boolean,
): PointerEvent {
var event: PointerEvent
do {
event = awaitPointerEvent(pass)
} while (!event.changes.all {
if (requireUnconsumed) it.changedToDown() else it.changedToDownIgnoreConsumed()
})
return event
}
/**
* Reads events until all pointers are up or the gesture was canceled. The gesture
* is considered canceled when a pointer leaves the event region, a position change
* has been consumed or a pointer down change event was consumed in the [PointerEventPass.Main]
* pass. If the gesture was not canceled, the final up change is returned or `null` if the
* event was canceled.
*/
internal suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerEvent? {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Main)
if (event.changes.all { it.changedToUp() }) {
// All pointers are up
return event
}
if (event.changes.any {
it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)
}
) {
return null // Canceled
}
// Check for cancel by position consumption. We can look on the Final pass of the
// existing pointer event because it comes after the Main pass we checked above.
val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
if (consumeCheck.changes.any { it.isConsumed }) {
return null
}
}
}
/**
* [detectTapGestures]'s implementation of [PressGestureScope].
*/
internal class PressGestureScopeImpl(
density: Density,
) : PressGestureScope, Density by density {
private var isReleased = false
private var isCanceled = false
private val mutex = Mutex(locked = false)
/**
* Called when a gesture has been canceled.
*/
fun cancel() {
isCanceled = true
mutex.unlock()
}
/**
* Called when all pointers are up.
*/
fun release() {
isReleased = true
mutex.unlock()
}
/**
* Called when a new gesture has started.
*/
fun reset() {
mutex.tryLock() // If tryAwaitRelease wasn't called, this will be unlocked.
isReleased = false
isCanceled = false
}
override suspend fun awaitRelease() {
if (!tryAwaitRelease()) {
throw GestureCancellationException("The press gesture was canceled.")
}
}
override suspend fun tryAwaitRelease(): Boolean {
if (!isReleased && !isCanceled) {
mutex.lock()
}
return isReleased
}
}

View File

@ -0,0 +1,189 @@
package center.sciprog.maps.compose
import androidx.compose.foundation.gestures.drag
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
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 kotlin.math.max
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>,
): Modifier = with(state) {
// //selecting all tapabales ahead of time
// val allTapable = buildMap {
// features.forEachWithAttribute(TapListenerAttribute) { _, feature, listeners ->
// put(feature, listeners)
// }
// }
pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
val coordinates = event.changes.first().position.toCoordinates(this)
val point = space.ViewPoint(coordinates, zoom)
if (event.type == PointerEventType.Move) {
features.forEachWithAttribute(HoverListenerAttribute) { _, feature, listeners ->
if (point in feature as DomainFeature) {
listeners.forEach { it.handle(event, point) }
return@forEachWithAttribute
}
}
}
}
}
}.pointerInput(Unit) {
detectClicks(
onDoubleClick = if (state.config.zoomOnDoubleClick) {
{ event ->
val invariant = event.position.toCoordinates(this)
viewPoint = with(space) {
viewPoint.zoomBy(
if (event.buttons.isPrimaryPressed) 1f else if (event.buttons.isSecondaryPressed) -1f else 0f,
invariant
)
}
}
} else null,
onClick = { event ->
val coordinates = event.position.toCoordinates(this)
val point = space.ViewPoint(coordinates, zoom)
config.onClick?.handle(
event,
point
)
features.forEachWithAttributeUntil(ClickListenerAttribute) { _, feature, listeners ->
if (point in (feature as DomainFeature)) {
listeners.forEach { it.handle(event, point) }
false
} else {
true
}
}
}
)
}.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event: PointerEvent = awaitPointerEvent()
event.changes.forEach { change ->
if (event.type == PointerEventType.Scroll) {
val (xPos, yPos) = change.position
//compute invariant point of translation
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
viewPoint = with(space) {
viewPoint.zoomBy(-change.scrollDelta.y * config.zoomSpeed, invariant)
}
change.consume()
}
//val dragStart = change.position
//val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
//start selection
val selectionStart: Offset? =
if (event.buttons.isPrimaryPressed && event.keyboardModifiers.isShiftPressed) {
change.position
} else {
null
}
drag(change.id) { dragChange ->
val dragAmount: Offset = dragChange.position - dragChange.previousPosition
//apply drag handle and check if it prohibits the drag even propagation
if (selectionStart == null) {
val dragStart = space.ViewPoint(
dragChange.previousPosition.toCoordinates(this),
zoom
)
val dragEnd = space.ViewPoint(
dragChange.position.toCoordinates(this),
zoom
)
val dragResult = config.dragHandle?.handle(event, dragStart, dragEnd)
if (dragResult?.handleNext == false) return@drag
var continueAfter = true
features.forEachWithAttributeUntil(DraggableAttribute) { _, _, handler ->
handler.handle(event, dragStart, dragEnd).handleNext.also {
if (!it) continueAfter = false
}
}
if (!continueAfter) return@drag
}
if (event.buttons.isPrimaryPressed) {
//If selection process is started, modify the frame
selectionStart?.let { start ->
val offset = dragChange.position
selectRect = DpRect(
min(offset.x, start.x).dp,
min(offset.y, start.y).dp,
max(offset.x, start.x).dp,
max(offset.y, start.y).dp
)
return@drag
}
// config.onClick(MapViewPoint(dpPos.toGeodetic(), viewPoint.zoom), event)
//If no selection, drag map
viewPoint = viewPoint.moveBy(
-dragAmount.x.toDp(),
dragAmount.y.toDp()
)
}
}
// evaluate selection
selectRect?.let { rect ->
//Use selection override if it is defined
val coordinateRect = space.Rectangle(
rect.topLeft.toCoordinates(),
rect.bottomRight.toCoordinates()
)
config.onSelect(coordinateRect)
if (config.zoomOnSelect) {
viewPoint = computeViewPoint(coordinateRect)
}
selectRect = null
}
}
}
}
}
}
/*
.pointerInput(Unit) {
allTapable.forEach { (feature, listeners) ->
listeners.forEach { listener ->
detectTapGestures(listener.pointerMatcher, listener.keyboardFilter) { offset ->
val point = space.ViewPoint(offset.toCoordinates(this@pointerInput), zoom)
if (point in feature as DomainFeature) {
listener.onTap(point)
}
}
}
}
}
*/

View File

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

View File

@ -0,0 +1,198 @@
package center.sciprog.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 space.kscience.kmath.PerformancePitfall
import space.kscience.kmath.geometry.degrees
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>,
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)
when (feature) {
is FeatureSelector -> drawFeature(state, painterCache, feature.selector(state.zoom))
is CircleFeature -> drawCircle(
color,
feature.radius.toPx(),
center = feature.center.toOffset()
)
is RectangleFeature -> drawRect(
color,
topLeft = feature.center.toOffset() - Offset(
feature.size.width.toPx() / 2,
feature.size.height.toPx() / 2
),
size = feature.size.toSize()
)
is LineFeature -> drawLine(
color,
feature.a.toOffset(),
feature.b.toOffset(),
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pathEffect = feature.attributes[PathEffectAttribute]
)
is ArcFeature -> {
val dpRect = feature.oval.toDpRect().toRect()
val size = Size(dpRect.width, dpRect.height)
drawArc(
color = color,
startAngle = (feature.startAngle.degrees).toFloat(),
sweepAngle = (feature.arcLength.degrees).toFloat(),
useCenter = false,
topLeft = dpRect.topLeft,
size = size,
style = Stroke(),
alpha = alpha
)
}
is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset())
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)
}
}
}
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 DrawFeature -> {
val offset = feature.position.toOffset()
translate(offset.x, offset.y) {
feature.drawFeature(this)
}
}
is FeatureGroup -> {
feature.featureMap.values.forEach {
drawFeature(state, painterCache, it.withAttributes {
feature.attributes + this
})
}
}
is PathFeature -> {
TODO("MapPathFeature not implemented")
// val offset = feature.rectangle.center.toOffset() - feature.targetRect.center
// translate(offset.x, offset.y) {
// sca
// drawPath(feature.path, brush = feature.brush, style = feature.style)
// }
}
is PointsFeature -> {
val points = feature.points.map { it.toOffset() }
drawPoints(
points = points,
color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Points,
pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha
)
}
is MultiLineFeature -> {
val points = feature.points.map { it.toOffset() }
drawPoints(
points = points,
color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Polygon,
pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha
)
}
is PolygonFeature -> {
val points = feature.points.map { it.toOffset() }
val last = points.last()
val polygonPath = Path()
polygonPath.moveTo(last.x, last.y)
for ((x, y) in points) {
polygonPath.lineTo(x, y)
}
drawPath(
path = polygonPath,
color = color,
alpha = alpha
)
}
is ScalableImageFeature -> {
val rect = feature.rectangle.toDpRect().toRect()
val offset = rect.topLeft
translate(offset.x, offset.y) {
with(painterCache[feature]!!) {
draw(rect.size)
}
}
}
is PixelMapFeature -> {
val rect = feature.rectangle.toDpRect().toRect()
val xStep = rect.size.width / feature.pixelMap.shape[0]
val yStep = rect.size.height / feature.pixelMap.shape[1]
val pixelSize = Size(xStep, yStep)
//TODO add re-clasterization for small pixel scales
val offset = rect.topLeft
translate(offset.x, offset.y) {
@OptIn(PerformancePitfall::class)
feature.pixelMap.elements().forEach { (index, color: Color?) ->
val (i, j) = index
if (color != null) {
drawRect(
color,
topLeft = Offset(
x = i * xStep,
y = rect.height - j * yStep
),
size = pixelSize
)
}
}
}
}
else -> {
//logger.error { "Unrecognized feature type: ${feature::class}" }
}
}
}

32
maps-kt-geojson/README.md Normal file
View File

@ -0,0 +1,32 @@
# Module maps-kt-geojson
## Usage
## Artifact:
The Maven coordinates of this project are `center.sciprog:maps-kt-geojson:0.2.2`.
**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 {
maven("https://repo.kotlin.link")
mavenCentral()
}
dependencies {
implementation("center.sciprog:maps-kt-geojson:0.2.2")
}
```

View File

@ -0,0 +1,224 @@
public abstract interface class center/sciprog/maps/geojson/GeoJson {
public static final field Companion Lcenter/sciprog/maps/geojson/GeoJson$Companion;
public static final field PROPERTIES_KEY Ljava/lang/String;
public static final field TYPE_KEY Ljava/lang/String;
public abstract fun getJson ()Lkotlinx/serialization/json/JsonObject;
public fun getType ()Ljava/lang/String;
}
public final class center/sciprog/maps/geojson/GeoJson$Companion {
public static final field PROPERTIES_KEY Ljava/lang/String;
public static final field TYPE_KEY Ljava/lang/String;
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class center/sciprog/maps/geojson/GeoJsonFeature : center/sciprog/maps/geojson/GeoJson {
public static final field Companion Lcenter/sciprog/maps/geojson/GeoJsonFeature$Companion;
public static final field GEOMETRY_KEY Ljava/lang/String;
public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJsonFeature;
public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z
public static final fun getGeometry-impl (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJsonGeometry;
public fun getJson ()Lkotlinx/serialization/json/JsonObject;
public static final fun getProperties-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject;
public fun hashCode ()I
public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject;
}
public final class center/sciprog/maps/geojson/GeoJsonFeature$Companion {
}
public final class center/sciprog/maps/geojson/GeoJsonFeatureCollection : center/sciprog/maps/geojson/GeoJson, java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker {
public static final field Companion Lcenter/sciprog/maps/geojson/GeoJsonFeatureCollection$Companion;
public static final field FEATURES_KEY Ljava/lang/String;
public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJsonFeatureCollection;
public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z
public static final fun getFeatures-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/util/List;
public fun getJson ()Lkotlinx/serialization/json/JsonObject;
public static final fun getProperties-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject;
public fun hashCode ()I
public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I
public fun iterator ()Ljava/util/Iterator;
public static fun iterator-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/util/Iterator;
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject;
}
public final class center/sciprog/maps/geojson/GeoJsonFeatureCollection$Companion {
public final fun parse-oOQ2h9Q (Ljava/lang/String;)Lkotlinx/serialization/json/JsonObject;
}
public final class center/sciprog/maps/geojson/GeoJsonFeatureJvmKt {
public static final fun geoJson (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/net/URL;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun geoJson$default (Lcenter/sciprog/maps/features/FeatureGroup;Ljava/net/URL;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
}
public abstract interface class center/sciprog/maps/geojson/GeoJsonGeometry : center/sciprog/maps/geojson/GeoJson {
public static final field COORDINATES_KEY Ljava/lang/String;
public static final field Companion Lcenter/sciprog/maps/geojson/GeoJsonGeometry$Companion;
}
public final class center/sciprog/maps/geojson/GeoJsonGeometry$Companion {
public static final field COORDINATES_KEY Ljava/lang/String;
}
public final class center/sciprog/maps/geojson/GeoJsonGeometryCollection : center/sciprog/maps/geojson/GeoJsonGeometry {
public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJsonGeometryCollection;
public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z
public static final fun getGeometries-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/util/List;
public fun getJson ()Lkotlinx/serialization/json/JsonObject;
public fun hashCode ()I
public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject;
}
public final class center/sciprog/maps/geojson/GeoJsonGeometryKt {
public static final fun GeoJsonGeometry (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJsonGeometry;
public static final fun GeoJsonGeometryCollection (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/JsonObject;
public static final fun GeoJsonLineString (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/JsonObject;
public static synthetic fun GeoJsonLineString$default (Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/json/JsonObject;
public static final fun GeoJsonMultiLineString (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/JsonObject;
public static synthetic fun GeoJsonMultiLineString$default (Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/json/JsonObject;
public static final fun GeoJsonMultiPoint (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/JsonObject;
public static synthetic fun GeoJsonMultiPoint$default (Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/json/JsonObject;
public static final fun GeoJsonMultiPolygon (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/JsonObject;
public static final fun GeoJsonPoint (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/JsonObject;
public static synthetic fun GeoJsonPoint$default (Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/json/JsonObject;
public static final fun GeoJsonPolygon (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/JsonObject;
}
public final class center/sciprog/maps/geojson/GeoJsonKt {
public static final fun GeoJson (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJson;
public static final fun GeoJsonFeature (Lcenter/sciprog/maps/geojson/GeoJsonGeometry;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/JsonObject;
public static synthetic fun GeoJsonFeature$default (Lcenter/sciprog/maps/geojson/GeoJsonGeometry;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/json/JsonObject;
public static final fun GeoJsonFeatureCollection (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/JsonObject;
public static synthetic fun GeoJsonFeatureCollection$default (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/json/JsonObject;
public static final fun getProperty-c0BrZC0 (Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;)Lkotlinx/serialization/json/JsonElement;
}
public final class center/sciprog/maps/geojson/GeoJsonLineString : center/sciprog/maps/geojson/GeoJsonGeometry {
public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJsonLineString;
public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z
public static final fun getCoordinates-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/util/List;
public fun getJson ()Lkotlinx/serialization/json/JsonObject;
public fun hashCode ()I
public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject;
}
public final class center/sciprog/maps/geojson/GeoJsonMultiLineString : center/sciprog/maps/geojson/GeoJsonGeometry {
public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJsonMultiLineString;
public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z
public static final fun getCoordinates-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/util/List;
public fun getJson ()Lkotlinx/serialization/json/JsonObject;
public fun hashCode ()I
public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject;
}
public final class center/sciprog/maps/geojson/GeoJsonMultiPoint : center/sciprog/maps/geojson/GeoJsonGeometry {
public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJsonMultiPoint;
public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z
public static final fun getCoordinates-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/util/List;
public fun getJson ()Lkotlinx/serialization/json/JsonObject;
public fun hashCode ()I
public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject;
}
public final class center/sciprog/maps/geojson/GeoJsonMultiPolygon : center/sciprog/maps/geojson/GeoJsonGeometry {
public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJsonMultiPolygon;
public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z
public static final fun getCoordinates-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/util/List;
public fun getJson ()Lkotlinx/serialization/json/JsonObject;
public fun hashCode ()I
public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject;
}
public final class center/sciprog/maps/geojson/GeoJsonPoint : center/sciprog/maps/geojson/GeoJsonGeometry {
public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJsonPoint;
public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z
public static final fun getCoordinates-impl (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/coordinates/GeodeticMapCoordinates;
public fun getJson ()Lkotlinx/serialization/json/JsonObject;
public fun hashCode ()I
public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject;
}
public final class center/sciprog/maps/geojson/GeoJsonPolygon : center/sciprog/maps/geojson/GeoJsonGeometry {
public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lcenter/sciprog/maps/geojson/GeoJsonPolygon;
public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject;
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z
public static final fun getCoordinates-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/util/List;
public fun getJson ()Lkotlinx/serialization/json/JsonObject;
public fun hashCode ()I
public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I
public fun toString ()Ljava/lang/String;
public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String;
public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject;
}
public final class center/sciprog/maps/geojson/GeoJsonPropertiesAttribute : center/sciprog/attributes/SerializableAttribute {
public static final field INSTANCE Lcenter/sciprog/maps/geojson/GeoJsonPropertiesAttribute;
}
public final class center/sciprog/maps/geojson/GeoJsonSerializer : kotlinx/serialization/KSerializer {
public static final field INSTANCE Lcenter/sciprog/maps/geojson/GeoJsonSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcenter/sciprog/maps/geojson/GeoJson;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcenter/sciprog/maps/geojson/GeoJson;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
}
public final class center/sciprog/maps/geojson/GeoJsonToMapKt {
public static final fun geoJson (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/geojson/GeoJson;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun geoJson$default (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/geojson/GeoJson;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun geoJsonFeature-wsmmyRA (Lcenter/sciprog/maps/features/FeatureGroup;Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun geoJsonFeature-wsmmyRA$default (Lcenter/sciprog/maps/features/FeatureGroup;Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
public static final fun geoJsonGeometry (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/geojson/GeoJsonGeometry;Ljava/lang/String;)Lcenter/sciprog/maps/features/FeatureRef;
public static synthetic fun geoJsonGeometry$default (Lcenter/sciprog/maps/features/FeatureGroup;Lcenter/sciprog/maps/geojson/GeoJsonGeometry;Ljava/lang/String;ILjava/lang/Object;)Lcenter/sciprog/maps/features/FeatureRef;
}

View File

@ -0,0 +1,18 @@
plugins {
id("space.kscience.gradle.mpp")
`maven-publish`
}
kscience{
jvm()
js()
useSerialization {
json()
}
dependencies{
api(projects.mapsKtCore)
api(projects.mapsKtFeatures)
api(spclibs.kotlinx.serialization.json)
}
}

View File

@ -0,0 +1,111 @@
package center.sciprog.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 kotlin.jvm.JvmInline
/**
* A utility class to work with GeoJson (https://geojson.org/)
*/
@Serializable(GeoJsonSerializer::class)
public sealed interface GeoJson {
public val json: JsonObject
public val type: String get() = json[TYPE_KEY]?.jsonPrimitive?.content ?: error("Not a GeoJson")
public companion object {
public const val TYPE_KEY: String = "type"
public const val PROPERTIES_KEY: String = "properties"
}
}
@JvmInline
public value class GeoJsonFeature(override val json: JsonObject) : GeoJson {
init {
require(type == "Feature") { "Not a GeoJson Feature" }
}
public val properties: JsonObject? get() = json[PROPERTIES_KEY]?.jsonObject
public val geometry: GeoJsonGeometry? get() = json[GEOMETRY_KEY]?.jsonObject?.let { GeoJsonGeometry(it) }
public companion object {
public const val GEOMETRY_KEY: String = "geometry"
}
}
/**
* A builder function for [GeoJsonFeature]
*/
public fun GeoJsonFeature(
geometry: GeoJsonGeometry?,
properties: JsonObject? = null,
builder: JsonObjectBuilder.() -> Unit = {},
): GeoJsonFeature = GeoJsonFeature(
buildJsonObject {
put(TYPE_KEY, "Feature")
geometry?.json?.let { put(GeoJsonFeature.GEOMETRY_KEY, it) }
properties?.let { put(PROPERTIES_KEY, it) }
builder()
}
)
public fun GeoJsonFeature.getProperty(key: String): JsonElement? = json[key] ?: properties?.get(key)
@JvmInline
public value class GeoJsonFeatureCollection(override val json: JsonObject) : GeoJson, Iterable<GeoJsonFeature> {
init {
require(type == "FeatureCollection") { "Not a GeoJson FeatureCollection" }
}
public val properties: JsonObject? get() = json[PROPERTIES_KEY]?.jsonObject
public val features: List<GeoJsonFeature>
get() = json[FEATURES_KEY]?.jsonArray?.map {
GeoJsonFeature(it.jsonObject)
} ?: error("Features not defined in GeoJson features collection")
override fun iterator(): Iterator<GeoJsonFeature> = features.iterator()
public companion object {
public const val FEATURES_KEY: String = "features"
public fun parse(string: String): GeoJsonFeatureCollection = GeoJsonFeatureCollection(
Json.parseToJsonElement(string).jsonObject
)
}
}
/**
* A builder for [GeoJsonFeatureCollection]
*/
public fun GeoJsonFeatureCollection(
features: List<GeoJsonFeature>,
properties: JsonObject? = null,
builder: JsonObjectBuilder.() -> Unit = {},
): GeoJsonFeatureCollection = GeoJsonFeatureCollection(
buildJsonObject {
put(TYPE_KEY, "FeatureCollection")
putJsonArray(FEATURES_KEY) {
features.forEach {
add(it.json)
}
}
properties?.let { put(PROPERTIES_KEY, it) }
builder()
}
)
/**
* Generic Json to GeoJson converter
*/
public fun GeoJson(json: JsonObject): GeoJson =
when (json[TYPE_KEY]?.jsonPrimitive?.contentOrNull ?: error("Not a GeoJson")) {
"Feature" -> GeoJsonFeature(json)
"FeatureCollection" -> GeoJsonFeatureCollection(json)
else -> GeoJsonGeometry(json)
}

View File

@ -0,0 +1,226 @@
package center.sciprog.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 kotlin.jvm.JvmInline
public sealed interface GeoJsonGeometry : GeoJson {
public companion object {
public const val COORDINATES_KEY: String = "coordinates"
}
}
public fun GeoJsonGeometry(json: JsonObject): GeoJsonGeometry {
return when (val type = json[GeoJson.TYPE_KEY]?.jsonPrimitive?.content ?: error("Not a GeoJson object")) {
"Point" -> GeoJsonPoint(json)
"MultiPoint" -> GeoJsonMultiPoint(json)
"LineString" -> GeoJsonLineString(json)
"MultiLineString" -> GeoJsonMultiLineString(json)
"Polygon" -> GeoJsonPolygon(json)
"MultiPolygon" -> GeoJsonMultiPolygon(json)
"GeometryCollection" -> GeoJsonGeometryCollection(json)
else -> error("Type '$type' is not recognised as a geometry type")
}
}
internal fun JsonElement.toGmc() = jsonArray.run {
Gmc.ofDegrees(
get(1).jsonPrimitive.double,
get(0).jsonPrimitive.double,
getOrNull(2)?.jsonPrimitive?.doubleOrNull?.meters
)
}
internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray {
add(longitude.degrees)
add(latitude.degrees)
elevation?.let {
add(it.meters)
}
}
private fun List<Gmc>.listToJsonArray(): JsonArray = buildJsonArray {
forEach {
add(it.toJsonArray())
}
}
private fun List<List<Gmc>>.listOfListsToJsonArray(): JsonArray = buildJsonArray {
forEach {
add(it.listToJsonArray())
}
}
@JvmInline
public value class GeoJsonPoint(override val json: JsonObject) : GeoJsonGeometry {
init {
require(type == "Point") { "Not a GeoJson Point geometry" }
}
public val coordinates: Gmc
get() = json[COORDINATES_KEY]?.toGmc()
?: error("Coordinates are not provided")
}
public fun GeoJsonPoint(
coordinates: Gmc,
modification: JsonObjectBuilder.() -> Unit = {},
): GeoJsonPoint = GeoJsonPoint(
buildJsonObject {
put(GeoJson.TYPE_KEY, "Point")
put(COORDINATES_KEY, coordinates.toJsonArray())
modification()
}
)
@JvmInline
public value class GeoJsonMultiPoint(override val json: JsonObject) : GeoJsonGeometry {
init {
require(type == "MultiPoint") { "Not a GeoJson MultiPoint geometry" }
}
public val coordinates: List<Gmc>
get() = json[COORDINATES_KEY]?.jsonArray
?.map { it.toGmc() }
?: error("Coordinates are not provided")
}
public fun GeoJsonMultiPoint(
coordinates: List<Gmc>,
modification: JsonObjectBuilder.() -> Unit = {},
): GeoJsonMultiPoint = GeoJsonMultiPoint(
buildJsonObject {
put(GeoJson.TYPE_KEY, "MultiPoint")
put(COORDINATES_KEY, coordinates.listToJsonArray())
modification()
}
)
@JvmInline
public value class GeoJsonLineString(override val json: JsonObject) : GeoJsonGeometry {
init {
require(type == "LineString") { "Not a GeoJson LineString geometry" }
}
public val coordinates: List<Gmc>
get() = json[COORDINATES_KEY]?.jsonArray
?.map { it.toGmc() }
?: error("Coordinates are not provided")
}
public fun GeoJsonLineString(
coordinates: List<Gmc>,
modification: JsonObjectBuilder.() -> Unit = {},
): GeoJsonLineString = GeoJsonLineString(
buildJsonObject {
put(GeoJson.TYPE_KEY, "LineString")
put(COORDINATES_KEY, coordinates.listToJsonArray())
modification()
}
)
@JvmInline
public value class GeoJsonMultiLineString(override val json: JsonObject) : GeoJsonGeometry {
init {
require(type == "MultiLineString") { "Not a GeoJson MultiLineString geometry" }
}
public val coordinates: List<List<Gmc>>
get() = json[COORDINATES_KEY]?.jsonArray?.map { lineJson ->
lineJson.jsonArray.map {
it.toGmc()
}
} ?: error("Coordinates are not provided")
}
public fun GeoJsonMultiLineString(
coordinates: List<List<Gmc>>,
modification: JsonObjectBuilder.() -> Unit = {},
): GeoJsonMultiLineString = GeoJsonMultiLineString(
buildJsonObject {
put(GeoJson.TYPE_KEY, "MultiLineString")
put(COORDINATES_KEY, coordinates.listOfListsToJsonArray())
modification()
}
)
@JvmInline
public value class GeoJsonPolygon(override val json: JsonObject) : GeoJsonGeometry {
init {
require(type == "Polygon") { "Not a GeoJson Polygon geometry" }
}
public val coordinates: List<List<Gmc>>
get() = json[COORDINATES_KEY]?.jsonArray?.map { polygon ->
polygon.jsonArray.map { point ->
point.toGmc()
}
} ?: error("Coordinates are not provided")
}
public fun GeoJsonPolygon(
coordinates: List<List<Gmc>>,
modification: JsonObjectBuilder.() -> Unit
): GeoJsonPolygon = GeoJsonPolygon(
buildJsonObject {
put(GeoJson.TYPE_KEY, "Polygon")
put(COORDINATES_KEY, coordinates.listOfListsToJsonArray())
modification()
}
)
@JvmInline
public value class GeoJsonMultiPolygon(override val json: JsonObject) : GeoJsonGeometry {
init {
require(type == "MultiPolygon") { "Not a GeoJson MultiPolygon geometry" }
}
public val coordinates: List<List<List<Gmc>>>
get() = json[COORDINATES_KEY]?.jsonArray?.map { allPolygons ->
allPolygons.jsonArray.map { polygon ->
polygon.jsonArray.map { point ->
point.toGmc()
}
}
} ?: error("Coordinates are not provided")
}
public fun GeoJsonMultiPolygon(
coordinates: List<List<List<Gmc>>>,
modification: JsonObjectBuilder.() -> Unit
): GeoJsonMultiPolygon = GeoJsonMultiPolygon(
buildJsonObject {
put(GeoJson.TYPE_KEY, "MultiPolygon")
put(COORDINATES_KEY, buildJsonArray { coordinates.forEach { add(it.listOfListsToJsonArray()) } })
modification()
}
)
@JvmInline
public value class GeoJsonGeometryCollection(override val json: JsonObject) : GeoJsonGeometry {
init {
require(type == "GeometryCollection") { "Not a GeoJson GeometryCollection geometry" }
}
public val geometries: List<GeoJsonGeometry>
get() = json["geometries"]?.jsonArray?.map { GeoJsonGeometry(it.jsonObject) } ?: emptyList()
}
public fun GeoJsonGeometryCollection(
geometries: List<GeoJsonGeometry>,
modification: JsonObjectBuilder.() -> Unit
): GeoJsonGeometryCollection =
GeoJsonGeometryCollection(
buildJsonObject {
put(GeoJson.TYPE_KEY, "GeometryCollection")
put("geometries", buildJsonArray {
geometries.forEach {
add(it.json)
}
})
modification()
}
)

View File

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

View File

@ -0,0 +1,21 @@
package center.sciprog.maps.geojson
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonObject
public object GeoJsonSerializer : KSerializer<GeoJson> {
private val serializer = JsonObject.serializer()
override val descriptor: SerialDescriptor
get() = serializer.descriptor
override fun deserialize(decoder: Decoder): GeoJson = GeoJson(serializer.deserialize(decoder))
override fun serialize(encoder: Encoder, value: GeoJson) {
serializer.serialize(encoder, value.json)
}
}

View File

@ -0,0 +1,87 @@
package center.sciprog.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
/**
* Add a single Json geometry to a feature builder
*/
public fun FeatureGroup<Gmc>.geoJsonGeometry(
geometry: GeoJsonGeometry,
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> = when (geometry) {
is GeoJsonLineString -> multiLine(
geometry.coordinates,
)
is GeoJsonMultiLineString -> group(id = id) {
geometry.coordinates.forEach {
multiLine(it)
}
}
is GeoJsonMultiPoint -> points(
geometry.coordinates,
)
is GeoJsonMultiPolygon -> group(id = id) {
geometry.coordinates.forEach {
polygon(
it.first(),
)
}
}
is GeoJsonPoint -> circle(geometry.coordinates, id = id)
is GeoJsonPolygon -> polygon(
geometry.coordinates.first(),
)
is GeoJsonGeometryCollection -> group(id = id) {
geometry.geometries.forEach {
geoJsonGeometry(it)
}
}
}
public fun FeatureGroup<Gmc>.geoJsonFeature(
geoJson: GeoJsonFeature,
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> {
val geometry = geoJson.geometry ?: return group {}
val idOverride = id ?: geoJson.getProperty("id")?.jsonPrimitive?.contentOrNull
return geoJsonGeometry(geometry, idOverride).modifyAttributes {
geoJson.properties?.let {
GeoJsonPropertiesAttribute(it)
}
geoJson.getProperty("name")?.jsonPrimitive?.contentOrNull?.let {
NameAttribute(it)
}
geoJson.getProperty("color")?.jsonPrimitive?.intOrNull?.let {
ColorAttribute(Color(it))
}
}
}
public fun FeatureGroup<Gmc>.geoJson(
geoJson: GeoJson,
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> = when (geoJson) {
is GeoJsonFeature -> geoJsonFeature(geoJson, id = id)
is GeoJsonFeatureCollection -> group(id = id) {
geoJson.features.forEach {
geoJsonFeature(it)
}
}
is GeoJsonGeometry -> geoJsonGeometry(geoJson, id = id)
}

View File

@ -0,0 +1,23 @@
package center.sciprog.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 java.net.URL
/**
* Add geojson features from url
*/
public fun FeatureGroup<Gmc>.geoJson(
geoJsonUrl: URL,
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> {
val jsonString = geoJsonUrl.readText()
val json = Json.parseToJsonElement(jsonString).jsonObject
val geoJson = GeoJson(json)
return geoJson(geoJson, id)
}

View File

@ -0,0 +1,17 @@
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)
}

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