From 2769cb8bf1e40e295d0c1d3cdba3c3e34c8eea35 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 13 Oct 2025 22:55:07 -0600 Subject: [PATCH] feat: add interactive map prototype --- assets/world_time_zones.svg | 1833 +++++++++++++++++++++++++++++++++++ index-map.html | 168 ++++ map-prototype.js | 160 +++ 3 files changed, 2161 insertions(+) create mode 100644 assets/world_time_zones.svg create mode 100644 index-map.html create mode 100644 map-prototype.js diff --git a/assets/world_time_zones.svg b/assets/world_time_zones.svg new file mode 100644 index 0000000..c73922d --- /dev/null +++ b/assets/world_time_zones.svg @@ -0,0 +1,1833 @@ + + +Time zones of the world + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/index-map.html b/index-map.html new file mode 100644 index 0000000..97fda33 --- /dev/null +++ b/index-map.html @@ -0,0 +1,168 @@ + + + + + + Weltze.it — Alle Zeitzonen im Blick + + + + + + + + + + + + + + +
+
+

Finde eine Stadt oder Zeitzone

+ + +
+
+ Tipp: Füge mehrere Orte hinzu, um Abweichungen direkt zu vergleichen. +
+
+ +
+
+

Interaktive Zeitzonen-Karte (Prototyp)

+

Tippe auf einen Punkt der Karte, um die nächstgelegene Zeitzone zu übernehmen. Die Karte verwendet das Wikimedia-Weltzeit-Zonen-SVG.

+
+
+
UTC±00:00
+
+
+

Quelle: Wikipedia – Time zone · Lizenz: CC-BY-SA 4.0

+
+ +
+
+

Ausgewählte Orte

+ +
+
+
Ort
+
Aktuelle Zeit
+
Abweichung zur lokalen Zeit
+
+
    +
    +
    + + + + + + + + diff --git a/map-prototype.js b/map-prototype.js new file mode 100644 index 0000000..9f0b006 --- /dev/null +++ b/map-prototype.js @@ -0,0 +1,160 @@ +const MAP_SVG_SOURCE = 'assets/world_time_zones.svg'; +const OFFSET_PRECISION = 15; // Minuten-Schrittweite + +const mapContainer = document.getElementById('map-container'); +const overlayEl = document.getElementById('map-overlay'); +const tooltipEl = document.getElementById('map-tooltip'); + +if (mapContainer) { + loadMap(); +} + +function loadMap() { + fetch(MAP_SVG_SOURCE) + .then((response) => response.text()) + .then((svgText) => { + mapContainer.insertAdjacentHTML('afterbegin', svgText); + const svg = mapContainer.querySelector('svg'); + if (!svg) { + throw new Error('SVG nicht gefunden.'); + } + + svg.setAttribute('viewBox', '0 0 3600 1500'); + svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); + svg.style.width = '100%'; + svg.style.height = '100%'; + + attachMapListeners(svg); + }) + .catch((error) => { + console.error('Karte konnte nicht geladen werden:', error); + mapContainer.innerHTML = '

    Die Zeitzonenkarte konnte nicht geladen werden.

    '; + }); +} + +function attachMapListeners(svg) { + const svgPoint = svg.createSVGPoint(); + + function getRelativePoint(event) { + svgPoint.x = event.clientX; + svgPoint.y = event.clientY; + const ctm = svg.getScreenCTM(); + if (!ctm) { + return { x: 0, y: 0 }; + } + const inverse = ctm.inverse(); + const transformed = svgPoint.matrixTransform(inverse); + return { x: transformed.x, y: transformed.y }; + } + + function handlePointerMove(event) { + const { x, y } = getRelativePoint(event); + const lon = normalizeLongitude(x, svg.viewBox.baseVal.width); + const lat = normalizeLatitude(y, svg.viewBox.baseVal.height); + const offsetMinutes = approximateOffsetMinutes(lon); + const { offsetLabel } = describeOffset(offsetMinutes); + + positionTooltip(event.clientX, event.clientY, `${offsetLabel}\n${formatCoordinate(lat, lon)}`); + } + + function handlePointerLeave() { + tooltipEl?.classList.remove('visible'); + } + + function handleClick(event) { + const { x } = getRelativePoint(event); + const lon = normalizeLongitude(x, svg.viewBox.baseVal.width); + const offsetMinutes = approximateOffsetMinutes(lon); + const { offsetLabel, offsetMinutesRounded } = describeOffset(offsetMinutes); + + showOverlay(offsetLabel); + + const candidate = findTimezoneByOffset(offsetMinutesRounded); + if (candidate && typeof addSelection === 'function') { + addSelection(candidate); + renderSelections(); + } + } + + svg.addEventListener('pointermove', handlePointerMove); + svg.addEventListener('pointerleave', handlePointerLeave); + svg.addEventListener('click', handleClick); +} + +function normalizeLongitude(x, width) { + const clampedWidth = width || 3600; + return (x / clampedWidth) * 360 - 180; +} + +function normalizeLatitude(y, height) { + const clampedHeight = height || 1500; + return 90 - (y / clampedHeight) * 180; +} + +function approximateOffsetMinutes(longitude) { + const offsetHours = Math.round((longitude / 15) * (60 / OFFSET_PRECISION)) * (OFFSET_PRECISION / 60); + const clampedHours = Math.min(14, Math.max(-12, offsetHours)); + return Math.round(clampedHours * 60); +} + +function describeOffset(offsetMinutes) { + const clamped = Math.round(offsetMinutes / OFFSET_PRECISION) * OFFSET_PRECISION; + const hours = Math.trunc(clamped / 60); + const minutes = Math.abs(clamped % 60); + const sign = clamped >= 0 ? '+' : '-'; + return { + offsetMinutesRounded: clamped, + offsetLabel: `UTC${sign}${String(Math.abs(hours)).padStart(2, '0')}:${String(minutes).padStart(2, '0')}` + }; +} + +function positionTooltip(clientX, clientY, text) { + if (!tooltipEl) { + return; + } + const containerRect = mapContainer.getBoundingClientRect(); + tooltipEl.textContent = text; + tooltipEl.style.left = `${clientX - containerRect.left}px`; + tooltipEl.style.top = `${clientY - containerRect.top}px`; + tooltipEl.classList.add('visible'); +} + +let overlayTimeoutId = null; +function showOverlay(text) { + if (!overlayEl) { + return; + } + overlayEl.textContent = text; + overlayEl.classList.add('visible'); + if (overlayTimeoutId) { + clearTimeout(overlayTimeoutId); + } + overlayTimeoutId = window.setTimeout(() => { + overlayEl?.classList.remove('visible'); + }, 1500); +} + +function formatCoordinate(lat, lon) { + const formatValue = (value, positiveSuffix, negativeSuffix) => { + const suffix = value >= 0 ? positiveSuffix : negativeSuffix; + return `${Math.abs(value).toFixed(1)}°${suffix}`; + }; + return `${formatValue(lat, 'N', 'S')} · ${formatValue(lon, 'E', 'W')}`; +} + +function findTimezoneByOffset(targetOffsetMinutes) { + const now = new Date(); + let bestCandidate = null; + let bestDelta = Infinity; + + SORTED_TIMEZONE_DATA.forEach((entry) => { + const offset = getOffsetMinutes(now, entry.timeZone); + const delta = Math.abs(offset - targetOffsetMinutes); + if (delta < bestDelta) { + bestDelta = delta; + bestCandidate = entry; + } + }); + + return bestCandidate; +}