0.3.0 #23
@ -37,7 +37,6 @@ ksciencePublish {
|
||||
|
||||
subprojects {
|
||||
repositories {
|
||||
maven("https://maven.pkg.jetbrains.space/mipt-npm/p/sci/dev")
|
||||
google()
|
||||
mavenCentral()
|
||||
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
|
||||
|
||||
compose.version=1.5.1
|
||||
compose.version=1.5.2
|
||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
||||
|
||||
agp.version=8.1.0
|
||||
|
@ -19,9 +19,21 @@ kotlin {
|
||||
api(projects.mapsKtFeatures)
|
||||
api(compose.foundation)
|
||||
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") {
|
||||
dependencies {
|
||||
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{
|
||||
jvm()
|
||||
js()
|
||||
native()
|
||||
useSerialization()
|
||||
|
||||
dependencies{
|
||||
|
@ -1,6 +1,9 @@
|
||||
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.unit.*
|
||||
|
||||
|
@ -48,11 +48,11 @@ public class ComposeFeatureDrawScope<T : Any>(
|
||||
drawScope: DrawScope,
|
||||
state: CanvasState<T>,
|
||||
private val painterCache: Map<PainterFeature<T>, Painter>,
|
||||
private val textMeasurer: TextMeasurer,
|
||||
private val textMeasurer: TextMeasurer?,
|
||||
) : FeatureDrawScope<T>(state), DrawScope by drawScope {
|
||||
override fun drawText(text: String, position: Offset, attributes: Attributes) {
|
||||
try {
|
||||
drawText(textMeasurer, text, position)
|
||||
drawText(textMeasurer?: error("Text measurer not defined"), text, position)
|
||||
} catch (ex: Exception) {
|
||||
logger.error(ex) { "Failed to measure text" }
|
||||
}
|
||||
@ -77,7 +77,7 @@ public fun <T : Any> FeatureCanvas(
|
||||
modifier: Modifier = Modifier,
|
||||
draw: FeatureDrawScope<T>.() -> Unit = {},
|
||||
) {
|
||||
val textMeasurer = rememberTextMeasurer(200)
|
||||
val textMeasurer = rememberTextMeasurer(0)
|
||||
|
||||
val painterCache: Map<PainterFeature<T>, Painter> = features.features.flatMap {
|
||||
if (it is FeatureGroup) it.features else listOf(it)
|
||||
|
@ -59,6 +59,7 @@ include(
|
||||
":demo:maps",
|
||||
":demo:scheme",
|
||||
":demo:polygon-editor",
|
||||
":demo:trajectory-playground"
|
||||
":demo:trajectory-playground",
|
||||
":demo:maps-js"
|
||||
)
|
||||
|
||||
|
@ -10,7 +10,7 @@ val kmathVersion: String by rootProject.extra
|
||||
kscience{
|
||||
jvm()
|
||||
js()
|
||||
// native()
|
||||
native()
|
||||
|
||||
useContextReceivers()
|
||||
useSerialization()
|
||||
|
Loading…
Reference in New Issue
Block a user