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:
MapStyleSpecarray is validated (colors must match/#[0-9A-Fa-f]{6}/)MapStyleiterates each spec entry- Feature/element types are matched against the internal
LayerRegistry applyStylers()intransforms.jsmodifies Mapbox GL layer paint properties- A
style_updateevent 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:
- Clones the metadata on first encounter (so the original is never mutated)
- Applies each styler to the matching classes
- 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 classvisibility: "off": removes the class from all case branchessaturation,lightness,gamma, etc.: applied independently to each zoom stop via theColortransform- 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,
}