0.3.0 #23
@ -37,7 +37,6 @@ ksciencePublish {
|
|||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
repositories {
|
repositories {
|
||||||
maven("https://maven.pkg.jetbrains.space/mipt-npm/p/sci/dev")
|
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
26
demo/maps-js/build.gradle.kts
Normal file
26
demo/maps-js/build.gradle.kts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
id("org.jetbrains.compose")
|
||||||
|
}
|
||||||
|
|
||||||
|
val ktorVersion: String by rootProject.extra
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
js {
|
||||||
|
browser()
|
||||||
|
binaries.executable()
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
val jsMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.mapsKtCompose)
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.html.core)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compose {
|
||||||
|
web {}
|
||||||
|
}
|
172
demo/maps-js/src/jsMain/kotlin/Main.kt
Normal file
172
demo/maps-js/src/jsMain/kotlin/Main.kt
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.PointerMatcher
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.text.font.createFontFamilyResolver
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import center.sciprog.attributes.Attributes
|
||||||
|
import center.sciprog.maps.compose.*
|
||||||
|
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
|
||||||
|
import center.sciprog.maps.coordinates.Gmc
|
||||||
|
import center.sciprog.maps.coordinates.kilometers
|
||||||
|
import center.sciprog.maps.features.*
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
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.launch
|
||||||
|
import org.jetbrains.compose.web.renderComposable
|
||||||
|
import space.kscience.kmath.geometry.Angle
|
||||||
|
import space.kscience.kmath.geometry.degrees
|
||||||
|
import space.kscience.kmath.geometry.radians
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
public fun GeodeticMapCoordinates.toShortString(): String =
|
||||||
|
"${(latitude.degrees).toString().take(6)}:${(longitude.degrees).toString().take(6)}"
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun App() {
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val mapTileProvider = remember {
|
||||||
|
OpenStreetMapTileProvider(
|
||||||
|
client = HttpClient(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val centerCoordinates = MutableStateFlow<Gmc?>(null)
|
||||||
|
|
||||||
|
val pointOne = 55.568548 to 37.568604
|
||||||
|
val pointTwo = 55.929444 to 37.518434
|
||||||
|
// val pointThree = 60.929444 to 37.518434
|
||||||
|
|
||||||
|
MapView(
|
||||||
|
mapTileProvider = mapTileProvider,
|
||||||
|
config = ViewConfig(
|
||||||
|
onViewChange = { centerCoordinates.value = focus },
|
||||||
|
onClick = { _, viewPoint ->
|
||||||
|
println(viewPoint)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// icon(pointOne, Icons.Filled.Home)
|
||||||
|
|
||||||
|
val marker1 = rectangle(55.744 to 38.614, size = DpSize(10.dp, 10.dp))
|
||||||
|
.color(Color.Magenta)
|
||||||
|
val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp))
|
||||||
|
.color(Color.Magenta)
|
||||||
|
val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp))
|
||||||
|
.color(Color.Magenta)
|
||||||
|
|
||||||
|
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(
|
||||||
|
55.742465 to 37.615812,
|
||||||
|
55.742713 to 37.616370,
|
||||||
|
55.742815 to 37.616659,
|
||||||
|
55.742320 to 37.617132,
|
||||||
|
55.742086 to 37.616566,
|
||||||
|
55.741715 to 37.616716
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
//remember feature ref
|
||||||
|
val circleId = circle(
|
||||||
|
centerCoordinates = pointTwo,
|
||||||
|
)
|
||||||
|
scope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
delay(200)
|
||||||
|
circleId.color(Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
arc(pointOne, 10.0.kilometers, (PI / 4).radians, -Angle.pi / 2)
|
||||||
|
|
||||||
|
|
||||||
|
line(pointOne, pointTwo, id = "line")
|
||||||
|
text(pointOne, "Home", font = { size = 32f })
|
||||||
|
|
||||||
|
|
||||||
|
pixelMap(
|
||||||
|
space.Rectangle(
|
||||||
|
Gmc(latitude = 55.58461879539754.degrees, longitude = 37.8746197303493.degrees),
|
||||||
|
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,
|
||||||
|
alpha = 0.3f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
centerCoordinates.filterNotNull().onEach {
|
||||||
|
group(id = "center") {
|
||||||
|
circle(center = it, id = "circle", size = 1.dp).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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
renderComposable(rootElementId = "root") {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalDensity provides Density(1.0f),
|
||||||
|
LocalLayoutDirection provides LayoutDirection.Ltr,
|
||||||
|
// LocalViewConfiguration provides DefaultViewConfiguration(Density(1.0f)),
|
||||||
|
// LocalInputModeManager provides InputModeManagerObject,
|
||||||
|
LocalFontFamilyResolver provides createFontFamilyResolver()
|
||||||
|
) {
|
||||||
|
App()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
demo/maps-js/src/jsMain/resources/index.html
Normal file
13
demo/maps-js/src/jsMain/resources/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Maps-kt demo</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"/>
|
||||||
|
</body>
|
||||||
|
<script src="maps-js.js"></script>
|
||||||
|
</html>
|
@ -1,6 +1,6 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
|
||||||
compose.version=1.5.1
|
compose.version=1.5.2
|
||||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
||||||
|
|
||||||
agp.version=8.1.0
|
agp.version=8.1.0
|
||||||
|
@ -19,9 +19,21 @@ kotlin {
|
|||||||
api(projects.mapsKtFeatures)
|
api(projects.mapsKtFeatures)
|
||||||
api(compose.foundation)
|
api(compose.foundation)
|
||||||
api(project.dependencies.platform(spclibs.ktor.bom))
|
api(project.dependencies.platform(spclibs.ktor.bom))
|
||||||
api("io.ktor:ktor-client-core")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getByName("jvmMain"){
|
||||||
|
dependencies {
|
||||||
|
api("io.ktor:ktor-client-cio")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getByName("jsMain"){
|
||||||
|
dependencies {
|
||||||
|
api("io.ktor:ktor-client-js")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getByName("jvmTest") {
|
getByName("jvmTest") {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("io.ktor:ktor-client-cio")
|
implementation("io.ktor:ktor-client-cio")
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
package center.sciprog.maps.compose
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.readBytes
|
||||||
|
import io.ktor.http.Url
|
||||||
|
import io.ktor.util.decodeBase64Bytes
|
||||||
|
import io.ktor.util.encodeBase64
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import org.jetbrains.skia.Image
|
||||||
|
import org.w3c.dom.Storage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [MapTileProvider] based on Open Street Map API. With in-memory and file cache
|
||||||
|
*/
|
||||||
|
public class OpenStreetMapTileProvider(
|
||||||
|
private val client: HttpClient,
|
||||||
|
private val storage: Storage = window.localStorage,
|
||||||
|
parallelism: Int = 4,
|
||||||
|
cacheCapacity: Int = 200,
|
||||||
|
private val osmBaseUrl: String = "https://tile.openstreetmap.org",
|
||||||
|
) : MapTileProvider {
|
||||||
|
private val semaphore = Semaphore(parallelism)
|
||||||
|
private val cache = LruCache<TileId, Deferred<Image>>(cacheCapacity)
|
||||||
|
|
||||||
|
private fun TileId.osmUrl() = Url("$osmBaseUrl/${zoom}/${i}/${j}.png")
|
||||||
|
|
||||||
|
private fun TileId.imageName() = "${zoom}/${i}/${j}.png"
|
||||||
|
|
||||||
|
private fun TileId.readImage() = storage.getItem(imageName())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download and cache the tile image
|
||||||
|
*/
|
||||||
|
private fun CoroutineScope.downloadImageAsync(id: TileId): Deferred<Image> = async {
|
||||||
|
|
||||||
|
id.readImage()?.let { imageString ->
|
||||||
|
try {
|
||||||
|
return@async Image.makeFromEncoded(imageString.decodeBase64Bytes())
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
logger.debug { "Failed to load image from $imageString" }
|
||||||
|
storage.removeItem(id.imageName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//semaphore works only for actual download
|
||||||
|
semaphore.withPermit {
|
||||||
|
val url = id.osmUrl()
|
||||||
|
val byteArray = client.get(url).readBytes()
|
||||||
|
logger.debug { "Finished downloading map tile with id $id from $url" }
|
||||||
|
val imageName = id.imageName()
|
||||||
|
logger.debug { "Caching map tile $id to $imageName" }
|
||||||
|
storage.setItem(imageName, byteArray.encodeBase64())
|
||||||
|
Image.makeFromEncoded(byteArray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun CoroutineScope.loadTileAsync(
|
||||||
|
tileId: TileId,
|
||||||
|
): Deferred<MapTile> {
|
||||||
|
|
||||||
|
//start image download
|
||||||
|
val imageDeferred: Deferred<Image> = cache.getOrPut(tileId) {
|
||||||
|
downloadImageAsync(tileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
//collect the result asynchronously
|
||||||
|
return async {
|
||||||
|
val image: Image = runCatching { imageDeferred.await() }.onFailure {
|
||||||
|
if (it !is CancellationException) {
|
||||||
|
logger.error(it) { "Failed to load tile image with id=$tileId" }
|
||||||
|
}
|
||||||
|
cache.remove(tileId)
|
||||||
|
}.getOrThrow()
|
||||||
|
|
||||||
|
MapTile(tileId, image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
private val logger = KotlinLogging.logger("OpenStreetMapCache")
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ val kmathVersion: String by rootProject.extra
|
|||||||
kscience{
|
kscience{
|
||||||
jvm()
|
jvm()
|
||||||
js()
|
js()
|
||||||
|
native()
|
||||||
useSerialization()
|
useSerialization()
|
||||||
|
|
||||||
dependencies{
|
dependencies{
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package center.sciprog.maps.features
|
package center.sciprog.maps.features
|
||||||
|
|
||||||
import androidx.compose.runtime.*
|
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.geometry.Offset
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
|
|
||||||
|
@ -48,11 +48,11 @@ public class ComposeFeatureDrawScope<T : Any>(
|
|||||||
drawScope: DrawScope,
|
drawScope: DrawScope,
|
||||||
state: CanvasState<T>,
|
state: CanvasState<T>,
|
||||||
private val painterCache: Map<PainterFeature<T>, Painter>,
|
private val painterCache: Map<PainterFeature<T>, Painter>,
|
||||||
private val textMeasurer: TextMeasurer,
|
private val textMeasurer: TextMeasurer?,
|
||||||
) : FeatureDrawScope<T>(state), DrawScope by drawScope {
|
) : FeatureDrawScope<T>(state), DrawScope by drawScope {
|
||||||
override fun drawText(text: String, position: Offset, attributes: Attributes) {
|
override fun drawText(text: String, position: Offset, attributes: Attributes) {
|
||||||
try {
|
try {
|
||||||
drawText(textMeasurer, text, position)
|
drawText(textMeasurer?: error("Text measurer not defined"), text, position)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
logger.error(ex) { "Failed to measure text" }
|
logger.error(ex) { "Failed to measure text" }
|
||||||
}
|
}
|
||||||
@ -77,7 +77,7 @@ public fun <T : Any> FeatureCanvas(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
draw: FeatureDrawScope<T>.() -> Unit = {},
|
draw: FeatureDrawScope<T>.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val textMeasurer = rememberTextMeasurer(200)
|
val textMeasurer = rememberTextMeasurer(0)
|
||||||
|
|
||||||
val painterCache: Map<PainterFeature<T>, Painter> = features.features.flatMap {
|
val painterCache: Map<PainterFeature<T>, Painter> = features.features.flatMap {
|
||||||
if (it is FeatureGroup) it.features else listOf(it)
|
if (it is FeatureGroup) it.features else listOf(it)
|
||||||
|
@ -59,6 +59,7 @@ include(
|
|||||||
":demo:maps",
|
":demo:maps",
|
||||||
":demo:scheme",
|
":demo:scheme",
|
||||||
":demo:polygon-editor",
|
":demo:polygon-editor",
|
||||||
":demo:trajectory-playground"
|
":demo:trajectory-playground",
|
||||||
|
":demo:maps-js"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ val kmathVersion: String by rootProject.extra
|
|||||||
kscience{
|
kscience{
|
||||||
jvm()
|
jvm()
|
||||||
js()
|
js()
|
||||||
// native()
|
native()
|
||||||
|
|
||||||
useContextReceivers()
|
useContextReceivers()
|
||||||
useSerialization()
|
useSerialization()
|
||||||
|
Loading…
Reference in New Issue
Block a user