Back to all examples

Map styling playground

This example on GitHub

Restyle the map from panels built off the knob catalogues of each module drawing it: sizes, the label typeface, road markings, hillshade, POI and traffic colours, map presets, and whole layer groups

Map styling playground
import { TomTomConfig } from '@tomtom-org/maps-sdk/core';
import {
    applyMapPreset,
    BaseMapModule,
    baseMapKnobCatalogue,
    GeometriesModule,
    type MapPresetId,
    mapPresetCatalogue,
    PlacesModule,
    POIsModule,
    poisKnobCatalogue,
    RoutingModule,
    resetKnob,
    StandardStyleID,
    StylingFoundationsModule,
    standardStyleIDs,
    stylingFoundationsKnobCatalogue,
    TerrainModule,
    TomTomMap,
    TrafficFlowModule,
    TrafficIncidentsModule,
    terrainKnobCatalogue,
    trafficFlowKnobCatalogue,
    trafficIncidentsKnobCatalogue,
} from '@tomtom-org/maps-sdk/map';
import {
    calculateRoute,
    discoverOnePlace,
    discoverPlaces,
    geocodeOne,
    geometryData,
} from '@tomtom-org/maps-sdk/services';
import './style.css';
import { initBaseMapGroupsPanel } from './baseMapGroupsPanel';
import { API_KEY } from './config';
import { initLayerEdits } from './layerEdits';
import { collapsibleSection, initStylingPanel, type LookKnobSource } from './stylingPanel';
import { initTogglePanel } from './togglePanel';
import { initTypefacePanel } from './typefacePanel';

// (Set your own API key when working in your own environment)
TomTomConfig.instance.put({ apiKey: API_KEY, language: 'en-GB' });

const showRouteAndPlaces = async (routing: RoutingModule, places: PlacesModule, area: GeometriesModule) => {
    const waypoints = await Promise.all([
        discoverOnePlace('Twin Peaks, San Francisco'),
        discoverOnePlace('Mission Dolores Park, San Francisco'),
        discoverOnePlace('Alamo Square, San Francisco'),
    ]);
    const [routes, museums, district] = await Promise.all([
        calculateRoute({ locations: waypoints }),
        discoverPlaces({ filters: { poiCategories: ['MUSEUM'] }, geoBias: { position: [-122.43, 37.775] }, limit: 10 }),
        geocodeOne('Haight-Ashbury, San Francisco, CA').then((place) =>
            geometryData({ geometries: [place], zoom: 14 }),
        ),
    ]);
    await routing.showWaypoints(waypoints);
    await routing.showRoutes(routes);
    await places.show(museums);
    // The labels draw each geometry's `title`.
    await area.show({
        ...district,
        features: district.features.map((feature) => ({
            ...feature,
            properties: { ...feature.properties, title: 'Haight-Ashbury' },
        })),
    });
};

(async () => {
    // San Francisco: hills for the shading knobs, traffic for the congestion colours, and water,
    // ferries and rail for the base-map groups.
    const map = new TomTomMap({
        mapLibre: { container: 'sdk-map', center: [-122.4372, 37.7652], zoom: 12.6 },
    });
    const [trafficFlow, trafficIncidents, pois, baseMap] = await Promise.all([
        TrafficFlowModule.get(map, { visible: true }),
        TrafficIncidentsModule.get(map),
        POIsModule.get(map),
        BaseMapModule.get(map),
    ]);
    // The standard styles fade the shading out by zoom 13; these keep it at every zoom.
    const terrain = await TerrainModule.get(map, { hillshade: { visible: true, intensity: 0.5, maxZoom: 22 } });
    const styling = await StylingFoundationsModule.get(map, { labels: { sizeFactor: 1.2 } });
    // A route, a few places and a district, whose labels follow the map's typeface until given their own.
    const area = await GeometriesModule.create(map, { fill: { opacity: 0.1 }, lineLabel: {} });
    const routing = await RoutingModule.create(map);
    const places = await PlacesModule.create(map);
    void showRouteAndPlaces(routing, places, area);

    // How the map looks is held by the module drawing each part: the foundations by StylingFoundationsModule,
    // road markings by BaseMapModule, and the POI, traffic and hillshade looks by their own modules.
    const isTrafficLook = (id: string) => id.startsWith('colors.') || id === 'widthFactor' || id === 'opacityFactor';
    const lookSources: LookKnobSource[] = [
        // The typeface dropdown sets the map's typeface, with font files as well as the style's faces.
        { module: styling, catalogue: stylingFoundationsKnobCatalogue, isLook: (id) => id !== 'labels.typeface' },
        { module: baseMap, catalogue: baseMapKnobCatalogue, isLook: (id) => id.startsWith('roads.'), section: 'roads' },
        {
            module: pois,
            catalogue: poisKnobCatalogue,
            isLook: (id) => id !== 'visible' && !id.startsWith('filters.'),
            section: 'pois',
        },
        { module: trafficFlow, catalogue: trafficFlowKnobCatalogue, isLook: isTrafficLook, section: 'traffic flow' },
        {
            module: trafficIncidents,
            catalogue: trafficIncidentsKnobCatalogue,
            isLook: isTrafficLook,
            section: 'traffic incidents',
        },
        {
            module: terrain,
            catalogue: terrainKnobCatalogue,
            isLook: (id) => id.startsWith('hillshade.') && id !== 'hillshade.visible',
            section: 'hillshade',
        },
    ];
    // The typefaces are label settings, `labels.typeface` beside `labels.sizeFactor`, so they join that section.
    const panel = initStylingPanel(lookSources, { labels: document.querySelector('#ui-typeface') as HTMLElement });
    collapsibleSection('#ui-layerEdits', 'layer edits');
    initLayerEdits(styling);

    // Showing or hiding a whole layer group is BaseMapModule's, so its toggles are a panel of their own.
    const baseMapGroups = initBaseMapGroupsPanel(baseMap);
    const resetTypefaces = initTypefacePanel({ styling, routing, places, area });

    // Resets every look knob and typeface, keeping traffic and hillshade shown.
    document.querySelector('#ui-reset')?.addEventListener('click', () => {
        for (const { module, catalogue, isLook } of lookSources) {
            for (const { id } of catalogue) if (isLook?.(id) ?? true) resetKnob(module, catalogue, id);
        }
        baseMap.resetConfig();
        resetTypefaces();
    });

    // Map presets are named bundles of knob settings across the modules, listed by their catalogue too.
    const presetsSelector = document.querySelector('#ui-presets') as HTMLSelectElement;
    for (const preset of mapPresetCatalogue) {
        presetsSelector.add(new Option(preset.name, preset.id));
    }
    presetsSelector.addEventListener('change', () => {
        if (presetsSelector.value) applyMapPreset(map, presetsSelector.value as MapPresetId);
    });

    const stylesSelector = document.querySelector('#ui-mapStyles') as HTMLSelectElement;
    for (const id of standardStyleIDs) {
        stylesSelector.add(new Option(id));
    }
    stylesSelector.addEventListener('change', async (event) => {
        // Knob settings survive the switch: the module re-applies them on the new style.
        await map.setStyle((event.target as HTMLOptionElement).value as StandardStyleID);
        panel.render();
        baseMapGroups.render();
    });

    initTogglePanel();
})();

Every control restyles the map live: sizes, the label typeface, road markings, base-map groups, hillshade, and POI, traffic and map colours.

  • The panel is generated from the knob catalogues of the modules that draw the map: stylingFoundationsKnobCatalogue for the foundations (sizes, typeface, map colours, projection, sky and light), and the look knobs of BaseMapModule (road markings), POIsModule, the traffic modules and TerrainModule (hillshade). Each control starts from getKnob and sets its value with setKnob, so no layer or paint property is yours to find.
  • The Labels section sets the map’s typeface, from ten open-source families or the style’s own serif and italic faces. A route, museums and a district follow it, until you give each a typeface of its own.
  • The View light section sets the light on the 3D buildings. To see it, turn on buildings3D among the layer groups, zoom in past 15 and tilt the map with a right-drag.
  • Each section folds away, so open only the ones you’re tuning.
  • Map presets come from mapPresetCatalogue, and applyMapPreset sets each one across those modules.
  • Whole layer groups (water, railways, 3D buildings, road shields…) are BaseMapModule’s, so their toggles come from baseMapKnobCatalogue, through the same control builder.
  • The knobs survive a setStyle, and so do the two layer edits made through styling.layers.query. A reset keeps those edits because they are not settings. A query’s group names base-map layers or another module’s, here the traffic flow lines.

Related examples