From 9bb7ae72137303307f10b357b029d939e7322d97 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 14 Oct 2025 08:25:32 -0600 Subject: [PATCH] feat: prefer svg region classes for map selection --- map-prototype.js | 284 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 276 insertions(+), 8 deletions(-) diff --git a/map-prototype.js b/map-prototype.js index 3855009..40b2344 100644 --- a/map-prototype.js +++ b/map-prototype.js @@ -1,6 +1,223 @@ const MAP_SVG_SOURCE = 'assets/world_time_zones.svg'; const OFFSET_PRECISION = 30; // Minuten-Schrittweite +const CLASS_TO_TIMEZONE = { + // Europe + ad: 'Europe/Madrid', + al: 'Europe/Athens', + at: 'Europe/Vienna', + ba: 'Europe/Bucharest', + be: 'Europe/Amsterdam', + bg: 'Europe/Sofia', + ch: 'Europe/Zurich', + cz: 'Europe/Berlin', + de: 'Europe/Berlin', + dk: 'Europe/Copenhagen', + ee: 'Europe/Helsinki', + es: 'Europe/Madrid', + fi: 'Europe/Helsinki', + fr: 'Europe/Paris', + gb: 'Europe/London', + gr: 'Europe/Athens', + hr: 'Europe/Rome', + hu: 'Europe/Vienna', + ie: 'Europe/Dublin', + is: 'Atlantic/Reykjavik', + it: 'Europe/Rome', + li: 'Europe/Zurich', + lt: 'Europe/Riga', + lu: 'Europe/Amsterdam', + lv: 'Europe/Riga', + mt: 'Europe/Rome', + nl: 'Europe/Amsterdam', + no: 'Europe/Oslo', + pl: 'Europe/Warsaw', + pt0: 'Europe/Lisbon', + pt: 'Europe/Lisbon', + 'pt-1': 'Europe/Lisbon', + ro: 'Europe/Bucharest', + rs: 'Europe/Athens', + se: 'Europe/Stockholm', + si: 'Europe/Vienna', + sk: 'Europe/Vienna', + ua: 'Europe/Kyiv', + ua2: 'Europe/Kyiv', + ua3: 'Europe/Kyiv', + va: 'Europe/Rome', + + // Americas + ar: 'America/Argentina/Buenos_Aires', + bb: 'America/Barbados', + bo: 'America/La_Paz', + 'br-4': 'America/Manaus', + 'br-3': 'America/Sao_Paulo', + 'br-2': 'America/Sao_Paulo', + 'ca-4': 'America/Halifax', + 'ca-5': 'America/Toronto', + 'ca-6': 'America/Winnipeg', + 'ca-7': 'America/Edmonton', + 'ca-8': 'America/Vancouver', + 'ca-330': 'America/St_Johns', + 'ca-4n': 'America/Halifax', + 'ca-5n': 'America/Toronto', + 'ca-6n': 'America/Winnipeg', + 'ca-7n': 'America/Edmonton', + 'ca-8n': 'America/Vancouver', + 'cl-3': 'America/Santiago', + 'cl-4': 'America/Santiago', + co: 'America/Bogota', + cu: 'America/Havana', + 'ec-5': 'America/Guayaquil', + 'ec-6': 'America/Guayaquil', + gt: 'America/Guatemala', + hn: 'America/Tegucigalpa', + jm: 'America/Jamaica', + 'mx-5': 'America/Mexico_City', + 'mx-6': 'America/Mexico_City', + 'mx-7': 'America/Ciudad_Juarez', + 'mx-8': 'America/Tijuana', + 'mx-5b': 'America/Mexico_City', + 'mx-6b': 'America/Mexico_City', + 'mx-7b': 'America/Ciudad_Juarez', + 'mx-8b': 'America/Tijuana', + ni: 'America/Managua', + pa: 'America/Panama', + pe: 'America/Lima', + pr: 'America/Puerto_Rico', + 'us-5': 'America/New_York', + 'us-6': 'America/Chicago', + 'us-7': 'America/Denver', + 'us-8': 'America/Los_Angeles', + 'us-9': 'America/Anchorage', + 'us-10': 'Pacific/Honolulu', + 'us-10n': 'Pacific/Honolulu', + 'us-9n': 'America/Anchorage', + 'us-8n': 'America/Los_Angeles', + 'us-7n': 'America/Denver', + 'us-6n': 'America/Chicago', + 'us-5n': 'America/New_York', + uy: 'America/Montevideo', + ve: 'America/Caracas', + + // Africa + ao: 'Africa/Luanda', + bf: 'Africa/Abidjan', + bi: 'Africa/Maputo', + bj: 'Africa/Lagos', + bw: 'Africa/Johannesburg', + cd1: 'Africa/Kinshasa', + cd2: 'Africa/Lubumbashi', + cf: 'Africa/Bangui', + cg: 'Africa/Lagos', + ci: 'Africa/Abidjan', + cm: 'Africa/Douala', + dz: 'Africa/Algiers', + eg: 'Africa/Cairo', + et: 'Africa/Addis_Ababa', + gh: 'Africa/Accra', + gm: 'Africa/Abidjan', + gn: 'Africa/Conakry', + gq: 'Africa/Lagos', + ke: 'Africa/Nairobi', + lr: 'Africa/Monrovia', + ls: 'Africa/Johannesburg', + ly: 'Africa/Tripoli', + ma: 'Africa/Casablanca', + mg: 'Africa/Nairobi', + ml: 'Africa/Bamako', + mr: 'Africa/Nouakchott', + mu: 'Africa/Nairobi', + mw: 'Africa/Maputo', + mz: 'Africa/Maputo', + na: 'Africa/Windhoek', + ne: 'Africa/Lagos', + ng: 'Africa/Lagos', + rw: 'Africa/Maputo', + sd: 'Africa/Khartoum', + sl: 'Africa/Abidjan', + sn: 'Africa/Dakar', + tn: 'Africa/Tunis', + tz: 'Africa/Nairobi', + ug: 'Africa/Nairobi', + za2: 'Africa/Johannesburg', + za3: 'Africa/Johannesburg', + zm: 'Africa/Maputo', + zw: 'Africa/Maputo', + + // Middle East & Asia + ae: 'Asia/Dubai', + af: 'Asia/Kabul', + am: 'Asia/Yerevan', + az: 'Asia/Baku', + bh: 'Asia/Bahrain', + cn: 'Asia/Shanghai', + ge: 'Asia/Tbilisi', + il: 'Asia/Jerusalem', + in: 'Asia/Kolkata', + iq: 'Asia/Baghdad', + ir: 'Asia/Tehran', + jo: 'Asia/Amman', + jp: 'Asia/Tokyo', + kg: 'Asia/Bishkek', + kz: 'Asia/Almaty', + kw: 'Asia/Kuwait', + lb: 'Asia/Beirut', + lk: 'Asia/Colombo', + mm: 'Asia/Yangon', + mn7: 'Asia/Irkutsk', + mn8: 'Asia/Irkutsk', + mv: 'Asia/Dubai', + my: 'Asia/Kuala_Lumpur', + np: 'Asia/Kathmandu', + om: 'Asia/Dubai', + ph: 'Asia/Manila', + pk: 'Asia/Karachi', + qa: 'Asia/Qatar', + ru2: 'Europe/Moscow', + ru3: 'Europe/Moscow', + ru4: 'Europe/Samara', + ru5: 'Asia/Yekaterinburg', + ru6: 'Asia/Omsk', + ru7: 'Asia/Krasnoyarsk', + ru8: 'Asia/Irkutsk', + ru9: 'Asia/Yakutsk', + ru10: 'Asia/Vladivostok', + ru11: 'Asia/Magadan', + ru12: 'Asia/Kamchatka', + sa: 'Asia/Riyadh', + sg: 'Asia/Singapore', + sy: 'Asia/Damascus', + th: 'Asia/Bangkok', + tj: 'Asia/Dushanbe', + tm: 'Asia/Ashgabat', + tr: 'Europe/Istanbul', + uz: 'Asia/Tashkent', + vn: 'Asia/Ho_Chi_Minh', + ye: 'Asia/Aden', + + // Oceania + au8: 'Australia/Perth', + au10: 'Australia/Sydney', + au10n: 'Australia/Sydney', + au1030: 'Australia/Sydney', + au930: 'Australia/Perth', + au930n: 'Australia/Perth', + au845: 'Australia/Perth', + fj: 'Pacific/Fiji', + gu: 'Pacific/Port_Moresby', + nc: 'Pacific/Noumea', + nz12: 'Pacific/Auckland', + nz1245: 'Pacific/Auckland', + 'pf-10': 'Pacific/Honolulu', + 'pf-9': 'Pacific/Honolulu', + 'pf-930': 'Pacific/Honolulu', + to: 'Pacific/Auckland', + ws: 'Pacific/Auckland', + tk: 'Pacific/Auckland' +}; + +const timezoneById = new Map(TIMEZONE_DATA.map((entry) => [entry.timeZone, entry])); + const mapContainer = document.getElementById('map-container'); const overlayEl = document.getElementById('map-overlay'); const tooltipEl = document.getElementById('map-tooltip'); @@ -9,6 +226,39 @@ if (mapContainer) { loadMap(); } +function getEntryForTimeZone(timeZoneId) { + if (!timeZoneId) { + return null; + } + if (timezoneById.has(timeZoneId)) { + return timezoneById.get(timeZoneId); + } + return SORTED_TIMEZONE_DATA.find((entry) => entry.timeZone === timeZoneId) || null; +} + +function resolveEntryFromElement(element) { + if (!element) { + return null; + } + const candidate = element.closest('[class]'); + if (!candidate) { + return null; + } + const classAttr = candidate.getAttribute('class'); + if (!classAttr) { + return null; + } + const classes = classAttr.split(/\s+/); + for (const cls of classes) { + const timeZoneId = CLASS_TO_TIMEZONE[cls]; + const entry = getEntryForTimeZone(timeZoneId); + if (entry) { + return entry; + } + } + return null; +} + function loadMap() { fetch(MAP_SVG_SOURCE) .then((response) => response.text()) @@ -51,6 +301,13 @@ function attachMapListeners(svg) { const { x, y } = getRelativePoint(event); const lon = normalizeLongitude(x, svg.viewBox.baseVal.width); const lat = normalizeLatitude(y, svg.viewBox.baseVal.height); + const classEntry = resolveEntryFromElement(event.target); + if (classEntry) { + const suggestion = buildSuggestion(classEntry); + positionTooltip(event.clientX, event.clientY, `${suggestion.offsetLabel}\n${classEntry.name}`); + return; + } + const offsetMinutes = approximateOffsetMinutes(lon); const candidate = findTimezoneByOffset(offsetMinutes); @@ -70,20 +327,31 @@ function attachMapListeners(svg) { function handleClick(event) { const { x } = getRelativePoint(event); const lon = normalizeLongitude(x, svg.viewBox.baseVal.width); - const offsetMinutes = approximateOffsetMinutes(lon); - const candidate = findTimezoneByOffset(offsetMinutes); + const classEntry = resolveEntryFromElement(event.target); let labelToShow; - if (candidate) { - const suggestion = buildSuggestion(candidate); - labelToShow = `${suggestion.offsetLabel}`; + if (classEntry) { + const suggestion = buildSuggestion(classEntry); + labelToShow = suggestion.offsetLabel; if (typeof addSelection === 'function') { - addSelection(candidate); + addSelection(classEntry); renderSelections(); } } else { - const { offsetLabel } = describeOffset(offsetMinutes); - labelToShow = offsetLabel; + const offsetMinutes = approximateOffsetMinutes(lon); + const candidate = findTimezoneByOffset(offsetMinutes); + + if (candidate) { + const suggestion = buildSuggestion(candidate); + labelToShow = suggestion.offsetLabel; + if (typeof addSelection === 'function') { + addSelection(candidate); + renderSelections(); + } + } else { + const { offsetLabel } = describeOffset(offsetMinutes); + labelToShow = offsetLabel; + } } showOverlay(labelToShow);