diff --git a/.gitignore b/.gitignore
index 62984142..94487d6c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,4 @@ publish-mavenCentral.sh
compile_and_install_demo_release.sh
.kotlin
+/demo/maplibreDebugApp/cache/
diff --git a/build.gradle.kts b/build.gradle.kts
index 2b709a74..e0bbfa96 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -6,5 +6,6 @@ plugins {
alias(libs.plugins.jetbrainsCompose) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
+ alias(libs.plugins.kotlinx.serialization).apply(false)
alias(libs.plugins.vanniktechPublish) apply false
}
diff --git a/demo/composeApp/build.gradle.kts b/demo/composeApp/build.gradle.kts
index bc1d7eac..1bb2b4dd 100644
--- a/demo/composeApp/build.gradle.kts
+++ b/demo/composeApp/build.gradle.kts
@@ -25,7 +25,8 @@ kotlin {
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi",
"-Xannotation-default-target=param-property",
- "-Xcontext-parameters"
+ "-Xcontext-parameters",
+ "-Xexpect-actual-classes"
)
}
}
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..7d47a12b
--- /dev/null
+++ b/demo/composeApp/src/androidMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.android.kt
@@ -0,0 +1,15 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import cafe.adriel.voyager.core.model.rememberScreenModel
+import cafe.adriel.voyager.core.screen.Screen
+import ovh.plrapps.mapcompose.demo.viewmodels.VectorDemoVM
+
+actual object VectorDemo : Screen {
+ @Composable
+ override fun Content() {
+ val screenModel = rememberScreenModel { VectorDemoVM() }
+
+ 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..eef5eaa5 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.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 329086d8..46433f60 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
@@ -14,6 +14,7 @@ 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.VectorDemo
import ovh.plrapps.mapcompose.demo.ui.screens.VisibleAreaPaddingDemo
const val HOME = "home"
@@ -74,6 +75,10 @@ enum class MainDestinations() {
MARKERS_LAZY_LOADING{
override val title = "Markers lazy loading"
override val screen = MarkersLazyLoadingDemo
+ },
+ VECTOR_DEMO {
+ override val title = "Vector tile demo"
+ override val screen = VectorDemo
};
abstract val title: String
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..1971cef2
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.kt
@@ -0,0 +1,17 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import cafe.adriel.voyager.core.screen.Screen
+import ovh.plrapps.mapcompose.demo.viewmodels.VectorDemoVM
+import ovh.plrapps.mapcompose.ui.MapUI
+
+expect object VectorDemo : Screen
+
+@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/VectorDemoVM.kt b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/VectorDemoVM.kt
new file mode 100644
index 00000000..c1ae8563
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/ovh/plrapps/mapcompose/demo/viewmodels/VectorDemoVM.kt
@@ -0,0 +1,57 @@
+package ovh.plrapps.mapcompose.demo.viewmodels
+
+import cafe.adriel.voyager.core.model.ScreenModel
+import cafe.adriel.voyager.core.model.screenModelScope
+import kotlinx.coroutines.launch
+import kotlinx.io.RawSource
+import ovh.plrapps.mapcompose.api.addVectorLayer
+import ovh.plrapps.mapcompose.api.scale
+import ovh.plrapps.mapcompose.api.shouldLoopScale
+import ovh.plrapps.mapcompose.core.VectorTileStreamProvider
+import ovh.plrapps.mapcompose.ui.state.MapState
+import kotlin.math.pow
+
+/**
+ * Shows how MapCompose behaves with remote HTTP tiles.
+ */
+class VectorDemoVM : ScreenModel {
+ 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
+ ).apply {
+ screenModelScope.launch {
+ addVectorLayer(vectorTileStreamProvider)
+ }
+ 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/VectorDemo.desktop.kt b/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.desktop.kt
new file mode 100644
index 00000000..7d6dcad5
--- /dev/null
+++ b/demo/composeApp/src/desktopMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.desktop.kt
@@ -0,0 +1,18 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import cafe.adriel.voyager.core.model.rememberScreenModel
+import cafe.adriel.voyager.core.screen.Screen
+import ovh.plrapps.mapcompose.demo.ui.MapWithZoomControl
+import ovh.plrapps.mapcompose.demo.viewmodels.VectorDemoVM
+
+actual object VectorDemo : Screen {
+ @Composable
+ override fun Content() {
+ val screenModel = rememberScreenModel { VectorDemoVM() }
+
+ MapWithZoomControl(state = screenModel.state) {
+ VectorCommonUi(screenModel)
+ }
+ }
+}
\ 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..eef5eaa5 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.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/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..7d47a12b
--- /dev/null
+++ b/demo/composeApp/src/iosMain/kotlin/ovh/plrapps/mapcompose/demo/ui/screens/VectorDemo.ios.kt
@@ -0,0 +1,15 @@
+package ovh.plrapps.mapcompose.demo.ui.screens
+
+import androidx.compose.runtime.Composable
+import cafe.adriel.voyager.core.model.rememberScreenModel
+import cafe.adriel.voyager.core.screen.Screen
+import ovh.plrapps.mapcompose.demo.viewmodels.VectorDemoVM
+
+actual object VectorDemo : Screen {
+ @Composable
+ override fun Content() {
+ val screenModel = rememberScreenModel { VectorDemoVM() }
+
+ VectorCommonUi(screenModel)
+ }
+}
\ 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..8a2030e7 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.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/iosApp/iosApp.xcodeproj/project.pbxproj b/demo/iosApp/iosApp.xcodeproj/project.pbxproj
index 4206a4be..17e6c742 100644
--- a/demo/iosApp/iosApp.xcodeproj/project.pbxproj
+++ b/demo/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -17,7 +17,7 @@
058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
- 7555FF7B242A565900829871 /* MapComposeKMP.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MapComposeKMP.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7555FF7B242A565900829871 /* MapComposeMP.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MapComposeMP.app; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
@@ -62,7 +62,7 @@
7555FF7C242A565900829871 /* Products */ = {
isa = PBXGroup;
children = (
- 7555FF7B242A565900829871 /* MapComposeKMP.app */,
+ 7555FF7B242A565900829871 /* MapComposeMP.app */,
);
name = Products;
sourceTree = "";
@@ -107,7 +107,7 @@
packageProductDependencies = (
);
productName = iosApp;
- productReference = 7555FF7B242A565900829871 /* MapComposeKMP.app */;
+ productReference = 7555FF7B242A565900829871 /* MapComposeMP.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@@ -316,7 +316,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
- DEVELOPMENT_TEAM = "${TEAM_ID}";
+ DEVELOPMENT_TEAM = 75AUC96H82;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(SRCROOT)/../../mapcompose/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
@@ -334,6 +334,7 @@
composeApp,
);
PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
+ "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = ovh.plrapps.mapcompose.demo;
PRODUCT_NAME = "${APP_NAME}";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
@@ -348,7 +349,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
- DEVELOPMENT_TEAM = "${TEAM_ID}";
+ DEVELOPMENT_TEAM = 75AUC96H82;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(SRCROOT)/../../mapcompose/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
@@ -366,6 +367,7 @@
composeApp,
);
PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
+ "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = ovh.plrapps.mapcompose.demo;
PRODUCT_NAME = "${APP_NAME}";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
diff --git a/demo/iosApp/iosApp/Info.plist b/demo/iosApp/iosApp/Info.plist
index 412e3781..aaf5e182 100644
--- a/demo/iosApp/iosApp/Info.plist
+++ b/demo/iosApp/iosApp/Info.plist
@@ -2,6 +2,8 @@
+ CADisableMinimumFrameDurationOnPhone
+
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
@@ -20,8 +22,6 @@
1
LSRequiresIPhoneOS
- CADisableMinimumFrameDurationOnPhone
-
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
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.properties b/gradle.properties
index 7971b8de..4b91e17c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -11,6 +11,7 @@ android.useAndroidX=true
#MPP
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.mpp.enableCInteropCommonization=true
-
+kotlin.native.ignoreDisabledTargets=true
+kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
#Development
development=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ccf8f144..c0c51b26 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,17 +14,21 @@ androidx-test-junit = "1.3.0"
androidx-navigation = "2.9.5"
compose = "1.9.3"
compose-plugin = "1.9.1"
+androidx-lifecycle = "2.8.4"
junit = "4.13.2"
-kotlin = "2.2.20"
+kotlin = "2.2.21"
coroutines = "1.10.2"
skia = "0.9.22.2" # should be the exact same version compose.ui depends on. See https://mvnrepository.com/artifact/androidx.compose.ui/ui
ktor = "3.3.1"
voyager = "1.0.1"
kotlinx-io = "0.8.0"
+lazytable = "1.10.0"
+pbandk = "0.16.0"
+slf4jSimple = "2.0.12"
+kotlinx-datetime = "0.6.2"
+kotlinx-serialization = "1.8.1"
[libraries]
-kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
-kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
@@ -38,17 +42,40 @@ androidx-navigation = { module = "androidx.navigation:navigation-compose", versi
compose-ui-util = { group = "androidx.compose.ui", name = "ui-util", version.ref = "compose" }
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" }
+androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
+lazytable = { module = "io.github.oleksandrbalan:lazytable", version.ref = "lazytable" }
+skia = { module = "org.jetbrains.skiko:skiko", version.ref = "skia" }
+slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4jSimple" }
+voyager-navigation = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
+voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
+
+# kotlin std
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
+kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" }
kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
-skia = { module = "org.jetbrains.skiko:skiko", version.ref = "skia" }
-ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
+kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
+kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
+# maplibre-rasterizer
+pbandk-runtime = { group = "pro.streem.pbandk", name = "pbandk-runtime", version.ref = "pbandk" }
+
+# ktor
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
-voyager-navigation = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
-voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
+ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
+ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
+ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
+ktor-client-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktor" }
+ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
+ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
+ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
+ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" }
+ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
@@ -56,4 +83,5 @@ androidLibrary = { id = "com.android.library", version.ref = "agp" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
-vanniktechPublish = { id = "com.vanniktech.maven.publish", version = "0.34.0" }
\ No newline at end of file
+vanniktechPublish = { id = "com.vanniktech.maven.publish", version = "0.34.0" }
+kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
diff --git a/library/build.gradle.kts b/library/build.gradle.kts
index 2e141c32..f05dde33 100644
--- a/library/build.gradle.kts
+++ b/library/build.gradle.kts
@@ -1,6 +1,8 @@
-@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
+@file:OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalWasmDsl::class)
+import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@@ -9,6 +11,7 @@ plugins {
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.vanniktechPublish)
+ alias(libs.plugins.kotlinx.serialization)
}
kotlin {
@@ -75,16 +78,19 @@ kotlin {
implementation(libs.kotlinx.coroutines)
implementation(libs.skia)
api(libs.kotlinx.io.core)
+ implementation(libs.pbandk.runtime)
+ implementation(libs.kotlinx.serialization.json)
}
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
- }
- iosMain.dependencies {
-
}
commonTest.dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
+ implementation(libs.kotlinx.datetime)
+
+ @OptIn(ExperimentalComposeLibrary::class)
+ implementation(compose.uiTest)
}
}
}
@@ -113,6 +119,16 @@ android {
dependencies {
debugImplementation(libs.compose.ui.tooling)
}
+
+ testOptions {
+ unitTests {
+ all {
+ // Excluded ui tests because of issues documented on
+ // https://kotlinlang.org/docs/multiplatform/compose-test.html#write-and-run-common-tests
+ it.exclude("**/mapcompose/vector/**")
+ }
+ }
+ }
}
task("testClasses")
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..8c32cc2b 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.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/core/VectorTileStreamProvider.kt b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/core/VectorTileStreamProvider.kt
new file mode 100644
index 00000000..4ee4abe0
--- /dev/null
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/core/VectorTileStreamProvider.kt
@@ -0,0 +1,41 @@
+package ovh.plrapps.mapcompose.core
+
+import kotlinx.io.RawSource
+
+/**
+ * Defines how vector tiles should be fetched. It must be supplied as part of the configuration of
+ * MapCompose.
+ *
+ * The [styleUrl] property is used to identify the style of the vector tiles.
+ *
+ * The [loadResources] method is used to load resources (e.g, stylesheets) required to render the vector
+ * tiles.
+ *
+ * The [getTileStream] method implementation may suspend, but it isn't required (e.g, it isn't
+ * required to switch context using withContext(Dispatcher.IO) { .. }) as MapCompose does that
+ * already. The [getTileStream] method is declared using the suspend modifier, as it is sometimes
+ * useful to provide an implementation which suspends.
+ *
+ * The [getTileStream] method is invoked with the following parameters:
+ * - [tileUrl]: the url of the tile to fetch.
+ * - [row]: the row index of the tile.
+ * - [col]: the column index of the tile.
+ * - [zoomLvl]: the zoom level of the tile.
+ * [row], [col] and [zoomLvl] are can be used to implement caching strategies, because the [tileUrl] may change
+ * when multiple servers are used.
+ *
+ * MapCompose leverages bitmap pooling to reduce the pressure on the garbage collector. However,
+ * there's no tile caching by default - this is an implementation detail of the supplied
+ * [VectorTileStreamProvider].
+ *
+ * If [getTileStream] returns null, the tile won't be rendered.
+ * The library does not handle exceptions thrown from [getTileStream]. Such errors are treated as
+ * unrecoverable failures.
+ */
+interface VectorTileStreamProvider {
+ val styleUrl: String
+
+ suspend fun loadResources(url: String): RawSource?
+
+ suspend fun getTileStream(tileUrl: String, row: Int, col: Int, zoomLvl: Int): RawSource?
+}
\ No newline at end of file
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 a258c8b1..46fa7b03 100644
--- a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/MapUI.kt
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/MapUI.kt
@@ -2,16 +2,20 @@ package ovh.plrapps.mapcompose.ui
import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
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.markers.MarkerComposer
import ovh.plrapps.mapcompose.ui.paths.PathComposer
import ovh.plrapps.mapcompose.ui.state.MapState
+import ovh.plrapps.mapcompose.ui.symbols.SymbolComposer
import ovh.plrapps.mapcompose.ui.view.TileCanvas
@Composable
@@ -21,6 +25,7 @@ fun MapUI(
content: @Composable () -> Unit = {}
) {
val zoomPRState = state.zoomPanRotateState
+ val symbolState = state.symbolState
val markerState = state.markerRenderState
val pathState = state.pathState
@@ -28,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(
@@ -48,6 +61,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 1f6dbc49..9bc3a411 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
@@ -61,6 +63,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,
@@ -71,6 +74,7 @@ class MapState(
) {
zoomPanRotateState.scale
}
+
internal val tileCanvasState = TileCanvasState(
scope,
tileSize,
@@ -98,6 +102,8 @@ class MapState(
applyLateInitialValues(initialValues)
}
internal val densityState = MutableStateFlow(null)
+ internal val fontFamilyResolverState = MutableStateFlow(null)
+ internal val textMeasurerState = MutableStateFlow(null)
/**
* Cancels all internal tasks.
@@ -163,6 +169,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 {
@@ -324,4 +338,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/SymbolState.kt b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/state/SymbolState.kt
new file mode 100644
index 00000000..1d7435d9
--- /dev/null
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/state/SymbolState.kt
@@ -0,0 +1,10 @@
+package ovh.plrapps.mapcompose.ui.state
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import ovh.plrapps.mapcompose.vector.renderer.Symbol
+
+internal class SymbolState {
+ var symbols by mutableStateOf>(emptyList())
+}
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 75d74cf9..75b8db21 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
@@ -43,6 +43,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)
@@ -455,3 +459,4 @@ internal class TileCanvasState(
internal expect fun Tile.sendToRecycle(recycleChannel: Channel)
internal expect fun Tile.performRecycle()
+
diff --git a/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/symbols/SymbolComposer.kt b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/symbols/SymbolComposer.kt
new file mode 100644
index 00000000..7a0cab00
--- /dev/null
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/ui/symbols/SymbolComposer.kt
@@ -0,0 +1,55 @@
+package ovh.plrapps.mapcompose.ui.symbols
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.drawscope.withTransform
+import ovh.plrapps.mapcompose.ui.layout.grid
+import ovh.plrapps.mapcompose.ui.state.SymbolState
+import ovh.plrapps.mapcompose.ui.state.ZoomPanRotateState
+import kotlin.math.ceil
+
+@Composable
+internal fun SymbolComposer(
+ modifier: Modifier,
+ zoomPRState: ZoomPanRotateState,
+ symbolState: SymbolState
+) {
+ Canvas(
+ modifier = modifier.fillMaxSize()
+ ) {
+ val x0 = ((ceil(zoomPRState.scrollX / grid) * grid)).toInt()
+ val y0 = ((ceil(zoomPRState.scrollY / grid) * grid)).toInt()
+
+ withTransform({
+ rotate(
+ degrees = zoomPRState.rotation,
+ pivot = Offset(
+ x = zoomPRState.pivotX.toFloat(),
+ y = zoomPRState.pivotY.toFloat()
+ )
+ )
+ translate(
+ left = (-zoomPRState.scrollX + x0).toFloat(),
+ top = (-zoomPRState.scrollY + y0).toFloat()
+ )
+ }) {
+ for (symbol in symbolState.symbols) {
+ val canvasX = (symbol.global.x * zoomPRState.fullWidth * zoomPRState.scale - x0)
+ val canvasY = (symbol.global.y * zoomPRState.fullHeight * zoomPRState.scale - y0)
+
+ val size = symbol.getInPixels()
+ val offsetX = size.width * symbol.align.x
+ val offsetY = size.height * symbol.align.y
+
+ withTransform({
+ translate(left = (canvasX + offsetX).toFloat(), top = (canvasY + offsetY).toFloat())
+ }) {
+ symbol.draw(this)
+ }
+ }
+ }
+ }
+}
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..258f25ce
--- /dev/null
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/core/VectorLayer.kt
@@ -0,0 +1,137 @@
+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.core.TileStreamProvider
+import ovh.plrapps.mapcompose.core.VectorTileStreamProvider
+import ovh.plrapps.mapcompose.core.Viewport
+import ovh.plrapps.mapcompose.core.VisibleTiles
+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)
+ }
+
+ val configuration = getMapLibreConfiguration(
+ style = style?.buffered()?.readString() ?: "",
+ 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 density = mapState.densityState.value ?: return@collectLatest
+ val tilePx = with(density) { 256.dp.toPx() }.toInt()
+
+ 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
+ )
+ }
+ }
+ }
+
+ private fun listenForViewportUpdates() {
+ var lastViewport: Viewport? = null
+ fun updateViewportInfo(visibleTiles: VisibleTiles, viewport: Viewport) {
+ viewportInfoFlow.value = ViewportInfo(
+ matrix = visibleTiles.tileMatrix,
+ size = IntSize(
+ width = (viewport.right - viewport.left),
+ height = (viewport.bottom - viewport.top),
+ ),
+ angleRad = viewport.angleRad,
+ pitch = 0f,
+ zoom = visibleTiles.level
+ )
+ }
+
+ mapState.addViewportChangeListener { viewport ->
+ val visibleTiles = visibleTiles.value
+ if (visibleTiles != null) {
+ lastViewport = viewport
+ }
+ }
+
+ scope.launch {
+ visibleTiles.collect { visibleTiles ->
+ visibleTiles ?: return@collect
+ val viewport = lastViewport ?: return@collect
+ updateViewportInfo(visibleTiles, viewport)
+ }
+ }
+ }
+}
\ 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..9ff7a019
--- /dev/null
+++ b/library/src/commonMain/kotlin/ovh/plrapps/mapcompose/vector/core/VectorRasterizer.kt
@@ -0,0 +1,342 @@
+package ovh.plrapps.mapcompose.vector.core
+
+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.Symbol
+import ovh.plrapps.mapcompose.vector.renderer.SymbolsProducer
+import ovh.plrapps.mapcompose.vector.renderer.TileRenderer
+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 pbandk.decodeFromByteArray
+import kotlin.collections.component1
+import kotlin.collections.component2
+
+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?
+) {
+ private val tileCache = LruCache(maxSize = 100)
+ private val byteCache = LruCache(maxSize = 100)
+ private val pathCache = LruCache(maxSize = 200)
+ private val propertyCache = LruCache>(maxSize = 500)
+ private val mutex = Mutex()
+
+ 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)
+
+ val imageBitmap = ImageBitmap(tileSize, tileSize)
+ val canvas = Canvas(imageBitmap)
+ val drawScope = CanvasDrawScope()
+ val tileRenderer = TileRenderer(
+ configuration = configuration,
+ pathCache = pathCache,
+ propertyCache = propertyCache,
+ mutex = mutex
+ )
+
+ drawScope.draw(
+ density = density,
+ layoutDirection = LayoutDirection.Ltr,
+ canvas = canvas,
+ size = Size(tileSize.toFloat(), tileSize.toFloat())
+ ) {
+ for (styleLayer in configuration.style.layers) {
+ val tileKey = styleLayer.source.takeIf { !it.isNullOrBlank() }?.let { styleLayerSourceName ->
+ getTileKey(styleLayerSourceName, z, x, y)
+ }
+ val tile: Tile? = tileKey?.let { key ->
+ mutex.withLock {
+ tileCache.get(key)
+ } ?: styleLayer.source
+ ?.let { sourceName -> pbfList[sourceName] }
+ ?.let { bytes -> decodePBFFromByteArray(bytes) }
+ ?.also { tile ->
+ mutex.withLock {
+ tileCache.put(key, tile)
+ }
+ }
+ }
+
+ 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)
+ mutex.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 response = withContext(IODispatcher) {
+ getTileStream(url, row, col, zoomLvl)
+ }
+
+ // Check again after network request
+ currentCoroutineContext().ensureActive()
+
+ if (response == null) {
+ return Result.failure(LoadTileException("fetch error"))
+ }
+ val result = response.buffered().use { bufferedSource ->
+ bufferedSource.readByteArray()
+ }
+ mutex.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