OpenStreetMap logo OpenStreetMap

It’s difficult to write in all map style languages. A style written in JSON, like MapLibre, has a few extra pain points because JSON is not designed for editing by humans.

Some “common” style languages are

  • CartoCSS
  • Mapnik XML
  • MapCSS
  • MapServer
  • MapLibre GL/Mapbox GL

Some, like CartoCSS, are designed for human editing, while others, like Mapnik XML, serve as a lower-level language. MapLibre GL falls into this category of not being designed for editing by humans. MapLibre GL preprocessors like glug were designed to help with this, but none of them have taken off. Other style projects like openstreetmap-americana have taken a different route. Their developers have written a program in JavaScript that generates the style.

I’m taking a different route. I’m creating a language that uses minimal pre-processing of its input to produce MapLibre GL. I don’t aim to solve every difficulty with MapLibre GL, only the ones that impact me the most. The end result will be a pre-processing language

The biggest problems I encounter when writing MapLibre GL are

  1. No comments

    Comments are essential so other readers understand what’s written

  2. Everything has to be in one file.

    With large styles this is a burden. More than one file makes it easier to edit.

  3. Having to repeat definitions instead of using a variable.

    Something like a color or symbol definition might appear a dozen times in the style. If you want to change it, you need to make sure you got all the occurrences.

  4. Inability to make versions of the style in different colors.

    When you only want to change a few superficial elements of the style, you want to contain those changes to one file.

  5. Not having support for more colorspaces

    I work in perceptual colorspaces like Lch. It’s a lot of converting that the computer should automate.

What issues have you found when writing MapLibre GL styles?

Discussion

Comment from SomeoneElse on 19 April 2025 at 22:33

I ended up creating a bunch of scripts to manage the loading and unloading of vector tiles and map styles, including the icons and sprites.

That meant that when I created a new test style I only needed to create one file - the style .json and run one command with a bunch of parameters to deploy it.

However, the “elephant in the room” is still the large, uncommentable .json file - having “something sane from which that is generated” would clearly be much better.. I’ve used layer ids that indicate sources layer and min/maxzoom, but styles are still a bit of a handful to edit.

Comment from pnorman on 19 April 2025 at 23:07

The large uncommentable JSON file is the one absolute must-have for me. I’ve tried that with OSM Carto before we had YAML MML files. Never again.

Comment from Minh Nguyen on 20 April 2025 at 02:02

Other style projects like openstreetmap-americana have taken a different route. Their developers have written a program in JavaScript that generates the style.

The Americana project has certainly run with this idea, but it started with some very specific pain points, which code generation or preprocessing helps to mitigate but doesn’t solve completely:

  1. Some features like roads and railways need to be repeated, along with accessory layers like casing and dashes, in order to depict vertical order (i.e., layer=*).
  2. line-dash-array doesn’t support data-driven styling, so the very many kinds of railways need very many separate layers.
  3. Route shields are implemented as icons generated on demand per route for more fine-grained text layout. (These modifications don’t make it into the generated style JSON.)
  4. To conflate name=* with a dynamically selected name:*=* for our glossed labels, we needed to parse semicolon-delimited lists. This requires basic text processing functionality that was declined. Instead, we reimplemented string find and replace by generating a string tokenizer made of recursively nested slice and case expressions. 😱 In principle, some of this work could be offloaded to a tile server that returns personalized tiles, but that would reduce the tile server’s portability.

The more we can streamline these steps in static style JSON, the more portable Americana would be, especially to other platforms.

Having to repeat definitions instead of using a variable.

Something like a color or symbol definition might appear a dozen times in the style. If you want to change it, you need to make sure you got all the occurrences.

For what it’s worth, this MapLibre proposal would introduce the notion of global state, which effectively also allows for design-time consolidation as a side effect. If it goes in, it could simplify your preprocessor somewhat. It could also reduce the need for maintaining (or generating) separate styles based on color scheme.

Comment from tordans on 20 April 2025 at 04:43

The biggest pain point for me is writing but also merging filters. I have a layer definition and want to use that in certain conditions so I have to merge those filter conditions into the base layer. The same would be true for conditional color styles (etc) but I did not even try that.

Mapbox had a JavaScript library once to build filters but I think that is abandoned.

The workflow we use for tiles-geo.de is to design the layers separately in one of the GUIs, extract the layers via API and manage interaction aspects in React. We still need to solve the layer order (I know how bit did not do it yet).

Comment from Richard on 20 April 2025 at 15:44

You would expect me to say this, but glug works well for me. Invisibly compiling “proper” expressions into the verbose M?GL JSON format is a massive timesaver.

There are things I would slightly rework in it (and might do if I have a moment) - the pre-processed cascading was intended for the days before data-driven styling and is probably less crucial now, so could perhaps be dropped. And there are a couple of minor infelicities that are implementation details in a Ruby DSL (precedence concerns mean that it’s best to surround everything with brackets; colours can’t be easily inferred within a data-driven query expression so need a .to_hex_color postfix). But generally I’m pretty happy with it.

Comment from Minh Nguyen on 20 April 2025 at 16:01

the pre-processed cascading was intended for the days before data-driven styling and is probably less crucial now, so could perhaps be dropped.

Yes, in general, you’d want to minimize the number of layers in the stylesheet for performance reasons. For example, if you can draw roads using only one layer, then the style’s size and power usage decrease dramatically and rendering becomes noticeably smoother. The two obstacles are some properties’ lack of support for data-driven styling and some tilesets being overstratified into too many layers.

Comment from pnorman on 22 April 2025 at 23:37

The Americana project has certainly run with this idea, but it started with some very specific pain points, which code generation or preprocessing helps to mitigate but doesn’t solve completely

Yes. You’d likely have to go for writing javascript code regardless, so you were able to lean into it. Americana’s target is a webpage with a style, not a self-contained MBGL style json.


You would expect me to say this, but glug works well for me. Invisibly compiling “proper” expressions into the verbose M?GL JSON format is a massive timesaver

I need to try Glug more, but my problem is that it creates unnecessary layers. For example, the example simple Glug stylesheet produces six layers. A road with color variations should be one layer.

I know there is filter expression support, but it seems like it’s largely a grammatical change. That’s useful - arrays are a pain to read - but it’s not really introducing anything new.

With most of the examples showing many layer based method, it feels like it is written without consideration of expressions everywhere. I think it was written before they were in widespread use, so it’s not anyone’s fault. If I’m starting a new project the most important part is how does the tool handle complex expressions.

How would you write this icon-image in Glug? It’s based off of Street Spirit’s transit-points.yaml

{"icon-image": 
["match",
["get", "mode"],
"bus", ["case", ["get", "station"], "bus_station", "bus_stop"],
"tram", "bus_stop",
"subway", "subway",
"train", "train",
"airplane", "airplane",
"airport", "airport",
""
]
}

The biggest pain point for me is writing but also merging filters. I have a layer definition and want to use that in certain conditions so I have to merge those filter conditions into the base layer.

Do you mean combining two filters with all or any?

I’m aware of two pain points with this

  1. styles can become pretty deeply nested
  2. if you have filters of the form A && X, B && X, C && X you have to repeat x three times and make sure they stay in sync

Is it one of these, or something else?

Comment from Richard on 23 April 2025 at 09:30

glug was indeed originally written before data-driven styling was a thing, but it got expressions support in April 2022. So you would write that example as

icon_image match(mode,
   "bus", (case_when(station, "bus_station", "bus_stop")),
   "tram", "bus_stop",
   "subway", "subway",
   "train", "train",
   "airplane", "airplane",
   "airport", "airport",
   "" )

It’s pretty much word-for-word. The main differences are:

  • glug doesn’t need “get”, you can just use a bareword
  • case in MBGL is case_when in glug to avoid a Ruby keyword clash
  • property names are hyphenated in MBGL, underscored in glug

If you want to copy and paste this into irb to try it out:

require 'glug'
puts Glug::Stylesheet.new {
  source :default, type: "vector", tiles: ["http://localhost:8080/{z}/{x}/{y}.pbf"], minzoom: 0, maxzoom: 14, default: true
  layer(:pois, zoom: 10..13) {
    icon_image match(mode,
      "bus", (case_when(station, "bus_station", "bus_stop")),
      "tram", "bus_stop",
      "subway", "subway",
      "train", "train",
      "airplane", "airplane",
      "airport", "airport",
      "" )
  }
}.to_json

and you’ll see that it compiles to exactly the JSON you’ve used above.

Comment from maelito2000 on 28 April 2025 at 13:42

For https://cartes.app, we’re taking the Typescript approache.

Any style langage will always be more limited and more costly to implement than just writing JS or TS that outputs a style.

E.g. building a dark version of the style without duplicating most things is hard.

In the long run, we expect this TS script to be quite complex.

TS lets us modularise layers too, for instance elevation layers across different styles.

See https://github.com/cartesapp/cartes/blob/master/app/styles/france.ts

The style list is also starting to be quite complex. See the integration of IGN’s new styles as “sub styles” in https://github.com/cartesapp/cartes/pull/928/files

Log in to leave a comment