|
diff --git a/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/mlnffi/ProductionBridgeTestRenderDriver.kt b/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/mlnffi/ProductionBridgeTestRenderDriver.kt |
|
index 231c4c3b5..bdc8a1404 100644 |
|
--- a/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/mlnffi/ProductionBridgeTestRenderDriver.kt |
|
+++ b/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/mlnffi/ProductionBridgeTestRenderDriver.kt |
|
@@ -106,6 +106,8 @@ private constructor( |
|
|
|
override fun readPixel(x: Int, y: Int): RgbaPixel = environment.readPixel(x, y) |
|
|
|
+ fun capturePng(): ByteArray = environment.capturePng() |
|
+ |
|
override fun discardPresentedFrame() { |
|
environment.discardPresentedFrame() |
|
} |
|
@@ -184,6 +186,16 @@ private abstract class DesktopTestGpuEnvironment : AutoCloseable { |
|
drew |
|
} |
|
|
|
+ fun capturePng(): ByteArray = withContext { |
|
+ Bitmap().use { bitmap -> |
|
+ check(bitmap.allocN32Pixels(destinationWidth, destinationHeight)) |
|
+ check(checkNotNull(destination).readPixels(bitmap, 0, 0)) |
|
+ org.jetbrains.skia.Image.makeFromBitmap(bitmap).use { image -> |
|
+ checkNotNull(image.encodeToData()).use { it.bytes } |
|
+ } |
|
+ } |
|
+ } |
|
+ |
|
fun readPixel(x: Int, y: Int): RgbaPixel = withContext { |
|
val surface = checkNotNull(destination) { "No production bridge frame has been presented" } |
|
require(x in 0 until destinationWidth && y in 0 until destinationHeight) { |
|
diff --git a/lib/maplibre-compose/src/maplibreNativeTest/kotlin/org/maplibre/compose/mlnffi/BridgeMapFixture.kt b/lib/maplibre-compose/src/maplibreNativeTest/kotlin/org/maplibre/compose/mlnffi/BridgeMapFixture.kt |
|
index c89af2d64..3216a7219 100644 |
|
--- a/lib/maplibre-compose/src/maplibreNativeTest/kotlin/org/maplibre/compose/mlnffi/BridgeMapFixture.kt |
|
+++ b/lib/maplibre-compose/src/maplibreNativeTest/kotlin/org/maplibre/compose/mlnffi/BridgeMapFixture.kt |
|
@@ -34,7 +34,7 @@ import org.maplibre.compose.testing.RgbaPixel |
|
*/ |
|
internal class BridgeMapFixture |
|
private constructor( |
|
- private val driver: FfiTestRenderDriver, |
|
+ internal val driver: FfiTestRenderDriver, |
|
private val cacheFile: Path, |
|
private val initialExtent: MapExtent, |
|
resourceConfig: MapResourceConfig, |
|
diff --git a/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/layers/MarkerZoomDiagnosticTest.kt b/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/layers/MarkerZoomDiagnosticTest.kt |
|
new file mode 100644 |
|
index 000000000..a7c24cc8b |
|
--- /dev/null |
|
+++ b/lib/maplibre-compose/src/jvmTest/kotlin/org/maplibre/compose/layers/MarkerZoomDiagnosticTest.kt |
|
@@ -0,0 +1,101 @@ |
|
+package org.maplibre.compose.layers |
|
+ |
|
+import androidx.compose.ui.graphics.Color |
|
+import androidx.compose.ui.graphics.painter.ColorPainter |
|
+import androidx.compose.ui.unit.DpSize |
|
+import androidx.compose.ui.unit.dp |
|
+import java.io.File |
|
+import javax.imageio.ImageIO |
|
+import kotlin.test.Test |
|
+import kotlinx.serialization.json.JsonPrimitive |
|
+import org.maplibre.compose.camera.CameraPosition |
|
+import org.maplibre.compose.expressions.dsl.const |
|
+import org.maplibre.compose.expressions.dsl.image |
|
+import org.maplibre.compose.expressions.value.SymbolAnchor |
|
+import org.maplibre.compose.mlnffi.ProductionBridgeTestRenderDriver |
|
+import org.maplibre.compose.sources.GeoJsonData |
|
+import org.maplibre.compose.sources.GeoJsonOptions |
|
+import org.maplibre.compose.sources.rememberGeoJsonSource |
|
+import org.maplibre.compose.style.BaseStyle |
|
+import org.maplibre.compose.style.LayerPropertyKind |
|
+import org.maplibre.compose.testing.MlnFfiMapFixture |
|
+import org.maplibre.compose.testing.createMapFixture |
|
+import org.maplibre.compose.testing.declare |
|
+import org.maplibre.compose.testing.runMapTest |
|
+import org.maplibre.spatialk.geojson.Feature |
|
+import org.maplibre.spatialk.geojson.Point |
|
+import org.maplibre.spatialk.geojson.Position |
|
+ |
|
+class MarkerZoomDiagnosticTest { |
|
+ @Test |
|
+ fun capture_icons_across_zoom_and_layout_changes() = runMapTest { |
|
+ val output = File("build/marker-zoom-diagnostic").apply { mkdirs() } |
|
+ for (synchronous in listOf(false, true)) for (withText in listOf(false, true)) { |
|
+ (createMapFixture() as MlnFfiMapFixture).use { fixture -> |
|
+ fixture.loadStyle( |
|
+ BaseStyle.Json( |
|
+ """{"version":8,"glyphs":"https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf","sources":{},"layers":[{"id":"bg","type":"background","paint":{"background-color":"#ffffff"}}]}""" |
|
+ ) |
|
+ ) |
|
+ fixture.state.setCameraPosition(CameraPosition(target = Position(0.0, 0.0), zoom = 14.0)) |
|
+ fixture.declare { |
|
+ for (index in 0..2) { |
|
+ val source = |
|
+ rememberGeoJsonSource( |
|
+ GeoJsonData.Features( |
|
+ Feature(geometry = Point(Position((index - 1) * 0.005, 0.0)), properties = null) |
|
+ ), |
|
+ GeoJsonOptions(synchronousUpdate = synchronous), |
|
+ ) |
|
+ CircleLayer("shadow-$index", source, radius = const(8.dp), color = const(Color.Black)) |
|
+ SymbolLayer( |
|
+ "pin-$index", |
|
+ source, |
|
+ iconImage = image(ColorPainter(Color.Red), size = DpSize(40.dp, 48.dp)), |
|
+ iconAnchor = const(SymbolAnchor.Bottom), |
|
+ iconSize = const(1f), |
|
+ textField = const(if (withText) "New place" else ""), |
|
+ textFont = const(listOf("Noto Sans Regular")), |
|
+ textAnchor = const(SymbolAnchor.Top), |
|
+ iconAllowOverlap = const(true), |
|
+ iconIgnorePlacement = const(true), |
|
+ textAllowOverlap = const(true), |
|
+ textIgnorePlacement = const(true), |
|
+ ) |
|
+ } |
|
+ } |
|
+ suspend fun capture(label: String) { |
|
+ fixture.pump(40) |
|
+ val file = File(output, "sync-$synchronous-text-$withText-$label.png") |
|
+ file.writeBytes((fixture.bridge.driver as ProductionBridgeTestRenderDriver).capturePng()) |
|
+ val bitmap = ImageIO.read(file) |
|
+ var red = 0 |
|
+ for (y in 0 until bitmap.height) for (x in 0 until bitmap.width) { |
|
+ val color = bitmap.getRGB(x, y) |
|
+ if ( |
|
+ (color ushr 16 and 255) > 200 && (color ushr 8 and 255) < 50 && (color and 255) < 50 |
|
+ ) |
|
+ red++ |
|
+ } |
|
+ println( |
|
+ "MARKER_DIAGNOSTIC sync=$synchronous text=$withText stage=$label redPixels=$red imagePresent=${fixture.style!!.imageExists("__MAPLIBRE_COMPOSE_image_0")} errors=${fixture.errors}" |
|
+ ) |
|
+ File(output, "sync-$synchronous-text-$withText-$label.json") |
|
+ .writeText(checkNotNull(fixture.style).getLayer("pin-0")!!.toJson().toString()) |
|
+ } |
|
+ capture("initial") |
|
+ for (zoom in listOf(13.5, 13.0, 12.5, 12.0, 11.0, 10.0, 8.0, 6.0, 14.0)) { |
|
+ fixture.state.setCameraPosition(CameraPosition(target = Position(0.0, 0.0), zoom = zoom)) |
|
+ capture("zoom-$zoom") |
|
+ } |
|
+ fixture.style!!.setLayerProperty( |
|
+ "pin-0", |
|
+ "icon-size", |
|
+ JsonPrimitive(1.12), |
|
+ LayerPropertyKind.LAYOUT, |
|
+ ) |
|
+ capture("animated") |
|
+ } |
|
+ } |
|
+ } |
|
+} |