186 lines
5.7 KiB
JavaScript
186 lines
5.7 KiB
JavaScript
const MAP_SVG_SOURCE = 'assets/world_time_zones.svg';
|
|
const OFFSET_PRECISION = 30; // 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 = '<p>Die Zeitzonenkarte konnte nicht geladen werden.</p>';
|
|
});
|
|
}
|
|
|
|
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 candidate = findTimezoneByOffset(offsetMinutes);
|
|
|
|
if (candidate) {
|
|
const suggestion = buildSuggestion(candidate);
|
|
positionTooltip(event.clientX, event.clientY, `${suggestion.offsetLabel}\n${candidate.name}`);
|
|
} else {
|
|
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 candidate = findTimezoneByOffset(offsetMinutes);
|
|
let labelToShow;
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function buildSuggestion(entry) {
|
|
const now = new Date();
|
|
const offsetMinutes = getOffsetMinutes(now, entry.timeZone);
|
|
const hours = Math.trunc(offsetMinutes / 60);
|
|
const minutes = Math.abs(offsetMinutes % 60);
|
|
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
return {
|
|
offsetMinutes,
|
|
offsetLabel: `UTC${sign}${String(Math.abs(hours)).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`
|
|
};
|
|
}
|