/* * Copyright (C) 2025 TomTom Navigation B.V. * Licensed under the Apache License, Version 2.0 */ import { type TomTomMap, TrafficFlowModule, type TrafficIncidentsModule, type StandardStyleID, } from "@tomtom-org/maps-sdk/map"; declare global { interface Window { __e2e_ml?: unknown; } } interface MapControlsOptions { position?: "top-left" | "top-right"; showTrafficToggle?: boolean; showIncidentsToggle?: boolean; showThemeToggle?: boolean; /** Pass existing TrafficFlowModule to control instead of creating new one */ externalTrafficModule?: TrafficFlowModule; /** Pass existing TrafficIncidentsModule to control instead of creating new one */ externalIncidentsModule?: TrafficIncidentsModule; /** Called after a theme change once the new style has loaded. Use this to re-add custom sources/layers. */ onThemeChange?: () => void; } const THEME_STYLES: Record<"light" | "dark", StandardStyleID> = { light: "streetLight", dark: "streetDark", }; /** * Adds theme, traffic flow and traffic incidents toggle buttons to the map. * Traffic starts hidden and the theme starts light. */ export async function createMapControls( map: TomTomMap, { position = "top-right", showTrafficToggle = true, showIncidentsToggle = false, showThemeToggle = true, externalTrafficModule, externalIncidentsModule, onThemeChange, }: MapControlsOptions = {} ): Promise { // Expose MapLibre map instance for E2E test automation (markers are canvas-rendered, not DOM) window.__e2e_ml = map.mapLibreMap; const container = document.createElement("div"); container.className = "map-controls"; container.setAttribute("data-position", position); if (showThemeToggle) { let theme: "light" | "dark" = "light"; const themeBtn = document.createElement("button"); themeBtn.className = "map-control-btn theme-btn"; themeBtn.title = "Toggle theme"; themeBtn.innerHTML = getSunIcon(); themeBtn.addEventListener("click", () => { theme = theme === "light" ? "dark" : "light"; map.setStyle(THEME_STYLES[theme]); themeBtn.innerHTML = theme === "light" ? getSunIcon() : getMoonIcon(); if (onThemeChange) map.mapLibreMap.once("style.load", () => onThemeChange()); }); container.appendChild(themeBtn); } if (showTrafficToggle) { const trafficModule = externalTrafficModule ?? (await TrafficFlowModule.get(map, { visible: false })); trafficModule.setVisible(false); container.appendChild( toggleButton("traffic-btn", "Toggle traffic flow", getTrafficIcon(), (visible) => trafficModule.setVisible(visible) ) ); } if (showIncidentsToggle && externalIncidentsModule) { const setIncidentsVisible = (visible: boolean) => { externalIncidentsModule.setVisible(visible); externalIncidentsModule.setIconsVisible(visible); }; setIncidentsVisible(false); container.appendChild( toggleButton( "incidents-btn", "Toggle traffic incidents", getIncidentsIcon(), setIncidentsVisible ) ); } map.mapLibreMap.getContainer().appendChild(container); injectStyles(); } /** A button that starts off and flips `active` and the layer visibility on each click. */ function toggleButton( className: string, title: string, icon: string, setVisible: (visible: boolean) => void ): HTMLButtonElement { let visible = false; const button = document.createElement("button"); button.className = `map-control-btn ${className}`; button.title = title; button.innerHTML = icon; button.addEventListener("click", () => { visible = !visible; setVisible(visible); button.classList.toggle("active", visible); }); return button; } function getSunIcon(): string { return ` `; } function getMoonIcon(): string { return ` `; } function getTrafficIcon(): string { return ` `; } function getIncidentsIcon(): string { return ` `; } let stylesInjected = false; function injectStyles(): void { if (stylesInjected) return; stylesInjected = true; const style = document.createElement("style"); style.textContent = ` .map-controls { position: absolute; z-index: 1000; display: flex; flex-direction: column; gap: 8px; padding: 10px; } .map-controls[data-position="top-right"] { top: 10px; right: 10px; } .map-controls[data-position="top-left"] { top: 10px; left: 10px; } .map-control-btn { display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border: none; border-radius: 8px; background: white; color: #333; cursor: pointer; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); transition: all 0.2s ease; } .map-control-btn:hover { background: #f5f5f5; transform: scale(1.05); } .map-control-btn:active { transform: scale(0.95); } .map-control-btn.active { background: #2196F3; color: white; } .map-control-btn.active:hover { background: #1976D2; } .map-control-btn svg { width: 20px; height: 20px; } `; document.head.appendChild(style); }