Skip to content

Commit

Permalink
feat: add images to the style (#168)
Browse files Browse the repository at this point in the history
  • Loading branch information
sargunv authored Dec 23, 2024
1 parent 90c1902 commit 190bd22
Show file tree
Hide file tree
Showing 15 changed files with 279 additions and 6 deletions.
25 changes: 25 additions & 0 deletions demo-app/src/commonMain/composeResources/drawable/marker.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="56dp"
android:viewportWidth="20"
android:viewportHeight="56">
<path
android:pathData="M1,27a9,5 0,1 0,18 0a9,5 0,1 0,-18 0z"
android:strokeAlpha="0.2"
android:fillColor="#262626"
android:fillAlpha="0.2"/>
<path
android:pathData="M19.5,10.4c0,6.3 -9.5,17.1 -9.5,17.1S0.5,16.6 0.5,10.4c0,-5.5 4.3,-9.9 9.5,-9.9S19.5,4.9 19.5,10.4z"
android:strokeLineJoin="round"
android:strokeWidth="1.0229"
android:fillColor="#F84D4D"
android:strokeColor="#951212"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M10,10m-3.8,0a3.8,3.8 0,1 1,7.6 0a3.8,3.8 0,1 1,-7.6 0"
android:strokeLineJoin="round"
android:fillColor="#FFFFFF"
android:strokeColor="#7C2525"
android:strokeLineCap="round"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ import dev.sargunv.maplibrecompose.demoapp.demos.CameraStateDemo
import dev.sargunv.maplibrecompose.demoapp.demos.ClusteredPointsDemo
import dev.sargunv.maplibrecompose.demoapp.demos.EdgeToEdgeDemo
import dev.sargunv.maplibrecompose.demoapp.demos.FrameRateDemo
import dev.sargunv.maplibrecompose.demoapp.demos.MarkersDemo
import dev.sargunv.maplibrecompose.demoapp.demos.StyleSwitcherDemo
import dev.sargunv.maplibrecompose.material3.controls.AttributionButton
import dev.sargunv.maplibrecompose.material3.controls.DisappearingCompassButton
import dev.sargunv.maplibrecompose.material3.controls.DisappearingScaleBar

private val DEMOS =
listOf(
MarkersDemo,
EdgeToEdgeDemo,
StyleSwitcherDemo,
ClusteredPointsDemo,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package dev.sargunv.maplibrecompose.demoapp.demos

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import dev.sargunv.maplibrecompose.compose.ClickResult
import dev.sargunv.maplibrecompose.compose.MaplibreMap
import dev.sargunv.maplibrecompose.compose.layer.SymbolLayer
import dev.sargunv.maplibrecompose.compose.rememberCameraState
import dev.sargunv.maplibrecompose.compose.rememberStyleState
import dev.sargunv.maplibrecompose.compose.source.rememberGeoJsonSource
import dev.sargunv.maplibrecompose.core.CameraPosition
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.const
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.image
import dev.sargunv.maplibrecompose.demoapp.DEFAULT_STYLE
import dev.sargunv.maplibrecompose.demoapp.Demo
import dev.sargunv.maplibrecompose.demoapp.DemoMapControls
import dev.sargunv.maplibrecompose.demoapp.DemoOrnamentSettings
import dev.sargunv.maplibrecompose.demoapp.DemoScaffold
import dev.sargunv.maplibrecompose.demoapp.generated.Res
import dev.sargunv.maplibrecompose.demoapp.generated.marker
import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.Position
import org.jetbrains.compose.resources.painterResource

@Composable
private fun Painter.rememberAsBitmap(): ImageBitmap {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
return remember(this, density, layoutDirection) {
ImageBitmap(intrinsicSize.width.toInt(), intrinsicSize.height.toInt()).also { bitmap ->
CanvasDrawScope().draw(density, layoutDirection, Canvas(bitmap), intrinsicSize) { draw(size) }
}
}
}

private val CHICAGO = Position(latitude = 41.878, longitude = -87.626)

object MarkersDemo : Demo {
override val name = "Markers"
override val description = "Add and interact with markers"

@Composable
override fun Component(navigateUp: () -> Unit) {
DemoScaffold(this, navigateUp) {
val marker = painterResource(Res.drawable.marker).rememberAsBitmap()
val cameraState =
rememberCameraState(firstPosition = CameraPosition(target = CHICAGO, zoom = 7.0))
val styleState = rememberStyleState(images = mapOf("demo-marker" to marker))
var selectedFeature by remember { mutableStateOf<Feature?>(null) }

Box(modifier = Modifier.fillMaxSize()) {
MaplibreMap(
styleUri = DEFAULT_STYLE,
cameraState = cameraState,
styleState = styleState,
ornamentSettings = DemoOrnamentSettings(),
) {
val amtrakStations =
rememberGeoJsonSource(
id = "amtrak-stations",
uri =
"https://raw.githubusercontent.com/datanews/amtrak-geojson/refs/heads/master/amtrak-stations.geojson",
)
SymbolLayer(
id = "amtrak-stations",
source = amtrakStations,
onClick = { features ->
selectedFeature = features.firstOrNull()
ClickResult.Consume
},
iconImage = image(const("demo-marker")),
iconAllowOverlap = const(true),
)
}
DemoMapControls(cameraState, styleState)
}

selectedFeature?.let { feature ->
AlertDialog(
onDismissRequest = { selectedFeature = null },
confirmButton = {},
title = { Text(feature.getStringProperty("STNNAME") ?: "") },
text = {
Column {
Text("Station Code: ${feature.getStringProperty("STNCODE") ?: ""}")
Text("Station Type: ${feature.getStringProperty("STNTYPE") ?: ""}")
Text("Address: ${feature.getStringProperty("ADDRESS1") ?: ""}")
Text("City: ${feature.getStringProperty("CITY") ?: ""}")
Text("State: ${feature.getStringProperty("STATE") ?: ""}")
Text("Zip: ${feature.getStringProperty("ZIP") ?: ""}")
}
},
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ import org.maplibre.android.maps.Style as MLNStyle
internal class AndroidStyle(style: MLNStyle) : Style {
private var impl: MLNStyle = style

override fun getImage(id: String): Image? {
return impl.getImage(id)?.let { Image(id, it) }
}

override fun addImage(image: Image) {
impl.addImage(image.id, image.impl)
}

override fun removeImage(image: Image) {
impl.removeImage(image.id)
}

override fun getSource(id: String): Source? {
return impl.getSource(id)?.let { UnknownSource(it) }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.sargunv.maplibrecompose.core

import android.graphics.Bitmap
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.unit.Density

internal actual class Image(actual val id: String, val impl: Bitmap) {
internal actual constructor(
id: String,
image: ImageBitmap,
density: Density,
) : this(id, image.asAndroidBitmap())
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,45 @@ package dev.sargunv.maplibrecompose.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import dev.sargunv.maplibrecompose.core.Image
import dev.sargunv.maplibrecompose.core.Style
import dev.sargunv.maplibrecompose.core.source.AttributionLink

@Composable
public fun rememberStyleState(): StyleState {
return remember { StyleState() }
public fun rememberStyleState(images: Map<String, ImageBitmap> = emptyMap()): StyleState {
val ret = remember { StyleState() }
val density = LocalDensity.current
remember(images, density) { ret.updateImages(images, density) }
return ret
}

public class StyleState internal constructor() {
private var style: Style? = null

private val addedImages = mutableSetOf<Image>()

internal fun attach(style: Style?) {
this.style = style
if (this.style != style) {
this.style = style
if (style != null) {
addedImages.forEach { style.addImage(it) }
}
}
}

internal fun updateImages(images: Map<String, ImageBitmap>, density: Density) {
val newImages = images.map { (id, bitmap) -> Image(id, bitmap, density) }.toSet()
style?.let { style ->
val toRemove = addedImages - newImages
val toAdd = newImages - addedImages
toRemove.forEach { style.removeImage(it) }
toAdd.forEach { style.addImage(it) }
}
addedImages.clear()
addedImages.addAll(newImages)
}

public fun queryAttributionLinks(): List<AttributionLink> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.sargunv.maplibrecompose.compose.engine

import co.touchlab.kermit.Logger
import dev.sargunv.maplibrecompose.compose.layer.Anchor
import dev.sargunv.maplibrecompose.core.Image
import dev.sargunv.maplibrecompose.core.Style
import dev.sargunv.maplibrecompose.core.layer.Layer
import dev.sargunv.maplibrecompose.core.source.Source
Expand All @@ -12,6 +13,7 @@ internal class StyleManager(var style: Style, private var logger: Logger?) {

// we queue up additions, but instantly execute removals
// this way if an id is added and removed in the same frame, it will be removed before it's added
private val imagesToAdd = mutableListOf<Image>()
private val sourcesToAdd = mutableListOf<Source>()
private val userLayers = mutableListOf<LayerNode<*>>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ public fun SymbolLayer(
iconTranslateAnchor: Expression<EnumValue<TranslateAnchor>> = const(TranslateAnchor.Map),

// text content
textField: Expression<FormattedValue> = nil(),
textField: Expression<FormattedValue> = const("").cast(),

// text glyph colors
textOpacity: Expression<FloatValue> = const(1f),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dev.sargunv.maplibrecompose.core

import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.Density

internal expect class Image {
val id: String

@Suppress("ConvertSecondaryConstructorToPrimary")
internal constructor(id: String, image: ImageBitmap, density: Density)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import dev.sargunv.maplibrecompose.core.layer.Layer
import dev.sargunv.maplibrecompose.core.source.Source

internal interface Style {
fun getImage(id: String): Image?

fun addImage(image: Image)

fun removeImage(image: Image)

fun getSource(id: String): Source?

fun getSources(): List<Source>
Expand All @@ -27,6 +33,12 @@ internal interface Style {
fun removeLayer(layer: Layer)

object Null : Style {
override fun getImage(id: String): Image? = null

override fun addImage(image: Image) {}

override fun removeImage(image: Image) {}

override fun getSource(id: String): Source? = null

override fun getSources(): List<Source> = emptyList()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
package dev.sargunv.maplibrecompose.compose

import dev.sargunv.maplibrecompose.core.Image
import dev.sargunv.maplibrecompose.core.Style
import dev.sargunv.maplibrecompose.core.layer.Layer
import dev.sargunv.maplibrecompose.core.source.Source

internal class FakeStyle(sources: List<Source>, layers: List<Layer>) : Style {
internal class FakeStyle(images: List<Image>, sources: List<Source>, layers: List<Layer>) : Style {
private val imageMap = images.associateBy { it.id }.toMutableMap()
private val sourceMap = sources.associateBy { it.id }.toMutableMap()
private val layerList = layers.toMutableList()
private val layerMap = layers.associateBy { it.id }.toMutableMap()

override fun getImage(id: String): Image? = imageMap[id]

override fun addImage(image: Image) {
if (image.id in imageMap) error("Image ID '${image.id}' already exists in style")
imageMap[image.id] = image
}

override fun removeImage(image: Image) {
if (image.id !in imageMap) error("Image ID '${image.id}' not found in style")
imageMap.remove(image.id)
}

override fun getSource(id: String): Source? = sourceMap[id]

override fun getSources(): List<Source> = sourceMap.values.toList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.ui.test.runComposeUiTest
import dev.sargunv.maplibrecompose.compose.engine.LayerNode
import dev.sargunv.maplibrecompose.compose.engine.StyleManager
import dev.sargunv.maplibrecompose.compose.layer.Anchor
import dev.sargunv.maplibrecompose.core.Image
import dev.sargunv.maplibrecompose.core.layer.Layer
import dev.sargunv.maplibrecompose.core.layer.LineLayer
import dev.sargunv.maplibrecompose.core.source.GeoJsonOptions
Expand All @@ -18,6 +19,8 @@ import kotlin.test.assertNull

@OptIn(ExperimentalTestApi::class)
abstract class StyleManagerTest {
private val testImages by lazy { emptyList<Image>() }

private val testSources by lazy {
listOf(
GeoJsonSource("foo", FeatureCollection(), GeoJsonOptions()),
Expand All @@ -35,7 +38,7 @@ abstract class StyleManagerTest {
}

private fun makeStyleManager(): StyleManager {
return StyleManager(FakeStyle(testSources, testLayers), null)
return StyleManager(FakeStyle(testImages, testSources, testLayers), null)
}

@BeforeTest open fun platformSetup() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.sargunv.maplibrecompose.core

import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.Density
import dev.sargunv.maplibrecompose.core.util.toUIImage
import platform.UIKit.UIImage

internal actual class Image(actual val id: String, val impl: UIImage) {
internal actual constructor(
id: String,
image: ImageBitmap,
density: Density,
) : this(id, image.toUIImage(density))
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ import dev.sargunv.maplibrecompose.core.source.UnknownSource
internal class IosStyle(style: MLNStyle) : Style {
private var impl: MLNStyle = style

override fun getImage(id: String): Image? {
return impl.imageForName(id)?.let {
return Image(id, it)
}
}

override fun addImage(image: Image) {
impl.setImage(image.impl, forName = image.id)
}

override fun removeImage(image: Image) {
impl.removeImageForName(image.id)
}

override fun getSource(id: String): Source? {
return impl.sourceWithIdentifier(id)?.let { UnknownSource(it) }
}
Expand Down
Loading

0 comments on commit 190bd22

Please sign in to comment.