import { TomTomConfig } from '@tomtom-org/maps-sdk/core';
import {
getKnob,
knobEntryOf,
type StandardStyleID,
setKnob,
standardStyleIDs,
type TerrainHillshadeConfig,
type TerrainKnobId,
TerrainModule,
TomTomMap,
terrainKnobCatalogue,
} from '@tomtom-org/maps-sdk/map';
import './style.css';
import { API_KEY } from './config';
import { type KnobControl, knobControl } from './knobControls';
import { looks } from './looks';
import { initTogglePanel } from './togglePanel';
TomTomConfig.instance.put({ apiKey: API_KEY, language: 'en-GB' });
const ALWAYS: TerrainHillshadeConfig = { visible: true, maxZoom: 22 };
const sections: Record<string, [TerrainKnobId, string][]> = {
'#ui-shading': [
['hillshade.method', 'Method'],
['hillshade.intensity', 'Strength'],
['hillshade.maxZoom', 'Drawn up to zoom'],
],
'#ui-light': [
['hillshade.light.direction', 'Light direction (°)'],
['hillshade.light.altitude', 'Light altitude (°)'],
['hillshade.light.alignment', 'Light measured from'],
],
'#ui-colors': [
['hillshade.colors.shaded', 'Shadow'],
['hillshade.colors.lit', 'Highlight'],
['hillshade.colors.steep', 'Accent'],
],
};
const DAY_MS = 12000;
const sunAt = (progress: number) => ({
direction: Math.round(90 + 180 * progress),
altitude: Math.round(4 + 56 * Math.sin(Math.PI * progress)),
});
(async () => {
const [initialLook] = looks;
const map = new TomTomMap({ mapLibre: { container: 'sdk-map', center: [7.94, 46.58], zoom: 10.4 } });
const terrain = await TerrainModule.get(map, { hillshade: { ...ALWAYS, ...initialLook.hillshade } });
const controls = new Map<TerrainKnobId, KnobControl>();
const showValues = (ids: TerrainKnobId[] = Array.from(controls.keys())) => {
for (const id of ids) controls.get(id)?.show(getKnob(terrain, terrainKnobCatalogue, id));
};
for (const [selector, knobs] of Object.entries(sections)) {
const container = document.querySelector(selector) as HTMLElement;
for (const [id, label] of knobs) {
const control = knobControl(knobEntryOf(terrainKnobCatalogue, id), label, (value) =>
setKnob(terrain, terrainKnobCatalogue, id, value as never),
);
controls.set(id, control);
container.append(control.element);
}
}
showValues();
const lookSelector = document.querySelector('#ui-look') as HTMLSelectElement;
looks.forEach((look, index) => lookSelector.add(new Option(look.label, String(index))));
const applyLook = () => {
terrain.applyConfig({ hillshade: { ...ALWAYS, ...looks[Number(lookSelector.value)].hillshade } });
showValues();
};
lookSelector.addEventListener('change', applyLook);
document.querySelector('#ui-reset')?.addEventListener('click', applyLook);
const styleSelector = document.querySelector('#ui-mapStyle') as HTMLSelectElement;
standardStyleIDs.forEach((styleId) => styleSelector.add(new Option(styleId)));
styleSelector.addEventListener('change', () => map.setStyle(styleSelector.value as StandardStyleID));
document.querySelector('#ui-rotate')?.addEventListener('click', () => {
map.mapLibreMap.easeTo({ bearing: map.mapLibreMap.getBearing() + 90, duration: 2000 });
});
const sun = document.querySelector('#ui-sun') as HTMLInputElement;
let frame = 0;
const playDay = (startedAt: number) => (now: number) => {
const { direction, altitude } = sunAt(((now - startedAt) % DAY_MS) / DAY_MS);
setKnob(terrain, terrainKnobCatalogue, 'hillshade.light.direction', direction);
setKnob(terrain, terrainKnobCatalogue, 'hillshade.light.altitude', altitude);
showValues(['hillshade.light.direction', 'hillshade.light.altitude']);
frame = requestAnimationFrame(playDay(startedAt));
};
sun.addEventListener('change', () => {
cancelAnimationFrame(frame);
if (!sun.checked) return;
const method = getKnob(terrain, terrainKnobCatalogue, 'hillshade.method');
if (method === 'standard' || method === 'igor') {
setKnob(terrain, terrainKnobCatalogue, 'hillshade.method', 'multidirectional');
showValues(['hillshade.method']);
}
frame = requestAnimationFrame(playDay(performance.now()));
});
initTogglePanel();
})();