import { TomTomConfig } from '@tomtom-org/maps-sdk/core';
import {
resetKnob,
type StandardStyleID,
StylingFoundationsModule,
setKnob,
standardStyleIDs,
stylingFoundationsKnobCatalogue,
type TerrainKnob,
type TerrainKnobId,
TerrainModule,
TomTomMap,
terrainKnobCatalogue,
} from '@tomtom-org/maps-sdk/map';
import { MapEffects } from '@tomtom-org/maps-sdk-plugin-map-effects';
import './style.css';
import { API_KEY } from './config';
import { initTogglePanel } from './togglePanel';
TomTomConfig.instance.put({ apiKey: API_KEY, language: 'en-GB' });
type Viewpoint = {
label: string;
center: [number, number];
zoom: number;
bearing: number;
};
const viewpoints: Viewpoint[] = [
{ label: 'Matterhorn, Switzerland', center: [7.6586, 45.9763], zoom: 12.6, bearing: 178 },
{ label: 'Mont Blanc, France', center: [6.8652, 45.8326], zoom: 12.2, bearing: 320 },
{ label: 'Grand Canyon, United States', center: [-112.14, 36.1], zoom: 12.2, bearing: 0 },
{ label: 'Mount Fuji, Japan', center: [138.7274, 35.3606], zoom: 11.4, bearing: 172 },
];
const INITIAL_PITCH = 72;
const INITIAL_EXAGGERATION = 1.4;
const terrainKnob = (id: TerrainKnobId): TerrainKnob => {
const entry = terrainKnobCatalogue.find((candidate) => candidate.id === id);
if (!entry) throw new Error(`No terrain knob '${id}'`);
return entry;
};
const DAYLIGHT_SKY_COLOR = '#88c6fc';
const DAYLIGHT_HORIZON_COLOR = '#ffffff';
const focusLabel = (focus: number) => {
if (focus <= 0) return 'Nearest';
if (focus >= 1) return 'Farthest';
return focus < 0.5 ? 'Near' : 'Far';
};
(async () => {
const [initialViewpoint] = viewpoints;
const map = new TomTomMap({
style: 'streetSatellite',
mapLibre: {
container: 'sdk-map',
center: initialViewpoint.center,
zoom: initialViewpoint.zoom,
bearing: initialViewpoint.bearing,
pitch: INITIAL_PITCH,
maxPitch: 85,
canvasContextAttributes: { preserveDrawingBuffer: true },
},
});
const [styling, terrainModule] = await Promise.all([
StylingFoundationsModule.get(map, { view: { sky: { visible: true } } }),
TerrainModule.get(map, {
elevation: { visible: true, exaggeration: INITIAL_EXAGGERATION },
hillshade: { visible: true },
}),
]);
const effects = new MapEffects(map);
const viewpointSelector = document.querySelector('#ui-viewpoint') as HTMLSelectElement;
const styleSelector = document.querySelector('#ui-mapStyle') as HTMLSelectElement;
const terrain = document.querySelector('#ui-terrain') as HTMLInputElement;
const hillshade = document.querySelector('#ui-hillshade') as HTMLInputElement;
const sky = document.querySelector('#ui-sky') as HTMLInputElement;
const depthOfField = document.querySelector('#ui-depthOfField') as HTMLInputElement;
const exaggeration = document.querySelector('#ui-exaggeration') as HTMLInputElement;
const exaggerationValue = document.querySelector('#ui-exaggerationValue') as HTMLElement;
const pitch = document.querySelector('#ui-pitch') as HTMLInputElement;
const pitchValue = document.querySelector('#ui-pitchValue') as HTMLElement;
const followPointer = document.querySelector('#ui-followPointer') as HTMLInputElement;
const focus = document.querySelector('#ui-focus') as HTMLInputElement;
const focusValue = document.querySelector('#ui-focusValue') as HTMLElement;
const band = document.querySelector('#ui-band') as HTMLInputElement;
const bandValue = document.querySelector('#ui-bandValue') as HTMLElement;
const blur = document.querySelector('#ui-blur') as HTMLInputElement;
const blurValue = document.querySelector('#ui-blurValue') as HTMLElement;
const applyDepthOfField = () =>
effects.updateConfig({
depthOfField: {
intensity: depthOfField.checked ? Number(blur.value) : 0,
focus: Number(focus.value),
band: Number(band.value),
},
});
const applySkyOf = (styleId: StandardStyleID) => {
if (styleId === 'streetSatellite') {
setKnob(styling, stylingFoundationsKnobCatalogue, 'view.sky.color', DAYLIGHT_SKY_COLOR);
setKnob(styling, stylingFoundationsKnobCatalogue, 'view.sky.horizonColor', DAYLIGHT_HORIZON_COLOR);
return;
}
resetKnob(styling, stylingFoundationsKnobCatalogue, 'view.sky.color');
resetKnob(styling, stylingFoundationsKnobCatalogue, 'view.sky.horizonColor');
};
const { range, description } = terrainKnob('elevation.exaggeration');
if (range) {
exaggeration.min = String(range.min);
exaggeration.max = String(range.max);
exaggeration.step = String(range.step ?? 'any');
}
exaggeration.title = description;
exaggeration.value = String(INITIAL_EXAGGERATION);
exaggerationValue.textContent = exaggeration.value;
terrain.parentElement?.setAttribute('title', terrainKnob('elevation.visible').description);
hillshade.parentElement?.setAttribute('title', terrainKnob('hillshade.visible').description);
viewpoints.forEach((viewpoint, index) => viewpointSelector.add(new Option(viewpoint.label, String(index))));
standardStyleIDs.forEach((styleId) => styleSelector.add(new Option(styleId)));
styleSelector.value = 'streetSatellite';
applySkyOf('streetSatellite');
viewpointSelector.addEventListener('change', () => {
const { center, zoom, bearing } = viewpoints[Number(viewpointSelector.value)];
map.mapLibreMap.flyTo({ center, zoom, bearing, pitch: Number(pitch.value), duration: 4000 });
});
styleSelector.addEventListener('change', () => {
const styleId = styleSelector.value as StandardStyleID;
map.setStyle(styleId);
applySkyOf(styleId);
});
terrain.addEventListener('change', () => terrainModule.setElevationVisible(terrain.checked));
sky.addEventListener('change', () =>
setKnob(styling, stylingFoundationsKnobCatalogue, 'view.sky.visible', sky.checked),
);
hillshade.addEventListener('change', () => terrainModule.setHillshadeVisible(hillshade.checked));
depthOfField.addEventListener('change', applyDepthOfField);
const showFocus = () => {
focusValue.textContent = focusLabel(Number(focus.value));
applyDepthOfField();
};
focus.addEventListener('input', showFocus);
const focusAtPointer = ({ point }: { point: { y: number } }) => {
if (!followPointer.checked || !depthOfField.checked) return;
const height = Math.max(map.mapLibreMap.getCanvas().clientHeight, 1);
focus.value = String(Math.min(Math.max(1 - point.y / height, 0), 1));
showFocus();
};
map.mapLibreMap.on('mousemove', focusAtPointer);
map.mapLibreMap.on('click', focusAtPointer);
followPointer.addEventListener('change', () => {
focus.disabled = followPointer.checked;
});
band.addEventListener('input', () => {
bandValue.textContent = Number(band.value) === 0 ? 'One plane' : `${Math.round(Number(band.value) * 100)}%`;
applyDepthOfField();
});
blur.addEventListener('input', () => {
blurValue.textContent = `${blur.value}px`;
applyDepthOfField();
});
exaggeration.addEventListener('input', () => {
exaggerationValue.textContent = exaggeration.value;
setKnob(terrainModule, terrainKnobCatalogue, 'elevation.exaggeration', Number(exaggeration.value));
});
pitch.addEventListener('input', () => {
pitchValue.textContent = `${pitch.value}°`;
map.mapLibreMap.setPitch(Number(pitch.value));
});
initTogglePanel();
})();