Add (not working yet) JS implementation

This commit is contained in:
Alexander Nozik 2023-10-01 14:05:03 +03:00
parent f05f6e137c
commit d21d6ebb2a
12 changed files with 327 additions and 9 deletions

View File

@ -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")

View 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 {}
}

View 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()
}
}
}

View 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>

View File

@ -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

View File

@ -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")

View File

@ -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")
}
}

View File

@ -8,6 +8,7 @@ val kmathVersion: String by rootProject.extra
kscience{ kscience{
jvm() jvm()
js() js()
native()
useSerialization() useSerialization()
dependencies{ dependencies{

View File

@ -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.*

View File

@ -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)

View File

@ -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"
) )

View File

@ -10,7 +10,7 @@ val kmathVersion: String by rootProject.extra
kscience{ kscience{
jvm() jvm()
js() js()
// native() native()
useContextReceivers() useContextReceivers()
useSerialization() useSerialization()