mirror of
https://github.com/Theodor-Springmann-Stiftung/kgpz_web.git
synced 2025-10-29 17:15:31 +00:00
Verbesserungen kategoireansicht, karte aof ortsansicht
This commit is contained in:
@@ -596,10 +596,10 @@ export class PlacesMap extends HTMLElement {
|
||||
});
|
||||
},
|
||||
{
|
||||
// Trigger when element enters viewport
|
||||
threshold: 0.1,
|
||||
// No root margin for precise detection
|
||||
rootMargin: "0px",
|
||||
// Trigger when any part enters viewport - better for small elements
|
||||
threshold: 0,
|
||||
// Add some margin to trigger slightly before/after entering viewport
|
||||
rootMargin: "10px 0px",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -608,20 +608,6 @@ export class PlacesMap extends HTMLElement {
|
||||
this.intersectionObserver.observe(container);
|
||||
});
|
||||
|
||||
// Force an immediate check by triggering a scroll event
|
||||
setTimeout(() => {
|
||||
// Manually trigger intersection calculation
|
||||
placeContainers.forEach((container) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
|
||||
const placeId = container.getAttribute("data-place-id");
|
||||
const mapPoint = this.mapPoints.get(placeId);
|
||||
|
||||
if (mapPoint && isVisible) {
|
||||
this.setPointActive(mapPoint);
|
||||
}
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
|
||||
setPointActive(point) {
|
||||
@@ -646,6 +632,7 @@ export class PlacesMap extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (this.tooltip && tooltipText) {
|
||||
// Set tooltip content and position
|
||||
this.tooltip.textContent = tooltipText;
|
||||
@@ -784,63 +771,18 @@ export class PlacesMap extends HTMLElement {
|
||||
// Track the currently hovered place ID
|
||||
this.currentHoveredPlaceId = placeId;
|
||||
|
||||
// Give the point a subtle highlight by making it larger immediately
|
||||
// Give the point a more visible highlight by making it larger immediately
|
||||
mapPoint.classList.remove("w-1", "h-1", "w-1.5", "h-1.5");
|
||||
mapPoint.classList.add("w-2", "h-2");
|
||||
mapPoint.classList.add("w-2.5", "h-2.5");
|
||||
mapPoint.style.zIndex = "25";
|
||||
|
||||
// Don't show NEW tooltip if scrolling blocked (behavior 4)
|
||||
if (this.isNewPopupBlocked(placeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tooltipText = mapPoint.dataset.tooltipText;
|
||||
if (this.tooltip && tooltipText) {
|
||||
// Set tooltip content and position
|
||||
this.tooltip.textContent = tooltipText;
|
||||
|
||||
// Position tooltip at the map point location
|
||||
const pointRect = mapPoint.getBoundingClientRect();
|
||||
const mapRect = this.mapElement.getBoundingClientRect();
|
||||
const x = pointRect.left - mapRect.left + pointRect.width / 2;
|
||||
const y = pointRect.top - mapRect.top + pointRect.height / 2;
|
||||
|
||||
this.tooltip.style.left = `${x}px`;
|
||||
this.tooltip.style.top = `${y}px`;
|
||||
|
||||
// Clear any existing timeouts
|
||||
this.clearTimeouts();
|
||||
|
||||
if (this.isTooltipVisible) {
|
||||
// Behavior 2b: Popup was visible -- instant popup
|
||||
this.tooltip.classList.remove("opacity-0");
|
||||
this.tooltip.classList.add("opacity-100");
|
||||
} else {
|
||||
// Behavior 2a: No popup was visible -- Popup after 150ms
|
||||
this.showTimeout = setTimeout(() => {
|
||||
this.tooltip.classList.remove("opacity-0");
|
||||
this.tooltip.classList.add("opacity-100");
|
||||
this.isTooltipVisible = true;
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
// No tooltip when hovering over place titles - only visual feedback
|
||||
} else if (action === "hide") {
|
||||
// Clear the currently hovered place ID
|
||||
this.currentHoveredPlaceId = "";
|
||||
|
||||
// Behavior 3: Leave tile or map point -- close after 150ms delay
|
||||
// NOTE: This is NOT blocked during scroll - existing popup should close when mouse leaves
|
||||
this.clearTimeouts();
|
||||
this.hideTimeout = setTimeout(() => {
|
||||
if (this.tooltip) {
|
||||
this.tooltip.classList.remove("opacity-100");
|
||||
this.tooltip.classList.add("opacity-0");
|
||||
this.isTooltipVisible = false;
|
||||
}
|
||||
}, 150);
|
||||
|
||||
// Remove point highlight - restore original size based on current state
|
||||
mapPoint.classList.remove("w-2", "h-2");
|
||||
mapPoint.classList.remove("w-2.5", "h-2.5");
|
||||
// Check if this point is currently active or inactive
|
||||
if (mapPoint.className.includes("bg-red-500")) {
|
||||
// Active point
|
||||
@@ -872,7 +814,229 @@ export class PlacesMap extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Places Map Single Web Component
|
||||
* Embeds an SVG map with a single highlighted place
|
||||
*/
|
||||
export class PlacesMapSingle extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.place = null;
|
||||
this.mapElement = null;
|
||||
this.pointsContainer = null;
|
||||
this.tooltip = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.parseData();
|
||||
this.render();
|
||||
this.initializeMap();
|
||||
}
|
||||
|
||||
parseData() {
|
||||
try {
|
||||
const placeData = this.dataset.place;
|
||||
if (placeData) {
|
||||
this.place = JSON.parse(placeData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse place data:", error);
|
||||
this.place = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<div class="map-container relative w-full aspect-[5/7] overflow-hidden bg-slate-100">
|
||||
<div class="transform-wrapper absolute top-0 left-0 w-full h-auto origin-top-left">
|
||||
<img src="/assets/Europe.svg" alt="Map of Europe" class="block w-full h-auto">
|
||||
<div class="points-container absolute top-0 left-0 w-full h-full"></div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0 h-auto text-[0.6rem] bg-white px-0.5 bg-opacity-[0.5] border">
|
||||
<i class="ri-creative-commons-line"></i>
|
||||
<a href="https://commons.wikimedia.org/wiki/File:Europe_laea_topography.svg" target="_blank" class="">
|
||||
Wikimedia Commons
|
||||
</a>
|
||||
</div>
|
||||
<!-- Tooltip -->
|
||||
<div class="map-tooltip absolute bg-slate-800 text-white text-sm px-2 py-1 rounded shadow-lg pointer-events-none opacity-0 transition-opacity duration-200 z-30 whitespace-nowrap" style="transform: translate(-50%, -100%); margin-top: -8px;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.mapElement = this.querySelector(".map-container");
|
||||
this.pointsContainer = this.querySelector(".points-container");
|
||||
this.tooltip = this.querySelector(".map-tooltip");
|
||||
}
|
||||
|
||||
initializeMap() {
|
||||
if (!this.place || !this.place.lat || !this.place.lng || !this.pointsContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map extent constants
|
||||
const MAP_EXTENT_METERS = { xmin: 2555000, ymin: 1350000, xmax: 7405000, ymax: 5500000 };
|
||||
const PROJECTION_CENTER = { lon: 10, lat: 52 };
|
||||
|
||||
// Convert lat/lng to % calculation
|
||||
const convertLatLngToPercent = (lat, lng) => {
|
||||
const R = 6371000;
|
||||
const FE = 4321000;
|
||||
const FN = 3210000;
|
||||
const lon_0_rad = (PROJECTION_CENTER.lon * Math.PI) / 180;
|
||||
const lat_0_rad = (PROJECTION_CENTER.lat * Math.PI) / 180;
|
||||
const lon_rad = (lng * Math.PI) / 180;
|
||||
const lat_rad = (lat * Math.PI) / 180;
|
||||
const k_prime = Math.sqrt(
|
||||
2 /
|
||||
(1 +
|
||||
Math.sin(lat_0_rad) * Math.sin(lat_rad) +
|
||||
Math.cos(lat_0_rad) * Math.cos(lat_rad) * Math.cos(lon_rad - lon_0_rad)),
|
||||
);
|
||||
const x_proj = R * k_prime * Math.cos(lat_rad) * Math.sin(lon_rad - lon_0_rad);
|
||||
const y_proj =
|
||||
R *
|
||||
k_prime *
|
||||
(Math.cos(lat_0_rad) * Math.sin(lat_rad) -
|
||||
Math.sin(lat_0_rad) * Math.cos(lat_rad) * Math.cos(lon_rad - lon_0_rad));
|
||||
const finalX = x_proj + FE;
|
||||
const finalY = y_proj + FN;
|
||||
const mapWidthMeters = MAP_EXTENT_METERS.xmax - MAP_EXTENT_METERS.xmin;
|
||||
const mapHeightMeters = MAP_EXTENT_METERS.ymax - MAP_EXTENT_METERS.ymin;
|
||||
const xPercent = ((finalX - MAP_EXTENT_METERS.xmin) / mapWidthMeters) * 100;
|
||||
const yPercent = ((MAP_EXTENT_METERS.ymax - finalY) / mapHeightMeters) * 100;
|
||||
return { x: xPercent, y: yPercent };
|
||||
};
|
||||
|
||||
const lat = parseFloat(this.place.lat);
|
||||
const lng = parseFloat(this.place.lng);
|
||||
const position = convertLatLngToPercent(lat, lng);
|
||||
|
||||
// Only add point if it's within the visible map area
|
||||
if (position.x >= 0 && position.x <= 100 && position.y >= 0 && position.y <= 100) {
|
||||
const point = document.createElement("div");
|
||||
point.style.left = `${position.x}%`;
|
||||
point.style.top = `${position.y}%`;
|
||||
point.style.transformOrigin = "center";
|
||||
|
||||
// Single highlighted point - moderately sized for zoomed view
|
||||
point.className = "absolute w-2 h-2 bg-red-500 border border-red-700 rounded-full shadow-md -translate-x-1/2 -translate-y-1/2 z-20";
|
||||
|
||||
const tooltipText = `${this.place.name}${this.place.toponymName && this.place.toponymName !== this.place.name ? ` (${this.place.toponymName})` : ""}`;
|
||||
point.dataset.tooltipText = tooltipText;
|
||||
|
||||
// Add hover event listeners
|
||||
point.addEventListener("mouseenter", (e) => this.showTooltip(e));
|
||||
point.addEventListener("mouseleave", () => this.hideTooltip());
|
||||
point.addEventListener("mousemove", (e) => this.updateTooltipPosition(e));
|
||||
|
||||
this.pointsContainer.appendChild(point);
|
||||
|
||||
// Auto-zoom to the single point with some padding
|
||||
this.autoZoomToPoint(position);
|
||||
}
|
||||
}
|
||||
|
||||
autoZoomToPoint(position) {
|
||||
// Create a balanced area around the point for zooming
|
||||
const PADDING = 20; // 20% padding around the point
|
||||
let minX = Math.max(0, position.x - PADDING);
|
||||
let maxX = Math.min(100, position.x + PADDING);
|
||||
let minY = Math.max(0, position.y - PADDING);
|
||||
let maxY = Math.min(100, position.y + PADDING);
|
||||
|
||||
let width = maxX - minX;
|
||||
let height = maxY - minY;
|
||||
|
||||
// Maintain 5:7 aspect ratio
|
||||
const ASPECT_RATIO = 5 / 7;
|
||||
const pointsAspectRatio = width / height;
|
||||
|
||||
let finalViewBox = { x: minX, y: minY, width: width, height: height };
|
||||
|
||||
if (pointsAspectRatio > ASPECT_RATIO) {
|
||||
const newTargetHeight = width / ASPECT_RATIO;
|
||||
finalViewBox.y = minY - (newTargetHeight - height) / 2;
|
||||
finalViewBox.height = newTargetHeight;
|
||||
} else {
|
||||
const newTargetWidth = height * ASPECT_RATIO;
|
||||
finalViewBox.x = minX - (newTargetWidth - width) / 2;
|
||||
finalViewBox.width = newTargetWidth;
|
||||
}
|
||||
|
||||
// Ensure the final viewbox stays within map boundaries (0-100%)
|
||||
if (finalViewBox.x < 0) {
|
||||
finalViewBox.width += finalViewBox.x;
|
||||
finalViewBox.x = 0;
|
||||
}
|
||||
if (finalViewBox.y < 0) {
|
||||
finalViewBox.height += finalViewBox.y;
|
||||
finalViewBox.y = 0;
|
||||
}
|
||||
if (finalViewBox.x + finalViewBox.width > 100) {
|
||||
finalViewBox.width = 100 - finalViewBox.x;
|
||||
}
|
||||
if (finalViewBox.y + finalViewBox.height > 100) {
|
||||
finalViewBox.height = 100 - finalViewBox.y;
|
||||
}
|
||||
|
||||
// Ensure minimum zoom level - don't zoom in too much if constrained by boundaries
|
||||
const MIN_VIEWPORT_SIZE = 30; // Minimum 30% of map should be visible
|
||||
if (finalViewBox.width < MIN_VIEWPORT_SIZE) {
|
||||
const expansion = (MIN_VIEWPORT_SIZE - finalViewBox.width) / 2;
|
||||
finalViewBox.x = Math.max(0, finalViewBox.x - expansion);
|
||||
finalViewBox.width = Math.min(MIN_VIEWPORT_SIZE, 100 - finalViewBox.x);
|
||||
}
|
||||
if (finalViewBox.height < MIN_VIEWPORT_SIZE) {
|
||||
const expansion = (MIN_VIEWPORT_SIZE - finalViewBox.height) / 2;
|
||||
finalViewBox.y = Math.max(0, finalViewBox.y - expansion);
|
||||
finalViewBox.height = Math.min(MIN_VIEWPORT_SIZE, 100 - finalViewBox.y);
|
||||
}
|
||||
|
||||
// Apply transformation
|
||||
const scale = 100 / finalViewBox.width;
|
||||
const translateX = -finalViewBox.x;
|
||||
const translateY = -finalViewBox.y;
|
||||
|
||||
const transformValue = `scale(${scale}) translate(${translateX}%, ${translateY}%)`;
|
||||
const transformWrapper = this.querySelector(".transform-wrapper");
|
||||
if (transformWrapper) {
|
||||
transformWrapper.style.transform = transformValue;
|
||||
}
|
||||
}
|
||||
|
||||
showTooltip(event) {
|
||||
const point = event.target;
|
||||
const tooltipText = point.dataset.tooltipText;
|
||||
|
||||
if (this.tooltip && tooltipText) {
|
||||
this.tooltip.textContent = tooltipText;
|
||||
this.updateTooltipPosition(event);
|
||||
this.tooltip.classList.remove("opacity-0");
|
||||
this.tooltip.classList.add("opacity-100");
|
||||
}
|
||||
}
|
||||
|
||||
hideTooltip() {
|
||||
if (this.tooltip) {
|
||||
this.tooltip.classList.remove("opacity-100");
|
||||
this.tooltip.classList.add("opacity-0");
|
||||
}
|
||||
}
|
||||
|
||||
updateTooltipPosition(event) {
|
||||
if (!this.tooltip) return;
|
||||
|
||||
const mapRect = this.mapElement.getBoundingClientRect();
|
||||
const x = event.clientX - mapRect.left;
|
||||
const y = event.clientY - mapRect.top;
|
||||
|
||||
this.tooltip.style.left = `${x}px`;
|
||||
this.tooltip.style.top = `${y}px`;
|
||||
}
|
||||
}
|
||||
|
||||
// Register the custom elements
|
||||
customElements.define("places-filter", PlacesFilter);
|
||||
customElements.define("place-accordion", PlaceAccordion);
|
||||
customElements.define("places-map", PlacesMap);
|
||||
customElements.define("places-map-single", PlacesMapSingle);
|
||||
|
||||
Reference in New Issue
Block a user