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

mourner commented 10 years ago

I like these changes a lot. Quick question:

Line casings are solved using stroke: strokeWidth, strokeColor, strokeOpacity, etc.

How would you specify the z-order of the casings in the structure? How do you know from the current style example where to render them?

tmcw commented 10 years ago

/cc @sgillies as arbiter of JSON style.

Random thoughts:

edenh commented 10 years ago

@mourner it would probably always be an outer-stroke and be drawn on top of the line layer, but having more control there would also be nice: strokePosition: outer/inner/center

So if lineWidth is 5 and strokeWidth is 1, the 'casing' would be automatically offset with width of the line, instead of drawing a casing at 6 and a line on top at 5.

The fill/line attributes seem really close to SVG terminology, which is the same as simplestyle-spec but differ just slightly and I'm not sure they match anything else. Same with font - close to CSS, but just a little different. Naming sucks but I'd love to strive for similarity where it's cheap to do.

I agree. Going with something more similar to SVG has advantages.

Instead of point: true, could we not have type: 'point'?

We could, but when defining a line or text type, it would be nice to be able to nest the attributes specific to that type like line: { cap: 'round', join: 'bevel' }, without having to also include line: true. I could see how this may be confusing though.

Is structure what determines z-index and nothing else? Should it be order?

Order: :+1:

Hard to tell what the use of class means here

This is specific field in mb streets: https://a.tiles.mapbox.com/v3/mapbox.mapbox-streets-v4.json Not sure how to clarify/name this better.

mourner commented 10 years ago

it would probably always be an outer-stroke and be drawn on top of the line layer, but having more control there would also be nice: strokePosition: outer/inner/center

@edenh what I mean is that in many cases with several road types you first need to render all the casings of each type, and then render all the lines. You can't just assume casing-road-casing-road everywhere, as far as I understand. @ajashton right?

Is structure what determines z-index and nothing else? Should it be order?

@tmcw It also groups some layers into a named composite layer. That's not exactly "order", so I'm not sure.

Instead of point: true, could we not have type: 'point'?

We want to specify several types on one bucket. Do you suggest e.g. type: ['line', 'fill']?

tmcw commented 10 years ago

Do you suggest e.g. type: ['line', 'fill']?

Sure, seems reasonable.

edenh commented 10 years ago

You can't just assume casing-road-casing-road everywhere, as far as I understand.

That's true. I'm wondering if we could rely on strokes being applied to composited layers in this case.

mourner commented 10 years ago

I'm wondering if we could rely on strokes being applied to composited layers in this case.

Looks a bit fishy. Besides imagine 4 different road types where we want to render 2 casings, 2 roads, 2 casings, 2 roads. This is why I suggested postfixes here https://github.com/mapbox/llmr/issues/279 — covers cases like this and allows for fine-grained control.

edenh commented 10 years ago

Yep, https://github.com/mapbox/llmr/issues/279 is better.

ansis commented 10 years ago

I think we need to get rid of the concept of buckets for outside users. We should only have layers and styles. Layers handle z order and data selection. Styles handle everything about appearance.

@edenh's example would look something like this:

{
    "layers": [
        {
            "streets": { "source": "mapbox-streets" },
            "layers": [
            {
                "wetland": { "layer": "landuse" }
            },
            {
                "poi": { "layer": "poi_label" },
                "layers": [
                    { "poi_park": { "maki": "park" } },
                    { "poi_other": { "maki": ["airport", "bus"] } }
                ]
            },
            {
                "road": { "layer": "road" },
                "layers": [
                    { "road_motorway_casing": { "class": ["motorway", "motorway_link"] } },
                    { "road_main_casing": { "class": "main", "oneway": 1 } },
                    { "road_motorway": { "class": ["motorway", "motorway_link"] } },
                    { "road_main": { "class": "main", "oneway": 1 } }
                ]
            },
            {
                "building": { "layer": "building" }
            }
        ]}
    ],
    "styles": [
        { "": {
            "wetland": {
                "fill-color": "#00ff00"
            },
            "poi": {
                "fill-clor": "#ffffff",
                "point-radius": 5
            },
            "poi_park": {
                "point-image": "park-18",
                "text-color": "#ffffff",
                "text-field": "name",
                "text-path": "curve",
                "text-font": "Open Sans",
                "text-size": 18,
                "text-translate": [0, 40]
            },
            "poi_other": {
                "point-image": "start-stroked-18"
            },
            "road": {
                "opacity": 0,
                "line-cap": "round",
                "line-join": "bevel"
            },
            "road_motorway_casing": {
                "fill-color": "#000000",
                "line-width": 3
            },
            "road_main_casing": {
                "fill-color": "#000000",
                "line-width": 1
            },
            "road_motorway": {
                "fill-color": "#ffffff",
                "line-width": 3
            },
            "road_main": {
                "fill-color": "#ffffff",
                "line-width": 1
            },
            "building": {
                "fill-color": "#333333",
                "fill-opacity": 0.8,
                "line-color": "#555555",
                "line-width": 3
            }
        }},
        { "superwideroads": {
            "streets": {
                "transition-line-width": { "duration": 2000, "delay": 500 }
            },
            "road_motorway_casing": {
                "line-width": 10
            },
            "road_motorway": {
                "line-width": 8
            }
        }}
    ]
}

Layers

Each layer looks like this: { "layername": filterobject }. The filter object describes how to select the data for that layer. The key/values in the object would be matched against feature tags. There are two special keys: source and layer. layer is a concept from mapbox vector tiles. source is for llmr sources.

If a layer object has a layers property, it is a layer group. The layers property is an array of layers. The filters from a layergroup get automatically included in its child layers' filters, to make them more concise.

Styles

Styles describe appearance. I think the example explains things fairly well. In the example superwideroads is a class that could be added with map.addClass("superwideroads") to transition motorways to a wider width. Styles on layer groups are included in their children's styles. Some of the style properties used to be found on buckets.

abstracting away buckets

Exposing buckets has only one potential "advantage". Forcing more of the underlying details on the designer may help them create less duplicate buckets which may make a noticeable performance impact. But in most cases it should be possible to automatically dedupe and share buckets. In the example above, road_motorway and road_motorway_casing have the same filters and linecaps, which would be automatically detected.

This style would be processed into an expanded renderer-friendly version with buckets similar to what we have now.

I think this approach reduces the number of concepts someone needs to know to write a style, and is pretty concise.

edenh commented 10 years ago

I think we need to get rid of the concept of buckets for outside users. We should only have layers and styles. Layers handle z order and data selection. Styles handle everything about appearance.

:+1: looks great

edenh commented 10 years ago

@ansis how would bucket/layer types be defined?

With the proposed layout, I take it features would be automatically drawn based on the existence of style attributes, such as text-name for text?

mourner commented 10 years ago

@ansis I think this looks great. One minor thing is that we don't need to specify order of the classes, so we can just do "styles": {"myclass": { .... The idea to move all the geometry-specific styles out of bucket config is really great too. Should simplify the style a lot. I also like how you define the global "streets" layer and set a transition on it all at once.

ansis commented 10 years ago

I take it features would be automatically drawn based on the existence of style attributes, such as text-name for text?

Yeah, I think so. Each type of drawing would have its own prefix.

we don't need to specify order of the classes

If we created a sortofwideroads class that set motorways to a width of 7, and then did map.addClass('sortofwideroads').addClass('superwideroads') what would be the width of motorways? Would it be based on the order the classes were added? I think css precedence is based on the order rules are defined in the stylesheet.

define the global "streets" layer and set a transition on it all at once

There should probably also be a magic layer below everything that could be used for default styles across sources. #327

We might want to support everything mapnik does for filters (and, or, not, =, !=, <, <=, >, >=, regex), but I'm not sure what that would look like here.

{ "motorway": ["or", ["=", "class", "motorway"], ["=", "class", "motorway_link"]] }

or maybe?

{ "motorway": "[class]='motorway' or [class]='motorway_link'" }

not sure

kkaefer commented 10 years ago
ansis commented 10 years ago

Think about adding data/ids to the buffers we create from bucket information so that we can e.g. draw all poi icons in one go. Not sure how we could do this on a generic level. Think a about things like assigning colors/widths/icons/textures/translations based on a feature property. It gets complicated when you add zoom-level/latitude/longitude dependent functions into the mix, e.g. line width that depends on feature property, zoom level, latitude.

I think we should start with only supporting constant values. To implement functions that take properties as args we'd need to evaluate the functions in shaders. The complexity doesn't seem like its worth it.

Mapnik has a boost spirit expression parser for the filters; we could do something similar in JS/native (e.g. use Lua/LuaJIT, parse the expressions ourselves...?)

What approach to filters do you think is best?

To make it easier conceptually, we could also think about moving all of the geometry-related properties which are currently in buckets to the style, then parse the style and create the appropriate buckets. It's going to be hard to explain why line width is in the style and line-cap in the bucket.

@kkaefer What do you think of the example where buckets are removed entirely from the exposed style?

sgillies commented 10 years ago

@tmcw this is all very tastefully done :) I definitely like it better without the bucket concept.

nickidlugash commented 10 years ago

I think removing exposed buckets and moving all geometry-related properties to styles is a great idea – would make styling much simpler.

Question about layers: how would layer declaration work if you wanted to place different parts of an mb vector tile layer in different positions in the order? E.g. say you wanted to place poi labels for parks in one place, and poi labels for everything else further down in the order? Can different layers have the same layer filterobject to allow for this?

edenh commented 10 years ago

@nickidlugash I think using @ansis's example, the layers order would look something like this:

{"layers": [
    {
        "streets": {"source": "mapbox-streets"},
        "layers": [
            {
                "poi_labels1": {"layer": "poi_labels" },
                "layers": [
                    {"parks": {"maki": "park"}}
                ]
            },    
            {
                "buildings": {"layer":"buildings"}
            },
            {
                "poi_labels2": {"layer":"poi_labels"},
                "layers": [
                    {"other": {"[maki] != 'park'"}}
                ]
            }
        ]
    }
]}

This would place park poi's, then buildings, then all other poi's (still not sure about syntax for the != part). Do you see any holes here in comparison to working with carto?

Edit: just updated the syntax a bit

kkaefer commented 10 years ago

@edenh: not sure I understand that syntax; what does the "streets" key mean? How are we going to distinguish between the name poi_labels1 and layers (which is a magic key)? {"[maki] != 'park'"} is not valid JSON.

mourner commented 10 years ago

If we created a sortofwideroads class that set motorways to a width of 7, and then did map.addClass('sortofwideroads').addClass('superwideroads') what would be the width of motorways? Would it be based on the order the classes were added? I think css precedence is based on the order rules are defined in the stylesheet.

@ansis Yes, CSS precedence is based on definition order, but considering that our primary goal is to help people design maps, we should decide if making sure precedence logic fits CSS spec is more important than keeping things easy and simple, allowing easy manipulation of classes dynamically etc. I'm not sure.

not sure I understand that syntax; what does the "streets" key mean?

This is a good point, initially I found the syntax a bit confusing too. It's compact, but semantically confusing. Maybe it would be better to sacrifice terseness and introduce "id" or "name" key:

{
    "id": "streets",
    "source": "mapbox-streets",
    "layers": [{
        "id": "poi_labels1",
        "layer": "poi_labels",
        "layers": [{
            "id": "parks",
            "maki": "park"
        }]
    }, {
        "id": "buildings",
        "layer": "buildings"
    }, {
        "id": "poi_labels2",
        "layer": "poi_labels",
        "layers": [{
            "id": "other",
            "maki": '!park'
        }]
    }]
}

I also don't like the "layer"-"layers" ambiguity, because similar keys mean totally different things. Maybe "layer" should be "source_layer" or something.

ansis commented 10 years ago

not sure I understand that syntax; what does the "streets" key mean?

This is a good point, initially I found the syntax a bit confusing too. It's compact, but semantically confusing.

I agree. It feels really weird.

ansis commented 10 years ago

revised. Getting closer?

{
    "layers": [
        {
            "id": "streets",
            "filter": "source == 'mapbox-streets'",
            "layers": [
            {
                "id": "wetland",
                "filter": "source_layer == 'landuse'"
            },
            {
                "id": "poi",
                "filter": "source_layer == 'poi_label'",
                "layers": [
                    {
                        "id": "poi_park",
                        "filter": "maki == 'park'"
                    },
                    {
                        "id": "poi_other",
                        "filter": "maki == 'bus' or maki == 'airport'"
                    }
                ]
            },
            {
                "id": "road",
                "filter": "source_layer == 'road'",
                "layers": [
                    {
                        "id": "road_motorway_casing",
                        "filter": "class == 'motorway' or class == 'motorway_link'"
                    },
                    {
                        "id": "road_main_casing",
                        "filter": "class == 'main' and oneway == 1"
                    },
                    {
                        "id": "road_motorway",
                        "filter": "class == 'motorway' or class == 'motorway_link'"
                    },
                    {
                        "id": "road_main",
                        "filter": "class == 'main' and oneway == 1"
                    }
                ]
            },
            {
                "id": "building",
                "filter": "source_layer == 'building'"
            }
        ]}
    ],
    "styles": {
        "": {
            "wetland": {
                "fill-color": "#00ff00"
            },
            "poi": {
                "fill-clor": "#ffffff",
                "point-radius": 5
            },
            "poi_park": {
                "point-image": "park-18",
                "text-color": "#ffffff",
                "text-field": "name",
                "text-path": "curve",
                "text-font": "Open Sans",
                "text-size": 18,
                "text-translate": [0, 40]
            },
            "poi_other": {
                "point-image": "start-stroked-18"
            },
            "road": {
                "opacity": 0,
                "line-cap": "round",
                "line-join": "bevel"
            },
            "road_motorway_casing": {
                "fill-color": "#000000",
                "line-width": 3
            },
            "road_main_casing": {
                "fill-color": "#000000",
                "line-width": 1
            },
            "road_motorway": {
                "fill-color": "#ffffff",
                "line-width": 3
            },
            "road_main": {
                "fill-color": "#ffffff",
                "line-width": 1
            },
            "building": {
                "fill-color": "#333333",
                "fill-opacity": 0.8,
                "line-color": "#555555",
                "line-width": 3
            }
        },
        "superwideroads": {
            "streets": {
                "transition-line-width": { "duration": 2000, "delay": 500 }
            },
            "road_motorway_casing": {
                "line-width": 10
            },
            "road_motorway": {
                "line-width": 8
            }
        }
    }
}

Are these kind of filters clear and easy to write? The parsing side is fine: https://gist.github.com/ansis/764f11827c1a07d2e0a5

yhahn commented 10 years ago

I'm not totally familiar with the problem space here so feel free to ignore.


Is it wise to make parsing the style more complex which will need to be handled and maintained in both JS and C++? To contrast, a preprocessor will likely only need to be written in one language, whether JS or C++, because it only needs to be run by authoring application/service.

I would aim to make the current style representation fit the data structure that the renderer needs to work with best. Other considerations like authoring convenience, etc. can always be delegated to preprocessing/templating/just doing the work.

ansis commented 10 years ago

Other considerations like authoring convenience, etc. can always be delegated to preprocessing/templating/just doing the work.

I should have written this explicitly, but (I think) this issue is about a style that would be pre-processed into something very similar to the current style. The currently implemented style is fairly close to where it should be for the renderer, and doesn't need huge restructuring.

yhahn commented 10 years ago

:+1: cool, i take it you are preserving this internal representation at some stage of style processing where you can always serialize it again for transport/reloading if necessary.

mourner commented 10 years ago

@ansis I like it, looks very readable and consistent. I'd go further with the filter syntax — values on the right side are always string so quotes can be dropped, and for consistency, I'd either go full-on on word operators or symbol operators or allowing both (the symbol ones look more readable):

"(maki == 'park' or maki == 'nope' and maki != 'airport') and maki =~ '.*'" // current
"(maki is park or maki is nope and maki isnt airport) and maki matches .*"  // words
"(maki == park || maki == nope && maki != airport) && maki =~ .*"           // symbols
"(maki = park | maki = nope & maki != airport) & maki =~ .*"                // one char
mourner commented 10 years ago

BTW, just playing with formatting, here's how the same layer config from above can look:

{"layers": [
    {"id": "streets", "filter": "source = mapbox-streets", "layers": [
        {"id": "wetland", "filter": "source_layer = landuse"}, 
        {"id": "poi", "filter": "source_layer = poi_label", "layers": [
            {"id": "poi_park", "filter": "maki = park"}, 
            {"id": "poi_other", "filter": "maki = bus | maki = airport"}
        ]},
        {"id": "road", "filter": "source_layer = road", "layers": [
            {"id": "road_motorway_casing", "filter": "class = motorway | class = motorway_link"}, 
            {"id": "road_main_casing", "filter": "class = main & oneway = 1"}, 
            {"id": "road_motorway", "filter": "class = motorway | class = motorway_link"}, 
            {"id": "road_main", "filter": "class = main & oneway = 1"}
        ]}, 
        {"id": "building", "filter": "source_layer = building"}
    ]}
]}
mourner commented 10 years ago

Or, we could even go further and make it somewhat lispy :)

["streets", "source = mapbox-streets", [
    ["wetland", "layer = landuse"],
    ["poi", "layer = poi_label", [
        ["poi_park", "maki = park"], 
        ["poi_other", "maki = bus | maki = airport"]
    ]],
    ["road", "layer = road", [
        ["road_motorway_casing", "class = motorway | class = motorway_link"], 
        ["road_main_casing", "class = main & oneway = 1"], 
        ["road_motorway", "class = motorway | class = motorway_link"], 
        ["road_main", "class = main & oneway = 1"]
    ]], 
    ["building", "layer = building"]
]]

Each layer would be [<name>, <filter>, <layers?>].

mourner commented 10 years ago

It would also be nice to be able to collapse class = motorway | class = motorway_link into class = motorway | motorway_link as it's such a common occurrence.

ansis commented 10 years ago

values on the right side are always string so quotes can be dropped

It should also support numbers, I think.

I'd either go full-on on word operators or symbol operators

I like the third option with || and &&.

Or, we could even go further and make it somewhat lispy :)

I personally like this. Do you think its clear enough to others without the keys?

It would also be nice to be able to collapse class = motorway | class = motorway_link into class = motorway | motorway_link as it's such a common occurrence.

It is common, but it adds a whole bunch of complexity to save a handful of characters. And you can always do class =~ '(motorway|motorway_link)'.

mourner commented 10 years ago

It should also support numbers, I think.

Yes, but numbers in the data are still represented in string form, right? And coerced to numbers in JS if you use comparators like "<".

I personally like this. Do you think its clear enough to others without the keys?

I think it's very clear, since the syntax is so simple, while keys add clutter which makes the structure harder to grasp visually in this case.

It is common, but it adds a whole bunch of complexity to save a handful of characters.

Agreed.

mikemorris commented 10 years ago

@ansis @mourner FWIW, I find the lispy style readable but very unfamiliar from a JavaScript context.

mourner commented 10 years ago

Starting to fiddle with the new style language implementation.

@ansis @yhahn One thing I'm not quite sure about is having a shared style preprocessor for both native and web vs separate implementations. Looking at the current proposed spec, preprocessing should be pretty simple and straightforward, while a big chunk of the complexity will still be handled in the renderer implementation anyway (things like handling transitions, functions, etc., because this is done at runtime and not in a preprocessing step). Is shared implementation worth the hassle of complicating the style authoring workflow (introducing another step and corresponding dependency)?

mourner commented 10 years ago

Working on style migration scripts in this repo now: https://github.com/mapbox/gl-style

The basic idea is to have a repo that:

I think I'll finish the first migration script today and then work on the llmr part.

tmcw commented 10 years ago

:-1: for lispy style. The explicit, obvious style isn't wordy enough to justify that kind of shorthand syntax. Words are good.

mourner commented 10 years ago

The migration script https://github.com/mapbox/gl-style now fully converts existing styles into the new revised format (like @ansis posted above), try it. Now the next step is converting it to something renderer-friendly back (closer to the old style) and then updating llmr to reflect any changes.

In addition to everything discussed, I did two more changes for better consistency:

tmcw commented 10 years ago

['stops', {z: 1, val: 10}, ...] to ['stops', [1, 10], ...]

Why? We have the chance to be explicit and straightforward: why make syntax less self-explanatory?

edenh commented 10 years ago

I'm with @tmcw here. Also, building a gui around the explicit syntax is easier than the lispy alternative.

mourner commented 10 years ago

@tmcw @edenh now that we have a migration script, we can do small adjustments like this easily. I'm playing with the style right now.

In this case it just feels super inconsistent - like, points are [10, 10] and not {x: 10, y: 10}, other functions are array-like - ["exponential", 10, 20, 0.5, 5, 7] — not something like {fn: "exponential", zBase: 10, val: 20, slope: 0.5, min: 5, max: 7}, so why would transitions and stops fn be exceptions? Another point is that current stops format looks extremely verbose, especially when pretty-printed and not manually formatted — it adds a lot of clutter, e.g. half of the @edenh's osm-bright style lines is z-stops.

If we want the style to be more verbose, lets update all other functions then (e.g. currently linear / exponential functions look very confusing).

tmcw commented 10 years ago

points are [10, 10]

This is fine - [x, y] is what most people would guess here.

{fn: "exponential", zBase: 10, val: 20, slope: 0.5, min: 5, max: 7}

This is way better.

Basically: unless it's easy to guess what the parameters mean, don't remove the words.

samanpwbb commented 10 years ago

Lurking this thread, but wanted to jump in and say I like the way tom is thinking about this. {fn: "exponential", zBase: 10, val: 20, slope: 0.5, min: 5, max: 7} seems a lot better than ["exponential", 10, 20, 0.5, 5, 7]

nickidlugash commented 10 years ago

Also agree that it's helpful to see what the actual names of the parameters.

mourner commented 10 years ago

Right, lets revise the functions:

mourner commented 10 years ago

BTW, after changes above OSM Bright style grows from 760 to 1476 lines of code. :) I know it's a matter of formatting (we need to customize it for migration scripts) and actual size change is 26k -> 35.5k, but still.

incanus commented 10 years ago

I'm a fan of the expressivity at the expense of brevity, too. Named arguments are much clearer and easier to jump into editing the style.

Of course, expressivity at the expense of brevity is why I'm an Objective-C programmer...

ansis commented 10 years ago

Does anyone have any suggestions for expressing filters nicely with json objects instead of strings?

tmcw commented 10 years ago

There are a bunch of experimental JSON query languages, but the MongoDB query language is pretty deployed & tire-kicked

mourner commented 10 years ago

@ansis don't like the string filters?

tmcw 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

mourner commented 10 years ago

Regarding stops... If we want to be consistent with {fn: ..., params ...} convention, they would look something like this:

"line-width": {
  "fn": "stops",
  "stops": [
    {"z": 0, "val": 1.5}, 
    {"z": 6, "val": 1.5}, 
    {"z": 8, "val": 3}, 
    {"z": 22, "val": 3}
  ]
}

This looks too verbose to me — maybe this would be a better compromise of being obvious vs lightweight:

"line-width": ["stops", {
  "0": 1.5,
  "6": 1.5,
  "8": 3,
  "22": 3
}]

This way we could keep lightweight ["min", 15], etc., while still introducing named params in linear/exp functions:

"line-width": ["linear", {
  "z": 5,
  "val": 10,
  "slope": 2,
  "min": 1,
  "max": 20
}]