├── parsers ├── __init__.py ├── base.py └── mbgl_to_tangram.py ├── requirements.txt ├── .gitignore ├── pyproject.toml ├── .pep8speaks.yml ├── cartogrify.py ├── LICENSE ├── README.md └── out.yaml /parsers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML~=5.3.1 2 | requests~=2.25.0 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | env/ 3 | __pycache__/ 4 | *.py[cod] 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | exclude = ''' 4 | /( 5 | \.git 6 | | env 7 | )/ 8 | ''' -------------------------------------------------------------------------------- /.pep8speaks.yml: -------------------------------------------------------------------------------- 1 | scanner: 2 | diff_only: True 3 | linter: flake8 4 | 5 | flake8: 6 | max-line-length: 120 7 | -------------------------------------------------------------------------------- /cartogrify.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | import yaml 4 | 5 | from parsers.mbgl_to_tangram import MBGLToTangramParser 6 | 7 | if __name__ == "__main__": 8 | parser = argparse.ArgumentParser(description="Transmogrify a cartographic theme into another format.") 9 | parser.add_argument("mode", type=str, choices=("mbgl2tangram",), help="The mode of transformation") 10 | parser.add_argument("infile", metavar="infile", type=str, help="The input file name") 11 | 12 | args = parser.parse_args() 13 | 14 | with open(args.infile, "r") as fp: 15 | # Only one option for now, but add others here as there is interest... 16 | if args.mode == "mbgl2tangram": 17 | import json 18 | 19 | style = json.load(fp) 20 | parser = MBGLToTangramParser(style) 21 | parser.parse() 22 | 23 | print(yaml.dump(parser.result)) 24 | 25 | sys.stderr.write("\n".join(f"Warning at {path}: {warning}" for (path, warning) in parser.warnings) + "\n") 26 | sys.stderr.flush() 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Stadia Maps 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /parsers/base.py: -------------------------------------------------------------------------------- 1 | """Parser base classes""" 2 | from typing import Union 3 | 4 | 5 | class JSONStyleParser: 6 | """A simple tree-walking parser base class for JSON styles. 7 | 8 | Subclasses must implement the visit method. 9 | 10 | Once initialising the parser with a `dict`, call the `parse` 11 | method to actually walk the tree. Each `dict` (or `dict` in 12 | a `list` of `dict`s) will be traversed, and terminal nodes 13 | (`list`s that are not populated with `dict`s, and primitive 14 | values) will be visited by `visit`. Subclasess may then make 15 | decisions based on the internal parser state, such as `tag_path` 16 | and `ctx` (a scratch space for subclasses to use for keeping 17 | track of contextual state). 18 | 19 | After `parse()` is complete, the result will be stored in the `result` 20 | property, and any warnings will be stored in the `warnings` property 21 | (as a list of the form `[(tag path, warning string)]`). 22 | """ 23 | 24 | def __init__(self, style: dict): 25 | self.style = style 26 | self.tag_path = [] 27 | self.result = {} 28 | self.warnings = [] 29 | self.ctx = {} # Scratch space for subclasses to use 30 | 31 | def parse(self): 32 | """Parser entrypoint.""" 33 | assert not self.result 34 | self.__traverse(self.style) 35 | 36 | def emit(self, tag_path, e): 37 | """Emit a JSON compatible primitive at a given tag path""" 38 | insertion_point = self.result 39 | for level in tag_path[:-1]: 40 | if not insertion_point.get(level): 41 | insertion_point[level] = {} 42 | 43 | insertion_point = insertion_point[level] 44 | 45 | insertion_point[tag_path[-1]] = e 46 | 47 | def emit_warning(self, warning: str): 48 | self.warnings.append((self.tag_path.copy(), warning)) 49 | 50 | def __traverse(self, e): 51 | for k, v in e.items(): 52 | self.tag_path.append(k) 53 | if isinstance(v, dict): 54 | self.__traverse(v) 55 | elif isinstance(v, list) and all(isinstance(x, dict) for x in v): 56 | for (idx, x) in enumerate(v): 57 | self.tag_path.append(idx) 58 | self.__traverse(x) 59 | self.exiting_scope() 60 | self.tag_path.pop() 61 | else: 62 | self.visit(v) 63 | 64 | self.exiting_scope() 65 | self.tag_path.pop() 66 | 67 | def visit(self, e: Union[str, int, float, list]): 68 | raise NotImplementedError 69 | 70 | def exiting_scope(self): 71 | """Called immediately *before* an element is popped from the tag path. 72 | 73 | Subclasses may use this to detect when exiting a nested structure 74 | that needs to be treated as a complete unit. 75 | """ 76 | raise NotImplementedError 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cartogrify 2 | 3 | Cartogrify is a utility that helps you transmogrify cartographic styles 4 | between formats. 5 | 6 | This has been developed and tested with Python 3.8, but should be compatible 7 | with 3.7+. (It will likely run in 3.6, but some assumptions are made about 8 | dictionary order that are not sound before Python 3.7.) 9 | 10 | Enough already though, how do I run it? First, do the usual `pip install -r requirements.txt`. 11 | Argparse will happily give you the usage info with `-h` if you run the main script. 12 | At the moment there is only one mode and one argument (the input file). 13 | Errors/warnings go to stderr, and the new style output goes to stdout. Pretty simple. 14 | 15 | ``` 16 | $ python cartogrify.py -h # Get the auto generated usage info 17 | $ python cartogrify.py mbgl2tangram /path/to/mbgl_style.json # Dumps everything to the console, including warnings 18 | $ python cartogrify.py mbgl2tangram /path/to/mbgl_style.json > /path/to/scene.yaml # Preserve the output in a yaml file 19 | ``` 20 | 21 | ## Supported formats 22 | 23 | At the moment, only Mapbox GL JS -> Tangram is supported. 24 | 25 | ## Current status 26 | 27 | This is EXPERIMENTAL software, and the codebase is littered with TODOs. If you have 28 | relevant expertise, pull requests are most welcome :) 29 | 30 | Don't expect it to do a perfect translation of your style out of the box, but it should 31 | get you most of the way there though, and hopefully point out common issues that you need to be 32 | aware of, as no two rendering engines share the same feature set. 33 | 34 | ### What should work 35 | 36 | * Vector map sources (either specified inline with URL format or loaded via TileJSON) 37 | * Background layer with a solid color 38 | * Basic decision expressions (all boolean operators, `all`, and `any`) in layer filters 39 | * Min and max zoom filters 40 | * Linear stops for colors and line widths 41 | * Basic color fill for polygons, lines, and text 42 | * Line cap and join 43 | * Text transformations 44 | 45 | ### What kind of works 46 | 47 | Transparency works a bit differently... MBGL lets you specify a layer alpha, but Tangram 48 | only lets you have colors with alpha, so you may notice a few rendering differences. 49 | 50 | Text effects like halo and blur will be lost. These do not appear to have a *direct* 51 | equivalent in Tangram, but there may be other ways to get the same effect. 52 | Text layout does not seem to be exactly correct in all cases. 53 | And Mapbox text format strings with more than one field referenced 54 | are ignored at the moment. 55 | 56 | You wil need to add a `fonts` section manually with the URLs of your fonts. A forthcoming version 57 | will probably allow you to specify a Google Fonts API Key to locate common fonts automatically. 58 | 59 | Most of the above errors should be reported at runtime. Error detection is not 60 | perfect, but well under way. 61 | 62 | ### What definitely doesn't work 63 | 64 | * Exponential stops for colors and line widths (base is completely ignored, so they will be linear) 65 | * Icons are not yet supported, but are definitely possible so expect these soon 66 | * Extrusions 67 | * Point layers 68 | * Probably everything else not mentioned here ;) 69 | -------------------------------------------------------------------------------- /parsers/mbgl_to_tangram.py: -------------------------------------------------------------------------------- 1 | """Mapbox GL JSON to Tangram Converter""" 2 | import re 3 | import requests 4 | 5 | from typing import Union 6 | 7 | from parsers.base import JSONStyleParser 8 | 9 | text_field_regex = re.compile(r"{([^}]+)}") 10 | 11 | 12 | layer_type_mappings = { 13 | "fill": "polygons", 14 | "line": "lines", 15 | "symbol": "symbol", 16 | "circle": "points", 17 | "fill-extrusion": "polygons", 18 | } 19 | boolean_mappings = { 20 | "any": "any", 21 | "all": "all", 22 | } 23 | binary_bool_ops = { 24 | "==", 25 | "!=", 26 | ">", 27 | ">=", 28 | "<", 29 | "<=", 30 | } 31 | geom_mappings = { 32 | "Point": "point", 33 | "MultiPoint": "point", 34 | "LineString": "line", 35 | "MultiLineString": "line", 36 | "Polygon": "polygon", 37 | "MultiPolygon": "polygon", 38 | } 39 | anchor_mappings = { 40 | # Mapbox swaps the order of what's anchored to what vs Tangram 41 | "center": "center", 42 | "left": "right", 43 | "right": "left", 44 | "top": "bottom", 45 | "bottom": "top", 46 | "top-left": "bottom-right", 47 | "bottom-right": "top-left", 48 | "top-right": "bottom-left", 49 | "bottom-left": "top-right", 50 | } 51 | passthrough_tags = { 52 | "icon-image", 53 | "icon-size", 54 | "line-cap", 55 | "line-join", 56 | "symbol-placement", 57 | "symbol-spacing", 58 | "text-anchor", 59 | "text-field", 60 | "text-font", 61 | "text-size", 62 | "text-transform", 63 | "visibility", 64 | } 65 | 66 | 67 | def convert_stops_or_value(expr, unit="", base=1.0, requires_meters_per_pixel=False): 68 | if isinstance(expr, list): 69 | if base == 1.0: 70 | return [[zoom, f"{value}{unit}"] for (zoom, value) in expr] 71 | else: 72 | # TODO: Handle colors and arrays of numbers(?) 73 | return f"""function() {{ 74 | const zoom = $zoom; 75 | const stops = {expr}; 76 | let lowerZoom = stops[0][0]; 77 | let upperZoom = stops[1][0]; 78 | let lowerValue = stops[0][1]; 79 | let upperValue = stops[1][1]; 80 | 81 | if (zoom < lowerZoom) return lowerValue; 82 | 83 | for (let i = 1; i < stops.length; i++) {{ 84 | if (stops[i][0] > zoom) break; 85 | 86 | lowerZoom = upperZoom; 87 | upperZoom = stops[i][0]; 88 | 89 | lowerValue = upperValue; 90 | upperValue = stops[i][1]; 91 | }} 92 | 93 | const base = {base}; 94 | const range = upperZoom - lowerZoom; 95 | const progress = zoom - lowerZoom; 96 | 97 | let ratio; 98 | 99 | if (range === 0) {{ 100 | ratio = 0; 101 | }} else if (base === 1) {{ 102 | ratio = progress / range; 103 | }} else {{ 104 | ratio = (Math.pow(base, progress) - 1) / (Math.pow(base, range) - 1); 105 | }} 106 | 107 | const value = Math.min(upperValue, ((lowerValue * (1 - ratio)) + (upperValue * ratio))); 108 | return {"value * $meters_per_pixel" if requires_meters_per_pixel else "value"}; 109 | }}""" 110 | else: 111 | return f"{expr}{unit}" 112 | 113 | 114 | def render_text_source(expr: str): 115 | fmt = text_field_regex.fullmatch(expr) 116 | if fmt: 117 | # Single field 118 | return fmt.group(1) 119 | else: 120 | 121 | def repl(match): 122 | return f"${{feature[\"{match.group(1)}\"] || ''}}" 123 | 124 | # NOTE: The trim is necessary in case the final field is blank/undefined. 125 | # Consider a multilingual map in which everything should be shown in Korean and 126 | # English. The Mapbox style may contain a format string like this: "{name:kr}\n{name:en}". 127 | # As most labels outside Korea will only be tagged in English, the resulting string may look 128 | # something like "\nErie". This will be rendered off-center by Tangram. 129 | return f"function() {{ return `{text_field_regex.sub(repl, expr)}`.trim() }}" 130 | 131 | 132 | class MBGLToTangramParser(JSONStyleParser): 133 | def render_sprite(self, url): 134 | result = { 135 | "url": f"{url}@2x.png", 136 | "density": 2, 137 | "sprites": {}, 138 | } 139 | 140 | res = requests.get(f"{url}@2x.json") 141 | 142 | for name, data in res.json().items(): 143 | result["sprites"][name] = [data["x"], data["y"], data["width"], data["height"]] 144 | 145 | self.emit(("textures", "spritesheet"), result) 146 | 147 | self.emit(("styles", "icons"), {"base": "points", "blend": "overlay", "texture": "spritesheet"}) 148 | 149 | def render_layer_filter(self, expr, ctx=None): 150 | if isinstance(expr[0], str) and expr[0] in binary_bool_ops: 151 | return self.render_layer_filter(expr[1:], expr[0]) 152 | elif isinstance(expr[0], str) and expr[0] in boolean_mappings: 153 | return {boolean_mappings[expr[0]]: self.render_layer_filter(expr[1:], boolean_mappings[expr[0]])} 154 | elif isinstance(expr[0], str): 155 | if expr[0] == "$type": 156 | return {"$geometry": geom_mappings[expr[1]]} 157 | elif expr[0] == "in": 158 | return {expr[1]: expr[2:]} 159 | elif expr[0] == "!in": 160 | return {"not": {expr[1]: expr[2:]}} 161 | elif ctx == "==": 162 | return {expr[0]: expr[1]} 163 | elif ctx == "!=": 164 | return {"not": {expr[0]: expr[1]}} 165 | elif ctx == ">": 166 | return {expr[0]: {"min": expr[1] + 1}} 167 | elif ctx == ">=": 168 | return {expr[0]: {"min": expr[1]}} 169 | elif ctx == "<": 170 | return {expr[0]: {"max": expr[1]}} 171 | elif ctx == "<=": 172 | return {expr[0]: {"max": expr[1] + 1}} 173 | elif ctx in {"any", "all"}: 174 | return [self.render_layer_filter(e) for e in expr] 175 | 176 | self.emit_warning(f"Unable to parse filter expr: {expr}\n") 177 | 178 | def render_draw(self, layer: dict): 179 | order = layer["idx"] 180 | layer_type = layer["type"] 181 | 182 | text_draw = None 183 | icons_draw = None 184 | geom_draw = None 185 | result = {} 186 | 187 | if layer_type == "symbol": 188 | if layer.get("text-field"): 189 | text_draw = {} 190 | 191 | if layer.get("text-anchor"): 192 | anchor = layer["text-anchor"] 193 | 194 | if isinstance(anchor, str): 195 | # Simple anchor 196 | text_draw["anchor"] = anchor_mappings[anchor] 197 | elif isinstance(anchor, dict) and isinstance(anchor.get("stops"), list): 198 | # Stops... not really supported 199 | value = anchor["stops"][0][1] 200 | text_draw["anchor"] = anchor_mappings[value] 201 | self.emit_warning( 202 | f"Complex text anchor expressions are not supported. Picking the first stop {repr(value)} from {repr(anchor)}" 203 | ) 204 | else: 205 | self.emit_warning(f"Unable to parse text anchor expression: {repr(anchor)}") 206 | 207 | if layer.get("icon-image"): 208 | icons_draw = { 209 | "text": text_draw, 210 | "priority": 999 - order, 211 | "blend_order": 999, 212 | } 213 | result["icons"] = icons_draw 214 | else: 215 | result["text"] = text_draw 216 | else: 217 | # TODO: Point only layers 218 | pass 219 | else: 220 | geom_draw = {} 221 | result[layer_type] = geom_draw 222 | 223 | # TODO: Figure out text and point ordering 224 | 225 | color = layer.get("color") 226 | if color: 227 | if text_draw is not None: 228 | text_draw["font"] = {"fill": color} 229 | else: 230 | geom_draw["color"] = color 231 | 232 | if layer.get("width") or layer.get("width-stops"): 233 | layer_width = layer.get("width", layer.get("width-stops")) 234 | base = layer.get("width-base", 1.0) 235 | 236 | layer_width = convert_stops_or_value(layer_width, unit="px", base=base, requires_meters_per_pixel=True) 237 | geom_draw["width"] = layer_width 238 | elif layer.get("type") == "lines": 239 | geom_draw["width"] = "1px" 240 | 241 | if layer.get("dash"): 242 | geom_draw["dash"] = layer["dash"] 243 | 244 | if not geom_draw and not text_draw: 245 | # At this point, we should have *something* 246 | # or the expression is invalid. 247 | self.emit_warning(f"Unable to render draw definition for layer {layer}") 248 | return None 249 | 250 | if text_draw: 251 | text_draw["blend_order"] = 999 252 | 253 | if not icons_draw: 254 | text_draw["priority"] = 999 - order 255 | 256 | if layer.get("symbol-placement") == "line": 257 | text_draw["placement"] = "spaced" 258 | 259 | spacing = layer.get("symbol-spacing") 260 | if spacing: 261 | text_draw["placement_spacing"] = f"{spacing}px" 262 | 263 | source = render_text_source(layer["text-field"]) 264 | if source: 265 | text_draw["text_source"] = source 266 | 267 | font = text_draw.get("font", {}) 268 | 269 | if layer.get("text-font"): 270 | text_font = layer["text-font"] 271 | 272 | if isinstance(text_font, list): 273 | # for family in text_font: 274 | # self.emit(("fonts", family), "external") 275 | 276 | text_font = ", ".join(text_font) 277 | else: 278 | pass 279 | # self.emit(("fonts", text_font), "external") 280 | 281 | font["family"] = text_font 282 | 283 | if layer.get("text-size"): 284 | size = layer["text-size"] 285 | if isinstance(size, dict): 286 | size = convert_stops_or_value(size["stops"], base=size.get("base", 1.0)) 287 | 288 | font["size"] = size 289 | 290 | if layer.get("text-transform"): 291 | font["transform"] = layer["text-transform"] 292 | 293 | if layer.get("halo-color") and (layer.get("halo-width") or layer.get("halo-width-stops")): 294 | halo_width = layer.get("halo-width", layer.get("halo-width-stops")) 295 | # TODO: Base not supported 296 | if layer.get("halo-width-base", 1) != 1: 297 | self.emit_warning("Unsupported exponential halo-width-base; this will be interpreted as linear.") 298 | 299 | halo_width = convert_stops_or_value(halo_width, unit="px") 300 | 301 | font["stroke"] = { 302 | "color": layer["halo-color"], 303 | "width": halo_width, 304 | } 305 | 306 | # Though dicts are mutable, if the font key did not previously exist, we still 307 | # need to set it here 308 | text_draw["font"] = font 309 | 310 | # TODO: Text blur? 311 | # TODO: Text translate? 312 | elif geom_draw: 313 | geom_draw["order"] = order 314 | geom_draw["blend_order"] = order 315 | # TODO: Check if we need alpha 316 | geom_draw["style"] = f"translucent_{layer_type}" 317 | 318 | if layer.get("line-join"): 319 | geom_draw["join"] = layer["line-join"] 320 | 321 | if layer.get("line-cap"): 322 | geom_draw["cap"] = layer["line-cap"] 323 | 324 | if icons_draw: 325 | icon = layer["icon-image"] 326 | if isinstance(icon, str): 327 | icons_draw["sprite"] = icon 328 | elif isinstance(icon, dict) and isinstance(icon.get("stops"), list): 329 | # Stops... not really supported 330 | value = icon["stops"][0][1] 331 | icons_draw["sprite"] = value 332 | self.emit_warning( 333 | f"Complex icon-image expressions are not supported. Picking the first stop {repr(value)} from {repr(icon)}" 334 | ) 335 | else: 336 | self.emit_warning(f"Unable to parse icon-image expression: {repr(icon)}") 337 | 338 | if layer.get("icon-size"): 339 | icons_draw["size"] = f"{layer['icon-size'] * 100}%" 340 | 341 | return result 342 | 343 | def exiting_scope(self): 344 | if self.tag_path[0] == "layers" and len(self.tag_path) == 2: 345 | if self.ctx.get("layer"): 346 | ctx_layer = self.ctx.pop("layer") 347 | if ctx_layer.get("background"): 348 | self.emit(("scene", "background", "color"), ctx_layer["background-color"]) 349 | else: 350 | layer = {} 351 | layer_id = ctx_layer["id"] 352 | source = ctx_layer.get("source") 353 | source_layer = ctx_layer.get("source-layer") 354 | zoom = {} 355 | 356 | if source: 357 | data = {"source": source} 358 | if source_layer and source_layer != layer_id: 359 | data["layer"] = source_layer 360 | 361 | layer["data"] = data 362 | else: 363 | self.emit_warning(f"Layer has no source: {ctx_layer}") 364 | return 365 | 366 | draw = self.render_draw(ctx_layer) 367 | if not draw: 368 | return # Errors reported internally 369 | 370 | layer["draw"] = draw 371 | 372 | if ctx_layer.get("minzoom"): 373 | zoom["min"] = ctx_layer["minzoom"] 374 | if ctx_layer.get("maxzoom"): 375 | zoom["max"] = ctx_layer["maxzoom"] 376 | 377 | if ctx_layer.get("filter"): 378 | layer["filter"] = ctx_layer["filter"] 379 | 380 | if zoom: 381 | if not layer["filter"]: 382 | layer["filter"] = {} 383 | 384 | layer["filter"]["$zoom"] = zoom 385 | 386 | self.emit(("layers", layer_id), layer) 387 | 388 | def visit_source(self, e: Union[str, int, float, list]): 389 | if self.tag_path[2] == "type": 390 | if e == "vector": 391 | self.emit(self.tag_path, "MVT") 392 | self.emit(self.tag_path[:-1] + ["tile_size"], 512) 393 | else: 394 | self.emit_warning(f"Unsupported source type: {e}") 395 | elif self.tag_path[2] == "url": 396 | res = requests.get(e) 397 | tilejson = res.json() 398 | tile_urls = tilejson["tiles"] 399 | if len(tile_urls) > 1: 400 | self.emit_warning("Only the first tile source URL has been imported") 401 | 402 | self.emit(self.tag_path, tile_urls[0]) 403 | 404 | if tilejson.get("maxzoom"): 405 | self.emit(self.tag_path[:-1] + ["max_zoom"], tilejson["maxzoom"]) 406 | elif self.tag_path[2] == "tiles": 407 | if len(e) > 1: 408 | self.emit_warning("Only the first tile source URL has been imported") 409 | 410 | self.emit(self.tag_path[:-1] + ["url"], e[0]) 411 | elif self.tag_path[2] == "maxzoom": 412 | self.emit(self.tag_path[:-1] + ["max_zoom"], e) 413 | 414 | def visit_layer(self, e: Union[str, int, float, list]): 415 | if not self.ctx.get("layer"): 416 | self.ctx["layer"] = {"idx": self.tag_path[1]} 417 | 418 | layer = self.ctx["layer"] 419 | 420 | if self.tag_path[2] == "id": 421 | layer["id"] = e 422 | elif self.tag_path[2] == "type": 423 | if e == "background": 424 | # Special case 425 | layer["background"] = True 426 | else: 427 | layer["type"] = layer_type_mappings[e] 428 | elif self.tag_path[2] in {"source", "source-layer", "minzoom", "maxzoom"}: 429 | layer[self.tag_path[2]] = e 430 | elif self.tag_path[2] == "filter": 431 | layer["filter"] = self.render_layer_filter(e) 432 | elif self.tag_path[2] == "paint": 433 | if self.tag_path[3] == "background-color": 434 | # Background color 435 | layer["background-color"] = e 436 | elif self.tag_path[3] in {"fill-color", "line-color", "text-color"}: 437 | # TODO: This is probably a hackish assumption that one of these reduces to the layer "color" 438 | if len(self.tag_path) == 4 or self.tag_path[4] == "stops": 439 | # TODO: Handle non-linear bases 440 | layer["color"] = convert_stops_or_value(e) 441 | elif self.tag_path[3] == "text-halo-color": 442 | if len(self.tag_path) == 4 or self.tag_path[4] == "stops": 443 | # TODO: Handle non-linear bases 444 | layer["halo-color"] = convert_stops_or_value(e) 445 | elif self.tag_path[3] == "text-halo-width": 446 | if len(self.tag_path) == 4: 447 | layer["halo-width"] = e 448 | elif self.tag_path[4] == "stops": 449 | layer["halo-width-stops"] = e 450 | elif self.tag_path[4] == "base": 451 | layer["halo-width-base"] = e 452 | elif self.tag_path[3] == "line-dasharray": 453 | layer["dash"] = e 454 | elif self.tag_path[3] == "line-width": 455 | if len(self.tag_path) == 4: 456 | layer["width"] = e 457 | elif self.tag_path[4] == "stops": 458 | layer["width-stops"] = e 459 | elif self.tag_path[4] == "base": 460 | layer["width-base"] = e 461 | elif self.tag_path[2] == "layout" and self.tag_path[3] in passthrough_tags: 462 | if len(self.tag_path) > 4: 463 | # Create nested structure if necessary 464 | last = None 465 | for key in self.tag_path[3:-1]: 466 | if last is None: 467 | layer[key] = layer.get(key, {}) 468 | last = layer[key] 469 | else: 470 | last[key] = last.get(key, {}) 471 | last = last[key] 472 | last[self.tag_path[-1]] = e 473 | else: 474 | layer[self.tag_path[-1]] = e 475 | 476 | def visit(self, e: Union[str, int, float, list]): 477 | # print(self.tag_path, e) 478 | if self.tag_path[0] == "layers": 479 | self.visit_layer(e) 480 | elif self.tag_path[0] == "sources": 481 | self.visit_source(e) 482 | elif self.tag_path[0] == "sprite": 483 | self.render_sprite(e) 484 | -------------------------------------------------------------------------------- /out.yaml: -------------------------------------------------------------------------------- 1 | { 2 | "scene": { 3 | "background": { 4 | "color": "hsl(0, 0%, 20%)" 5 | } 6 | }, 7 | "layers": { 8 | "park_fill": { 9 | "data": { 10 | "source": "openmaptiles", 11 | "layer": "park" 12 | }, 13 | "draw": { 14 | "polygons": { 15 | "color": "hsla(109, 41%, 55%, 8%)", 16 | "order": 1, 17 | "blend_order": 1, 18 | "style": "inlay_polygons" 19 | } 20 | }, 21 | "filter": { 22 | "$geometry": "polygon" 23 | } 24 | }, 25 | "landcover_ice_shelf": { 26 | "data": { 27 | "source": "openmaptiles", 28 | "layer": "landcover" 29 | }, 30 | "draw": { 31 | "polygons": { 32 | "color": "hsla(0, 0%, 75%, 60%)", 33 | "order": 2, 34 | "blend_order": 2, 35 | "style": "inlay_polygons" 36 | } 37 | }, 38 | "filter": { 39 | "all": [ 40 | { 41 | "$geometry": "polygon" 42 | }, 43 | { 44 | "subclass": "ice_shelf" 45 | } 46 | ], 47 | "$zoom": { 48 | "max": 8 49 | } 50 | } 51 | }, 52 | "landcover_glacier": { 53 | "data": { 54 | "source": "openmaptiles", 55 | "layer": "landcover" 56 | }, 57 | "draw": { 58 | "polygons": { 59 | "color": "hsla(0, 0%, 75%, 50%)", 60 | "order": 3, 61 | "blend_order": 3, 62 | "style": "inlay_polygons" 63 | } 64 | }, 65 | "filter": { 66 | "all": [ 67 | { 68 | "$geometry": "polygon" 69 | }, 70 | { 71 | "subclass": "glacier" 72 | } 73 | ], 74 | "$zoom": { 75 | "max": 8 76 | } 77 | } 78 | }, 79 | "landuse_residential": { 80 | "data": { 81 | "source": "openmaptiles", 82 | "layer": "landuse" 83 | }, 84 | "draw": { 85 | "polygons": { 86 | "color": "hsla(60, 8%, 22%, 20%)", 87 | "order": 4, 88 | "blend_order": 4, 89 | "style": "inlay_polygons" 90 | } 91 | }, 92 | "filter": { 93 | "all": [ 94 | { 95 | "$geometry": "polygon" 96 | }, 97 | { 98 | "class": "residential" 99 | } 100 | ], 101 | "$zoom": { 102 | "max": 16 103 | } 104 | } 105 | }, 106 | "landcover_wood": { 107 | "data": { 108 | "source": "openmaptiles", 109 | "layer": "landcover" 110 | }, 111 | "draw": { 112 | "polygons": { 113 | "color": "hsla(120, 23%, 59%, 10%)", 114 | "order": 5, 115 | "blend_order": 5, 116 | "style": "inlay_polygons" 117 | } 118 | }, 119 | "filter": { 120 | "all": [ 121 | { 122 | "$geometry": "polygon" 123 | }, 124 | { 125 | "class": "wood" 126 | } 127 | ], 128 | "$zoom": { 129 | "min": 10 130 | } 131 | } 132 | }, 133 | "landcover_park": { 134 | "data": { 135 | "source": "openmaptiles", 136 | "layer": "landcover" 137 | }, 138 | "draw": { 139 | "polygons": { 140 | "color": "hsla(120, 23%, 55%, 8%)", 141 | "order": 6, 142 | "blend_order": 6, 143 | "style": "inlay_polygons" 144 | } 145 | }, 146 | "filter": { 147 | "all": [ 148 | { 149 | "$geometry": "polygon" 150 | }, 151 | { 152 | "subclass": "park" 153 | } 154 | ], 155 | "$zoom": { 156 | "min": 10 157 | } 158 | } 159 | }, 160 | "boundary_state": { 161 | "data": { 162 | "source": "openmaptiles", 163 | "layer": "boundary" 164 | }, 165 | "draw": { 166 | "lines": { 167 | "color": "hsla(353, 34%, 80%, 30%)", 168 | "width": [ 169 | [ 170 | 3, 171 | "0.5px" 172 | ], 173 | [ 174 | 22, 175 | "7.5px" 176 | ] 177 | ], 178 | "dash": [ 179 | 2, 180 | 2 181 | ], 182 | "order": 7, 183 | "blend_order": 7, 184 | "style": "inlay_lines", 185 | "join": "round", 186 | "cap": "round" 187 | } 188 | }, 189 | "filter": { 190 | "admin_level": 4 191 | } 192 | }, 193 | "boundary_country": { 194 | "data": { 195 | "source": "openmaptiles", 196 | "layer": "boundary" 197 | }, 198 | "draw": { 199 | "lines": { 200 | "color": "hsla(353, 34%, 80%, 30%)", 201 | "width": [ 202 | [ 203 | 3, 204 | "0.5px" 205 | ], 206 | [ 207 | 22, 208 | "10.0px" 209 | ] 210 | ], 211 | "order": 8, 212 | "blend_order": 8, 213 | "style": "inlay_lines", 214 | "join": "round", 215 | "cap": "round" 216 | } 217 | }, 218 | "filter": { 219 | "admin_level": 2 220 | } 221 | }, 222 | "water": { 223 | "data": { 224 | "source": "openmaptiles" 225 | }, 226 | "draw": { 227 | "polygons": { 228 | "color": "#222", 229 | "order": 9, 230 | "blend_order": 9, 231 | "style": "inlay_polygons" 232 | } 233 | }, 234 | "filter": { 235 | "$geometry": "polygon" 236 | } 237 | }, 238 | "waterway": { 239 | "data": { 240 | "source": "openmaptiles" 241 | }, 242 | "draw": { 243 | "lines": { 244 | "color": "#222", 245 | "width": "1px", 246 | "order": 10, 247 | "blend_order": 10, 248 | "style": "inlay_lines" 249 | } 250 | }, 251 | "filter": { 252 | "$geometry": "line" 253 | } 254 | }, 255 | "building": { 256 | "data": { 257 | "source": "openmaptiles" 258 | }, 259 | "draw": { 260 | "polygons": { 261 | "color": "hsl(95, 5%, 22%)", 262 | "order": 11, 263 | "blend_order": 11, 264 | "style": "inlay_polygons" 265 | } 266 | }, 267 | "filter": { 268 | "$geometry": "polygon", 269 | "$zoom": { 270 | "min": 12 271 | } 272 | } 273 | }, 274 | "tunnel_motorway_casing": { 275 | "data": { 276 | "source": "openmaptiles", 277 | "layer": "transportation" 278 | }, 279 | "draw": { 280 | "lines": { 281 | "color": "hsla(0, 0%, 40%, 20%)", 282 | "width": [ 283 | [ 284 | 5.8, 285 | "0.0px" 286 | ], 287 | [ 288 | 6, 289 | "1.5px" 290 | ], 291 | [ 292 | 20, 293 | "17.5px" 294 | ] 295 | ], 296 | "order": 12, 297 | "blend_order": 12, 298 | "style": "inlay_lines", 299 | "join": "miter", 300 | "cap": "butt" 301 | } 302 | }, 303 | "filter": { 304 | "all": [ 305 | { 306 | "$geometry": "line" 307 | }, 308 | { 309 | "all": [ 310 | { 311 | "brunnel": "tunnel" 312 | }, 313 | { 314 | "class": "motorway" 315 | } 316 | ] 317 | } 318 | ], 319 | "$zoom": { 320 | "min": 6 321 | } 322 | } 323 | }, 324 | "tunnel_motorway_inner": { 325 | "data": { 326 | "source": "openmaptiles", 327 | "layer": "transportation" 328 | }, 329 | "draw": { 330 | "lines": { 331 | "color": "hsla(60, 1%, 21%, 10%)", 332 | "width": [ 333 | [ 334 | 4, 335 | "1.0px" 336 | ], 337 | [ 338 | 6, 339 | "0.65px" 340 | ], 341 | [ 342 | 20, 343 | "15.0px" 344 | ] 345 | ], 346 | "order": 13, 347 | "blend_order": 13, 348 | "style": "inlay_lines", 349 | "join": "round", 350 | "cap": "round" 351 | } 352 | }, 353 | "filter": { 354 | "all": [ 355 | { 356 | "$geometry": "line" 357 | }, 358 | { 359 | "all": [ 360 | { 361 | "brunnel": "tunnel" 362 | }, 363 | { 364 | "class": "motorway" 365 | } 366 | ] 367 | } 368 | ], 369 | "$zoom": { 370 | "min": 6 371 | } 372 | } 373 | }, 374 | "highway_path": { 375 | "data": { 376 | "source": "openmaptiles", 377 | "layer": "transportation" 378 | }, 379 | "draw": { 380 | "lines": { 381 | "color": "hsl(0, 0%, 30%)", 382 | "width": [ 383 | [ 384 | 13, 385 | "0.5px" 386 | ], 387 | [ 388 | 20, 389 | "5.0px" 390 | ] 391 | ], 392 | "order": 14, 393 | "blend_order": 14, 394 | "style": "inlay_lines", 395 | "join": "round", 396 | "cap": "round" 397 | } 398 | }, 399 | "filter": { 400 | "all": [ 401 | { 402 | "$geometry": "line" 403 | }, 404 | { 405 | "class": [ 406 | "path", 407 | "construction" 408 | ] 409 | } 410 | ] 411 | } 412 | }, 413 | "highway_minor": { 414 | "data": { 415 | "source": "openmaptiles", 416 | "layer": "transportation" 417 | }, 418 | "draw": { 419 | "lines": { 420 | "color": [ 421 | [ 422 | 13, 423 | "hsl(0, 0%, 32%)" 424 | ], 425 | [ 426 | 16, 427 | "hsl(0, 0%, 30%)" 428 | ] 429 | ], 430 | "width": [ 431 | [ 432 | 13, 433 | "0.5px" 434 | ], 435 | [ 436 | 18, 437 | "4.0px" 438 | ] 439 | ], 440 | "order": 15, 441 | "blend_order": 15, 442 | "style": "inlay_lines", 443 | "join": "round", 444 | "cap": "round" 445 | } 446 | }, 447 | "filter": { 448 | "all": [ 449 | { 450 | "$geometry": "line" 451 | }, 452 | { 453 | "class": [ 454 | "minor", 455 | "service", 456 | "track" 457 | ] 458 | } 459 | ], 460 | "$zoom": { 461 | "min": 8 462 | } 463 | } 464 | }, 465 | "highway_major_casing": { 466 | "data": { 467 | "source": "openmaptiles", 468 | "layer": "transportation" 469 | }, 470 | "draw": { 471 | "lines": { 472 | "color": "hsla(0, 0%, 30%, 80%)", 473 | "width": [ 474 | [ 475 | 10, 476 | "1.5px" 477 | ], 478 | [ 479 | 20, 480 | "10.0px" 481 | ] 482 | ], 483 | "dash": [ 484 | 12, 485 | 0 486 | ], 487 | "order": 16, 488 | "blend_order": 16, 489 | "style": "inlay_lines", 490 | "join": "miter", 491 | "cap": "butt" 492 | } 493 | }, 494 | "filter": { 495 | "all": [ 496 | { 497 | "$geometry": "line" 498 | }, 499 | { 500 | "class": [ 501 | "primary", 502 | "secondary", 503 | "tertiary", 504 | "trunk" 505 | ] 506 | } 507 | ], 508 | "$zoom": { 509 | "min": 12 510 | } 511 | } 512 | }, 513 | "highway_major_inner": { 514 | "data": { 515 | "source": "openmaptiles", 516 | "layer": "transportation" 517 | }, 518 | "draw": { 519 | "lines": { 520 | "color": "hsla(60, 1%, 16%, 90%)", 521 | "width": [ 522 | [ 523 | 10, 524 | "1.0px" 525 | ], 526 | [ 527 | 20, 528 | "9.0px" 529 | ] 530 | ], 531 | "order": 17, 532 | "blend_order": 17, 533 | "style": "inlay_lines", 534 | "join": "round", 535 | "cap": "round" 536 | } 537 | }, 538 | "filter": { 539 | "all": [ 540 | { 541 | "$geometry": "line" 542 | }, 543 | { 544 | "class": [ 545 | "primary", 546 | "secondary", 547 | "tertiary", 548 | "trunk" 549 | ] 550 | } 551 | ], 552 | "$zoom": { 553 | "min": 12 554 | } 555 | } 556 | }, 557 | "highway_major_subtle": { 558 | "data": { 559 | "source": "openmaptiles", 560 | "layer": "transportation" 561 | }, 562 | "draw": { 563 | "lines": { 564 | "color": "hsla(0, 0%, 32%, 70%)", 565 | "width": "0.5px", 566 | "order": 18, 567 | "blend_order": 18, 568 | "style": "inlay_lines", 569 | "join": "round", 570 | "cap": "round" 571 | } 572 | }, 573 | "filter": { 574 | "all": [ 575 | { 576 | "$geometry": "line" 577 | }, 578 | { 579 | "class": [ 580 | "primary", 581 | "secondary", 582 | "tertiary", 583 | "trunk" 584 | ] 585 | } 586 | ], 587 | "$zoom": { 588 | "max": 12 589 | } 590 | } 591 | }, 592 | "highway_motorway_casing": { 593 | "data": { 594 | "source": "openmaptiles", 595 | "layer": "transportation" 596 | }, 597 | "draw": { 598 | "lines": { 599 | "color": "hsla(0, 0%, 30%, 80%)", 600 | "width": [ 601 | [ 602 | 5.8, 603 | "0.0px" 604 | ], 605 | [ 606 | 6, 607 | "1.5px" 608 | ], 609 | [ 610 | 20, 611 | "15.0px" 612 | ] 613 | ], 614 | "dash": [ 615 | 2, 616 | 0 617 | ], 618 | "order": 19, 619 | "blend_order": 19, 620 | "style": "inlay_lines", 621 | "join": "miter", 622 | "cap": "butt" 623 | } 624 | }, 625 | "filter": { 626 | "all": [ 627 | { 628 | "$geometry": "line" 629 | }, 630 | { 631 | "all": [ 632 | { 633 | "not": { 634 | "brunnel": [ 635 | "bridge", 636 | "tunnel" 637 | ] 638 | } 639 | }, 640 | { 641 | "class": "motorway" 642 | } 643 | ] 644 | } 645 | ], 646 | "$zoom": { 647 | "min": 6 648 | } 649 | } 650 | }, 651 | "highway_motorway_inner": { 652 | "data": { 653 | "source": "openmaptiles", 654 | "layer": "transportation" 655 | }, 656 | "draw": { 657 | "lines": { 658 | "color": "hsla(60, 1%, 16%, 90%)", 659 | "width": [ 660 | [ 661 | 4, 662 | "1.0px" 663 | ], 664 | [ 665 | 6, 666 | "0.65px" 667 | ], 668 | [ 669 | 20, 670 | "12.5px" 671 | ] 672 | ], 673 | "order": 20, 674 | "blend_order": 20, 675 | "style": "inlay_lines", 676 | "join": "round", 677 | "cap": "round" 678 | } 679 | }, 680 | "filter": { 681 | "all": [ 682 | { 683 | "$geometry": "line" 684 | }, 685 | { 686 | "all": [ 687 | { 688 | "not": { 689 | "brunnel": [ 690 | "bridge", 691 | "tunnel" 692 | ] 693 | } 694 | }, 695 | { 696 | "class": "motorway" 697 | } 698 | ] 699 | } 700 | ], 701 | "$zoom": { 702 | "min": 6 703 | } 704 | } 705 | }, 706 | "highway_motorway_subtle": { 707 | "data": { 708 | "source": "openmaptiles", 709 | "layer": "transportation" 710 | }, 711 | "draw": { 712 | "lines": { 713 | "color": "hsla(0, 0%, 35%, 0.53)", 714 | "width": [ 715 | [ 716 | 4, 717 | "0.375px" 718 | ], 719 | [ 720 | 5, 721 | "0.75px" 722 | ] 723 | ], 724 | "order": 21, 725 | "blend_order": 21, 726 | "style": "inlay_lines", 727 | "join": "round", 728 | "cap": "round" 729 | } 730 | }, 731 | "filter": { 732 | "all": [ 733 | { 734 | "$geometry": "line" 735 | }, 736 | { 737 | "class": "motorway" 738 | } 739 | ], 740 | "$zoom": { 741 | "max": 6 742 | } 743 | } 744 | }, 745 | "railway_service": { 746 | "data": { 747 | "source": "openmaptiles", 748 | "layer": "transportation" 749 | }, 750 | "draw": { 751 | "lines": { 752 | "color": "#545353", 753 | "width": "1.5px", 754 | "order": 22, 755 | "blend_order": 22, 756 | "style": "inlay_lines", 757 | "join": "round" 758 | } 759 | }, 760 | "filter": { 761 | "all": [ 762 | { 763 | "$geometry": "line" 764 | }, 765 | { 766 | "all": [ 767 | { 768 | "class": "rail" 769 | }, 770 | null 771 | ] 772 | } 773 | ], 774 | "$zoom": { 775 | "min": 16 776 | } 777 | } 778 | }, 779 | "railway_service_dashline": { 780 | "data": { 781 | "source": "openmaptiles", 782 | "layer": "transportation" 783 | }, 784 | "draw": { 785 | "lines": { 786 | "color": "#7f7d7e", 787 | "width": "1.0px", 788 | "dash": [ 789 | 3, 790 | 3 791 | ], 792 | "order": 23, 793 | "blend_order": 23, 794 | "style": "inlay_lines", 795 | "join": "round" 796 | } 797 | }, 798 | "filter": { 799 | "all": [ 800 | { 801 | "$geometry": "line" 802 | }, 803 | { 804 | "class": "rail" 805 | }, 806 | null 807 | ], 808 | "$zoom": { 809 | "min": 16 810 | } 811 | } 812 | }, 813 | "railway": { 814 | "data": { 815 | "source": "openmaptiles", 816 | "layer": "transportation" 817 | }, 818 | "draw": { 819 | "lines": { 820 | "color": "#545353", 821 | "width": [ 822 | [ 823 | 16, 824 | "1.5px" 825 | ], 826 | [ 827 | 20, 828 | "3.5px" 829 | ] 830 | ], 831 | "order": 24, 832 | "blend_order": 24, 833 | "style": "inlay_lines", 834 | "join": "round" 835 | } 836 | }, 837 | "filter": { 838 | "all": [ 839 | { 840 | "$geometry": "line" 841 | }, 842 | { 843 | "all": [ 844 | null, 845 | { 846 | "class": "rail" 847 | } 848 | ] 849 | } 850 | ], 851 | "$zoom": { 852 | "min": 13 853 | } 854 | } 855 | }, 856 | "aeroway_line": { 857 | "data": { 858 | "source": "openmaptiles", 859 | "layer": "aeroway" 860 | }, 861 | "draw": { 862 | "lines": { 863 | "color": "#545353", 864 | "width": "1px", 865 | "order": 26, 866 | "blend_order": 26, 867 | "style": "inlay_lines" 868 | } 869 | }, 870 | "filter": { 871 | "$geometry": "line" 872 | } 873 | }, 874 | "highway_motorway_bridge_casing": { 875 | "data": { 876 | "source": "openmaptiles", 877 | "layer": "transportation" 878 | }, 879 | "draw": { 880 | "lines": { 881 | "color": "hsla(0, 0%, 30%, 80%)", 882 | "width": [ 883 | [ 884 | 5.8, 885 | "0.0px" 886 | ], 887 | [ 888 | 6, 889 | "2.5px" 890 | ], 891 | [ 892 | 20, 893 | "17.5px" 894 | ] 895 | ], 896 | "dash": [ 897 | 2, 898 | 0 899 | ], 900 | "order": 27, 901 | "blend_order": 27, 902 | "style": "inlay_lines", 903 | "join": "miter", 904 | "cap": "butt" 905 | } 906 | }, 907 | "filter": { 908 | "all": [ 909 | { 910 | "$geometry": "line" 911 | }, 912 | { 913 | "all": [ 914 | { 915 | "brunnel": "bridge" 916 | }, 917 | { 918 | "class": "motorway" 919 | } 920 | ] 921 | } 922 | ], 923 | "$zoom": { 924 | "min": 6 925 | } 926 | } 927 | }, 928 | "highway_motorway_bridge_inner": { 929 | "data": { 930 | "source": "openmaptiles", 931 | "layer": "transportation" 932 | }, 933 | "draw": { 934 | "lines": { 935 | "color": "hsla(60, 1%, 16%, 90%)", 936 | "width": [ 937 | [ 938 | 4, 939 | "1.0px" 940 | ], 941 | [ 942 | 6, 943 | "0.65px" 944 | ], 945 | [ 946 | 20, 947 | "15.0px" 948 | ] 949 | ], 950 | "order": 28, 951 | "blend_order": 28, 952 | "style": "inlay_lines", 953 | "join": "round", 954 | "cap": "round" 955 | } 956 | }, 957 | "filter": { 958 | "all": [ 959 | { 960 | "$geometry": "line" 961 | }, 962 | { 963 | "all": [ 964 | { 965 | "brunnel": "bridge" 966 | }, 967 | { 968 | "class": "motorway" 969 | } 970 | ] 971 | } 972 | ], 973 | "$zoom": { 974 | "min": 6 975 | } 976 | } 977 | }, 978 | "highway_name_other": { 979 | "data": { 980 | "source": "openmaptiles", 981 | "layer": "transportation_name" 982 | }, 983 | "draw": { 984 | "text": { 985 | "font": { 986 | "fill": "#aaa" 987 | }, 988 | "move_into_tile": false, 989 | "style": "overlay_text", 990 | "blend_order": 29 991 | } 992 | }, 993 | "filter": { 994 | "all": [ 995 | { 996 | "not": { 997 | "class": [ 998 | "motorway", 999 | "trunk", 1000 | "primary" 1001 | ] 1002 | } 1003 | }, 1004 | { 1005 | "$geometry": "line" 1006 | } 1007 | ] 1008 | } 1009 | }, 1010 | "water_name_ocean": { 1011 | "data": { 1012 | "source": "openmaptiles", 1013 | "layer": "water_name" 1014 | }, 1015 | "draw": { 1016 | "text": { 1017 | "font": { 1018 | "fill": "#999" 1019 | }, 1020 | "move_into_tile": false, 1021 | "style": "overlay_text", 1022 | "blend_order": 30 1023 | } 1024 | }, 1025 | "filter": { 1026 | "all": [ 1027 | { 1028 | "$geometry": "point" 1029 | }, 1030 | { 1031 | "class": "ocean" 1032 | } 1033 | ] 1034 | } 1035 | }, 1036 | "water_name_nonocean": { 1037 | "data": { 1038 | "source": "openmaptiles", 1039 | "layer": "water_name" 1040 | }, 1041 | "draw": { 1042 | "text": { 1043 | "font": { 1044 | "fill": "#999" 1045 | }, 1046 | "move_into_tile": false, 1047 | "style": "overlay_text", 1048 | "blend_order": 31 1049 | } 1050 | }, 1051 | "filter": { 1052 | "all": [ 1053 | { 1054 | "$geometry": "point" 1055 | }, 1056 | { 1057 | "not": { 1058 | "class": [ 1059 | "ocean" 1060 | ] 1061 | } 1062 | } 1063 | ] 1064 | } 1065 | }, 1066 | "water_name_line": { 1067 | "data": { 1068 | "source": "openmaptiles", 1069 | "layer": "water_name" 1070 | }, 1071 | "draw": { 1072 | "text": { 1073 | "font": { 1074 | "fill": "#999" 1075 | }, 1076 | "move_into_tile": false, 1077 | "style": "overlay_text", 1078 | "blend_order": 32 1079 | } 1080 | }, 1081 | "filter": { 1082 | "$geometry": "line" 1083 | } 1084 | }, 1085 | "poi_gen1": { 1086 | "data": { 1087 | "source": "openmaptiles", 1088 | "layer": "poi" 1089 | }, 1090 | "draw": { 1091 | "text": { 1092 | "font": { 1093 | "fill": "#aaa" 1094 | }, 1095 | "move_into_tile": false, 1096 | "style": "overlay_text", 1097 | "blend_order": 33 1098 | } 1099 | }, 1100 | "filter": { 1101 | "all": [ 1102 | { 1103 | "class": [ 1104 | "park" 1105 | ] 1106 | }, 1107 | null, 1108 | { 1109 | "$geometry": "point" 1110 | } 1111 | ], 1112 | "$zoom": { 1113 | "min": 15 1114 | } 1115 | } 1116 | }, 1117 | "poi_gen0_parks": { 1118 | "data": { 1119 | "source": "openmaptiles", 1120 | "layer": "poi" 1121 | }, 1122 | "draw": { 1123 | "text": { 1124 | "font": { 1125 | "fill": "#aaa" 1126 | }, 1127 | "move_into_tile": false, 1128 | "style": "overlay_text", 1129 | "blend_order": 34 1130 | } 1131 | }, 1132 | "filter": { 1133 | "all": [ 1134 | { 1135 | "subclass": "park" 1136 | }, 1137 | { 1138 | "rank": 1 1139 | }, 1140 | { 1141 | "$geometry": "point" 1142 | } 1143 | ] 1144 | } 1145 | }, 1146 | "poi_gen0_other": { 1147 | "data": { 1148 | "source": "openmaptiles", 1149 | "layer": "poi" 1150 | }, 1151 | "draw": { 1152 | "text": { 1153 | "font": { 1154 | "fill": "#aaa" 1155 | }, 1156 | "move_into_tile": false, 1157 | "style": "overlay_text", 1158 | "blend_order": 35 1159 | } 1160 | }, 1161 | "filter": { 1162 | "all": [ 1163 | { 1164 | "subclass": [ 1165 | "university", 1166 | "hospital" 1167 | ] 1168 | }, 1169 | null, 1170 | { 1171 | "$geometry": "point" 1172 | } 1173 | ] 1174 | } 1175 | }, 1176 | "place_other": { 1177 | "data": { 1178 | "source": "openmaptiles", 1179 | "layer": "place" 1180 | }, 1181 | "draw": { 1182 | "text": { 1183 | "font": { 1184 | "fill": "hsl(214.3, 11.3%, 70%)" 1185 | }, 1186 | "move_into_tile": false, 1187 | "style": "overlay_text", 1188 | "blend_order": 36 1189 | } 1190 | }, 1191 | "filter": { 1192 | "all": [ 1193 | { 1194 | "not": { 1195 | "class": [ 1196 | "city", 1197 | "suburb", 1198 | "town", 1199 | "village" 1200 | ] 1201 | } 1202 | }, 1203 | { 1204 | "$geometry": "point" 1205 | } 1206 | ], 1207 | "$zoom": { 1208 | "min": 11, 1209 | "max": 14 1210 | } 1211 | } 1212 | }, 1213 | "highway_name_major": { 1214 | "data": { 1215 | "source": "openmaptiles", 1216 | "layer": "transportation_name" 1217 | }, 1218 | "draw": { 1219 | "text": { 1220 | "font": { 1221 | "fill": "#ccc" 1222 | }, 1223 | "move_into_tile": false, 1224 | "style": "overlay_text", 1225 | "blend_order": 37 1226 | } 1227 | }, 1228 | "filter": { 1229 | "all": [ 1230 | { 1231 | "class": [ 1232 | "trunk", 1233 | "primary" 1234 | ] 1235 | }, 1236 | { 1237 | "$geometry": "line" 1238 | } 1239 | ] 1240 | } 1241 | }, 1242 | "highway_name_motorway": { 1243 | "data": { 1244 | "source": "openmaptiles", 1245 | "layer": "transportation_name" 1246 | }, 1247 | "draw": { 1248 | "text": { 1249 | "font": { 1250 | "fill": "hsl(214, 11%, 65%)" 1251 | }, 1252 | "move_into_tile": false, 1253 | "style": "overlay_text", 1254 | "blend_order": 38, 1255 | "text_source": "ref" 1256 | } 1257 | }, 1258 | "filter": { 1259 | "all": [ 1260 | { 1261 | "$geometry": "line" 1262 | }, 1263 | { 1264 | "class": "motorway" 1265 | } 1266 | ] 1267 | } 1268 | }, 1269 | "place_suburb": { 1270 | "data": { 1271 | "source": "openmaptiles", 1272 | "layer": "place" 1273 | }, 1274 | "draw": { 1275 | "text": { 1276 | "font": { 1277 | "fill": "#9aa2ac" 1278 | }, 1279 | "move_into_tile": false, 1280 | "style": "overlay_text", 1281 | "blend_order": 39 1282 | } 1283 | }, 1284 | "filter": { 1285 | "all": [ 1286 | { 1287 | "$geometry": "point" 1288 | }, 1289 | { 1290 | "class": "suburb" 1291 | } 1292 | ], 1293 | "$zoom": { 1294 | "max": 15 1295 | } 1296 | } 1297 | }, 1298 | "place_village": { 1299 | "data": { 1300 | "source": "openmaptiles", 1301 | "layer": "place" 1302 | }, 1303 | "draw": { 1304 | "text": { 1305 | "font": { 1306 | "fill": "#9aa2ac" 1307 | }, 1308 | "move_into_tile": false, 1309 | "style": "overlay_text", 1310 | "blend_order": 40 1311 | } 1312 | }, 1313 | "filter": { 1314 | "all": [ 1315 | { 1316 | "$geometry": "point" 1317 | }, 1318 | { 1319 | "class": "village" 1320 | } 1321 | ], 1322 | "$zoom": { 1323 | "max": 14 1324 | } 1325 | } 1326 | }, 1327 | "airport_label_gen0": { 1328 | "data": { 1329 | "source": "openmaptiles", 1330 | "layer": "aerodrome_label" 1331 | }, 1332 | "draw": { 1333 | "text": { 1334 | "font": { 1335 | "fill": "#aaa" 1336 | }, 1337 | "move_into_tile": false, 1338 | "style": "overlay_text", 1339 | "blend_order": 41 1340 | } 1341 | }, 1342 | "filter": { 1343 | "all": [ 1344 | null 1345 | ], 1346 | "$zoom": { 1347 | "min": 10 1348 | } 1349 | } 1350 | }, 1351 | "place_town": { 1352 | "data": { 1353 | "source": "openmaptiles", 1354 | "layer": "place" 1355 | }, 1356 | "draw": { 1357 | "text": { 1358 | "font": { 1359 | "fill": "#9aa2ac" 1360 | }, 1361 | "move_into_tile": false, 1362 | "style": "overlay_text", 1363 | "blend_order": 42 1364 | } 1365 | }, 1366 | "filter": { 1367 | "all": [ 1368 | { 1369 | "$geometry": "point" 1370 | }, 1371 | { 1372 | "class": "town" 1373 | } 1374 | ], 1375 | "$zoom": { 1376 | "max": 15 1377 | } 1378 | } 1379 | }, 1380 | "place_city": { 1381 | "data": { 1382 | "source": "openmaptiles", 1383 | "layer": "place" 1384 | }, 1385 | "draw": { 1386 | "text": { 1387 | "font": { 1388 | "fill": "#9aa2ac" 1389 | }, 1390 | "move_into_tile": false, 1391 | "style": "overlay_text", 1392 | "blend_order": 43 1393 | } 1394 | }, 1395 | "filter": { 1396 | "all": [ 1397 | { 1398 | "$geometry": "point" 1399 | }, 1400 | { 1401 | "all": [ 1402 | { 1403 | "not": { 1404 | "capital": 2 1405 | } 1406 | }, 1407 | { 1408 | "class": "city" 1409 | }, 1410 | null 1411 | ] 1412 | } 1413 | ] 1414 | } 1415 | }, 1416 | "place_city_large": { 1417 | "data": { 1418 | "source": "openmaptiles", 1419 | "layer": "place" 1420 | }, 1421 | "draw": { 1422 | "text": { 1423 | "font": { 1424 | "fill": "#9aa2ac" 1425 | }, 1426 | "move_into_tile": false, 1427 | "style": "overlay_text", 1428 | "blend_order": 44 1429 | } 1430 | }, 1431 | "filter": { 1432 | "all": [ 1433 | { 1434 | "$geometry": "point" 1435 | }, 1436 | { 1437 | "all": [ 1438 | { 1439 | "not": { 1440 | "capital": 2 1441 | } 1442 | }, 1443 | null, 1444 | { 1445 | "class": "city" 1446 | } 1447 | ] 1448 | } 1449 | ] 1450 | } 1451 | }, 1452 | "place_capital_gen1": { 1453 | "data": { 1454 | "source": "openmaptiles", 1455 | "layer": "place" 1456 | }, 1457 | "draw": { 1458 | "text": { 1459 | "font": { 1460 | "fill": "#9aa2ac" 1461 | }, 1462 | "move_into_tile": false, 1463 | "style": "overlay_text", 1464 | "blend_order": 45 1465 | } 1466 | }, 1467 | "filter": { 1468 | "all": [ 1469 | { 1470 | "$geometry": "point" 1471 | }, 1472 | { 1473 | "all": [ 1474 | { 1475 | "capital": 2 1476 | }, 1477 | { 1478 | "class": "city" 1479 | }, 1480 | null 1481 | ] 1482 | } 1483 | ], 1484 | "$zoom": { 1485 | "min": 4 1486 | } 1487 | } 1488 | }, 1489 | "place_capital_gen0": { 1490 | "data": { 1491 | "source": "openmaptiles", 1492 | "layer": "place" 1493 | }, 1494 | "draw": { 1495 | "text": { 1496 | "font": { 1497 | "fill": "#9aa2ac" 1498 | }, 1499 | "move_into_tile": false, 1500 | "style": "overlay_text", 1501 | "blend_order": 46 1502 | } 1503 | }, 1504 | "filter": { 1505 | "all": [ 1506 | { 1507 | "$geometry": "point" 1508 | }, 1509 | { 1510 | "all": [ 1511 | { 1512 | "capital": 2 1513 | }, 1514 | { 1515 | "class": "city" 1516 | }, 1517 | null 1518 | ] 1519 | } 1520 | ] 1521 | } 1522 | }, 1523 | "place_state": { 1524 | "data": { 1525 | "source": "openmaptiles", 1526 | "layer": "place" 1527 | }, 1528 | "draw": { 1529 | "text": { 1530 | "font": { 1531 | "fill": "#97a1ac" 1532 | }, 1533 | "move_into_tile": false, 1534 | "style": "overlay_text", 1535 | "blend_order": 47 1536 | } 1537 | }, 1538 | "filter": { 1539 | "all": [ 1540 | { 1541 | "$geometry": "point" 1542 | }, 1543 | { 1544 | "class": "state" 1545 | } 1546 | ], 1547 | "$zoom": { 1548 | "max": 12 1549 | } 1550 | } 1551 | }, 1552 | "place_country_other": { 1553 | "data": { 1554 | "source": "openmaptiles", 1555 | "layer": "place" 1556 | }, 1557 | "draw": { 1558 | "text": { 1559 | "font": { 1560 | "fill": [ 1561 | [ 1562 | 3, 1563 | "#d4d4dc" 1564 | ], 1565 | [ 1566 | 4, 1567 | "#c4c4c4" 1568 | ] 1569 | ] 1570 | }, 1571 | "move_into_tile": false, 1572 | "style": "overlay_text", 1573 | "blend_order": 48 1574 | } 1575 | }, 1576 | "filter": { 1577 | "all": [ 1578 | { 1579 | "$geometry": "point" 1580 | }, 1581 | { 1582 | "all": [ 1583 | { 1584 | "class": "country" 1585 | }, 1586 | null 1587 | ] 1588 | } 1589 | ], 1590 | "$zoom": { 1591 | "max": 10 1592 | } 1593 | } 1594 | }, 1595 | "place_country_major": { 1596 | "data": { 1597 | "source": "openmaptiles", 1598 | "layer": "place" 1599 | }, 1600 | "draw": { 1601 | "text": { 1602 | "font": { 1603 | "fill": [ 1604 | [ 1605 | 3, 1606 | "#d4d4dc" 1607 | ], 1608 | [ 1609 | 4, 1610 | "#c4c4c4" 1611 | ] 1612 | ] 1613 | }, 1614 | "move_into_tile": false, 1615 | "style": "overlay_text", 1616 | "blend_order": 49 1617 | } 1618 | }, 1619 | "filter": { 1620 | "all": [ 1621 | { 1622 | "$geometry": "point" 1623 | }, 1624 | { 1625 | "all": [ 1626 | null, 1627 | { 1628 | "class": "country" 1629 | } 1630 | ] 1631 | } 1632 | ], 1633 | "$zoom": { 1634 | "max": 10 1635 | } 1636 | } 1637 | } 1638 | }, 1639 | "sources": { 1640 | "openmaptiles": { 1641 | "type": "MVT", 1642 | "tile_size": 512, 1643 | "url": "https://tiles.stadiamaps.com/data/openmaptiles/{z}/{x}/{y}.pbf", 1644 | "max_zoom": 14 1645 | } 1646 | } 1647 | } 1648 | --------------------------------------------------------------------------------