Skip to content

Map Styling

MapStyleSpec

Custom map styling is applied via an array of MapStyleSpec objects passed to the Map constructor or map.setMapStyle():

var map = new woosmap.map.Map(document.getElementById("map"), {
    styles: [
        {
            featureType: "road",
            elementType: "geometry",
            stylers: [{color: "#ff0000"}],
        },
        {
            featureType: "water",
            stylers: [{visibility: "off"}],
        },
    ],
});

Or applied after initialization:

map.setMapStyle([
    {
        featureType: "poi",
        stylers: [{visibility: "off"}],
    },
]);

MapStyleSpec Type

{
    featureType?: string,   // Feature group to target (default: "all")
    elementType?: string,   // Element group to target (default: "all")
    stylers: MapStyler[],   // Array of style rules
}

Styler Properties

Property Type Description
color string Hex color (#RRGGBB)
visibility string "on", "off", or "simplified"
hue string Hex color — extracts hue component only
saturation number Saturation adjustment
lightness number Lightness adjustment
gamma number Gamma correction
invert_lightness boolean Invert lightness values
weight number Stroke weight

Styling Pipeline

When styles are applied:

  1. MapStyleSpec array is validated (colors must match /#[0-9A-Fa-f]{6}/)
  2. MapStyle iterates each spec entry
  3. Feature/element types are matched against the internal LayerRegistry
  4. applyStylers() in transforms.js modifies Mapbox GL layer paint properties
  5. A style_update event fires on completion

Internal modules:

File Purpose
map-style/map-style.js MapStyleSpec / MapStyler types, MapStyle class
map-style/transforms.js applyStylers() — color/weight transform engine
map-style/color.js Color manipulation utilities
map-style/layer-registry.js Maps feature/element types to Mapbox GL layers
map-style/multi-class-layer-style-accumulator.js Multiclass layer styling (POI symbols and fill layers)
map-style/tree/ ColorsTree, WeightTree, MultilayerTree, ClassTree for hierarchical style resolution

Multiclass Layers

Some layers pack multiple feature classes into a single Mapbox GL layer. Instead of one layer per class, the style source embeds class metadata in the layer's metadata property. The MultiClassLayerStyleAccumulator reads that metadata and builds Mapbox GL expressions that branch on the feature's class property at render time.

This keeps the layer count low while still allowing per-class color, visibility, and transform control through the standard MapStyleSpec API.

How it works

When transforms.js encounters a layer that LayerRegistry.isMultiClassLayer() recognizes, it delegates to the accumulator instead of applying stylers directly. The accumulator:

  1. Clones the metadata on first encounter (so the original is never mutated)
  2. Applies each styler to the matching classes
  3. On applyDiff(), builds the appropriate Mapbox GL expressions and sets them on the map

POI multiclass (poiMetadata)

Used for symbol layers where multiple POI categories share a single layer. Each class carries flat color strings for icon and text properties.

Metadata shape

{
    metadata: {
        featureType: "poi",
        poiBaseFilter: ["<=", ["get", "rank"], 10],
        poiMetadata: {
            "restaurant": {
                filter: ["==", ["get", "class"], "restaurant"],
                symbol_color: "#fff",
                symbol_halo_color: "orange",
                text_color: "orange",
                icon: "restaurant",
                visible: true,
                visibility: {},
                colors: {}
            },
            "park": {
                filter: ["==", ["get", "class"], "park"],
                // ...
            }
        }
    }
}

Element type tree

labels
├── icon
│   ├── stroke  → symbol
│   └── fill    → symbol_halo
└── text
    ├── stroke  → text_halo
    └── fill    → text

Targeting elementType: "labels.icon" affects symbol and symbol_halo. Targeting "all" affects all four properties.

Generated expression

The accumulator produces flat case expressions for each paint property:

// icon-halo-color
["case",
    ["==", ["get", "class"], "restaurant"], "orange",
    ["==", ["get", "class"], "park"], "green",
    "transparent"]

Visibility is handled by omitting hidden classes from the case branches and combining visible filters:

["all", poiBaseFilter, ["any", ...visible_class_filters]]

Fill multiclass (classMetadata)

Used for fill layers (like landcover) where each class has zoom-interpolated colors. Instead of flat color strings, each class carries an array of [zoom, color] stops.

Metadata shape

{
    metadata: {
        featureType: "landcover",
        classBaseFilter: ["has", "class"],
        classMetadata: {
            "wood": {
                filter: ["==", ["get", "class"], "wood"],
                colors: {
                    fill_color: [[0, "#f7f7f7"], [14, "#e0ece0"]],
                    fill_outline_color: [[0, "#cccccc"], [14, "#b0c4b0"]]
                },
                visibility: {},
                visible: true
            },
            "grass": {
                filter: ["==", ["get", "class"], "grass"],
                // ...
            }
        }
    }
}

Element type tree

geometry
├── fill    → fill_color
└── stroke  → fill_outline_color

Targeting elementType: "geometry.fill" only affects fill_color. Targeting "all" affects both.

Generated expression

The accumulator produces an interpolate-at-root expression with case sub-expressions at each zoom level:

// fill-color
["interpolate", ["linear"], ["zoom"],
    0,  ["case",
            ["==", ["get", "class"], "wood"], "#f7f7f7",
            ["==", ["get", "class"], "grass"], "#f0f0f0",
            "transparent"],
    14, ["case",
            ["==", ["get", "class"], "wood"], "#e0ece0",
            ["==", ["get", "class"], "grass"], "#d4ecd4",
            "transparent"]]

The interpolate must stay at the root — flipping it (case at root, interpolate per branch) is not valid because Mapbox GL cannot interpolate inside case branches.

Styler behavior

  • color: replaces the color at every zoom stop for the targeted class
  • visibility: "off": removes the class from all case branches
  • saturation, lightness, gamma, etc.: applied independently to each zoom stop via the Color transform
  • Untouched classes: keep their original color strings as-is

Targeting sub-classes

Both systems support targeting individual classes via dotted featureType paths:

// Target all POI classes
{ featureType: "poi", stylers: [{visibility: "off"}] }

// Target only the restaurant class
{ featureType: "poi.restaurant", stylers: [{color: "red"}] }

// Target all landcover classes
{ featureType: "landcover", stylers: [{saturation: -50}] }

// Target only the wood class
{ featureType: "landcover.wood", stylers: [{visibility: "off"}] }

Types

ZoomColorStop = [number, string]
ClassColors   = { [string]: ZoomColorStop[] }

ClassMetadata = {
    filter: FilterSpecification,
    colors: ClassColors,
    visibility: { [string]: boolean },
    visible: boolean
}

Metadata = {
    featureType?: string,
    elementTypes?: string[],

    // POI multiclass
    poiMetadata?: { [string]: POIMetadata },
    poiBaseFilter?: FilterSpecification,

    // Fill multiclass
    classMetadata?: { [string]: ClassMetadata },
    classBaseFilter?: FilterSpecification,
}