Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: web ornament and gesture settings #212

Merged
merged 1 commit into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,26 @@ to express an interactive map API in Compose.
We don't yet support Wasm because one of our dependencies,
[Spatial-K][spatial-k], doesn't support it.

| Feature | Android | iOS | Desktop (JVM) | Web (JS) | Web (Wasm) |
| ------------------------------------------------- | ---------------------- | ---------------------- | ----------------------------------- | ------------------- | ---------- |
| Renderer | [MapLibre Native][MLN] | [MapLibre Native][MLN] | [MapLibre JS][MLJS] in [KCEF][kcef] | [MapLibre JS][MLJS] | :x: |
| Load Compose resource URIs | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
| Configure ornaments (compass, logo, attribution) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: |
| Configure gestures (pan, zoom, rotate, pitch) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: |
| Respond to a map click or long click | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Query visible map features | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Get, set, and animate the camera position | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Convert between screen and geographic coordinates | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Get the currently visible region and bounding box | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Insert, remove, and replace layers | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Configure layers with expressions | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Add data sources by URI or GeoJSON | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Add images to the style | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Add Material 3 controls | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Add Compose UI annotations | :x: | :x: | :x: | :x: | :x: |
| Snapshot the map as an image | :x: | :x: | :x: | :x: | :x: |
| Configure the offline cache | :x: | :x: | :x: | :x: | :x: |
| Configure layer transitions | :x: | :x: | :x: | :x: | :x: |
| Feature | Android | iOS | Desktop (JVM) | Web (JS) | Web (Wasm) |
| :------------------------------------------------ | :--------------------: | :--------------------: | :---------------------------------: | :-----------------: | :--------: |
| Renderer | [MapLibre Native][MLN] | [MapLibre Native][MLN] | [MapLibre JS][MLJS] in [KCEF][kcef] | [MapLibre JS][MLJS] | :x: |
| Load Compose resource URIs | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
| Configure ornaments (compass, logo, attribution) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
| Configure gestures (pan, zoom, rotate, pitch) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
| Respond to a map click or long click | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Query visible map features | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Get, set, and animate the camera position | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Convert between screen and geographic coordinates | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Get the currently visible region and bounding box | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Insert, remove, and replace layers | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Configure layers with expressions | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Add data sources by URI or GeoJSON | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Add images to the style | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Add Material 3 controls | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: |
| Add Compose UI annotations | :x: | :x: | :x: | :x: | :x: |
| Snapshot the map as an image | :x: | :x: | :x: | :x: | :x: |
| Configure the offline cache | :x: | :x: | :x: | :x: | :x: |
| Configure layer transitions | :x: | :x: | :x: | :x: | :x: |

[compose]: https://www.jetbrains.com/compose-multiplatform/
[maplibre]: https://maplibre.org/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ object WebviewMapBridge {
map.touchZoomRotate.enableRotation()
} else {
map.dragRotate.disable()
map.keyboard.enableRotation()
map.touchZoomRotate.enableRotation()
map.keyboard.disableRotation()
map.touchZoomRotate.disableRotation()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.sargunv.maplibrecompose.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand All @@ -9,6 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import co.touchlab.kermit.Logger
import dev.sargunv.composehtmlinterop.HtmlElement
import dev.sargunv.maplibrecompose.core.JsMap
import dev.sargunv.maplibrecompose.core.MaplibreMap
import dev.sargunv.maplibrejs.Map
import dev.sargunv.maplibrejs.MapOptions
Expand Down Expand Up @@ -42,10 +44,10 @@ internal fun WebMapView(
logger: Logger?,
callbacks: MaplibreMap.Callbacks,
) {
var maybeMap by remember { mutableStateOf<Map?>(null) }
var maybeMap by remember { mutableStateOf<JsMap?>(null) }

HtmlElement(
modifier = modifier.onGloballyPositioned { maybeMap?.resize() },
modifier = modifier.onGloballyPositioned { maybeMap?.map?.resize() },
factory = {
document.createElement("div").unsafeCast<HTMLElement>().apply {
style.apply {
Expand All @@ -55,12 +57,14 @@ internal fun WebMapView(
}
},
update = { element ->
val map =
maybeMap
?: Map(MapOptions(container = element, disableAttributionControl = true)).also {
maybeMap = it
}
map.setStyle(styleUri)
val map = maybeMap ?: initMap(element).also { maybeMap = it }
map.setStyleUri(styleUri)
update(map)
},
)

DisposableEffect(Unit) { onDispose { onReset() } }
}

private fun initMap(element: HTMLElement) =
JsMap(Map(MapOptions(container = element, disableAttributionControl = true)))
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package dev.sargunv.maplibrecompose.core

import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.LayoutDirection
import dev.sargunv.maplibrecompose.expressions.ast.CompiledExpression
import dev.sargunv.maplibrecompose.expressions.value.BooleanValue
import dev.sargunv.maplibrejs.AttributionControl
import dev.sargunv.maplibrejs.LogoControl
import dev.sargunv.maplibrejs.Map
import dev.sargunv.maplibrejs.NavigationControl
import dev.sargunv.maplibrejs.NavigationControlOptions
import dev.sargunv.maplibrejs.ScaleControl
import io.github.dellisd.spatialk.geojson.BoundingBox
import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.Position
import kotlin.time.Duration

internal class JsMap(internal val map: Map) : StandardMaplibreMap {

private var lastStyleUri: String = ""

override fun setStyleUri(styleUri: String) {
if (styleUri == lastStyleUri) return
lastStyleUri = styleUri
map.setStyle(styleUri)
}

override fun setDebugEnabled(enabled: Boolean) {
map.showCollisionBoxes = enabled
map.showPadding = enabled
map.showTileBoundaries = enabled
}

override fun setMinPitch(minPitch: Double) {
map.setMinPitch(minPitch)
}

override fun setMaxPitch(maxPitch: Double) {
map.setMaxPitch(maxPitch)
}

override fun setMinZoom(minZoom: Double) {
map.setMinZoom(minZoom)
}

override fun setMaxZoom(maxZoom: Double) {
map.setMaxZoom(maxZoom)
}

override fun getVisibleBoundingBox(): BoundingBox {
return BoundingBox(0.0, 0.0, 0.0, 0.0) // TODO
}

override fun getVisibleRegion(): VisibleRegion {
// TODO
return VisibleRegion(
Position(0.0, 0.0),
Position(0.0, 0.0),
Position(0.0, 0.0),
Position(0.0, 0.0),
)
}

override fun setMaximumFps(maximumFps: Int) {
// not supported on web
}

override fun setGestureSettings(value: GestureSettings) {
if (value.isTiltGesturesEnabled) {
map.touchPitch.enable()
} else {
map.touchPitch.disable()
}

if (value.isRotateGesturesEnabled) {
map.dragRotate.enable()
map.keyboard.enableRotation()
map.touchZoomRotate.enableRotation()
} else {
map.dragRotate.disable()
map.keyboard.disableRotation()
map.touchZoomRotate.disableRotation()
}

if (value.isScrollGesturesEnabled) {
map.dragPan.enable()
} else {
map.dragPan.disable()
}

if (value.isZoomGesturesEnabled) {
map.doubleClickZoom.enable()
map.scrollZoom.enable()
map.touchZoomRotate.enable()
} else {
map.doubleClickZoom.disable()
map.scrollZoom.disable()
map.touchZoomRotate.disable()
}

if (value.isKeyboardGesturesEnabled) {
map.keyboard.enable()
} else {
map.keyboard.disable()
}
}

private var compassPosition: String? = null
private var logoPosition: String? = null
private var scalePosition: String? = null
private var attributionPosition: String? = null

private val navigationControl = NavigationControl(NavigationControlOptions(visualizePitch = true))
private val logoControl = LogoControl()
private val scaleControl = ScaleControl()
private val attributionControl = AttributionControl()

override fun setOrnamentSettings(value: OrnamentSettings) {
val layoutDir = LayoutDirection.Ltr // TODO: Get from composition

val desiredCompassPosition =
if (value.isCompassEnabled) value.compassAlignment.toControlPosition(layoutDir) else null
val desiredLogoPosition =
if (value.isLogoEnabled) value.logoAlignment.toControlPosition(layoutDir) else null
val desiredScalePosition =
if (value.isScaleBarEnabled) value.scaleBarAlignment.toControlPosition(layoutDir) else null
val desiredAttributionPosition =
if (value.isAttributionEnabled) value.attributionAlignment.toControlPosition(layoutDir)
else null

if (compassPosition != desiredCompassPosition) {
if (desiredCompassPosition == null) map.removeControl(navigationControl)
else map.addControl(navigationControl, desiredCompassPosition)
compassPosition = desiredCompassPosition
}

if (logoPosition != desiredLogoPosition) {
if (desiredLogoPosition == null) map.removeControl(logoControl)
else map.addControl(logoControl, desiredLogoPosition)
logoPosition = desiredLogoPosition
}

if (scalePosition != desiredScalePosition) {
if (desiredScalePosition == null) map.removeControl(scaleControl)
else map.addControl(scaleControl, desiredScalePosition)
scalePosition = desiredScalePosition
}

if (attributionPosition != desiredAttributionPosition) {
if (desiredAttributionPosition == null) map.removeControl(attributionControl)
else map.addControl(attributionControl, desiredAttributionPosition)
attributionPosition = desiredAttributionPosition
}
}

override fun getCameraPosition(): CameraPosition {
return CameraPosition() // TODO
}

override fun setCameraPosition(cameraPosition: CameraPosition) {
// TODO
}

override suspend fun animateCameraPosition(finalPosition: CameraPosition, duration: Duration) {
// TODO
}

override fun positionFromScreenLocation(offset: DpOffset): Position {
return Position(0.0, 0.0) // TODO
}

override fun screenLocationFromPosition(position: Position): DpOffset {
return DpOffset.Zero // TODO
}

override fun queryRenderedFeatures(
offset: DpOffset,
layerIds: Set<String>?,
predicate: CompiledExpression<BooleanValue>?,
): List<Feature> {
return emptyList() // TODO
}

override fun queryRenderedFeatures(
rect: DpRect,
layerIds: Set<String>?,
predicate: CompiledExpression<BooleanValue>?,
): List<Feature> {
return emptyList() // TODO
}

override fun metersPerDpAtLatitude(latitude: Double): Double {
return 0.0 // TODO
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dev.sargunv.maplibrecompose.core

import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection

internal fun Alignment.toControlPosition(layoutDir: LayoutDirection): String {
val (x, y) = align(IntSize(1, 1), IntSize(2, 2), layoutDir)
val h =
when (x) {
0 -> "left"
1 -> "right"
else -> error("Invalid alignment")
}
val v =
when (y) {
0 -> "top"
1 -> "bottom"
else -> error("Invalid alignment")
}
return "$v-$h"
}
Loading