mapbox / mapbox-gl-js

Interactive, thoroughly customizable maps in the browser, powered by vector tiles and WebGL
https://docs.mapbox.com/mapbox-gl-js/
Other
11.14k stars 2.22k forks source link

Stylesheet restructure #356

Closed edenh closed 10 years ago

edenh commented 10 years ago

After working with the stylesheet structure a bit, I think there are a few improvements we can make without losing configuration flexibility.

This is my personal wishlist, which draws from many of the previously discussed ideas in https://github.com/mapbox/llmr/issues/244, https://github.com/mapbox/llmr/issues/279, and https://github.com/mapbox/llmr/issues/276. I'm not sure if this should be the content for a preprocessor, a replacement for the current stylesheet, or a step towards a styling language.

Here is an example to illustrate the idea:

{
    buckets: {
        'streets': {
            'poi': {
                layer: 'poi_label',
                point: true,
                children: {
                    'poi_park': {
                        filter: { 'maki': 'park' }
                        point: true,
                        text: {
                            textField: 'name',
                            path: 'curve',
                            font: 'Open Sans',
                            fontSize: 18
                        }
                    },
                    'poi_other': {
                        filter: { 'maki': ['airport', 'bus'] }
                        point: true,
                    }
                }
            },
            'road': {
                layer: 'road',
                line: true,
                'road_motorway': {
                    filter: { 'class': ['motorway', 'motorway_link'] },
                    line: {
                        cap: 'round',
                        join: 'bevel'
                    }
                },
                'road_main': {
                    filter: {
                        'class': 'main',
                        'oneway': 1
                    },
                    line: {
                        cap: 'round',
                        join: 'bevel'
                    }
                }
            },
            'building': {
                layer: 'building',
                fill: true
                stroke: {
                    cap: 'round',
                    join: 'round'
                }
            },
            'wetland': {
                layer: 'landuse_overlay',
                filter: { 'class': 'wetland' },
                fill: true
            }
        }
    },
    structure: [
        'wetland',
        'poi',
        'poi_park',
        'poi_other',
        { 
            composite: 'road',
            layers: [
                'road_motorway',
                'road_main'
            ] 
        },
        'building'
    ],
    layers: {
        'default': {
            'background': { fillColor: 'black' },
            'poi': {
                fillColor: 'white',
                pointRadius: 5
            },
            'poi_park': {
                pointImage: 'park-18',
                textColor: 'white',
                textTranslate: [40, 0]
            },
            'poi_other': {
                pointImage: 'star-stroked-18'
            },
            'road': {
                opacity: 0.9
            },
            'road_motorway': {
                fillColor: 'white',
                lineWidth: 3,
                strokeColor: 'green',
                strokeWidth: 1,
                transition: {
                    fillColor: { duration: 300 },
                    strokeColor: { 
                        duration: 300,
                        delay: 100
                    }
                }
            },
            'road_main': {
                fillColor: 'white',
                lineWidth: 1
            },
            'building': {
                fillColor: '#333333',
                fillOpacity: 0.8,
                strokeColor: '#555555',
                strokeWidth: 3
            }
        }
    }
}

Buckets

Bucket structure

filter: {
    'class': ['motorway', 'motorway_link'],
    'oneway': 1
}

Multiple types in buckets

Specifying multiple types in a bucket can alleviate a lot of the confusion around line casings and fill strokes, cut down on repeating bucket code, more closely resemble carto patterns, and most importantly, remove the need for multiple layers pointing at a single bucket.

'building': {
    layer: 'building',
    fill: true
    stroke: {
        cap: 'round',
        join: 'round'
    }
}
'poi_park': {
    filter: { 'maki': 'park' }
    point: true,
    text: {
        textField: 'name',
        path: 'curve',
        font: 'Open Sans',
        fontSize: 18
    }
}

Structure

By baking multiple types in bucket specification and solving for the road casing issue in the class (see layer updates below), there doesn't seem to be a need for multiple layers pointing to a bucket. We can move to a flatter array for the structure:

structure: [
    'wetland',
    'poi',
    'poi_park',
    'poi_other',
    { 
        composite: 'road',
        layers: [
            'road_motorway',
            'road_main'
        ] 
    },
    'building'
]

This requires layer names to match up with bucket names.

Layers

transition: {
    fillColor: { duration: 300 },
    strokeColor: { 
        duration: 300,
        delay: 100
    }
}

With these changes, I tried to cover all current functionality while simplifying the process conceptually.

cc @nickidlugash @ansis @mourner @kkaefer @tmcw

tmcw commented 10 years ago
"line-width": ["stops", {
  "0": 1.5,
  "6": 1.5,
  "8": 3,
  "22": 3
}]

This makes the x values stringified, and it'll be harder to change these in code - objects aren't sorted and are harder to modify in-place. Also, if we have that format, and we want to add another argument to stops which isn't another stop, like interpolate with a function of how to interpolate between stops, where do we put it?

I think a decent middle ground would be:

"line-width": {
  "fn": "stops",
  "stops": [
    [0, 1.5],
    [6, 1.5],
    [8, 3],
    [22, 3]
  ]
}
mourner commented 10 years ago

@tmcw makes sense, I'll try that.

kkaefer commented 10 years ago

I'd definitely favor non-string filters: if we can find a reasonably lightweight way of representing filters as JSON, it'll be easier to create filters graphically, and implementations that read the llmr format don't need a string tokenizer & parser

String tokenization/expression parsing is complicated. If we can avoid it, we should. Mapnik's use of spirit for the expression parsing adds a lot of complexity and compile time. Lastly, if we do things like layer = "foo" as expressions, it'll be hard to optimize this because they could be in nexted and/or expressions.

mourner commented 10 years ago

I agree that JSON-based queries are much easier to parse/manipulate, but so far haven't seen any examples of an easy to use and readable JSON-based query language — the Mongo one is pretty confusing, many people really don't like it.

One thing that makes gl filters a better fit for a string-based language is that it's limited to basic property comparisons, by design there's not enough complexity to make tokenization/expression parsing very hard.

tmcw commented 10 years ago

@mourner there is at least one person on the internet who hates each thing on the internet.

springmeyer commented 10 years ago

String tokenization/expression parsing is complicated. If we can avoid it, we should. Mapnik's use of spirit for the expression parsing adds a lot of complexity and compile time.

I agree on complexity. Wrt to compile time and c++ specifically I presume we could do better with a fresh start + spirit is oddball because it was implemented before c++11

tmcw commented 10 years ago

Okay, spitballing a simpler json encoding:

[{
    "and": [
        {"=": ["@class", "motorway_link"]},
        {"=": ["@oneway", "1"]}
    ]
}]

One lesson we learned the hard way in carto-land is that a comparison like class = motorway is extremely problematic: is class the column, or motorway? Is motorway a field, a string, or a variable?

Would strongly advise using sigils or something similar to make that clearer.

edenh commented 10 years ago

Can we just assume 'and' unless something else is specified? Something like:

"filter": {
    "layer": "waterway",
    "or": [
          {"type": "ditch"},
          {"type": "drain"}
    ]
}
tmcw commented 10 years ago

@edenh you will always have someone who has a field that is named "and" or "or"

edenh commented 10 years ago

@tmcw

"filter": {
    "layer": "waterway",
    "$or": [
          {"type": "ditch"},
          {"type": "drain"}
    ]
}
mourner commented 10 years ago

Another option we have for filters is keeping the string syntax and then parsing it into JSON (however ugly it may be) on the preprocessing step (with a JS script).

mourner commented 10 years ago

FYI there's now a script in the gl-style repo that converts user-friendly styles into renderer-friendly styles. It generates buckets and structure from layers (handling duplicates), detects bucket types and moves bucket-related styles into bucket config.

Sample output (OSM Bright): https://github.com/mapbox/gl-style/blob/master/test/styles/bright-raw.js

mourner commented 10 years ago

@edenh if we go with the JSON filter approach, I'd go further and assume "or" for any array values, so your example would turn to:

{
  "layer": "waterway",
  "type": ["ditch", "drain"]
}

This way we could simplify most of the filters while still allowing $or and $and.

mourner commented 10 years ago

Looking at how more complex JSON filters would look like, compared to @ansis's first example:

// string-based
"(maki == 'park' or maki == 'nope' and maki != 'airport') and maki =~ '.*'"

// JSON
{
    "$or": [{
        "maki": "park"
    }, {
        "maki": "nope",
        "$not": {"maki": "airport"}
    }],
    "$match": {"maki": ".*"}
}

The JSON one looks much more verbose. On the other hand, I don't think complex filters are a common use case — most will be trivial, like here: https://github.com/mapbox/gl-style/blob/master/test/styles/bright-v1.js

mourner commented 10 years ago

OK, I went ahead with simple JSON filters in migrations, we can change this later if needed: https://github.com/mapbox/gl-style/pull/10

yhahn commented 10 years ago

Capturing now in https://github.com/mapbox/mapbox-gl-style-spec