diff --git a/build.gradle.kts b/build.gradle.kts
index a1a0bc7a..9af6c02d 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,5 +8,6 @@ plugins {
alias(libs.plugins.jetbrainsCompose) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
+ alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.vanniktechPublish) apply false
-}
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/androidMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.android.kt b/demo/composeApp/src/androidMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.android.kt
new file mode 100644
index 00000000..9c0ae03d
--- /dev/null
+++ b/demo/composeApp/src/androidMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.android.kt
@@ -0,0 +1,14 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import androidx.lifecycle.viewmodel.compose.viewModel
+import ovh.plrapps.mapcompose.demo.viewmodels.SwissMapVectorDemoVM
+
+actual object SwissMapVectorDemo {
+ @Composable
+ actual fun Content() {
+ val viewModel: SwissMapVectorDemoVM = viewModel()
+
+ SwissMapVectorCommonUi(viewModel)
+ }
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/androidMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.android.kt b/demo/composeApp/src/androidMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.android.kt
new file mode 100644
index 00000000..70cdced5
--- /dev/null
+++ b/demo/composeApp/src/androidMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.android.kt
@@ -0,0 +1,14 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import androidx.lifecycle.viewmodel.compose.viewModel
+import ovh.plrapps.mapcompose.demo.viewmodels.VectorDemoVM
+
+actual object VectorDemo {
+ @Composable
+ actual fun Content() {
+ val screenModel: VectorDemoVM = viewModel()
+
+ VectorCommonUi(screenModel)
+ }
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/androidMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt b/demo/composeApp/src/androidMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt
index aec69332..552856ca 100644
--- a/demo/composeApp/src/androidMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt
+++ b/demo/composeApp/src/androidMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt
@@ -1,7 +1,12 @@
package ovh.plrapps.mapcompose.demo.viewmodels
+import kotlinx.io.Buffer
+import kotlinx.io.RawSource
import kotlinx.io.asSource
+import org.jetbrains.compose.resources.ExperimentalResourceApi
import ovh.plrapps.mapcompose.core.TileStreamProvider
+import ovh.plrapps.mapcompose.vector.core.VectorTileStreamProvider
+import ovh.plrapps.mapcomposemp.demo.Res
import java.net.HttpURLConnection
import java.net.URL
@@ -42,4 +47,44 @@ actual fun makeOsmTileStreamProvider() : TileStreamProvider {
null
}
}
+}
+
+actual class OSMVectorTileStreamProvider actual constructor(actual override val styleUrl: String) :
+ VectorTileStreamProvider {
+
+ @OptIn(ExperimentalResourceApi::class)
+ actual override suspend fun loadResources(url: String): RawSource? {
+ return when (url) {
+ "files/style_street_v2.json" -> {
+ val buffer = Buffer()
+ buffer.write(Res.readBytes(url))
+ buffer
+ }
+ else -> getResourceAsStream(url)
+ }
+ }
+
+ actual override suspend fun getTileStream(
+ tileUrl: String,
+ row: Int,
+ col: Int,
+ zoomLvl: Int
+ ): RawSource? {
+ return getResourceAsStream(tileUrl)
+ }
+
+ private fun getResourceAsStream(url: String): RawSource? {
+ return try {
+ val url = URL(url)
+ val connection = url.openConnection() as HttpURLConnection
+ // OSM requires a user-agent
+ connection.setRequestProperty("User-Agent", "Chrome/120.0.0.0 Safari/537.36")
+ connection.doInput = true
+ connection.connect()
+ connection.inputStream.asSource()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
}
\ No newline at end of file
diff --git a/demo/composeApp/src/commonMain/composeResources/files/style_street_v2.json b/demo/composeApp/src/commonMain/composeResources/files/style_street_v2.json
new file mode 100644
index 00000000..51e3556f
--- /dev/null
+++ b/demo/composeApp/src/commonMain/composeResources/files/style_street_v2.json
@@ -0,0 +1,8247 @@
+{
+ "version": 8,
+ "id": "streets-v2",
+ "name": "Streets",
+ "sources": {
+ "maptiler_attribution": {
+ "attribution": "© MapTiler © OpenStreetMap contributors",
+ "type": "vector"
+ },
+ "maptiler_planet": {
+ "url": "https://api.maptiler.com/tiles/v3/tiles.json?key=Tp9K7a8qzMTkBSzO4EZb",
+ "type": "vector"
+ }
+ },
+ "layers": [
+ {
+ "id": "Background",
+ "type": "background",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "background-color": {
+ "stops": [
+ [
+ 6,
+ "hsl(47,79%,94%)"
+ ],
+ [
+ 14,
+ "hsl(42,49%,93%)"
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "id": "Meadow",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "globallandcover",
+ "maxzoom": 8,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(75,51%,85%)",
+ "fill-opacity": {
+ "stops": [
+ [
+ 0,
+ 1
+ ],
+ [
+ 8,
+ 0.1
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "==",
+ "class",
+ "grass"
+ ]
+ },
+ {
+ "id": "Scrub",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "globallandcover",
+ "maxzoom": 8,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(97,51%,80%)",
+ "fill-opacity": {
+ "stops": [
+ [
+ 0,
+ 1
+ ],
+ [
+ 8,
+ 0.1
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "==",
+ "class",
+ "scrub"
+ ]
+ },
+ {
+ "id": "Crop",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "globallandcover",
+ "maxzoom": 8,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(50,67%,86%)",
+ "fill-opacity": {
+ "stops": [
+ [
+ 0,
+ 1
+ ],
+ [
+ 8,
+ 0.1
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "==",
+ "class",
+ "crop"
+ ]
+ },
+ {
+ "id": "Glacier",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "landcover",
+ "maxzoom": 24,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(0,0%,100%)",
+ "fill-opacity": {
+ "stops": [
+ [
+ 0,
+ 1
+ ],
+ [
+ 10,
+ 0.7
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "==",
+ "class",
+ "ice"
+ ]
+ },
+ {
+ "id": "Forest",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "globallandcover",
+ "maxzoom": 8,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(119,38%,76%)",
+ "fill-opacity": {
+ "stops": [
+ [
+ 1,
+ 0.8
+ ],
+ [
+ 8,
+ 0
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "in",
+ "class",
+ "forest",
+ "tree"
+ ]
+ },
+ {
+ "id": "Sand",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "landcover",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": false,
+ "fill-color": "hsl(52,93%,89%)",
+ "fill-opacity": 0.85
+ },
+ "metadata": {},
+ "filter": [
+ "==",
+ "class",
+ "sand"
+ ]
+ },
+ {
+ "id": "Wood",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "landcover",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-color": "hsl(87,46%,85%)",
+ "fill-opacity": 1
+ },
+ "metadata": {},
+ "filter": [
+ "==",
+ "class",
+ "wood"
+ ]
+ },
+ {
+ "id": "Residential",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "landuse",
+ "maxzoom": 24,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-color": {
+ "base": 1,
+ "stops": [
+ [
+ 4,
+ "hsl(44,34%,87%)"
+ ],
+ [
+ 16,
+ "hsl(54, 45%, 91%)"
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "in",
+ "class",
+ "residential",
+ "suburbs",
+ "neighbourhood"
+ ]
+ },
+ {
+ "id": "Industrial",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "landuse",
+ "maxzoom": 24,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-color": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "industrial"
+ ],
+ "hsl(40,67%,90%)",
+ "quarry",
+ "hsla(32, 47%, 87%, 0.2)",
+ "hsl(60, 31%, 87%)"
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "industrial"
+ ],
+ "hsl(49,54%,90%)",
+ "quarry",
+ "hsla(32, 47%, 87%, 0.5)",
+ "hsl(60, 31%, 87%)"
+ ]
+ ],
+ "fill-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 1,
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "quarry",
+ 0,
+ 1
+ ],
+ 10,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "in",
+ "class",
+ "industrial",
+ "quarry"
+ ]
+ },
+ {
+ "id": "Grass",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "landcover",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": false,
+ "fill-color": "hsl(103, 40%, 85%)",
+ "fill-opacity": 0.5
+ },
+ "metadata": {},
+ "filter": [
+ "==",
+ "class",
+ "grass"
+ ]
+ },
+ {
+ "id": "Airport zone",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "aeroway",
+ "minzoom": 11,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-color": "hsl(0,0%,93%)",
+ "fill-opacity": 1
+ },
+ "metadata": {},
+ "filter": [
+ "==",
+ "$type",
+ "Polygon"
+ ]
+ },
+ {
+ "id": "Pedestrian",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-color": "hsl(43,100%,99%)",
+ "fill-opacity": 0.7
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Polygon"
+ ],
+ [
+ "!has",
+ "brunnel"
+ ],
+ [
+ "!in",
+ "class",
+ "bridge",
+ "pier"
+ ],
+ [
+ "in",
+ "subclass",
+ "pedestrian",
+ "platform"
+ ]
+ ]
+ },
+ {
+ "id": "Cemetery",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "landuse",
+ "minzoom": 9,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(0,0%,88%)",
+ "fill-opacity": {
+ "stops": [
+ [
+ 9,
+ 0.25
+ ],
+ [
+ 16,
+ 1
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "==",
+ "class",
+ "cemetery"
+ ]
+ },
+ {
+ "id": "Hospital",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "landuse",
+ "minzoom": 9,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(12,63%,94%)",
+ "fill-opacity": {
+ "stops": [
+ [
+ 9,
+ 0.25
+ ],
+ [
+ 16,
+ 1
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "==",
+ "class",
+ "hospital"
+ ]
+ },
+ {
+ "id": "Stadium",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "landuse",
+ "minzoom": 9,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(94, 100%, 88%)",
+ "fill-opacity": {
+ "stops": [
+ [
+ 9,
+ 0.25
+ ],
+ [
+ 16,
+ 1
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "in",
+ "class",
+ "pitch",
+ "stadium",
+ "playground"
+ ]
+ },
+ {
+ "id": "School",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "landuse",
+ "minzoom": 9,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(194,52%,94%)",
+ "fill-opacity": {
+ "stops": [
+ [
+ 9,
+ 0.25
+ ],
+ [
+ 16,
+ 1
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "in",
+ "class",
+ "college",
+ "school",
+ "university"
+ ]
+ },
+ {
+ "id": "River tunnel",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "waterway",
+ "minzoom": 14,
+ "layout": {
+ "line-cap": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(210,73%,78%)",
+ "line-dasharray": [
+ 2,
+ 4
+ ],
+ "line-opacity": 0.5,
+ "line-width": {
+ "base": 1.3,
+ "stops": [
+ [
+ 12,
+ 0.5
+ ],
+ [
+ 20,
+ 6
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "==",
+ "brunnel",
+ "tunnel"
+ ]
+ },
+ {
+ "id": "River",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "waterway",
+ "layout": {
+ "line-cap": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(210,73%,78%)",
+ "line-width": {
+ "stops": [
+ [
+ 12,
+ 0.5
+ ],
+ [
+ 20,
+ 6
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "!=",
+ "brunnel",
+ "tunnel"
+ ]
+ },
+ {
+ "id": "Water intermittent",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "water",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(205,91%,83%)",
+ "fill-opacity": 0.85
+ },
+ "metadata": {},
+ "filter": [
+ "==",
+ "intermittent",
+ 1
+ ]
+ },
+ {
+ "id": "Water",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "water",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(204,92%,75%)",
+ "fill-opacity": [
+ "match",
+ [
+ "get",
+ "intermittent"
+ ],
+ 1,
+ 0.85,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "!=",
+ "intermittent",
+ 1
+ ]
+ },
+ {
+ "id": "Aeroway",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "aeroway",
+ "minzoom": 11,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,100%)",
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ 1
+ ],
+ [
+ "zoom"
+ ],
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "runway"
+ ],
+ 3,
+ 0.5
+ ],
+ 20,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "runway"
+ ],
+ 16,
+ 6
+ ]
+ ]
+ },
+ "metadata": {}
+ },
+ {
+ "id": "Heliport",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "aeroway",
+ "minzoom": 11,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-color": "hsl(0,0%,100%)",
+ "fill-opacity": 1
+ },
+ "metadata": {},
+ "filter": [
+ "in",
+ "class",
+ "helipad",
+ "heliport"
+ ]
+ },
+ {
+ "id": "Ferry line",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 6,
+ "layout": {
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": {
+ "stops": [
+ [
+ 10,
+ "hsl(205,61%,63%)"
+ ],
+ [
+ 16,
+ "hsl(205,67%,47%)"
+ ]
+ ]
+ },
+ "line-dasharray": [
+ 2,
+ 2
+ ],
+ "line-opacity": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 6,
+ 0.5,
+ 7,
+ 0.8,
+ 8,
+ 1
+ ],
+ "line-width": {
+ "stops": [
+ [
+ 10,
+ 0.5
+ ],
+ [
+ 14,
+ 1.1
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "==",
+ "class",
+ "ferry"
+ ]
+ },
+ {
+ "id": "Tunnel outline",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 4,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "motorway",
+ "hsl(28,72%,69%)",
+ [
+ "trunk",
+ "primary"
+ ],
+ "hsl(28,72%,69%)",
+ "hsl(36,5%,80%)"
+ ],
+ "line-dasharray": [
+ 0.5,
+ 0.25
+ ],
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ 2
+ ],
+ [
+ "zoom"
+ ],
+ 6,
+ 0,
+ 7,
+ 0.5,
+ 10,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 0,
+ 2.5
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 2,
+ 0
+ ],
+ 12,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 2,
+ 6
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 3,
+ [
+ "secondary",
+ "tertiary"
+ ],
+ 2,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 1,
+ 0.5
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 5,
+ 8
+ ],
+ [
+ "trunk"
+ ],
+ 4,
+ [
+ "primary"
+ ],
+ 6,
+ [
+ "secondary"
+ ],
+ 6,
+ [
+ "tertiary"
+ ],
+ 4,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 3,
+ 3
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway",
+ "trunk",
+ "primary"
+ ],
+ 10,
+ [
+ "secondary"
+ ],
+ 8,
+ [
+ "tertiary"
+ ],
+ 8,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 4,
+ 4
+ ],
+ 20,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway",
+ "trunk",
+ "primary"
+ ],
+ 26,
+ [
+ "secondary"
+ ],
+ 26,
+ [
+ "tertiary"
+ ],
+ 26,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 18,
+ 18
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "!in",
+ "class",
+ "bridge",
+ "ferry",
+ "rail",
+ "transit",
+ "pier",
+ "path",
+ "aerialway",
+ "motorway_construction",
+ "trunk_construction",
+ "primary_construction",
+ "secondary_construction",
+ "tertiary_construction",
+ "minor_construction",
+ "service_construction",
+ "track_construction"
+ ]
+ ]
+ },
+ {
+ "id": "Tunnel",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 4,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "motorway",
+ "hsl(35,100%,76%)",
+ [
+ "trunk",
+ "primary"
+ ],
+ "hsl(48,100%,88%)",
+ "hsl(0,0%,96%)"
+ ],
+ "line-opacity": 1,
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ 2
+ ],
+ [
+ "zoom"
+ ],
+ 5,
+ 0,
+ 6,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "brunnel"
+ ],
+ [
+ "bridge"
+ ],
+ 0,
+ 1
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 0,
+ 0
+ ],
+ 10,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 0,
+ 2.5
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 1.5,
+ 1
+ ],
+ 12,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 1,
+ 4
+ ],
+ [
+ "trunk"
+ ],
+ 2.5,
+ [
+ "primary"
+ ],
+ 2.5,
+ [
+ "secondary",
+ "tertiary"
+ ],
+ 1.5,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 1,
+ 1
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 5,
+ 6
+ ],
+ [
+ "trunk"
+ ],
+ 3,
+ [
+ "primary"
+ ],
+ 5,
+ [
+ "secondary"
+ ],
+ 4,
+ [
+ "tertiary"
+ ],
+ 3,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 2,
+ 2
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway",
+ "trunk",
+ "primary"
+ ],
+ 8,
+ [
+ "secondary"
+ ],
+ 7,
+ [
+ "tertiary"
+ ],
+ 6,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 4,
+ 4
+ ],
+ 20,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway",
+ "trunk",
+ "primary"
+ ],
+ 24,
+ [
+ "secondary"
+ ],
+ 24,
+ [
+ "tertiary"
+ ],
+ 24,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 16,
+ 16
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "!in",
+ "class",
+ "ferry",
+ "rail",
+ "transit",
+ "pier",
+ "bridge",
+ "path",
+ "aerialway",
+ "motorway_construction",
+ "trunk_construction",
+ "primary_construction",
+ "secondary_construction",
+ "tertiary_construction",
+ "minor_construction",
+ "service_construction",
+ "track_construction"
+ ]
+ ]
+ },
+ {
+ "id": "Railway tunnel",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,73%)",
+ "line-opacity": 0.5,
+ "line-width": {
+ "base": 1.4,
+ "stops": [
+ [
+ 14,
+ 0.4
+ ],
+ [
+ 15,
+ 0.75
+ ],
+ [
+ 20,
+ 2
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "==",
+ "class",
+ "rail"
+ ]
+ ]
+ },
+ {
+ "id": "Railway tunnel hatching",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,73%)",
+ "line-dasharray": [
+ 0.2,
+ 8
+ ],
+ "line-opacity": 0.5,
+ "line-width": {
+ "base": 1.4,
+ "stops": [
+ [
+ 14.5,
+ 0
+ ],
+ [
+ 15,
+ 3
+ ],
+ [
+ 20,
+ 8
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "==",
+ "class",
+ "rail"
+ ]
+ ]
+ },
+ {
+ "id": "Footway tunnel outline",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 12,
+ "layout": {
+ "line-cap": "round",
+ "line-join": "miter",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,100%)",
+ "line-opacity": 1,
+ "line-width": {
+ "base": 1.2,
+ "stops": [
+ [
+ 14,
+ 0
+ ],
+ [
+ 16,
+ 0
+ ],
+ [
+ 18,
+ 4
+ ],
+ [
+ 22,
+ 8
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "in",
+ "class",
+ "path",
+ "pedestrian"
+ ],
+ [
+ "==",
+ "brunnel",
+ "tunnel"
+ ]
+ ]
+ },
+ {
+ "id": "Footway tunnel",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 12,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,63%)",
+ "line-dasharray": {
+ "stops": [
+ [
+ 14,
+ [
+ 1,
+ 0.5
+ ]
+ ],
+ [
+ 18,
+ [
+ 1,
+ 0.25
+ ]
+ ]
+ ]
+ },
+ "line-opacity": 0.4,
+ "line-width": {
+ "base": 1.2,
+ "stops": [
+ [
+ 14,
+ 0.5
+ ],
+ [
+ 16,
+ 1
+ ],
+ [
+ 18,
+ 2
+ ],
+ [
+ 22,
+ 5
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "in",
+ "class",
+ "path",
+ "pedestrian"
+ ],
+ [
+ "==",
+ "brunnel",
+ "tunnel"
+ ]
+ ]
+ },
+ {
+ "id": "Pier",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(42,49%,93%)"
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Polygon"
+ ],
+ [
+ "==",
+ "class",
+ "pier"
+ ]
+ ]
+ },
+ {
+ "id": "Pier road",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "layout": {
+ "line-cap": "round",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(42,49%,93%)",
+ "line-width": {
+ "base": 1.2,
+ "stops": [
+ [
+ 15,
+ 1
+ ],
+ [
+ 17,
+ 4
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "==",
+ "class",
+ "pier"
+ ]
+ ]
+ },
+ {
+ "id": "Bridge outline",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 13,
+ "maxzoom": 17,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(43, 50%, 93%)",
+ "line-opacity": 0.5,
+ "line-width": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 13,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 5,
+ 8
+ ],
+ 15,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 15,
+ 22
+ ],
+ 17,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 20,
+ 24
+ ]
+ ]
+ },
+ "filter": [
+ "all",
+ [
+ "!=",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "in",
+ "class",
+ "motorway",
+ "primary",
+ "trunk"
+ ]
+ ]
+ },
+ {
+ "id": "Bridge",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-antialias": true,
+ "fill-color": "hsl(42,49%,93%)",
+ "fill-opacity": 0.6
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Polygon"
+ ],
+ [
+ "==",
+ "brunnel",
+ "bridge"
+ ]
+ ]
+ },
+ {
+ "id": "Minor road outline",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 4,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(36,5%,80%)",
+ "line-opacity": 1,
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ 2
+ ],
+ [
+ "zoom"
+ ],
+ 6,
+ 0,
+ 7,
+ 0.5,
+ 12,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "secondary",
+ "tertiary"
+ ],
+ 2,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 1,
+ 0.5
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "secondary"
+ ],
+ 6,
+ [
+ "tertiary"
+ ],
+ 4,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 3,
+ 3
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "secondary"
+ ],
+ 8,
+ [
+ "tertiary"
+ ],
+ 8,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 4,
+ 4
+ ],
+ 20,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "secondary"
+ ],
+ 26,
+ [
+ "tertiary"
+ ],
+ 26,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 18,
+ 18
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "!=",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "!in",
+ "class",
+ "aerialway",
+ "bridge",
+ "ferry",
+ "minor_construction",
+ "motorway",
+ "motorway_construction",
+ "path",
+ "path_construction",
+ "pier",
+ "primary",
+ "primary_construction",
+ "rail",
+ "secondary_construction",
+ "service_construction",
+ "tertiary_construction",
+ "track_construction",
+ "transit",
+ "trunk_construction"
+ ]
+ ]
+ },
+ {
+ "id": "Major road outline",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 4,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(28,72%,69%)",
+ "line-opacity": 1,
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ 2
+ ],
+ [
+ "zoom"
+ ],
+ 6,
+ 0,
+ 7,
+ 0.5,
+ 10,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 2.4,
+ 0
+ ],
+ 12,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 3,
+ 0.5
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk"
+ ],
+ 4,
+ [
+ "primary"
+ ],
+ 6,
+ 3
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 10,
+ 4
+ ],
+ 20,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 26,
+ 18
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "!=",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "in",
+ "class",
+ "primary",
+ "trunk"
+ ]
+ ]
+ },
+ {
+ "id": "Highway outline",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 4,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(28,72%,69%)",
+ "line-opacity": 1,
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ 2
+ ],
+ [
+ "zoom"
+ ],
+ 6,
+ 0,
+ 7,
+ 0.5,
+ 10,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 0,
+ 2.5
+ ],
+ 0
+ ],
+ 12,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 2,
+ 6
+ ],
+ 0.5
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 5,
+ 8
+ ],
+ 3
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ 10,
+ 4
+ ],
+ 20,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ 26,
+ 18
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "!=",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "==",
+ "class",
+ "motorway"
+ ]
+ ]
+ },
+ {
+ "id": "Road under construction",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 4,
+ "layout": {
+ "line-cap": "square",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "motorway_construction",
+ "hsl(35,100%,76%)",
+ [
+ "trunk_construction",
+ "primary_construction"
+ ],
+ "hsl(48,100%,83%)",
+ "hsl(0,0%,100%)"
+ ],
+ "line-dasharray": [
+ 2,
+ 2
+ ],
+ "line-opacity": [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "brunnel"
+ ],
+ "tunnel"
+ ],
+ 0.7,
+ 1
+ ],
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ 2
+ ],
+ [
+ "zoom"
+ ],
+ 5,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway_construction"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "brunnel"
+ ],
+ [
+ "bridge"
+ ],
+ 0,
+ 0.5
+ ],
+ [
+ "trunk_construction",
+ "primary_construction"
+ ],
+ 0,
+ 0
+ ],
+ 10,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway_construction"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 0,
+ 2.5
+ ],
+ [
+ "trunk_construction",
+ "primary_construction"
+ ],
+ 1.5,
+ 1
+ ],
+ 12,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway_construction"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 1,
+ 4
+ ],
+ [
+ "trunk_construction"
+ ],
+ 2.5,
+ [
+ "primary_construction"
+ ],
+ 2.5,
+ [
+ "secondary_construction",
+ "tertiary_construction"
+ ],
+ 1.5,
+ [
+ "minor_construction",
+ "service_construction",
+ "track_construction"
+ ],
+ 1,
+ 1
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway_construction"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 5,
+ 6
+ ],
+ [
+ "trunk_construction"
+ ],
+ 3,
+ [
+ "primary_construction"
+ ],
+ 5,
+ [
+ "secondary_construction"
+ ],
+ 4,
+ [
+ "tertiary_construction"
+ ],
+ 3,
+ [
+ "minor_construction",
+ "service_construction",
+ "track_construction"
+ ],
+ 2,
+ 2
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway_construction",
+ "trunk_construction",
+ "primary_construction"
+ ],
+ 8,
+ [
+ "secondary_construction"
+ ],
+ 7,
+ [
+ "tertiary_construction"
+ ],
+ 6,
+ [
+ "minor_construction",
+ "service_construction",
+ "track_construction"
+ ],
+ 4,
+ 4
+ ],
+ 20,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway_construction",
+ "trunk_construction",
+ "primary_construction"
+ ],
+ 24,
+ [
+ "secondary_construction"
+ ],
+ 24,
+ [
+ "tertiary_construction"
+ ],
+ 24,
+ [
+ "minor_construction",
+ "service_construction",
+ "track_construction"
+ ],
+ 16,
+ 16
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "in",
+ "class",
+ "motorway_construction",
+ "trunk_construction",
+ "primary_construction",
+ "secondary_construction",
+ "tertiary_construction",
+ "minor_construction",
+ "service_construction",
+ "track_construction"
+ ]
+ },
+ {
+ "id": "Minor road",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 4,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,100%)",
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ 2
+ ],
+ [
+ "zoom"
+ ],
+ 5,
+ 0.5,
+ 10,
+ 1,
+ 12,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "secondary",
+ "tertiary"
+ ],
+ 1.5,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 1,
+ 1
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "secondary"
+ ],
+ 4,
+ [
+ "tertiary"
+ ],
+ 3,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 2,
+ 2
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "secondary"
+ ],
+ 7,
+ [
+ "tertiary"
+ ],
+ 6,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 4,
+ 4
+ ],
+ 20,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "secondary"
+ ],
+ 24,
+ [
+ "tertiary"
+ ],
+ 24,
+ [
+ "minor",
+ "service",
+ "track"
+ ],
+ 16,
+ 16
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "!=",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "!in",
+ "class",
+ "aerialway",
+ "bridge",
+ "ferry",
+ "minor_construction",
+ "motorway",
+ "motorway_construction",
+ "path",
+ "path_construction",
+ "pier",
+ "primary",
+ "primary_construction",
+ "rail",
+ "secondary_construction",
+ "service_construction",
+ "tertiary_construction",
+ "track_construction",
+ "transit",
+ "trunk_construction"
+ ]
+ ]
+ },
+ {
+ "id": "Major road",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 4,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(48,100%,83%)",
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ 2
+ ],
+ [
+ "zoom"
+ ],
+ 10,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 1.5,
+ 1
+ ],
+ 12,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 2.5,
+ 1
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk"
+ ],
+ 3,
+ [
+ "primary"
+ ],
+ 5,
+ 2
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 8,
+ 4
+ ],
+ 20,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "trunk",
+ "primary"
+ ],
+ 24,
+ 16
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "!=",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "in",
+ "class",
+ "primary",
+ "trunk"
+ ]
+ ]
+ },
+ {
+ "id": "Highway",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 4,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(35,100%,76%)",
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ 2
+ ],
+ [
+ "zoom"
+ ],
+ 5,
+ 0.5,
+ 6,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "brunnel"
+ ],
+ [
+ "bridge"
+ ],
+ 0,
+ 1
+ ],
+ 0
+ ],
+ 10,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 0,
+ 2.5
+ ],
+ 1
+ ],
+ 12,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 1,
+ 4
+ ],
+ 1
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ [
+ "match",
+ [
+ "get",
+ "ramp"
+ ],
+ 1,
+ 5,
+ 6
+ ],
+ 2
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ 8,
+ 4
+ ],
+ 20,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "motorway"
+ ],
+ 24,
+ 16
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "!=",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "==",
+ "class",
+ "motorway"
+ ]
+ ]
+ },
+ {
+ "id": "Path outline",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 12,
+ "layout": {
+ "line-cap": "round",
+ "line-join": "miter",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,100%)",
+ "line-width": {
+ "base": 1.2,
+ "stops": [
+ [
+ 14,
+ 0
+ ],
+ [
+ 16,
+ 0
+ ],
+ [
+ 18,
+ 4
+ ],
+ [
+ 22,
+ 8
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "in",
+ "class",
+ "path",
+ "pedestrian"
+ ],
+ [
+ "!=",
+ "brunnel",
+ "tunnel"
+ ]
+ ]
+ },
+ {
+ "id": "Path minor",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 12,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0, 0%, 79%)",
+ "line-dasharray": {
+ "stops": [
+ [
+ 14,
+ [
+ 1,
+ 0.5
+ ]
+ ],
+ [
+ 18,
+ [
+ 1,
+ 0.25
+ ]
+ ]
+ ]
+ },
+ "line-width": {
+ "base": 1.2,
+ "stops": [
+ [
+ 14,
+ 0.5
+ ],
+ [
+ 16,
+ 1
+ ],
+ [
+ 18,
+ 2
+ ],
+ [
+ 22,
+ 5
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "in",
+ "class",
+ "path_pedestrian"
+ ],
+ [
+ "!=",
+ "brunnel",
+ "tunnel"
+ ]
+ ]
+ },
+ {
+ "id": "Path",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 12,
+ "layout": {
+ "line-cap": "butt",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0, 0%, 79%)",
+ "line-dasharray": {
+ "stops": [
+ [
+ 14,
+ [
+ 1,
+ 0.5
+ ]
+ ],
+ [
+ 18,
+ [
+ 1,
+ 0.25
+ ]
+ ]
+ ]
+ },
+ "line-width": {
+ "base": 1.2,
+ "stops": [
+ [
+ 14,
+ 0.5
+ ],
+ [
+ 16,
+ 1
+ ],
+ [
+ 18,
+ 2
+ ],
+ [
+ 22,
+ 5
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "in",
+ "class",
+ "path",
+ "pedestrian"
+ ],
+ [
+ "!=",
+ "brunnel",
+ "tunnel"
+ ]
+ ]
+ },
+ {
+ "id": "Major rail",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": {
+ "stops": [
+ [
+ 8,
+ "hsl(0,0%,72%)"
+ ],
+ [
+ 16,
+ "hsl(0,0%,70%)"
+ ]
+ ]
+ },
+ "line-opacity": [
+ "match",
+ [
+ "get",
+ "service"
+ ],
+ "yard",
+ 0.5,
+ 1
+ ],
+ "line-width": {
+ "base": 1.4,
+ "stops": [
+ [
+ 14,
+ 0.4
+ ],
+ [
+ 15,
+ 0.75
+ ],
+ [
+ 20,
+ 2
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "!in",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "==",
+ "class",
+ "rail"
+ ]
+ ]
+ },
+ {
+ "id": "Major rail hatching",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,72%)",
+ "line-dasharray": [
+ 0.2,
+ 9
+ ],
+ "line-opacity": [
+ "match",
+ [
+ "get",
+ "service"
+ ],
+ "yard",
+ 0.5,
+ 1
+ ],
+ "line-width": {
+ "base": 1.4,
+ "stops": [
+ [
+ 14.5,
+ 0
+ ],
+ [
+ 15,
+ 3
+ ],
+ [
+ 20,
+ 8
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "!in",
+ "brunnel",
+ "tunnel"
+ ],
+ [
+ "==",
+ "class",
+ "rail"
+ ]
+ ]
+ },
+ {
+ "id": "Minor rail",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,73%)",
+ "line-width": {
+ "base": 1.4,
+ "stops": [
+ [
+ 14,
+ 0.4
+ ],
+ [
+ 15,
+ 0.75
+ ],
+ [
+ 20,
+ 2
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "in",
+ "subclass",
+ "light_rail",
+ "tram"
+ ]
+ },
+ {
+ "id": "Minor rail hatching",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,73%)",
+ "line-dasharray": [
+ 0.2,
+ 4
+ ],
+ "line-width": {
+ "base": 1.4,
+ "stops": [
+ [
+ 14.5,
+ 0
+ ],
+ [
+ 15,
+ 2
+ ],
+ [
+ 20,
+ 6
+ ]
+ ]
+ }
+ },
+ "metadata": {},
+ "filter": [
+ "in",
+ "subclass",
+ "tram",
+ "light_rail"
+ ]
+ },
+ {
+ "id": "Building",
+ "type": "fill",
+ "source": "maptiler_planet",
+ "source-layer": "building",
+ "minzoom": 13,
+ "maxzoom": 15,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-color": "hsl(30,6%,73%)",
+ "fill-opacity": 0.3,
+ "fill-outline-color": {
+ "base": 1,
+ "stops": [
+ [
+ 13,
+ "hsla(35, 6%, 79%, 0.3)"
+ ],
+ [
+ 14,
+ "hsl(35, 6%, 79%)"
+ ]
+ ]
+ }
+ },
+ "metadata": {}
+ },
+ {
+ "id": "Building 3D",
+ "type": "fill-extrusion",
+ "source": "maptiler_planet",
+ "source-layer": "building",
+ "minzoom": 15,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "fill-extrusion-base": {
+ "property": "render_min_height",
+ "type": "identity"
+ },
+ "fill-extrusion-color": "hsl(44,14%,79%)",
+ "fill-extrusion-height": {
+ "property": "render_height",
+ "type": "identity"
+ },
+ "fill-extrusion-opacity": 0.4
+ },
+ "metadata": {},
+ "filter": [
+ "!has",
+ "hide_3d"
+ ]
+ },
+ {
+ "id": "Aqueduct outline",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "waterway",
+ "layout": {
+ "line-cap": "round",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,51%)",
+ "line-width": {
+ "base": 1.3,
+ "stops": [
+ [
+ 14,
+ 1
+ ],
+ [
+ 20,
+ 6
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "==",
+ "brunnel",
+ "bridge"
+ ]
+ },
+ {
+ "id": "Aqueduct",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "waterway",
+ "layout": {
+ "line-cap": "round",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(204,92%,75%)",
+ "line-width": {
+ "base": 1.3,
+ "stops": [
+ [
+ 12,
+ 0.5
+ ],
+ [
+ 20,
+ 5
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "==",
+ "brunnel",
+ "bridge"
+ ]
+ ]
+ },
+ {
+ "id": "Cablecar",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 13,
+ "layout": {
+ "line-cap": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-blur": 1,
+ "line-color": "hsl(0,0%,100%)",
+ "line-width": {
+ "base": 1,
+ "stops": [
+ [
+ 13,
+ 2
+ ],
+ [
+ 19,
+ 4
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "==",
+ "class",
+ "aerialway"
+ ]
+ },
+ {
+ "id": "Cablecar dash",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 13,
+ "layout": {
+ "line-cap": "round",
+ "line-join": "bevel",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0,0%,64%)",
+ "line-dasharray": [
+ 2,
+ 2
+ ],
+ "line-width": {
+ "base": 1,
+ "stops": [
+ [
+ 13,
+ 1
+ ],
+ [
+ 19,
+ 2
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "==",
+ "class",
+ "aerialway"
+ ]
+ },
+ {
+ "id": "Other border",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "boundary",
+ "minzoom": 3,
+ "layout": {
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0, 0%, 70%)",
+ "line-dasharray": [
+ 2,
+ 1
+ ],
+ "line-width": [
+ "interpolate",
+ [
+ "linear",
+ 1
+ ],
+ [
+ "zoom"
+ ],
+ 3,
+ 0.75,
+ 4,
+ 0.8,
+ 11,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "admin_level"
+ ],
+ 6
+ ],
+ 1.75,
+ 1.5
+ ],
+ 18,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "admin_level"
+ ],
+ 6
+ ],
+ 3,
+ 2
+ ]
+ ]
+ },
+ "filter": [
+ "all",
+ [
+ "in",
+ "admin_level",
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ [
+ "==",
+ "maritime",
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "Disputed border",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "boundary",
+ "minzoom": 0,
+ "layout": {
+ "line-cap": "round",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0, 0%, 63%)",
+ "line-dasharray": [
+ 2,
+ 2
+ ],
+ "line-width": {
+ "stops": [
+ [
+ 1,
+ 0.5
+ ],
+ [
+ 5,
+ 1.5
+ ],
+ [
+ 10,
+ 2
+ ],
+ [
+ 24,
+ 12
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "all",
+ [
+ "==",
+ "admin_level",
+ 2
+ ],
+ [
+ "==",
+ "disputed",
+ 1
+ ],
+ [
+ "==",
+ "maritime",
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "Country border",
+ "type": "line",
+ "source": "maptiler_planet",
+ "source-layer": "boundary",
+ "minzoom": 0,
+ "layout": {
+ "line-cap": "round",
+ "line-join": "round",
+ "visibility": "visible"
+ },
+ "paint": {
+ "line-color": "hsl(0, 0%, 54%)",
+ "line-width": {
+ "stops": [
+ [
+ 1,
+ 0.5
+ ],
+ [
+ 5,
+ 1.5
+ ],
+ [
+ 10,
+ 2
+ ],
+ [
+ 24,
+ 12
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "all",
+ [
+ "==",
+ "admin_level",
+ 2
+ ],
+ [
+ "==",
+ "disputed",
+ 0
+ ],
+ [
+ "==",
+ "maritime",
+ 0
+ ]
+ ]
+ },
+ {
+ "id": "River labels",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "waterway",
+ "minzoom": 13,
+ "layout": {
+ "symbol-placement": "line",
+ "symbol-spacing": 400,
+ "text-field": [
+ "coalesce",
+ [
+ "get",
+ "name:en"
+ ],
+ [
+ "get",
+ "name"
+ ]
+ ],
+ "text-font": [
+ "Roboto Italic",
+ "Noto Sans Italic"
+ ],
+ "text-letter-spacing": 0.2,
+ "text-max-width": 5,
+ "text-rotation-alignment": "map",
+ "text-size": {
+ "stops": [
+ [
+ 12,
+ 8
+ ],
+ [
+ 16,
+ 14
+ ],
+ [
+ 22,
+ 20
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": "hsl(205,84%,39%)",
+ "text-halo-blur": 1,
+ "text-halo-color": "hsl(202, 76%, 82%)",
+ "text-halo-width": {
+ "stops": [
+ [
+ 10,
+ 1
+ ],
+ [
+ 18,
+ 2
+ ]
+ ]
+ }
+ },
+ "filter": [
+ "==",
+ "$type",
+ "LineString"
+ ]
+ },
+ {
+ "id": "Ocean labels",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "water_name",
+ "minzoom": 0,
+ "layout": {
+ "symbol-placement": "point",
+ "text-field": "{name:en}",
+ "text-font": [
+ "Roboto Italic",
+ "Noto Sans Italic"
+ ],
+ "text-max-width": 5,
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ 1
+ ],
+ [
+ "zoom"
+ ],
+ 1,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "ocean"
+ ],
+ 14,
+ 10
+ ],
+ 3,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "ocean"
+ ],
+ 18,
+ 14
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "ocean"
+ ],
+ 22,
+ 18
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "lake"
+ ],
+ 14,
+ [
+ "sea"
+ ],
+ 20,
+ 26
+ ]
+ ],
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": {
+ "stops": [
+ [
+ 1,
+ "hsl(203,54%,54%)"
+ ],
+ [
+ 4,
+ "hsl(203,72%,39%)"
+ ]
+ ]
+ },
+ "text-halo-blur": 1,
+ "text-halo-color": {
+ "stops": [
+ [
+ 1,
+ "hsla(196, 72%, 80%, 0.05)"
+ ],
+ [
+ 3,
+ "hsla(200, 100%, 88%, 0.75)"
+ ]
+ ]
+ },
+ "text-halo-width": 1,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 1,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "ocean"
+ ],
+ 1,
+ 0
+ ],
+ 3,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Point"
+ ],
+ [
+ "has",
+ "name"
+ ],
+ [
+ "!=",
+ "class",
+ "lake"
+ ]
+ ]
+ },
+ {
+ "id": "Lake labels",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "water_name",
+ "minzoom": 0,
+ "layout": {
+ "symbol-placement": "line",
+ "text-field": [
+ "coalesce",
+ [
+ "get",
+ "name:en"
+ ],
+ [
+ "get",
+ "name"
+ ]
+ ],
+ "text-font": [
+ "Roboto Italic",
+ "Noto Sans Italic"
+ ],
+ "text-letter-spacing": 0.1,
+ "text-max-width": 5,
+ "text-size": {
+ "stops": [
+ [
+ 10,
+ 13
+ ],
+ [
+ 14,
+ 16
+ ],
+ [
+ 22,
+ 20
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": "hsl(205,84%,39%)",
+ "text-halo-color": "hsla(0, 100%, 100%, 0.45)",
+ "text-halo-width": 1.5
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "==",
+ "class",
+ "lake"
+ ]
+ ]
+ },
+ {
+ "id": "Housenumber",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "housenumber",
+ "minzoom": 18,
+ "layout": {
+ "text-field": "{housenumber}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-size": 10,
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": "hsl(26,10%,44%)",
+ "text-halo-blur": 1,
+ "text-halo-color": "hsl(21,64%,96%)",
+ "text-halo-width": 1
+ }
+ },
+ {
+ "id": "Gondola",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "transportation_name",
+ "minzoom": 13,
+ "layout": {
+ "symbol-placement": "line",
+ "text-anchor": "center",
+ "text-field": [
+ "coalesce",
+ [
+ "get",
+ "name:en"
+ ],
+ [
+ "get",
+ "name"
+ ]
+ ],
+ "text-font": [
+ "Roboto Italic",
+ "Noto Sans Italic"
+ ],
+ "text-offset": [
+ 0.8,
+ 0.8
+ ],
+ "text-size": {
+ "base": 1,
+ "stops": [
+ [
+ 13,
+ 11
+ ],
+ [
+ 15,
+ 12
+ ],
+ [
+ 18,
+ 13
+ ],
+ [
+ 22,
+ 14
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": "hsl(0,0%,40%)",
+ "text-halo-blur": 1,
+ "text-halo-color": "hsl(0,0%,100%)",
+ "text-halo-width": 1
+ },
+ "metadata": {},
+ "filter": [
+ "in",
+ "subclass",
+ "gondola",
+ "cable_car"
+ ]
+ },
+ {
+ "id": "Ferry",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "transportation_name",
+ "minzoom": 12,
+ "layout": {
+ "symbol-placement": "line",
+ "text-anchor": "center",
+ "text-field": [
+ "coalesce",
+ [
+ "get",
+ "name:en"
+ ],
+ [
+ "get",
+ "name"
+ ]
+ ],
+ "text-font": [
+ "Roboto Italic",
+ "Noto Sans Italic"
+ ],
+ "text-offset": [
+ 0.8,
+ 0.8
+ ],
+ "text-size": {
+ "base": 1,
+ "stops": [
+ [
+ 13,
+ 11
+ ],
+ [
+ 15,
+ 12
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": "hsl(205,84%,39%)",
+ "text-halo-blur": 0.5,
+ "text-halo-color": "hsla(0, 0%, 100%, 0.15)",
+ "text-halo-width": 1
+ },
+ "filter": [
+ "==",
+ "class",
+ "ferry"
+ ]
+ },
+ {
+ "id": "Oneway",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "transportation",
+ "minzoom": 16,
+ "layout": {
+ "icon-image": "oneway",
+ "icon-padding": 2,
+ "icon-rotate": [
+ "match",
+ [
+ "get",
+ "oneway"
+ ],
+ 1,
+ 0,
+ 0
+ ],
+ "icon-rotation-alignment": "map",
+ "icon-size": {
+ "stops": [
+ [
+ 16,
+ 0.7
+ ],
+ [
+ 19,
+ 1
+ ]
+ ]
+ },
+ "symbol-placement": "line",
+ "symbol-spacing": 75,
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(0, 0%, 65%)",
+ "icon-opacity": 0.5
+ },
+ "filter": [
+ "all",
+ [
+ "has",
+ "oneway"
+ ],
+ [
+ "in",
+ "class",
+ "motorway",
+ "trunk",
+ "primary",
+ "secondary",
+ "tertiary",
+ "minor",
+ "service"
+ ]
+ ]
+ },
+ {
+ "id": "Road labels",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "transportation_name",
+ "minzoom": 8,
+ "layout": {
+ "icon-allow-overlap": false,
+ "icon-ignore-placement": false,
+ "icon-keep-upright": false,
+ "symbol-placement": "line",
+ "symbol-spacing": [
+ "step",
+ [
+ "zoom"
+ ],
+ 250,
+ 21,
+ 1000
+ ],
+ "text-allow-overlap": false,
+ "text-anchor": "center",
+ "text-field": [
+ "coalesce",
+ [
+ "get",
+ "name:en"
+ ],
+ [
+ "get",
+ "name"
+ ]
+ ],
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-ignore-placement": false,
+ "text-justify": "center",
+ "text-max-width": 10,
+ "text-offset": [
+ 0,
+ 0.15
+ ],
+ "text-optional": false,
+ "text-size": {
+ "stops": [
+ [
+ 13,
+ 10
+ ],
+ [
+ 14,
+ 11
+ ],
+ [
+ 18,
+ 13
+ ],
+ [
+ 22,
+ 15
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(0, 0%, 16%)",
+ "text-color": "hsl(0, 0%, 16%)",
+ "text-halo-blur": 0.5,
+ "text-halo-color": "hsl(0,0%,100%)",
+ "text-halo-width": 1
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "!in",
+ "subclass",
+ "gondola",
+ "cable_car"
+ ],
+ [
+ "!in",
+ "class",
+ "ferry",
+ "service"
+ ]
+ ]
+ },
+ {
+ "id": "Highway junction",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "transportation_name",
+ "minzoom": 16,
+ "layout": {
+ "icon-image": "exit_{ref_length}",
+ "icon-rotation-alignment": "viewport",
+ "icon-size": 1,
+ "symbol-avoid-edges": true,
+ "symbol-placement": "point",
+ "symbol-spacing": 200,
+ "symbol-z-order": "auto",
+ "text-field": "{ref}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-offset": [
+ 0,
+ 0.1
+ ],
+ "text-rotation-alignment": "viewport",
+ "text-size": 9,
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": "hsl(0,0%,21%)",
+ "text-halo-color": "hsl(0,0%,100%)",
+ "text-halo-width": 1
+ },
+ "filter": [
+ "all",
+ [
+ ">",
+ "ref_length",
+ 0
+ ],
+ [
+ "==",
+ "$type",
+ "Point"
+ ],
+ [
+ "==",
+ "subclass",
+ "junction"
+ ]
+ ]
+ },
+ {
+ "id": "Highway shield",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "transportation_name",
+ "minzoom": 8,
+ "layout": {
+ "icon-image": "road_{ref_length}",
+ "icon-rotation-alignment": "viewport",
+ "icon-size": 1,
+ "symbol-avoid-edges": true,
+ "symbol-placement": "line",
+ "symbol-spacing": {
+ "stops": [
+ [
+ 10,
+ 200
+ ],
+ [
+ 18,
+ 400
+ ]
+ ]
+ },
+ "text-field": "{ref}",
+ "text-font": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "motorway",
+ [
+ "literal",
+ [
+ "Roboto Bold",
+ "Noto Sans Bold"
+ ]
+ ],
+ [
+ "literal",
+ [
+ "Roboto Regular"
+ ]
+ ]
+ ],
+ "text-offset": [
+ 0,
+ 0.05
+ ],
+ "text-padding": 2,
+ "text-rotation-alignment": "viewport",
+ "text-size": 10,
+ "text-transform": "uppercase",
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(0, 0%, 100%)",
+ "icon-halo-color": "hsl(0, 0%, 29%)",
+ "icon-halo-width": 1,
+ "text-color": "hsl(0, 0%, 29%)",
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 1
+ },
+ "filter": [
+ "all",
+ [
+ "<=",
+ "ref_length",
+ 6
+ ],
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "!in",
+ "network",
+ "us-interstate",
+ "us-highway",
+ "us-state"
+ ],
+ [
+ "!in",
+ "class",
+ "path"
+ ]
+ ]
+ },
+ {
+ "id": "Highway shield (US)",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "transportation_name",
+ "minzoom": 7,
+ "layout": {
+ "icon-image": "{network}_{ref_length}",
+ "icon-rotation-alignment": "viewport",
+ "icon-size": 1.1,
+ "symbol-avoid-edges": true,
+ "symbol-placement": {
+ "base": 1,
+ "stops": [
+ [
+ 7,
+ "point"
+ ],
+ [
+ 7,
+ "line"
+ ],
+ [
+ 8,
+ "line"
+ ]
+ ]
+ },
+ "symbol-spacing": 200,
+ "text-field": "{ref}",
+ "text-font": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "motorway",
+ [
+ "literal",
+ [
+ "Roboto Bold",
+ "Noto Sans Bold"
+ ]
+ ],
+ [
+ "literal",
+ [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ]
+ ]
+ ],
+ "text-offset": [
+ 0,
+ 0.05
+ ],
+ "text-rotation-alignment": "viewport",
+ "text-size": 9,
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(0, 0%, 100%)",
+ "icon-halo-color": "hsl(0, 0%, 29%)",
+ "icon-halo-width": 1,
+ "text-color": "hsl(0, 0%, 29%)",
+ "text-halo-color": "rgba(255, 255, 255, 1)",
+ "text-halo-width": 0
+ },
+ "filter": [
+ "all",
+ [
+ "<=",
+ "ref_length",
+ 6
+ ],
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "in",
+ "network",
+ "us-highway",
+ "us-state"
+ ],
+ [
+ "!in",
+ "class",
+ "path"
+ ]
+ ]
+ },
+ {
+ "id": "Highway shield interstate top (US)",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "transportation_name",
+ "minzoom": 7,
+ "layout": {
+ "icon-image": "{network}_{ref_length}",
+ "icon-rotation-alignment": "viewport",
+ "icon-size": 1,
+ "symbol-placement": {
+ "base": 1,
+ "stops": [
+ [
+ 7,
+ "point"
+ ],
+ [
+ 7,
+ "line"
+ ],
+ [
+ 8,
+ "line"
+ ]
+ ]
+ },
+ "symbol-spacing": 200,
+ "text-field": "{ref}",
+ "text-font": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "motorway",
+ [
+ "literal",
+ [
+ "Roboto Bold",
+ "Noto Sans Bold"
+ ]
+ ],
+ [
+ "literal",
+ [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ]
+ ]
+ ],
+ "text-offset": [
+ 0,
+ 0.1
+ ],
+ "text-rotation-alignment": "viewport",
+ "text-size": 9,
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(21, 100%, 45%)",
+ "icon-halo-color": "rgba(255, 255, 255, 1)",
+ "icon-halo-width": 1,
+ "icon-translate": [
+ 0,
+ -4
+ ],
+ "icon-translate-anchor": "viewport",
+ "text-color": "hsl(21, 100%, 45%)",
+ "text-halo-color": "rgba(255, 255, 255, 1)",
+ "text-halo-width": 0
+ },
+ "filter": [
+ "all",
+ [
+ "<=",
+ "ref_length",
+ 6
+ ],
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "==",
+ "network",
+ "us-interstate"
+ ],
+ [
+ "!in",
+ "class",
+ "path"
+ ]
+ ]
+ },
+ {
+ "id": "Highway shield interstate (US)",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "transportation_name",
+ "minzoom": 7,
+ "layout": {
+ "icon-image": "{network}_{ref_length}",
+ "icon-rotation-alignment": "viewport",
+ "icon-size": 1,
+ "symbol-placement": {
+ "base": 1,
+ "stops": [
+ [
+ 7,
+ "point"
+ ],
+ [
+ 7,
+ "line"
+ ],
+ [
+ 8,
+ "line"
+ ]
+ ]
+ },
+ "symbol-spacing": 200,
+ "text-field": "{ref}",
+ "text-font": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "motorway",
+ [
+ "literal",
+ [
+ "Roboto Bold",
+ "Noto Sans Bold"
+ ]
+ ],
+ [
+ "literal",
+ [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ]
+ ]
+ ],
+ "text-offset": [
+ 0,
+ 0.1
+ ],
+ "text-rotation-alignment": "viewport",
+ "text-size": 9,
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(212, 79%, 42%)",
+ "icon-halo-color": "rgba(255, 255, 255, 1)",
+ "icon-halo-width": 1,
+ "text-color": "hsl(0, 0%, 100%)",
+ "text-halo-color": "rgba(255, 255, 255, 1)",
+ "text-halo-width": 0,
+ "text-translate": [
+ 0,
+ -0.5
+ ]
+ },
+ "filter": [
+ "all",
+ [
+ "<=",
+ "ref_length",
+ 6
+ ],
+ [
+ "==",
+ "$type",
+ "LineString"
+ ],
+ [
+ "==",
+ "network",
+ "us-interstate"
+ ],
+ [
+ "!in",
+ "class",
+ "path"
+ ]
+ ]
+ },
+ {
+ "id": "Public",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "poi",
+ "minzoom": 16,
+ "layout": {
+ "icon-image": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "atm",
+ "bank",
+ "bbq",
+ "cemetery",
+ "courthouse",
+ "drinking_water",
+ "fire_station",
+ "fountain",
+ "hairdresser",
+ "office",
+ "post",
+ "prison",
+ "recycling",
+ "shower",
+ "telephone",
+ "toilets",
+ "townhall",
+ "town_hall"
+ ],
+ [
+ "get",
+ "class"
+ ],
+ [
+ "case",
+ [
+ "has",
+ "class"
+ ],
+ "",
+ "dot"
+ ]
+ ],
+ "icon-size": 1,
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ 0.8
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 12,
+ 10
+ ],
+ [
+ 16,
+ 12
+ ],
+ [
+ 22,
+ 14
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(51, 10%, 40%)",
+ "icon-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.8,
+ 16,
+ 0
+ ],
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": 2,
+ "icon-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "atm",
+ "bank",
+ "cemetery",
+ "courthouse",
+ "townhall",
+ "town_hall"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "atm",
+ "bank",
+ "cemetery",
+ "courthouse",
+ "fire_station",
+ "townhall",
+ "town_hall",
+ "post"
+ ],
+ 1,
+ 0
+ ],
+ 18,
+ 1
+ ],
+ "text-color": "hsl(51, 10%, 40%)",
+ "text-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.5,
+ 16,
+ 0
+ ],
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "atm",
+ "bank",
+ "cemetery",
+ "courthouse",
+ "townhall",
+ "town_hall"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "atm",
+ "bank",
+ "cemetery",
+ "courthouse",
+ "fire_station",
+ "townhall",
+ "town_hall",
+ "post"
+ ],
+ 1,
+ 0
+ ],
+ 18,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Point"
+ ],
+ [
+ "in",
+ "class",
+ "atm",
+ "bank",
+ "bbq",
+ "cemetery",
+ "courthouse",
+ "drinking_water",
+ "fire_station",
+ "fountain",
+ "hairdresser",
+ "office",
+ "post",
+ "prison",
+ "recycling",
+ "shower",
+ "telephone",
+ "toilets",
+ "townhall",
+ "town_hall"
+ ]
+ ]
+ },
+ {
+ "id": "Sport",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "poi",
+ "minzoom": 16,
+ "layout": {
+ "icon-image": [
+ "coalesce",
+ [
+ "image",
+ [
+ "to-string",
+ [
+ "get",
+ "subclass"
+ ]
+ ]
+ ],
+ [
+ "image",
+ [
+ "get",
+ "class"
+ ]
+ ],
+ [
+ "image",
+ "dot"
+ ]
+ ],
+ "icon-size": 1,
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ 0.8
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 12,
+ 10
+ ],
+ [
+ 16,
+ 12
+ ],
+ [
+ 22,
+ 14
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(129, 65%, 30%)",
+ "icon-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.8,
+ 16,
+ 0
+ ],
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": 2,
+ "icon-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "playground",
+ "pitch",
+ "stadium",
+ "sports_hall",
+ "swimming_pool"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ],
+ "text-color": "hsl(129, 65%, 30%)",
+ "text-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.5,
+ 16,
+ 0
+ ],
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "playground",
+ "pitch",
+ "stadium",
+ "sports_hall",
+ "swimming_pool"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Point"
+ ],
+ [
+ "in",
+ "class",
+ "american_football",
+ "athletics",
+ "archery",
+ "baseball",
+ "basketball",
+ "climbing",
+ "equestrian",
+ "fitness",
+ "fitness_centre",
+ "golf",
+ "motor",
+ "multi",
+ "playground",
+ "pitch",
+ "running",
+ "sauna",
+ "soccer",
+ "sport",
+ "stadium",
+ "sports_centre",
+ "sports_hall",
+ "swimming",
+ "swimming_area",
+ "swimming_pool",
+ "tennis",
+ "volleyball",
+ "water_park"
+ ],
+ [
+ "has",
+ "name"
+ ]
+ ]
+ },
+ {
+ "id": "Education",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "poi",
+ "minzoom": 15,
+ "layout": {
+ "icon-image": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "college",
+ "childcare",
+ "dancing_school",
+ "driving_school",
+ "kindergarten",
+ "school",
+ "university"
+ ],
+ [
+ "get",
+ "class"
+ ],
+ [
+ "case",
+ [
+ "has",
+ "class"
+ ],
+ "",
+ "dot"
+ ]
+ ],
+ "icon-size": 1,
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ 0.8
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 12,
+ 10
+ ],
+ [
+ 16,
+ 12
+ ],
+ [
+ 22,
+ 14
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(175, 50%, 40%)",
+ "icon-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.8,
+ 16,
+ 0
+ ],
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": 2,
+ "icon-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 15,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "university"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "school",
+ "university"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ],
+ "text-color": "hsl(175, 50%, 40%)",
+ "text-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.5,
+ 16,
+ 0
+ ],
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 15,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "university"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "school",
+ "university"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Point"
+ ],
+ [
+ "in",
+ "class",
+ "college",
+ "childcare",
+ "dancing_school",
+ "driving_school",
+ "kindergarten",
+ "school",
+ "university"
+ ],
+ [
+ "has",
+ "name"
+ ]
+ ]
+ },
+ {
+ "id": "Tourism",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "poi",
+ "minzoom": 14,
+ "layout": {
+ "icon-image": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "apartment",
+ "aquarium",
+ "attraction",
+ "campsite",
+ "camp_site",
+ "caravan_site",
+ "castle",
+ "chalet",
+ "guest_house",
+ "hotel",
+ "hostel",
+ "information",
+ "lodging",
+ "motel",
+ "reservoir",
+ "ruins",
+ "theme_park",
+ "zoo"
+ ],
+ [
+ "get",
+ "class"
+ ],
+ [
+ "case",
+ [
+ "has",
+ "class"
+ ],
+ "",
+ "dot"
+ ]
+ ],
+ "icon-size": 1,
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ 0.8
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 12,
+ 10
+ ],
+ [
+ 16,
+ 12
+ ],
+ [
+ 22,
+ 14
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(283, 55%, 35%)",
+ "icon-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.8,
+ 16,
+ 0
+ ],
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": 2,
+ "icon-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "attraction"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "attraction",
+ "castle",
+ "hotel",
+ "zoo"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ],
+ "text-color": "hsl(283, 55%, 35%)",
+ "text-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.5,
+ 16,
+ 0
+ ],
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "attraction"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "attraction",
+ "castle",
+ "hotel",
+ "zoo"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Point"
+ ],
+ [
+ "in",
+ "class",
+ "apartment",
+ "aquarium",
+ "attraction",
+ "campsite",
+ "camp_site",
+ "caravan_site",
+ "castle",
+ "chalet",
+ "guest_house",
+ "hotel",
+ "hostel",
+ "information",
+ "lodging",
+ "motel",
+ "reservoir",
+ "ruins",
+ "theme_park",
+ "zoo"
+ ],
+ [
+ "!=",
+ "subclass",
+ "board"
+ ],
+ [
+ "has",
+ "name"
+ ]
+ ]
+ },
+ {
+ "id": "Culture",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "poi",
+ "minzoom": 14,
+ "layout": {
+ "icon-image": [
+ "coalesce",
+ [
+ "image",
+ [
+ "to-string",
+ [
+ "get",
+ "subclass"
+ ]
+ ]
+ ],
+ [
+ "image",
+ [
+ "get",
+ "class"
+ ]
+ ],
+ [
+ "image",
+ "dot"
+ ]
+ ],
+ "icon-size": 1,
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ 0.8
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 12,
+ 10
+ ],
+ [
+ 16,
+ 12
+ ],
+ [
+ 22,
+ 14
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(315, 35%, 50%)",
+ "icon-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.8,
+ 16,
+ 0
+ ],
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": 2,
+ "icon-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "museum"
+ ],
+ 1,
+ 0
+ ],
+ 15,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "cinema",
+ "art_gallery",
+ "museum",
+ "theatre"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "cinema",
+ "art_gallery",
+ "library",
+ "museum",
+ "place_of_worship",
+ "theatre"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ],
+ "text-color": "hsl(315, 35%, 50%)",
+ "text-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.5,
+ 16,
+ 0
+ ],
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "museum"
+ ],
+ 1,
+ 0
+ ],
+ 15,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "cinema",
+ "art_gallery",
+ "museum",
+ "theatre"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "cinema",
+ "art_gallery",
+ "library",
+ "museum",
+ "place_of_worship",
+ "theatre"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Point"
+ ],
+ [
+ "in",
+ "class",
+ "art_gallery",
+ "archeological_site",
+ "cinema",
+ "community_centre",
+ "gallery",
+ "library",
+ "monastery",
+ "monument",
+ "museum",
+ "opera",
+ "place_of_worship",
+ "planetarium",
+ "theatre"
+ ],
+ [
+ "!=",
+ "subclass",
+ "artwork"
+ ],
+ [
+ "has",
+ "name"
+ ]
+ ]
+ },
+ {
+ "id": "Shopping",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "poi",
+ "minzoom": 14,
+ "layout": {
+ "icon-image": [
+ "coalesce",
+ [
+ "image",
+ [
+ "to-string",
+ [
+ "get",
+ "subclass"
+ ]
+ ]
+ ],
+ [
+ "image",
+ [
+ "get",
+ "class"
+ ]
+ ],
+ [
+ "image",
+ "dot"
+ ]
+ ],
+ "icon-size": 1,
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ 0.8
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 12,
+ 10
+ ],
+ [
+ 16,
+ 12
+ ],
+ [
+ 22,
+ 14
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(18, 17%, 30%)",
+ "icon-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.8,
+ 16,
+ 0
+ ],
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": 2,
+ "icon-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "mall"
+ ],
+ 1,
+ 0
+ ],
+ 15,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "mall",
+ "supermarket"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "bakery",
+ "chemist",
+ "mall",
+ "supermarket"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ],
+ "text-color": "hsl(18, 17%, 30%)",
+ "text-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.5,
+ 16,
+ 0
+ ],
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "mall"
+ ],
+ 1,
+ 0
+ ],
+ 15,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "mall",
+ "supermarket"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "bakery",
+ "chemist",
+ "mall",
+ "supermarket"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Point"
+ ],
+ [
+ "all",
+ [
+ "in",
+ "class",
+ "alcohol_shop",
+ "bakery",
+ "book",
+ "books",
+ "butcher",
+ "chemist",
+ "clothing_store",
+ "convenience",
+ "gift",
+ "grocery",
+ "laundry",
+ "mall",
+ "music",
+ "shop",
+ "supermarket"
+ ],
+ [
+ "has",
+ "name"
+ ]
+ ]
+ ]
+ },
+ {
+ "id": "Food",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "poi",
+ "minzoom": 14,
+ "maxzoom": 22,
+ "layout": {
+ "icon-image": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "bar",
+ "beer",
+ "cafe",
+ "fast_food",
+ "ice_cream",
+ "restaurant"
+ ],
+ [
+ "get",
+ "class"
+ ],
+ [
+ "biergarten",
+ "pub"
+ ],
+ "beer",
+ "",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "food_court"
+ ],
+ "restaurant",
+ "dot"
+ ],
+ "icon-size": 1,
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ 0.8
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 12,
+ 10
+ ],
+ [
+ 16,
+ 12
+ ],
+ [
+ 22,
+ 14
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(18, 24%, 44%)",
+ "icon-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.8,
+ 16,
+ 0
+ ],
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": 2,
+ "icon-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 20
+ ],
+ 1,
+ 0
+ ],
+ 15,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 99
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 499
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ],
+ "text-color": "hsl(18, 24%, 44%)",
+ "text-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.5,
+ 16,
+ 0
+ ],
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 20
+ ],
+ 1,
+ 0
+ ],
+ 15,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 99
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 499
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Point"
+ ],
+ [
+ "in",
+ "class",
+ "bar",
+ "beer",
+ "biergarten",
+ "cafe",
+ "fast_food",
+ "food_court",
+ "ice_cream",
+ "pub",
+ "restaurant"
+ ],
+ [
+ "has",
+ "name"
+ ]
+ ]
+ },
+ {
+ "id": "Transport",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "poi",
+ "minzoom": 14,
+ "layout": {
+ "icon-image": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "aerialway",
+ "bicycle",
+ "bicycle_parking",
+ "car",
+ "car_rental",
+ "car_repair",
+ "charging_station",
+ "cycle_barrier",
+ "ferry_terminal",
+ "fuel",
+ "harbor",
+ "motorcycle_parking",
+ "parking",
+ "parking_garage",
+ "parking_paid"
+ ],
+ [
+ "get",
+ "class"
+ ],
+ [
+ "case",
+ [
+ "has",
+ "class"
+ ],
+ "dot",
+ ""
+ ]
+ ],
+ "icon-size": 1,
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ 0.8
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 12,
+ 10
+ ],
+ [
+ 16,
+ 12
+ ],
+ [
+ 22,
+ 14
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(215, 81%, 35%)",
+ "icon-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.8,
+ 16,
+ 0
+ ],
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": 2,
+ "icon-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "ferry_terminal",
+ "fuel",
+ "terminal"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "car",
+ "car_rental",
+ "car_repair",
+ "charging_station",
+ "ferry_terminal",
+ "fuel",
+ "harbor",
+ "heliport",
+ "highway_rest_area",
+ "terminal",
+ "toll"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "car",
+ "car_rental",
+ "car_repair",
+ "charging_station",
+ "ferry_terminal",
+ "fuel",
+ "harbor",
+ "heliport",
+ "highway_rest_area",
+ "parking",
+ "parking_garage",
+ "parking_paid",
+ "terminal",
+ "toll"
+ ],
+ 1,
+ 0
+ ],
+ 18,
+ 1
+ ],
+ "text-color": "hsl(215, 81%, 35%)",
+ "text-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.5,
+ 16,
+ 0
+ ],
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "ferry_terminal",
+ "fuel",
+ "terminal"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "car",
+ "car_rental",
+ "car_repair",
+ "charging_station",
+ "ferry_terminal",
+ "fuel",
+ "harbor",
+ "heliport",
+ "highway_rest_area",
+ "terminal",
+ "toll"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "car",
+ "car_rental",
+ "car_repair",
+ "charging_station",
+ "ferry_terminal",
+ "fuel",
+ "harbor",
+ "heliport",
+ "highway_rest_area",
+ "parking",
+ "parking_garage",
+ "parking_paid",
+ "terminal",
+ "toll"
+ ],
+ 1,
+ 0
+ ],
+ 18,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Point"
+ ],
+ [
+ "in",
+ "class",
+ "bicycle",
+ "bicycle_parking",
+ "bicycle_rental",
+ "car",
+ "car_rental",
+ "car_repair",
+ "charging_station",
+ "ferry_terminal",
+ "fuel",
+ "harbor",
+ "heliport",
+ "highway_rest_area",
+ "motorcycle_parking",
+ "parking",
+ "parking_garage",
+ "parking_paid",
+ "scooter",
+ "terminal",
+ "toll"
+ ]
+ ]
+ },
+ {
+ "id": "Park",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "poi",
+ "minzoom": 14,
+ "layout": {
+ "icon-anchor": "center",
+ "icon-image": "park",
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ 0.8
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 12,
+ 10
+ ],
+ [
+ 16,
+ 12
+ ],
+ [
+ 22,
+ 14
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(82, 83%, 25%)",
+ "icon-halo-blur": 0,
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": 2,
+ "icon-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 10
+ ],
+ 1,
+ 0
+ ],
+ 15,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 99
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ 1
+ ],
+ "text-color": "hsl(82, 83%, 25%)",
+ "text-halo-blur": 0,
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 10
+ ],
+ 1,
+ 0
+ ],
+ 15,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 99
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "class",
+ "park"
+ ],
+ [
+ "has",
+ "name"
+ ]
+ ]
+ },
+ {
+ "id": "Healthcare",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "poi",
+ "minzoom": 14,
+ "layout": {
+ "icon-image": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "clinic",
+ "dentist",
+ "doctors",
+ "doctor",
+ "first_aid",
+ "hospital",
+ "pharmacy",
+ "veterinary"
+ ],
+ [
+ "get",
+ "class"
+ ],
+ [
+ "case",
+ [
+ "has",
+ "class"
+ ],
+ "",
+ "dot"
+ ]
+ ],
+ "icon-size": 1,
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ 0.8
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 12,
+ 10
+ ],
+ [
+ 16,
+ 12
+ ],
+ [
+ 22,
+ 14
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(6, 96%, 35%)",
+ "icon-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.8,
+ 16,
+ 0
+ ],
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": 2,
+ "icon-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "hospital"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "clinic",
+ "hospital"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ],
+ "text-color": "hsl(6, 96%, 35%)",
+ "text-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.5,
+ 16,
+ 0
+ ],
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "hospital"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "clinic",
+ "hospital"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "$type",
+ "Point"
+ ],
+ [
+ "in",
+ "class",
+ "clinic",
+ "dentist",
+ "doctors",
+ "first_aid",
+ "hospital",
+ "pharmacy",
+ "veterinary"
+ ],
+ [
+ "has",
+ "name"
+ ]
+ ]
+ },
+ {
+ "id": "Place labels",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "place",
+ "minzoom": 4,
+ "layout": {
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "center",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-letter-spacing": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "suburb",
+ "neighborhood",
+ "neighbourhood",
+ "quarter",
+ "island"
+ ],
+ 0.2,
+ 0
+ ],
+ "text-max-width": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "island"
+ ],
+ 6,
+ 8
+ ],
+ "text-offset": [
+ 0,
+ 0
+ ],
+ "text-padding": 2,
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ 1
+ ],
+ [
+ "zoom"
+ ],
+ 3,
+ 11,
+ 8,
+ 13,
+ 11,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "village",
+ 12,
+ [
+ "suburb",
+ "neighbourhood",
+ "quarter",
+ "hamlet",
+ "isolated_dwelling"
+ ],
+ 9,
+ "island",
+ 8,
+ 12
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "village",
+ 18,
+ [
+ "suburb",
+ "neighbourhood",
+ "quarter",
+ "hamlet",
+ "isolated_dwelling"
+ ],
+ 15,
+ "island",
+ 11,
+ 16
+ ]
+ ],
+ "text-transform": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "suburb",
+ "neighborhood",
+ "neighbourhood",
+ "quarter",
+ "island"
+ ],
+ "uppercase",
+ "none"
+ ],
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": "hsl(0,0%,25%)",
+ "text-halo-color": "hsl(0,0%,100%)",
+ "text-halo-width": 1.2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 1,
+ 8,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "island"
+ ],
+ 0,
+ 1
+ ],
+ 9,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ [
+ "island"
+ ],
+ 1,
+ 1
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "!in",
+ "class",
+ "continent",
+ "country",
+ "state",
+ "region",
+ "province",
+ "city",
+ "town",
+ "place"
+ ]
+ },
+ {
+ "id": "Station",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "poi",
+ "minzoom": 12,
+ "layout": {
+ "icon-image": [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "bus_stop",
+ "subway"
+ ],
+ [
+ "get",
+ "subclass"
+ ],
+ "bus_station",
+ "bus_stop",
+ "station",
+ "railway",
+ "tram_stop",
+ "tramway",
+ [
+ "case",
+ [
+ "has",
+ "subclass"
+ ],
+ "dot",
+ ""
+ ]
+ ],
+ "icon-size": 1,
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": "{name}",
+ "text-font": [
+ "Roboto Medium",
+ "Noto Sans Regular"
+ ],
+ "text-line-height": 0.9,
+ "text-max-width": 9,
+ "text-offset": [
+ 0,
+ 0.9
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 13,
+ 11
+ ],
+ [
+ 16,
+ 12
+ ],
+ [
+ 22,
+ 16
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(215, 83%, 53%)",
+ "icon-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.8,
+ 16,
+ 0
+ ],
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": 2,
+ "icon-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 12,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "station"
+ ],
+ 1,
+ 0
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "station",
+ "subway",
+ "tram_stop"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "bus_stop",
+ "station",
+ "subway",
+ "tram_stop"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ],
+ "text-color": "hsl(215, 83%, 53%)",
+ "text-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 12,
+ 1,
+ 14,
+ 0.5,
+ 16,
+ 0
+ ],
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": 2,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 12,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "station"
+ ],
+ 1,
+ 0
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "station",
+ "subway",
+ "tram_stop"
+ ],
+ 1,
+ 0
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "subclass"
+ ],
+ [
+ "bus_stop",
+ "station",
+ "subway",
+ "tram_stop"
+ ],
+ 1,
+ 0
+ ],
+ 17,
+ 1
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "in",
+ "class",
+ "bus",
+ "railway"
+ ],
+ [
+ "has",
+ "name"
+ ]
+ ]
+ },
+ {
+ "id": "Airport gate",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "aeroway",
+ "minzoom": 15,
+ "layout": {
+ "text-field": "{ref}",
+ "text-font": [
+ "Roboto Medium",
+ "Noto Sans Regular"
+ ],
+ "text-size": {
+ "stops": [
+ [
+ 15,
+ 10
+ ],
+ [
+ 22,
+ 18
+ ]
+ ]
+ },
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": "hsl(0,0%,40%)",
+ "text-halo-blur": 0.5,
+ "text-halo-color": "hsl(0,0%,100%)",
+ "text-halo-width": 1
+ },
+ "filter": [
+ "==",
+ "class",
+ "gate"
+ ]
+ },
+ {
+ "id": "Airport",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "aerodrome_label",
+ "minzoom": 8,
+ "layout": {
+ "icon-image": [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "international",
+ "airport",
+ "airfield"
+ ],
+ "icon-size": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 8,
+ 0.6,
+ 10,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "international",
+ 0.8,
+ 0.6
+ ],
+ 16,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "international",
+ 1,
+ 0.8
+ ]
+ ],
+ "text-anchor": "top",
+ "text-field": {
+ "stops": [
+ [
+ 8,
+ " "
+ ],
+ [
+ 9,
+ "{iata}"
+ ],
+ [
+ 12,
+ "{name:en}"
+ ]
+ ]
+ },
+ "text-font": [
+ "Roboto Medium",
+ "Noto Sans Regular"
+ ],
+ "text-line-height": 1.2,
+ "text-max-width": 9,
+ "text-offset": [
+ 0,
+ 0.8
+ ],
+ "text-optional": true,
+ "text-padding": 2,
+ "text-size": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 9,
+ 9,
+ 10,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "international",
+ 10,
+ 7
+ ],
+ 14,
+ [
+ "match",
+ [
+ "get",
+ "class"
+ ],
+ "international",
+ 13,
+ 11
+ ]
+ ],
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(215, 83%, 53%)",
+ "icon-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 8,
+ 1,
+ 10,
+ 0.5,
+ 12,
+ 0
+ ],
+ "icon-halo-color": "hsl(0, 0%, 100%)",
+ "icon-halo-width": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 8,
+ 1,
+ 12,
+ 2
+ ],
+ "icon-opacity": 1,
+ "text-color": "hsl(215, 83%, 53%)",
+ "text-halo-blur": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 8,
+ 1,
+ 10,
+ 0.5,
+ 12,
+ 0
+ ],
+ "text-halo-color": "hsl(0, 0%, 100%)",
+ "text-halo-width": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 8,
+ 1,
+ 12,
+ 2
+ ]
+ },
+ "filter": [
+ "all",
+ [
+ "has",
+ "iata"
+ ],
+ [
+ "!in",
+ "class",
+ "public"
+ ]
+ ]
+ },
+ {
+ "id": "State labels",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "place",
+ "minzoom": 3,
+ "maxzoom": 9,
+ "layout": {
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-field": [
+ "coalesce",
+ [
+ "get",
+ "name:en"
+ ],
+ [
+ "get",
+ "name"
+ ]
+ ],
+ "text-font": [
+ "Roboto Medium",
+ "Noto Sans Regular"
+ ],
+ "text-letter-spacing": 0.1,
+ "text-max-width": 8,
+ "text-padding": 2,
+ "text-size": {
+ "stops": [
+ [
+ 3,
+ 9
+ ],
+ [
+ 5,
+ 10
+ ],
+ [
+ 6,
+ 11
+ ]
+ ]
+ },
+ "text-transform": "uppercase",
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": "hsl(48,4%,44%)",
+ "text-halo-color": "hsla(0,0%,100%,0.75)",
+ "text-halo-width": 0.8,
+ "text-opacity": [
+ "step",
+ [
+ "zoom"
+ ],
+ 0,
+ 3,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 3
+ ],
+ 1,
+ 0
+ ],
+ 8,
+ [
+ "case",
+ [
+ "==",
+ [
+ "get",
+ "rank"
+ ],
+ 0
+ ],
+ 0,
+ 1
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "in",
+ "class",
+ "state",
+ "province"
+ ],
+ [
+ "<=",
+ "rank",
+ 6
+ ]
+ ]
+ },
+ {
+ "id": "Town labels",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "place",
+ "minzoom": 4,
+ "maxzoom": 16,
+ "layout": {
+ "icon-allow-overlap": true,
+ "icon-image": {
+ "stops": [
+ [
+ 6,
+ "circle"
+ ],
+ [
+ 12,
+ " "
+ ]
+ ]
+ },
+ "icon-optional": false,
+ "icon-size": [
+ "interpolate",
+ [
+ "exponential",
+ 1
+ ],
+ [
+ "zoom"
+ ],
+ 6,
+ 0.3,
+ 14,
+ 0.4
+ ],
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "bottom",
+ "text-field": [
+ "coalesce",
+ [
+ "get",
+ "name:en"
+ ],
+ [
+ "get",
+ "name"
+ ]
+ ],
+ "text-font": [
+ "Roboto Regular",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ -0.15
+ ],
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ 1
+ ],
+ [
+ "zoom"
+ ],
+ 6,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 12
+ ],
+ 11,
+ 10
+ ],
+ 9,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 15
+ ],
+ 13,
+ 12
+ ],
+ 16,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 15
+ ],
+ 22,
+ 20
+ ]
+ ],
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(0, 20%, 99%)",
+ "icon-halo-color": "hsl(0, 0%, 29%)",
+ "icon-halo-width": 1,
+ "text-color": [
+ "interpolate",
+ [
+ "linear"
+ ],
+ [
+ "zoom"
+ ],
+ 6,
+ "hsl(0,0%,20%)",
+ 12,
+ "hsl(0,0%,0%)"
+ ],
+ "text-halo-blur": 0.5,
+ "text-halo-color": "hsl(0,0%,100%)",
+ "text-halo-width": 1
+ },
+ "metadata": {},
+ "filter": [
+ "==",
+ "class",
+ "town"
+ ]
+ },
+ {
+ "id": "City labels",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "place",
+ "minzoom": 4,
+ "maxzoom": 16,
+ "layout": {
+ "icon-allow-overlap": true,
+ "icon-image": [
+ "step",
+ [
+ "zoom"
+ ],
+ "circle",
+ 13,
+ ""
+ ],
+ "icon-optional": false,
+ "icon-size": 0.4,
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "bottom",
+ "text-field": "{name:en}",
+ "text-font": [
+ "Roboto Medium",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ -0.15
+ ],
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ 1
+ ],
+ [
+ "zoom"
+ ],
+ 4,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 2
+ ],
+ 14,
+ 12
+ ],
+ 8,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 4
+ ],
+ 18,
+ 14
+ ],
+ 12,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 4
+ ],
+ 24,
+ 18
+ ],
+ 16,
+ [
+ "case",
+ [
+ "<=",
+ [
+ "get",
+ "rank"
+ ],
+ 4
+ ],
+ 32,
+ 26
+ ]
+ ],
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(0, 0%, 100%)",
+ "icon-halo-color": "hsl(0, 0%, 29%)",
+ "icon-halo-width": 1,
+ "icon-opacity": 1,
+ "text-color": "hsl(0,0%,20%)",
+ "text-halo-blur": 0.5,
+ "text-halo-color": "hsl(0,0%,100%)",
+ "text-halo-width": 0.8
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "class",
+ "city"
+ ],
+ [
+ "!=",
+ "capital",
+ 2
+ ]
+ ]
+ },
+ {
+ "id": "Capital city labels",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "place",
+ "minzoom": 4,
+ "maxzoom": 16,
+ "layout": {
+ "icon-allow-overlap": true,
+ "icon-image": [
+ "step",
+ [
+ "zoom"
+ ],
+ "circle",
+ 13,
+ ""
+ ],
+ "icon-optional": false,
+ "icon-size": [
+ "interpolate",
+ [
+ "linear",
+ 1
+ ],
+ [
+ "zoom"
+ ],
+ 4,
+ 0.45,
+ 10,
+ 0.5,
+ 11,
+ 0.6
+ ],
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-anchor": "bottom",
+ "text-field": "{name:en}",
+ "text-font": [
+ "Roboto Medium",
+ "Noto Sans Regular"
+ ],
+ "text-max-width": 8,
+ "text-offset": [
+ 0,
+ -0.15
+ ],
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ 1
+ ],
+ [
+ "zoom"
+ ],
+ 4,
+ 14,
+ 8,
+ 18,
+ 12,
+ 24,
+ 16,
+ 32
+ ],
+ "visibility": "visible"
+ },
+ "paint": {
+ "icon-color": "hsl(0, 0%, 100%)",
+ "icon-halo-color": "hsl(0, 0%, 29%)",
+ "icon-halo-width": 1,
+ "icon-opacity": 1,
+ "text-color": "hsl(0,0%,20%)",
+ "text-halo-blur": 0.5,
+ "text-halo-color": "hsl(0,0%,100%)",
+ "text-halo-width": 0.8
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "class",
+ "city"
+ ],
+ [
+ "==",
+ "capital",
+ 2
+ ]
+ ]
+ },
+ {
+ "id": "Country labels",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "place",
+ "minzoom": 1,
+ "maxzoom": 12,
+ "layout": {
+ "symbol-sort-key": [
+ "to-number",
+ [
+ "get",
+ "rank"
+ ]
+ ],
+ "text-allow-overlap": false,
+ "text-field": "{name:en}",
+ "text-font": [
+ "Roboto Medium",
+ "Noto Sans Regular"
+ ],
+ "text-letter-spacing": 0.07,
+ "text-max-width": {
+ "stops": [
+ [
+ 1,
+ 5
+ ],
+ [
+ 5,
+ 8
+ ]
+ ]
+ },
+ "text-padding": 1,
+ "text-size": [
+ "interpolate",
+ [
+ "linear",
+ 1
+ ],
+ [
+ "zoom"
+ ],
+ 0,
+ 8,
+ 1,
+ 10,
+ 4,
+ [
+ "case",
+ [
+ ">",
+ [
+ "get",
+ "rank"
+ ],
+ 2
+ ],
+ 15,
+ 17
+ ],
+ 8,
+ [
+ "case",
+ [
+ ">",
+ [
+ "get",
+ "rank"
+ ],
+ 2
+ ],
+ 19,
+ 23
+ ]
+ ],
+ "text-transform": "none",
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": "hsl(0, 0%, 20%)",
+ "text-halo-blur": 0.8,
+ "text-halo-color": "hsl(0,0%,100%)",
+ "text-halo-width": 1,
+ "text-opacity": [
+ "interpolate",
+ [
+ "linear",
+ 1
+ ],
+ [
+ "zoom"
+ ],
+ 4,
+ [
+ "case",
+ [
+ ">",
+ [
+ "get",
+ "rank"
+ ],
+ 4
+ ],
+ 0,
+ 1
+ ],
+ 5.9,
+ [
+ "case",
+ [
+ ">",
+ [
+ "get",
+ "rank"
+ ],
+ 4
+ ],
+ 0,
+ 1
+ ],
+ 6,
+ [
+ "case",
+ [
+ ">",
+ [
+ "get",
+ "rank"
+ ],
+ 4
+ ],
+ 1,
+ 1
+ ]
+ ]
+ },
+ "metadata": {},
+ "filter": [
+ "all",
+ [
+ "==",
+ "class",
+ "country"
+ ],
+ [
+ "has",
+ "iso_a2"
+ ],
+ [
+ "!=",
+ "iso_a2",
+ "VA"
+ ]
+ ]
+ },
+ {
+ "id": "Continent labels",
+ "type": "symbol",
+ "source": "maptiler_planet",
+ "source-layer": "place",
+ "maxzoom": 1,
+ "layout": {
+ "text-field": "{name:en}",
+ "text-font": [
+ "Roboto Medium",
+ "Noto Sans Regular"
+ ],
+ "text-justify": "center",
+ "text-size": {
+ "stops": [
+ [
+ 0,
+ 12
+ ],
+ [
+ 2,
+ 13
+ ]
+ ]
+ },
+ "text-transform": "uppercase",
+ "visibility": "visible"
+ },
+ "paint": {
+ "text-color": "hsl(0,0%,19%)",
+ "text-halo-blur": 1,
+ "text-halo-color": "hsl(0,0%,100%)",
+ "text-halo-width": 1
+ },
+ "metadata": {},
+ "filter": [
+ "==",
+ "class",
+ "continent"
+ ]
+ }
+ ],
+ "metadata": {
+ "maptiler:copyright": "You are licensed to use the style or its derivate for serving map tiles exclusively with MapTiler Server or MapTiler Cloud and in accordance with their licenses and terms. If you plan to use the style in a different way, contact us at sales@maptiler.com.",
+ "spaceColor": "hsl(203, 100%, 85%)"
+ },
+ "glyphs": "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key=Tp9K7a8qzMTkBSzO4EZb",
+ "sprite": "https://api.maptiler.com/maps/streets-v2/sprite",
+ "bearing": 0,
+ "pitch": 0,
+ "center": [
+ 0,
+ 0
+ ],
+ "zoom": 1
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/ui/NavGraph.kt b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/ui/NavGraph.kt
index b464a151..a2b314db 100644
--- a/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/ui/NavGraph.kt
+++ b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/ui/NavGraph.kt
@@ -23,6 +23,8 @@ import ovh.plrapps.mapcompose.demo.ui.screens.MarkersLazyLoadingDemo
import ovh.plrapps.mapcompose.demo.ui.screens.OsmDemo
import ovh.plrapps.mapcompose.demo.ui.screens.PathsDemo
import ovh.plrapps.mapcompose.demo.ui.screens.RotationDemo
+import ovh.plrapps.mapcompose.demo.ui.screens.SwissMapVectorDemo
+import ovh.plrapps.mapcompose.demo.ui.screens.VectorDemo
import ovh.plrapps.mapcompose.demo.ui.screens.VisibleAreaPaddingDemo
/* Type-safe navigation routes. Each demo screen and the home list has its own route object. */
@@ -42,6 +44,8 @@ import ovh.plrapps.mapcompose.demo.ui.screens.VisibleAreaPaddingDemo
@Serializable object VisibleAreaPaddingRoute
@Serializable object MarkersClusteringRoute
@Serializable object MarkersLazyLoadingRoute
+@Serializable object VectorRoute
+@Serializable object SwissMapVectorRoute
enum class MainDestinations(val title: String, val route: Any) {
MAP_ALONE("Simple map", MapAlone),
@@ -58,7 +62,9 @@ enum class MainDestinations(val title: String, val route: Any) {
HTTP_TILES_DEMO("Remote HTTP tiles", HttpTilesRoute),
VISIBLE_AREA_PADDING("Visible area padding", VisibleAreaPaddingRoute),
MARKERS_CLUSTERING("Markers clustering", MarkersClusteringRoute),
- MARKERS_LAZY_LOADING("Markers lazy loading", MarkersLazyLoadingRoute);
+ MARKERS_LAZY_LOADING("Markers lazy loading", MarkersLazyLoadingRoute),
+ VECTOR_TILE("Vector tile ", VectorRoute),
+ SWISS_VECTOR_TILE("Swiss map vector tile", SwissMapVectorRoute);
}
/**
@@ -82,6 +88,8 @@ fun NavGraphBuilder.demoDestinations() {
composable { VisibleAreaPaddingDemo.Content() }
composable { MarkersClusteringDemo.Content() }
composable { MarkersLazyLoadingDemo.Content() }
+ composable { VectorDemo.Content() }
+ composable { SwissMapVectorDemo.Content() }
}
/**
diff --git a/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.kt b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.kt
new file mode 100644
index 00000000..e1619de4
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.kt
@@ -0,0 +1,19 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import ovh.plrapps.mapcompose.demo.viewmodels.SwissMapVectorDemoVM
+import ovh.plrapps.mapcompose.ui.MapUI
+
+expect object SwissMapVectorDemo {
+ @Composable
+ fun Content()
+}
+
+@Composable
+fun SwissMapVectorCommonUi(screenModel: SwissMapVectorDemoVM) {
+ MapUI(
+ Modifier,
+ state = screenModel.state
+ )
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.kt b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.kt
new file mode 100644
index 00000000..a6ba54f3
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.kt
@@ -0,0 +1,19 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import ovh.plrapps.mapcompose.demo.viewmodels.VectorDemoVM
+import ovh.plrapps.mapcompose.ui.MapUI
+
+expect object VectorDemo {
+ @Composable
+ fun Content()
+}
+
+@Composable
+fun VectorCommonUi(screenModel: VectorDemoVM) {
+ MapUI(
+ Modifier,
+ state = screenModel.state
+ )
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/SwissMapVectorDemoVM.kt b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/SwissMapVectorDemoVM.kt
new file mode 100644
index 00000000..d132495b
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/SwissMapVectorDemoVM.kt
@@ -0,0 +1,52 @@
+package ovh.plrapps.mapcompose.demo.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+import ovh.plrapps.mapcompose.api.addVectorLayer
+import ovh.plrapps.mapcompose.api.enableRotation
+import ovh.plrapps.mapcompose.api.scale
+import ovh.plrapps.mapcompose.api.shouldLoopScale
+import ovh.plrapps.mapcompose.ui.layout.Forced
+import ovh.plrapps.mapcompose.ui.state.MapState
+import kotlin.math.pow
+
+/**
+ * Shows how MapCompose behaves with remote HTTP tiles.
+ */
+class SwissMapVectorDemoVM : ViewModel() {
+ private val vectorTileStreamProvider = OSMVectorTileStreamProvider(
+ styleUrl = "https://vectortiles.geo.admin.ch/styles/ch.swisstopo.lightbasemap.vt/style.json" // ch.swisstopo.basemap.vt
+ )
+
+ private val maxLevel = 15
+ private val minLevel = 7
+ private val tileSize = 512
+ private val mapSize = mapSizeAtLevel(maxLevel, tileSize = tileSize)
+
+ val state = MapState(
+ levelCount = maxLevel + 1,
+ fullWidth = mapSize,
+ fullHeight = mapSize,
+ workerCount = 16,
+ tileSize = tileSize
+ ) {
+ minimumScaleMode(Forced(1 / 2.0.pow(maxLevel - minLevel)))
+ scroll(0.5231943373, 0.3542287256)
+ }.apply {
+ viewModelScope.launch {
+ addVectorLayer(vectorTileStreamProvider)
+ }
+ enableRotation()
+ scale = 0.0
+ shouldLoopScale = true
+ }
+}
+
+/**
+ * wmts level are 0 based.
+ * At level 0, the map corresponds to just one tile.
+ */
+private fun mapSizeAtLevel(wmtsLevel: Int, tileSize: Int): Int {
+ return tileSize * 2.0.pow(wmtsLevel).toInt()
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/VectorDemoVM.kt b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/VectorDemoVM.kt
new file mode 100644
index 00000000..c91785c2
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/VectorDemoVM.kt
@@ -0,0 +1,61 @@
+package ovh.plrapps.mapcompose.demo.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+import kotlinx.io.RawSource
+import ovh.plrapps.mapcompose.api.addVectorLayer
+import ovh.plrapps.mapcompose.api.enableRotation
+import ovh.plrapps.mapcompose.api.scale
+import ovh.plrapps.mapcompose.api.shouldLoopScale
+import ovh.plrapps.mapcompose.vector.core.VectorTileStreamProvider
+import ovh.plrapps.mapcompose.ui.state.MapState
+import kotlin.math.pow
+
+/**
+ * Shows how MapCompose behaves with remote HTTP tiles.
+ */
+class VectorDemoVM : ViewModel() {
+ private val vectorTileStreamProvider = OSMVectorTileStreamProvider(
+ styleUrl = "files/style_street_v2.json"
+ )
+
+ private val maxLevel = 16
+ private val mapSize = mapSizeAtLevel(maxLevel, tileSize = 256)
+
+ val state = MapState(
+ levelCount = maxLevel + 1,
+ fullWidth = mapSize,
+ fullHeight = mapSize,
+ workerCount = 16
+ ){
+ infiniteScrollX(true)
+ }.apply {
+ viewModelScope.launch {
+ addVectorLayer(vectorTileStreamProvider)
+ }
+ enableRotation()
+ scale = 0.0
+ shouldLoopScale = true
+ }
+}
+
+expect class OSMVectorTileStreamProvider(styleUrl: String) : VectorTileStreamProvider {
+ override val styleUrl: String
+
+ override suspend fun loadResources(url: String): RawSource?
+ override suspend fun getTileStream(
+ tileUrl: String,
+ row: Int,
+ col: Int,
+ zoomLvl: Int
+ ): RawSource?
+}
+
+/**
+ * wmts level are 0 based.
+ * At level 0, the map corresponds to just one tile.
+ */
+private fun mapSizeAtLevel(wmtsLevel: Int, tileSize: Int): Int {
+ return tileSize * 2.0.pow(wmtsLevel).toInt()
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.desktop.kt b/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.desktop.kt
new file mode 100644
index 00000000..11417cf9
--- /dev/null
+++ b/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.desktop.kt
@@ -0,0 +1,17 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import androidx.lifecycle.viewmodel.compose.viewModel
+import ovh.plrapps.mapcompose.demo.ui.MapWithZoomControl
+import ovh.plrapps.mapcompose.demo.viewmodels.SwissMapVectorDemoVM
+
+actual object SwissMapVectorDemo {
+ @Composable
+ actual fun Content() {
+ val viewModel = viewModel { SwissMapVectorDemoVM() }
+
+ MapWithZoomControl(state = viewModel.state) {
+ SwissMapVectorCommonUi(viewModel)
+ }
+ }
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.desktop.kt b/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.desktop.kt
new file mode 100644
index 00000000..b7bf774a
--- /dev/null
+++ b/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.desktop.kt
@@ -0,0 +1,17 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import androidx.lifecycle.viewmodel.compose.viewModel
+import ovh.plrapps.mapcompose.demo.ui.MapWithZoomControl
+import ovh.plrapps.mapcompose.demo.viewmodels.VectorDemoVM
+
+actual object VectorDemo {
+ @Composable
+ actual fun Content() {
+ val viewModel = viewModel { VectorDemoVM() }
+
+ MapWithZoomControl(state = viewModel.state) {
+ VectorCommonUi(viewModel)
+ }
+ }
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt b/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt
index aec69332..b42f35e6 100644
--- a/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt
+++ b/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt
@@ -1,7 +1,12 @@
package ovh.plrapps.mapcompose.demo.viewmodels
+import kotlinx.io.Buffer
+import kotlinx.io.RawSource
import kotlinx.io.asSource
+import org.jetbrains.compose.resources.ExperimentalResourceApi
import ovh.plrapps.mapcompose.core.TileStreamProvider
+import ovh.plrapps.mapcompose.vector.core.VectorTileStreamProvider
+import ovh.plrapps.mapcomposemp.demo.Res
import java.net.HttpURLConnection
import java.net.URL
@@ -42,4 +47,51 @@ actual fun makeOsmTileStreamProvider() : TileStreamProvider {
null
}
}
+}
+
+actual class OSMVectorTileStreamProvider actual constructor(actual override val styleUrl: String) :
+ VectorTileStreamProvider {
+
+ @OptIn(ExperimentalResourceApi::class)
+ actual override suspend fun loadResources(url: String): RawSource? {
+ return when (url) {
+ "files/style_street_v2.json" -> {
+ val buffer = Buffer()
+ buffer.write(Res.readBytes(url))
+ buffer
+ }
+ else -> getResourceAsStream(url)
+ }
+ }
+
+ actual override suspend fun getTileStream(
+ tileUrl: String,
+ row: Int,
+ col: Int,
+ zoomLvl: Int
+ ): RawSource? {
+ return getResourceAsStream(tileUrl)
+ }
+
+ private fun getResourceAsStream(url: String): RawSource? {
+ return try {
+ val connection = URL(url).openConnection() as HttpURLConnection
+ connection.setRequestProperty("User-Agent", "Chrome/120.0.0.0 Safari/537.36")
+ // Explicitly request gzip — this disables Java's auto-decompression, so we
+ // decompress manually below when the server confirms gzip encoding.
+ connection.setRequestProperty("Accept-Encoding", "gzip")
+ connection.doInput = true
+ connection.connect()
+
+ val stream = if (connection.contentEncoding?.equals("gzip", ignoreCase = true) == true) {
+ java.util.zip.GZIPInputStream(connection.inputStream)
+ } else {
+ connection.inputStream
+ }
+ stream.asSource()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
}
\ No newline at end of file
diff --git a/demo/composeApp/src/iosMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.ios.kt b/demo/composeApp/src/iosMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.ios.kt
new file mode 100644
index 00000000..8901d691
--- /dev/null
+++ b/demo/composeApp/src/iosMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/SwissMapVectorDemo.ios.kt
@@ -0,0 +1,14 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import androidx.lifecycle.viewmodel.compose.viewModel
+import ovh.plrapps.mapcompose.demo.viewmodels.SwissMapVectorDemoVM
+
+actual object SwissMapVectorDemo {
+ @Composable
+ actual fun Content() {
+ val viewModel = viewModel { SwissMapVectorDemoVM() }
+
+ SwissMapVectorCommonUi(viewModel)
+ }
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/iosMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.ios.kt b/demo/composeApp/src/iosMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.ios.kt
new file mode 100644
index 00000000..429467dd
--- /dev/null
+++ b/demo/composeApp/src/iosMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.ios.kt
@@ -0,0 +1,14 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import androidx.lifecycle.viewmodel.compose.viewModel
+import ovh.plrapps.mapcompose.demo.viewmodels.VectorDemoVM
+
+actual object VectorDemo {
+ @Composable
+ actual fun Content() {
+ val viewModel = viewModel { VectorDemoVM() }
+
+ VectorCommonUi(viewModel)
+ }
+}
\ No newline at end of file
diff --git a/demo/composeApp/src/iosMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt b/demo/composeApp/src/iosMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt
index d697c278..f565808f 100644
--- a/demo/composeApp/src/iosMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt
+++ b/demo/composeApp/src/iosMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/Http.kt
@@ -1,8 +1,13 @@
package ovh.plrapps.mapcompose.demo.viewmodels
+import kotlinx.io.Buffer
+import kotlinx.io.RawSource
+import org.jetbrains.compose.resources.ExperimentalResourceApi
import ovh.plrapps.mapcompose.core.TileStreamProvider
+import ovh.plrapps.mapcompose.vector.core.VectorTileStreamProvider
import ovh.plrapps.mapcompose.demo.utils.getKtorClient
import ovh.plrapps.mapcompose.demo.utils.readBuffer
+import ovh.plrapps.mapcomposemp.demo.Res
actual fun makeHttpTileStreamProvider(): TileStreamProvider {
// TODO: use a blocking http client. MapCompose already switches context when calling the provided TileStreamProvider,
@@ -33,4 +38,40 @@ actual fun makeOsmTileStreamProvider(): TileStreamProvider {
null
}
}
+}
+
+actual class OSMVectorTileStreamProvider actual constructor(actual override val styleUrl: String) :
+ VectorTileStreamProvider {
+ val httpClient = getKtorClient()
+
+ @OptIn(ExperimentalResourceApi::class)
+ actual override suspend fun loadResources(url: String): RawSource? {
+ return try {
+ when (url) {
+ "files/style_street_v2.json" -> {
+ val buffer = Buffer()
+ buffer.write(Res.readBytes(url))
+ buffer
+ }
+ else -> readBuffer(httpClient, url)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ actual override suspend fun getTileStream(
+ tileUrl: String,
+ row: Int,
+ col: Int,
+ zoomLvl: Int
+ ): RawSource? {
+ return try {
+ readBuffer(httpClient, tileUrl)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
}
\ No newline at end of file
diff --git a/demo/composeApp/src/wasmJsMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/VectorDemoVM.wasmJs.kt b/demo/composeApp/src/wasmJsMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/VectorDemoVM.wasmJs.kt
new file mode 100644
index 00000000..390b32ad
--- /dev/null
+++ b/demo/composeApp/src/wasmJsMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/VectorDemoVM.wasmJs.kt
@@ -0,0 +1,44 @@
+package ovh.plrapps.mapcompose.demo.viewmodels
+
+import kotlinx.io.Buffer
+import kotlinx.io.RawSource
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import ovh.plrapps.mapcompose.vector.core.VectorTileStreamProvider
+import ovh.plrapps.mapcompose.demo.utils.getKtorClient
+import ovh.plrapps.mapcompose.demo.utils.readBuffer
+import ovh.plrapps.mapcomposemp.demo.Res
+
+actual class OSMVectorTileStreamProvider actual constructor(actual override val styleUrl: String) : VectorTileStreamProvider {
+ val httpClient = getKtorClient()
+
+ @OptIn(ExperimentalResourceApi::class)
+ actual override suspend fun loadResources(url: String): RawSource? {
+ return try {
+ when (url) {
+ "files/style_street_v2.json" -> {
+ val buffer = Buffer()
+ buffer.write(Res.readBytes(url))
+ buffer
+ }
+ else -> readBuffer(httpClient, url)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ actual override suspend fun getTileStream(
+ tileUrl: String,
+ row: Int,
+ col: Int,
+ zoomLvl: Int
+ ): RawSource? {
+ return try {
+ readBuffer(httpClient, tileUrl)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+}
\ No newline at end of file
diff --git a/doc/wip/trouble_direct_raster.mp4 b/doc/wip/trouble_direct_raster.mp4
new file mode 100644
index 00000000..207de402
Binary files /dev/null and b/doc/wip/trouble_direct_raster.mp4 differ
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e085e968..6323c71c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -11,11 +11,13 @@ jetbrains-compose = "1.11.1" # JetBrains Compose (KMP)
compose = "1.11.2" # AndroidX Compose (Android-only)
coroutines = "1.11.0"
kotlinx-io = "0.9.0"
+pbandk = "0.16.0"
+kotlinx-datetime = "0.6.2"
+kotlinx-serialization = "1.11.0"
# ---- Demo ----
jetbrains-compose-material3 = "1.9.0"
ktor = "3.4.3"
-kotlinx-serialization = "1.11.0"
jetbrains-navigation = "2.9.2"
jetbrains-lifecycle = "2.11.0-beta01"
androidx-activityCompose = "1.13.0"
@@ -30,12 +32,16 @@ jetbrains-compose-foundation = { module = "org.jetbrains.compose.foundation:foun
jetbrains-compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "jetbrains-compose" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
+pbandk-runtime = { group = "pro.streem.pbandk", name = "pbandk-runtime", version.ref = "pbandk" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
# -- Android --
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
# -- Test --
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
+kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
+compose-ui-test = { module = "org.jetbrains.compose.ui:ui-test", version.ref = "jetbrains-compose" }
# ============================== Demo ==============================
# Dependencies the demo app adds on top of what the library already needs.
@@ -50,6 +56,7 @@ jetbrains-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifec
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
# -- Android --
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
+androidx-core = { module = "androidx.core:core-ktx", version = "1.19.0" }
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
# -- iOS --
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
@@ -69,7 +76,7 @@ vanniktechPublish = { id = "com.vanniktech.maven.publish", version = "0.36.0" }
# Declared `apply false` in the root build but not applied by any module:
androidLibrary = { id = "com.android.library", version.ref = "agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
# ---- Demo ----
androidApplication = { id = "com.android.application", version.ref = "agp" }
-kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
diff --git a/library/build.gradle.kts b/library/build.gradle.kts
index 898f520e..1d03ac11 100644
--- a/library/build.gradle.kts
+++ b/library/build.gradle.kts
@@ -10,6 +10,7 @@ plugins {
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.vanniktechPublish)
+ alias(libs.plugins.kotlinSerialization)
}
kotlin {
@@ -71,13 +72,17 @@ kotlin {
androidMain.dependencies {
implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.ui.tooling)
+ implementation(libs.androidx.core)
}
commonMain.dependencies {
implementation(libs.jetbrains.compose.runtime)
implementation(libs.jetbrains.compose.foundation)
implementation(libs.jetbrains.compose.ui)
+ implementation(libs.components.resources)
implementation(libs.kotlinx.coroutines)
api(libs.kotlinx.io.core)
+ implementation(libs.pbandk.runtime)
+ implementation(libs.kotlinx.serialization.json)
}
named("skiaMain").configure {
dependencies {
@@ -92,6 +97,9 @@ kotlin {
commonTest.dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
+ implementation(libs.kotlinx.datetime)
+
+ implementation(libs.compose.ui.test)
}
}
}
diff --git a/library/src/androidMain/kotlin/ovh/plrapps/mapcompose/vector/data/SpriteManager.android.kt b/library/src/androidMain/kotlin/ovh/plrapps/mapcompose/vector/data/SpriteManager.android.kt
new file mode 100644
index 00000000..aaa4552a
--- /dev/null
+++ b/library/src/androidMain/kotlin/ovh/plrapps/mapcompose/vector/data/SpriteManager.android.kt
@@ -0,0 +1,16 @@
+package ovh.plrapps.mapcompose.vector.data
+
+import android.graphics.BitmapFactory
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.core.graphics.createBitmap
+
+internal actual fun imageBitmapFromArgb(argb: IntArray, width: Int, height: Int): ImageBitmap {
+ val bmp = createBitmap(width, height)
+ bmp.setPixels(argb, 0, width, 0, 0, width, height)
+ return bmp.asImageBitmap()
+}
+
+internal actual fun byteArrayToImageBitmap(bytes: ByteArray): ImageBitmap {
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.size).asImageBitmap()
+}
\ No newline at end of file
diff --git a/library/src/androidMain/kotlin/ovh/plrapps/mapcompose/vector/data/extension/ImageBitmap.android.kt b/library/src/androidMain/kotlin/ovh/plrapps/mapcompose/vector/data/extension/ImageBitmap.android.kt
new file mode 100644
index 00000000..3279343b
--- /dev/null
+++ b/library/src/androidMain/kotlin/ovh/plrapps/mapcompose/vector/data/extension/ImageBitmap.android.kt
@@ -0,0 +1,16 @@
+package ovh.plrapps.mapcompose.vector.data.extension
+
+import android.graphics.Bitmap
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asAndroidBitmap
+import java.io.ByteArrayOutputStream
+
+actual fun ImageBitmap.toBytes(): ByteArray? {
+ val bitmap = this.asAndroidBitmap()
+ val stream = ByteArrayOutputStream()
+ return if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
+ stream.toByteArray()
+ } else {
+ null
+ }
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/api/LayerApi.kt b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/api/LayerApi.kt
index 3e7ee774..6c60adf5 100644
--- a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/api/LayerApi.kt
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/api/LayerApi.kt
@@ -2,9 +2,18 @@
package ovh.plrapps.mapcompose.api
-import ovh.plrapps.mapcompose.core.*
+import ovh.plrapps.mapcompose.core.AboveAll
+import ovh.plrapps.mapcompose.core.AboveLayer
+import ovh.plrapps.mapcompose.core.BelowAll
+import ovh.plrapps.mapcompose.core.BelowLayer
+import ovh.plrapps.mapcompose.core.Layer
+import ovh.plrapps.mapcompose.core.LayerPlacement
+import ovh.plrapps.mapcompose.core.TileStreamProvider
+import ovh.plrapps.mapcompose.vector.core.VectorTileStreamProvider
+import ovh.plrapps.mapcompose.core.makeLayerId
import ovh.plrapps.mapcompose.ui.state.MapState
import ovh.plrapps.mapcompose.utils.swap
+import ovh.plrapps.mapcompose.vector.core.VectorLayer
/**
@@ -55,6 +64,17 @@ fun MapState.addLayer(
return id
}
+suspend fun MapState.addVectorLayer(
+ vectorTileStreamProvider: VectorTileStreamProvider,
+ initialOpacity: Float = 1f,
+ placement: LayerPlacement = AboveAll
+): String {
+ val vectorLayer = VectorLayer(mapState = this, vectorTileStreamProvider)
+ val tileStreamProvider = vectorLayer.makeTileStreamProvider()
+ // TODO: honor placement parameter
+ return addLayer(tileStreamProvider)
+}
+
/**
* Replaces a layer. If the layer doesn't exist, no layer is added.
*
diff --git a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/MapUI.kt b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/MapUI.kt
index bee44821..41ceb39e 100644
--- a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/MapUI.kt
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/MapUI.kt
@@ -7,12 +7,15 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalFontFamilyResolver
+import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.zIndex
import ovh.plrapps.mapcompose.ui.layout.ZoomPanRotate
import ovh.plrapps.mapcompose.ui.layout.ZoomPanRotateConfig
import ovh.plrapps.mapcompose.ui.markers.MarkerComposer
import ovh.plrapps.mapcompose.ui.paths.PathComposer
import ovh.plrapps.mapcompose.ui.state.MapState
+import ovh.plrapps.mapcompose.vector.ui.symbols.SymbolComposer
import ovh.plrapps.mapcompose.ui.view.TileCanvas
@Composable
@@ -22,6 +25,7 @@ fun MapUI(
content: @Composable () -> Unit = {}
) {
val zoomPRState = state.zoomPanRotateState
+ val symbolState = state.symbolState
val markerState = state.markerRenderState
val pathState = state.pathState
@@ -29,6 +33,14 @@ fun MapUI(
remember(density) {
state.densityState.value = density
}
+ val fontFamilyResolver = LocalFontFamilyResolver.current
+ remember(fontFamilyResolver) {
+ state.fontFamilyResolverState.value = fontFamilyResolver
+ }
+ val textMeasurer = rememberTextMeasurer()
+ remember(textMeasurer) {
+ state.textMeasurerState.value = textMeasurer
+ }
key(state) {
ZoomPanRotate(
@@ -55,6 +67,12 @@ fun MapUI(
isFilteringBitmap = state.isFilteringBitmap,
)
+ SymbolComposer(
+ modifier = Modifier,
+ zoomPRState = zoomPRState,
+ symbolState = symbolState
+ )
+
MarkerComposer(
modifier = Modifier.zIndex(1f),
zoomPRState = zoomPRState,
diff --git a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/state/MapState.kt b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/state/MapState.kt
index 7198a88c..e449289a 100644
--- a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/state/MapState.kt
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/state/MapState.kt
@@ -6,6 +6,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextMeasurer
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.Density
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -23,6 +25,7 @@ import ovh.plrapps.mapcompose.ui.state.markers.MarkerRenderState
import ovh.plrapps.mapcompose.ui.state.markers.MarkerState
import ovh.plrapps.mapcompose.utils.AngleDegree
import ovh.plrapps.mapcompose.utils.toRad
+import ovh.plrapps.mapcompose.vector.ui.state.SymbolState
/**
* The state of the map. All public APIs are extensions functions or extension properties of this
@@ -62,6 +65,7 @@ class MapState(
internal val markerRenderState = MarkerRenderState()
internal val markerState = MarkerState(scope, markerRenderState)
internal val pathState = PathState(fullWidth, fullHeight)
+ internal val symbolState = SymbolState()
internal val visibleTilesResolver =
VisibleTilesResolver(
levelCount = levelCount,
@@ -73,6 +77,7 @@ class MapState(
) {
zoomPanRotateState.scale
}
+
internal val tileCanvasState = TileCanvasState(
scope,
tileSize,
@@ -100,6 +105,8 @@ class MapState(
applyLateInitialValues(initialValues)
}
internal val densityState = MutableStateFlow(null)
+ internal val fontFamilyResolverState = MutableStateFlow(null)
+ internal val textMeasurerState = MutableStateFlow(null)
/**
* Cancels all internal tasks.
@@ -167,6 +174,14 @@ class MapState(
private suspend fun renderVisibleTiles() {
val viewport = updateViewport()
tileCanvasState.setViewport(viewport)
+ for (listener in viewportListeners) {
+ listener.onViewportChanged(viewport)
+ }
+ }
+
+ private var viewportListeners = mutableListOf()
+ internal fun addViewportChangeListener(listener: ViewportListener) {
+ viewportListeners.add(listener)
}
private fun updateViewport(): Viewport {
@@ -337,4 +352,8 @@ class InitialValues internal constructor() {
internal typealias LayoutTapCb = (x: Double, y: Double) -> Unit
+internal fun interface ViewportListener {
+ fun onViewportChanged(viewport: Viewport)
+}
+
expect fun getProcessorCount(): Int
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/state/TileCanvasState.kt b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/state/TileCanvasState.kt
index 9f9fe77e..68b316ed 100644
--- a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/state/TileCanvasState.kt
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/state/TileCanvasState.kt
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ovh.plrapps.mapcompose.core.ColorFilterProvider
@@ -65,6 +66,10 @@ internal class TileCanvasState(
private val visibleTileLocationsChannel = Channel(capacity = Channel.RENDEZVOUS)
private val tilesOutput = Channel(capacity = Channel.RENDEZVOUS)
private val visibleStateFlow = MutableStateFlow(null)
+ internal val visibleTiles = visibleStateFlow.asStateFlow().map {
+ it?.visibleTiles
+ }
+
internal var alphaTick = 0.07f
set(value) {
field = value.coerceIn(0.01f, 1f)
diff --git a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/README.md b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/README.md
new file mode 100644
index 00000000..c26059dd
--- /dev/null
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/README.md
@@ -0,0 +1,104 @@
+## Specifications
+- style-spec https://maplibre.org/maplibre-style-spec/
+ - [Expressions](https://maplibre.org/maplibre-style-spec/expressions/)
+ - [Layers](https://maplibre.org/maplibre-style-spec/layers/)
+- tilejson-spec https://github.com/mapbox/tilejson-spec/blob/master/2.2.0/README.md
+- vector-tile-spec https://github.com/mapbox/vector-tile-spec/tree/master
+
+## Reference implementations :
+- Maplibre-gl-js https://github.com/maplibre/maplibre-gl-js/tree/main/src/render
+- Maplibre-native https://github.com/maplibre/maplibre-native/blob/main/src/mbgl/renderer/layers/render_symbol_layer.cpp
+
+## Status
+The overall project structure and, most importantly, the parsers and decoders have been implemented.
+For rendering, only the basic painters have been implemented (background, lines, polygons, labels).
+
+Three main directions
+- Expression parser and processing. Needs to be covered with tests, and the existing tests require reorganization.
+- Rendering.Implement the remaining painters and fix bugs in the existing ones.
+- Tests. Not enough tests, more needed
+
+TL;DR
+### ✅ Decoders
+
+- ✅ PBF decoder ->
+ - ✅ Geometry decoder
+- ✅ Style decoder
+- ✅ interpolation for:
+ - ✅ Color
+ - ✅ Number
+ - ✅ String
+- ✅ Expressions (may require further analysis)
+- ✅ Filters (MVP)
+
+### 🚧 Layers
+
+- 🛠 Background
+- 🛠️ Lines
+- 🛠️ Polygons
+- 🛠️ Symbols
+- ❌ Sprites (part of Symbols)
+- ❌ Raster
+- ❌ Circle
+- ❌ FillExtrusion
+- ❌ Heatmap
+- ❌ Hillshade
+- ❌ Sky
+
+### What is implemented and close to MapLibre
+
+#### MVT (Vector Tile) Geometry Decoding
+ZigZag decoding, correct coordinate handling, support for POINT, LINESTRING, POLYGON.
+Tests for the decoder and reversibility.
+
+#### Symbol Painter
+Rendering text on lines and polygons.
+Text centering on lines/polygons, angle calculation.
+Correct text rotation (so it’s never upside down).
+Support for text-halo (outline), color, size, opacity.
+Support for text-offset, text-anchor.
+Correct work with text templates (substituteTemplate).
+Basic collision system implemented: labels do not overlap (within a tile).
+Support for allowOverlap, ignorePlacement, priorities.
+Visual debugging (borders, color).
+Collision reset.
+Currently, collision reset is implemented at the tile level, but MapLibre has nuances with global placement (especially when rendering multiple tiles in one frame).
+No spatial index (R-tree), but for small tiles this is not critical. No Fonts(WIP).
+#### Line, Background & Polygons
+WIP
+#### Styles
+Using expressions (process()) to obtain styles.
+LineLabelPlacement
+Placing labels along a line, taking symbol-spacing into account.
+Tests for even and correct placement.
+Tests
+Unit test coverage for decoder, collision, and line placement.
+
+What is partially or simplistically implemented
+Not implemented (or not fully implemented)
+What is not yet implemented (or only partially implemented):
+
+Icon placement and icon support
+In MapLibre, labels can be not only text but also icons (icon-image, icon-size, etc.).
+
+Layout Expressions
+In MapLibre, layout expressions can be very complex (e.g., depending on zoom, feature-state, data-driven styling).
+
+Collision Groups
+MapLibre allows specifying collision groups (collision-group) so that labels from different groups do not interfere with each other.
+
+Rotated collision box
+In MapLibre, rotated collision boxes are used for text along lines, so collisions are calculated based on the actual position of the text, not just AABB.
+
+Complex scenarios with multilingual labels, fallback, bidirectional text
+MapLibre supports advanced language scenarios.
+
+Dynamic loading and removal of labels when zoom/viewport changes
+In MapLibre, placement can be global for the entire frame, not just per tile.
+
+Everything related to basic rendering, collisions, and text placement is implemented close to MapLibre.
+To be improved: rotated collision box, spatial index, icon support, complex expressions, collision groups.
+
+
+
+
diff --git a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/compose/rememberRasterizer.kt b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/compose/rememberRasterizer.kt
new file mode 100644
index 00000000..161b3765
--- /dev/null
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/compose/rememberRasterizer.kt
@@ -0,0 +1,38 @@
+package ovh.plrapps.mapcompose.vector.compose
+
+//import androidx.compose.runtime.Composable
+//import androidx.compose.runtime.getValue
+//import androidx.compose.runtime.produceState
+//import androidx.compose.runtime.remember
+//import androidx.compose.ui.platform.LocalDensity
+//import androidx.compose.ui.platform.LocalFontFamilyResolver
+//import androidx.compose.ui.text.rememberTextMeasurer
+//import org.jetbrains.compose.resources.ExperimentalResourceApi
+//import ovh.plrapps.mapcompose.vector.data.MapLibreConfiguration
+//import ovh.plrapps.mapcompose.vector.data.getMapLibreConfiguration
+//import kotlin.math.roundToInt
+
+//@OptIn(ExperimentalResourceApi::class)
+//@Composable
+//fun rememberRasterizer(styleSource: suspend () -> String): MapLibreRasterizer? {
+// val density = LocalDensity.current
+// val fontFamilyResolver = LocalFontFamilyResolver.current
+// val textMeasurer = rememberTextMeasurer()
+// val configuration by produceState(null) {
+// value = styleSource().let {
+// getMapLibreConfiguration(it, pixelRatio = density.density.roundToInt()).getOrThrow()
+// }
+// }
+//
+// return remember(configuration) {
+// configuration?.let {
+// MapLibreRasterizer(
+// configuration = it,
+// density = density,
+// fontFamilyResolver = fontFamilyResolver,
+// textMeasurer = textMeasurer,
+// tileCache = null
+// )
+// }
+// }
+//}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/core/VectorLayer.kt b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/core/VectorLayer.kt
new file mode 100644
index 00000000..7cdd420b
--- /dev/null
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/core/VectorLayer.kt
@@ -0,0 +1,179 @@
+package ovh.plrapps.mapcompose.vector.core
+
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.io.Buffer
+import kotlinx.io.buffered
+import kotlinx.io.readString
+import ovh.plrapps.mapcompose.api.centroidX
+import ovh.plrapps.mapcompose.api.centroidY
+import ovh.plrapps.mapcompose.api.fullSize
+import ovh.plrapps.mapcompose.api.scale
+import ovh.plrapps.mapcompose.core.TileMatrix
+import ovh.plrapps.mapcompose.core.TileStreamProvider
+import ovh.plrapps.mapcompose.vector.core.VectorTileStreamProvider
+import ovh.plrapps.mapcompose.core.Viewport
+import ovh.plrapps.mapcompose.core.VisibleTiles
+import ovh.plrapps.mapcompose.core.VisibleWindow
+import ovh.plrapps.mapcompose.ui.state.MapState
+import ovh.plrapps.mapcompose.utils.IODispatcher
+import ovh.plrapps.mapcompose.utils.throttle
+import ovh.plrapps.mapcompose.vector.data.extension.toBytes
+import ovh.plrapps.mapcompose.vector.data.extension.toMVTViewport
+import ovh.plrapps.mapcompose.vector.data.getMapLibreConfiguration
+
+internal class VectorLayer(
+ private val mapState: MapState,
+ private val vectorTileStreamProvider: VectorTileStreamProvider,
+) {
+ private val scope = mapState.scope
+
+ val visibleTiles =
+ mapState.tileCanvasState.visibleTiles.stateIn(scope, SharingStarted.Eagerly, null)
+ val viewportInfoFlow = MutableStateFlow(null)
+
+ init {
+ listenForViewportUpdates()
+ }
+
+ suspend fun makeTileStreamProvider(): TileStreamProvider {
+ val style = withContext(IODispatcher) {
+ vectorTileStreamProvider.loadResources(vectorTileStreamProvider.styleUrl)?.buffered()?.readString()
+ }
+
+ val configuration = getMapLibreConfiguration(
+ style = style ?: "",
+ loadResource = vectorTileStreamProvider::loadResources
+ ).getOrThrow()
+
+ val rasterizer = VectorRasterizer(
+ configuration = configuration,
+ densityState = mapState.densityState,
+ fontFamilyResolverState = mapState.fontFamilyResolverState,
+ textMeasurerState = mapState.textMeasurerState,
+ getTileStream = vectorTileStreamProvider::getTileStream
+ )
+
+ startSymbolsProcessing(rasterizer)
+
+ return TileStreamProvider { row, col, zoomLvl ->
+ val density = mapState.densityState.value ?: return@TileStreamProvider null
+ val tilePx = with(density) { 256.dp.toPx() }.toInt()
+
+ val imageBitmap = rasterizer.getTile(
+ x = col,
+ y = row,
+ zoom = zoomLvl.toDouble(),
+ tileSize = tilePx
+ )
+
+ val bytes = imageBitmap.toBytes()
+ ?: return@TileStreamProvider null
+
+ Buffer().apply {
+ write(bytes)
+ }
+ }
+ }
+
+ private fun startSymbolsProcessing(rasterizer: VectorRasterizer) {
+ scope.launch {
+ viewportInfoFlow
+ .throttle(250)
+ .collectLatest { viewportInfo ->
+ viewportInfo ?: return@collectLatest
+
+ val zoomLvl = viewportInfo.zoom
+ val tilePx = mapState.tileSize
+
+ val nextSymbols = rasterizer.produceSymbols(
+ viewport = viewportInfo.toMVTViewport(),
+ tileSize = tilePx,
+ z = zoomLvl.toDouble()
+ ).getOrElse { e ->
+ println("[ERROR] produceSymbols(): ${e.message}")
+ return@collectLatest
+ }
+
+ rasterizer.updateSymbols(
+ nextSymbols = nextSymbols,
+ state = mapState,
+ viewportInfo = viewportInfo
+ )
+ mapState.symbolState.visiblePhases = viewportInfo.visiblePhases
+ }
+ }
+ }
+
+ private fun listenForViewportUpdates() {
+ var lastViewport: Viewport? = null
+ fun updateViewportInfo(visibleTiles: VisibleTiles, viewport: Viewport) {
+ val visibleWindow = visibleTiles.visibleWindow
+ val (mergedMatrix, leftVisible, rightVisible) = when (visibleWindow) {
+ is VisibleWindow.InfiniteScrollX -> Triple(
+ mergeMatrices(
+ visibleWindow.tileMatrix,
+ visibleWindow.leftOverflow?.tileMatrix,
+ visibleWindow.rightOverflow?.tileMatrix
+ ),
+ visibleWindow.leftOverflow != null,
+ visibleWindow.rightOverflow != null
+ )
+ is VisibleWindow.BoundsConstrained -> Triple(visibleWindow.tileMatrix, false, false)
+ }
+ viewportInfoFlow.value = ViewportInfo(
+ matrix = mergedMatrix,
+ size = IntSize(
+ width = (viewport.right - viewport.left),
+ height = (viewport.bottom - viewport.top),
+ ),
+ angleRad = viewport.angleRad,
+ pitch = 0f,
+ zoom = visibleTiles.level,
+ centroidX = mapState.centroidX,
+ centroidY = mapState.centroidY,
+ scale = mapState.scale,
+ fullWidth = mapState.fullSize.width,
+ fullHeight = mapState.fullSize.height,
+ infiniteScrollX = leftVisible || rightVisible,
+ visiblePhases = (if (leftVisible) -1 else 0)..(if (rightVisible) 1 else 0),
+ )
+ }
+
+ mapState.addViewportChangeListener { viewport ->
+ val currentVisibleTiles = visibleTiles.value
+ if (currentVisibleTiles != null) {
+ lastViewport = viewport
+ updateViewportInfo(currentVisibleTiles, viewport)
+ }
+ }
+
+ scope.launch {
+ visibleTiles.collect { visibleTiles ->
+ visibleTiles ?: return@collect
+ val viewport = lastViewport ?: return@collect
+ updateViewportInfo(visibleTiles, viewport)
+ }
+ }
+ }
+
+ private fun mergeMatrices(vararg matrices: TileMatrix?): TileMatrix {
+ val result = mutableMapOf()
+ for (matrix in matrices) {
+ matrix ?: continue
+ for ((row, cols) in matrix) {
+ val existing = result[row]
+ result[row] = if (existing == null) cols else {
+ minOf(existing.first, cols.first)..maxOf(existing.last, cols.last)
+ }
+ }
+ }
+ return result
+ }
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/core/VectorRasterizer.kt b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/core/VectorRasterizer.kt
new file mode 100644
index 00000000..b5d07a14
--- /dev/null
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/core/VectorRasterizer.kt
@@ -0,0 +1,718 @@
+package ovh.plrapps.mapcompose.vector.core
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
+import androidx.compose.ui.text.TextMeasurer
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.supervisorScope
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.io.RawSource
+import kotlinx.io.buffered
+import kotlinx.io.readByteArray
+import ovh.plrapps.mapcompose.core.TileMatrix
+import ovh.plrapps.mapcompose.ui.state.MapState
+import ovh.plrapps.mapcompose.utils.AngleRad
+import ovh.plrapps.mapcompose.utils.IODispatcher
+import ovh.plrapps.mapcompose.vector.data.MapLibreConfiguration
+import ovh.plrapps.mapcompose.vector.renderer.CompoundLabelPlacement
+import ovh.plrapps.mapcompose.vector.renderer.Point
+import ovh.plrapps.mapcompose.vector.renderer.Symbol
+import ovh.plrapps.mapcompose.vector.renderer.SymbolsProducer
+import ovh.plrapps.mapcompose.vector.renderer.TextPlacementCandidate
+import ovh.plrapps.mapcompose.vector.renderer.TileRenderer
+import ovh.plrapps.mapcompose.vector.renderer.collision.CollisionDetector
+import ovh.plrapps.mapcompose.vector.renderer.collision.LabelPlacement
+import ovh.plrapps.mapcompose.vector.renderer.utils.MVTViewport
+import ovh.plrapps.mapcompose.vector.spec.Tile
+import ovh.plrapps.mapcompose.vector.utils.LruCache
+import ovh.plrapps.mapcompose.vector.spec.style.SymbolLayer
+import ovh.plrapps.mapcompose.vector.utils.obb.OBB
+import ovh.plrapps.mapcompose.vector.utils.obb.ObbPoint
+import pbandk.decodeFromByteArray
+import kotlin.collections.component1
+import kotlin.collections.component2
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.math.sqrt
+
+class VectorRasterizer(
+ val configuration: MapLibreConfiguration,
+ val densityState: MutableStateFlow,
+ val fontFamilyResolverState: MutableStateFlow,
+ val textMeasurerState: MutableStateFlow,
+ val getTileStream: suspend (url: String, row: Int, col: Int, zoomLvl: Int) -> RawSource?,
+ val tileSize: Int = 256,
+) {
+ // Decoded protobuf Tile objects are large (up to several MB each in dense areas).
+ // Keep size modest; raw bytes remain available in byteCache for cheap re-decoding.
+ private val tileCache = LruCache(maxSize = 30)
+ private val byteCache = LruCache(maxSize = 100)
+ // Rendered tile output (PNG bytes). Cache hit avoids full geometry re-render.
+ private val renderedByteCache = LruCache(maxSize = 50)
+ private val pathCache = LruCache(maxSize = 200)
+ // Separate mutexes per cache eliminate cross-cache contention when tiles render concurrently.
+ private val byteCacheMutex = Mutex()
+ private val tileCacheMutex = Mutex()
+ private val pathCacheMutex = Mutex()
+ private val renderedByteCacheMutex = Mutex()
+
+ // Precomputed set of all source names referenced by style layers (constant after init).
+ private val allSourceNames: Set by lazy {
+ configuration.style.layers.mapNotNull { it.source?.takeIf { s -> s.isNotBlank() } }.toSet()
+ }
+
+ // Minimum viewport-pixel distance between repetitions of the same line label across tiles.
+ // Mirrors MapLibre's default symbol-spacing (250px).
+ private val MIN_LINE_LABEL_REPEAT_DIST = 250f
+
+ private val stableAnchorCache = LruCache(maxSize = 1000)
+
+ private fun getTileKey(sourceName: String, z: Int, x: Int, y: Int): String {
+ return "$sourceName-$z-$x-$y"
+ }
+
+ fun decodePBFFromByteArray(bytes: ByteArray): Tile? {
+ return try {
+ Tile.decodeFromByteArray(bytes)
+ } catch (e: Exception) {
+ println("Error decoding PBF: ${e.message}")
+ null
+ }
+ }
+
+ private suspend fun renderTile(
+ pbfList: Map,
+ zoom: Double,
+ tileSize: Int,
+ actualZoom: Double,
+ x: Int,
+ y: Int,
+ ): ImageBitmap {
+ val z = zoom.toInt()
+ val density = densityState.value ?: return emptyBitmap(tileSize)
+
+ // One tileCache lookup per source (not per style layer) — reduces mutex ops from
+ // O(style_layers) to O(sources).
+ val tileForSource: Map = allSourceNames.associateWith { sourceName ->
+ val key = getTileKey(sourceName, z, x, y)
+ tileCacheMutex.withLock { tileCache.get(key) }
+ ?: pbfList[sourceName]?.let { bytes ->
+ decodePBFFromByteArray(bytes)?.also { t ->
+ tileCacheMutex.withLock { tileCache.put(key, t) }
+ }
+ }
+ }
+
+ val imageBitmap = ImageBitmap(tileSize, tileSize)
+ val canvas = Canvas(imageBitmap)
+ val drawScope = CanvasDrawScope()
+ val localPropCache = HashMap>()
+ val tileRenderer = TileRenderer(
+ configuration = configuration,
+ pathCache = pathCache,
+ pathCacheMutex = pathCacheMutex,
+ localPropCache = localPropCache
+ )
+
+ drawScope.draw(
+ density = density,
+ layoutDirection = LayoutDirection.Ltr,
+ canvas = canvas,
+ size = Size(tileSize.toFloat(), tileSize.toFloat())
+ ) {
+ for (styleLayer in configuration.style.layers) {
+ val sourceName = styleLayer.source.takeIf { !it.isNullOrBlank() }
+ val tileKey = sourceName?.let { getTileKey(it, z, x, y) }
+ val tile = sourceName?.let { tileForSource[it] }
+
+ tileRenderer.render(
+ canvas = this,
+ tile = tile,
+ styleLayer = styleLayer,
+ zoom = zoom,
+ canvasSize = tileSize,
+ actualZoom = actualZoom,
+ tileKey = tileKey
+ )
+ }
+ }
+ return imageBitmap
+ }
+
+ private suspend fun fetchTile(url: String, row: Int, col: Int, zoomLvl: Int, sourceName: String): Result {
+ val key = getTileKey(sourceName, zoomLvl, col, row)
+ byteCacheMutex.withLock {
+ byteCache.get(key)?.let { return Result.success(it) }
+ }
+
+ try {
+ // Check if the coroutine is cancelled before the network request
+ currentCoroutineContext().ensureActive()
+
+// println("fetch the tile $url")
+ val result = withContext(IODispatcher) {
+ val response = getTileStream(url, row, col, zoomLvl)
+ response?.buffered()?.use { bufferedSource ->
+ bufferedSource.readByteArray()
+ }
+ } ?: return Result.failure(LoadTileException("fetch error"))
+ byteCacheMutex.withLock {
+ byteCache.put(key, result)
+ }
+ return Result.success(result)
+ } catch (e: CancellationException) {
+ // We do not log cancellation as an error - this is normal behavior
+ throw e
+ } catch (e: Throwable) {
+ return Result.failure(e)
+ }
+ }
+
+ // return sourceName: String and tile: ByteArray
+ private suspend fun fetch(z: Int, x: Int, y: Int): Result