Punkte -> SVG

This commit is contained in:
Simon Martens
2025-09-29 13:52:23 +02:00
parent 64e4cdde14
commit 0d7886a664
3 changed files with 510 additions and 358 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -436,6 +436,39 @@ export class PlacesMap extends HTMLElement {
return; return;
} }
// Create one large SVG overlay that matches the map size
const mapOverlaySvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
mapOverlaySvg.setAttribute("class", "absolute top-0 left-0 w-full h-full");
mapOverlaySvg.style.pointerEvents = "none"; // Let map interactions pass through
mapOverlaySvg.style.overflow = "visible";
mapOverlaySvg.setAttribute("viewBox", "0 0 100 100");
mapOverlaySvg.setAttribute("preserveAspectRatio", "none");
// Create defs element for gradients
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
// Create radial gradient for red dots
const redGradient = document.createElementNS("http://www.w3.org/2000/svg", "radialGradient");
redGradient.setAttribute("id", "redDotGradient");
redGradient.setAttribute("cx", "30%");
redGradient.setAttribute("cy", "30%");
redGradient.setAttribute("r", "70%");
const stopLight = document.createElementNS("http://www.w3.org/2000/svg", "stop");
stopLight.setAttribute("offset", "0%");
stopLight.setAttribute("stop-color", "#f56565"); // Slightly lighter red
const stopDark = document.createElementNS("http://www.w3.org/2000/svg", "stop");
stopDark.setAttribute("offset", "100%");
stopDark.setAttribute("stop-color", "#e53e3e"); // Slightly darker red
redGradient.appendChild(stopLight);
redGradient.appendChild(stopDark);
defs.appendChild(redGradient);
mapOverlaySvg.appendChild(defs);
this.pointsContainer.appendChild(mapOverlaySvg);
// Map extent constants // Map extent constants
const MAP_EXTENT_METERS = { xmin: 2555000, ymin: 1350000, xmax: 7405000, ymax: 5500000 }; const MAP_EXTENT_METERS = { xmin: 2555000, ymin: 1350000, xmax: 7405000, ymax: 5500000 };
const PROJECTION_CENTER = { lon: 10, lat: 52 }; const PROJECTION_CENTER = { lon: 10, lat: 52 };
@@ -470,7 +503,7 @@ export class PlacesMap extends HTMLElement {
return { x: xPercent, y: yPercent }; return { x: xPercent, y: yPercent };
}; };
// Create points and track positions // Create circles and track positions
const pointPositions = []; const pointPositions = [];
this.places.forEach((place) => { this.places.forEach((place) => {
if (place.lat && place.lng) { if (place.lat && place.lng) {
@@ -482,25 +515,49 @@ export class PlacesMap extends HTMLElement {
if (position.x >= 0 && position.x <= 100 && position.y >= 0 && position.y <= 100) { if (position.x >= 0 && position.x <= 100 && position.y >= 0 && position.y <= 100) {
pointPositions.push(position); pointPositions.push(position);
const point = document.createElement("div"); // Create circle element directly in the overlay SVG
point.className = "map-point hidden"; const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
point.style.left = `${position.x}%`; circle.setAttribute("cx", position.x.toString());
point.style.top = `${position.y}%`; circle.setAttribute("cy", position.y.toString());
point.style.transformOrigin = "center"; circle.setAttribute("r", "0.4"); // Small radius in % units
circle.setAttribute("fill", "white");
circle.setAttribute("opacity", "0.7");
circle.setAttribute("filter", "drop-shadow(0 0.05 0.08 rgba(0,0,0,0.15))");
circle.style.cursor = "pointer";
circle.style.pointerEvents = "all";
circle.style.transition = "r 0.3s ease, fill 0.3s ease, stroke 0.3s ease, opacity 0.3s ease";
// Add hover effects for white dots
circle.addEventListener("mouseenter", () => {
if (circle.getAttribute("fill") === "white") {
circle.setAttribute("r", "0.6"); // Bigger on hover
circle.setAttribute("fill", "#f87171"); // Light red on hover
circle.setAttribute("opacity", "1");
}
});
circle.addEventListener("mouseleave", () => {
if (circle.getAttribute("fill") === "#f87171") {
circle.setAttribute("r", "0.4"); // Back to original size
circle.setAttribute("fill", "white"); // Back to white
circle.setAttribute("opacity", "0.7");
}
});
const tooltipText = `${place.name}${place.toponymName && place.toponymName !== place.name ? ` (${place.toponymName})` : ""}`; const tooltipText = `${place.name}${place.toponymName && place.toponymName !== place.name ? ` (${place.toponymName})` : ""}`;
point.dataset.placeId = place.id; circle.dataset.placeId = place.id;
point.dataset.tooltipText = tooltipText; circle.dataset.tooltipText = tooltipText;
// Add hover and click event listeners // Add hover and click event listeners
point.addEventListener("mouseenter", (e) => this.showTooltip(e)); circle.addEventListener("mouseenter", (e) => this.showTooltip(e));
point.addEventListener("mouseleave", () => this.hideTooltip()); circle.addEventListener("mouseleave", () => this.hideTooltip());
point.addEventListener("mousemove", (e) => this.updateTooltipPosition(e)); circle.addEventListener("mousemove", (e) => this.updateTooltipPosition(e));
point.addEventListener("click", (e) => this.scrollToPlace(e)); circle.addEventListener("click", (e) => this.scrollToPlace(e));
this.pointsContainer.appendChild(point); mapOverlaySvg.appendChild(circle);
// Store reference to point for scrollspy // Store reference to circle for scrollspy
this.mapPoints.set(place.id, point); this.mapPoints.set(place.id, circle);
} }
} }
}); });
@@ -610,18 +667,28 @@ export class PlacesMap extends HTMLElement {
} }
setPointActive(point) { setPointActive(circle) {
// Active state: larger, full color, full opacity, higher z-index // Active state: darker red circle with wider dark border and small shadow
point.className = circle.setAttribute("r", "0.8"); // Bigger radius in % units
"map-point absolute w-1.5 h-1.5 bg-red-500 rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2 transition-all duration-300 opacity-100 saturate-100 z-20 cursor-pointer hover:w-2 hover:h-2 hover:bg-red-600 hover:z-30"; circle.setAttribute("fill", "#dc2626");
point.style.border = "0.5px solid #b91c1c"; circle.setAttribute("stroke", "#b91c1c");
circle.setAttribute("stroke-width", "0.12");
circle.setAttribute("opacity", "1");
circle.setAttribute("filter", "drop-shadow(0 0.05 0.1 rgba(0,0,0,0.2))");
// Move to end of SVG to appear on top
if (circle.parentNode) {
circle.parentNode.appendChild(circle);
}
} }
setPointInactive(point) { setPointInactive(circle) {
// Inactive state: small light red dots, no border // Inactive state: small white circle
point.className = circle.setAttribute("r", "0.4"); // Small radius in % units
"map-point absolute w-[0.18rem] h-[0.18rem] bg-white opacity-[0.7] rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2 transition-all duration-300 z-10 cursor-pointer hover:w-1.5 hover:h-1.5 hover:bg-red-400 hover:z-30 hover:opacity-[1.0]"; circle.setAttribute("fill", "white");
point.style.border = ""; circle.setAttribute("stroke", "none");
circle.setAttribute("opacity", "0.7");
circle.setAttribute("filter", "drop-shadow(0 0.05 0.08 rgba(0,0,0,0.15))");
// No need to reorder for inactive state
} }
showTooltip(event) { showTooltip(event) {
@@ -773,27 +840,45 @@ export class PlacesMap extends HTMLElement {
// Track the currently hovered place ID // Track the currently hovered place ID
this.currentHoveredPlaceId = placeId; this.currentHoveredPlaceId = placeId;
// Give the point a more visible highlight by making it larger immediately // Give the circle a more visible highlight by enlarging it and adding border
mapPoint.classList.remove("w-1", "h-1", "w-1.5", "h-1.5"); const currentRadius = mapPoint.getAttribute("r");
mapPoint.classList.add("w-2.5", "h-2.5"); mapPoint.setAttribute("data-original-radius", currentRadius);
mapPoint.style.zIndex = "25"; mapPoint.setAttribute("r", (parseFloat(currentRadius) * 1.7).toString());
mapPoint.setAttribute("filter", "none");
// Make sure it's darker red with darker red border for highlighted points
mapPoint.setAttribute("fill", "#dc2626");
mapPoint.setAttribute("stroke", "#b91c1c");
mapPoint.setAttribute("stroke-width", "0.24");
mapPoint.setAttribute("opacity", "1");
// Move to end of SVG to appear on top when highlighted
if (mapPoint.parentNode) {
mapPoint.parentNode.appendChild(mapPoint);
}
// No tooltip when hovering over place titles - only visual feedback // No tooltip when hovering over place titles - only visual feedback
} else if (action === "hide") { } else if (action === "hide") {
// Clear the currently hovered place ID // Clear the currently hovered place ID
this.currentHoveredPlaceId = ""; this.currentHoveredPlaceId = "";
// Remove point highlight - restore original size based on current state // Remove highlight - restore original circle size, shadow, and stroke
mapPoint.classList.remove("w-2.5", "h-2.5"); const originalRadius = mapPoint.getAttribute("data-original-radius");
// Check if this point is currently active or inactive if (originalRadius) {
if (mapPoint.className.includes("bg-red-500")) { mapPoint.setAttribute("r", originalRadius);
// Active point mapPoint.removeAttribute("data-original-radius");
mapPoint.classList.add("w-1.5", "h-1.5"); // Restore original shadow and stroke based on circle state
} else { const fill = mapPoint.getAttribute("fill");
// Inactive point if (fill === "white") {
mapPoint.classList.add("w-1", "h-1"); // White inactive points: shadow but no stroke
mapPoint.setAttribute("filter", "drop-shadow(0 0.05 0.08 rgba(0,0,0,0.15))");
mapPoint.setAttribute("stroke", "none");
} else {
// Red active points: small shadow and wider border with darker red
mapPoint.setAttribute("filter", "drop-shadow(0 0.05 0.1 rgba(0,0,0,0.2))");
mapPoint.setAttribute("fill", "#dc2626");
mapPoint.setAttribute("stroke", "#b91c1c");
mapPoint.setAttribute("stroke-width", "0.12");
}
} }
mapPoint.style.zIndex = ""; // Reset to default
} }
} }
@@ -915,24 +1000,61 @@ export class PlacesMapSingle extends HTMLElement {
// Only add point if it's within the visible map area // Only add point if it's within the visible map area
if (position.x >= 0 && position.x <= 100 && position.y >= 0 && position.y <= 100) { if (position.x >= 0 && position.x <= 100 && position.y >= 0 && position.y <= 100) {
const point = document.createElement("div"); // Create one large SVG overlay that matches the map size
point.style.left = `${position.x}%`; const mapOverlaySvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
point.style.top = `${position.y}%`; mapOverlaySvg.setAttribute("class", "absolute top-0 left-0 w-full h-full");
point.style.transformOrigin = "center"; mapOverlaySvg.style.pointerEvents = "none";
mapOverlaySvg.style.overflow = "visible";
mapOverlaySvg.setAttribute("viewBox", "0 0 100 100");
mapOverlaySvg.setAttribute("preserveAspectRatio", "none");
// Single highlighted point - moderately sized for zoomed view // Create defs element for gradients
point.className = "absolute w-2 h-2 bg-red-500 rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2 z-20"; const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
point.style.border = "0.5px solid #b91c1c";
// Create radial gradient for red dots
const redGradient = document.createElementNS("http://www.w3.org/2000/svg", "radialGradient");
redGradient.setAttribute("id", "redDotGradientSingle");
redGradient.setAttribute("cx", "30%");
redGradient.setAttribute("cy", "30%");
redGradient.setAttribute("r", "70%");
const stopLight = document.createElementNS("http://www.w3.org/2000/svg", "stop");
stopLight.setAttribute("offset", "0%");
stopLight.setAttribute("stop-color", "#f56565"); // Slightly lighter red
const stopDark = document.createElementNS("http://www.w3.org/2000/svg", "stop");
stopDark.setAttribute("offset", "100%");
stopDark.setAttribute("stop-color", "#e53e3e"); // Slightly darker red
redGradient.appendChild(stopLight);
redGradient.appendChild(stopDark);
defs.appendChild(redGradient);
mapOverlaySvg.appendChild(defs);
this.pointsContainer.appendChild(mapOverlaySvg);
// Create circle element directly in the overlay SVG
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("cx", position.x.toString());
circle.setAttribute("cy", position.y.toString());
circle.setAttribute("r", "0.8"); // Larger radius for single place view
circle.setAttribute("fill", "#dc2626");
circle.setAttribute("stroke", "#b91c1c");
circle.setAttribute("stroke-width", "0.05");
circle.setAttribute("filter", "drop-shadow(0 0.05 0.1 rgba(0,0,0,0.2))"); // Small shadow for red single place dot
circle.style.cursor = "pointer";
circle.style.pointerEvents = "all";
circle.style.transition = "r 0.3s ease, fill 0.3s ease, stroke 0.3s ease, opacity 0.3s ease";
const tooltipText = `${this.place.name}${this.place.toponymName && this.place.toponymName !== this.place.name ? ` (${this.place.toponymName})` : ""}`; const tooltipText = `${this.place.name}${this.place.toponymName && this.place.toponymName !== this.place.name ? ` (${this.place.toponymName})` : ""}`;
point.dataset.tooltipText = tooltipText; circle.dataset.tooltipText = tooltipText;
// Add hover event listeners // Add hover event listeners
point.addEventListener("mouseenter", (e) => this.showTooltip(e)); circle.addEventListener("mouseenter", (e) => this.showTooltip(e));
point.addEventListener("mouseleave", () => this.hideTooltip()); circle.addEventListener("mouseleave", () => this.hideTooltip());
point.addEventListener("mousemove", (e) => this.updateTooltipPosition(e)); circle.addEventListener("mousemove", (e) => this.updateTooltipPosition(e));
this.pointsContainer.appendChild(point); mapOverlaySvg.appendChild(circle);
// Auto-zoom to the single point with some padding // Auto-zoom to the single point with some padding
this.autoZoomToPoint(position); this.autoZoomToPoint(position);