feat(map): Implement interactive area overlays
This major update introduces a complete system for visualizing protected areas on the OpenLayers map. It adds a new sync script to process area data and heavily modifies the frontend to support interactive, multi-layer display with custom popups. Additionally, data-sync scripts were refactored to run globally on all configured worlds, simplifying cron automation.
This commit is contained in:
parent
fa94f0e23d
commit
a225feef98
11 changed files with 568 additions and 687 deletions
|
|
@ -26,32 +26,44 @@
|
|||
// Globale Referenzen
|
||||
window.olMap_%%current_world_key%% = null;
|
||||
window.playerMarkerSource_%%current_world_key%% = new ol.source.Vector();
|
||||
window.parentAreaLayerSource_%%current_world_key%% = new ol.source.Vector();
|
||||
window.subAreaLayerSource_%%current_world_key%% = new ol.source.Vector();
|
||||
window.mapData_%%current_world_key%% = {};
|
||||
|
||||
// Konvertiert Minetest-Koordinaten in OpenLayers-Pixel-Koordinaten
|
||||
function convertMinetestToOpenLayers_%%current_world_key%%(posX, posZ) {
|
||||
const mapData = window.mapData_%%current_world_key%%;
|
||||
if (!mapData || !mapData.mapWidth) return null;
|
||||
|
||||
const percentX = (posX - mapData.minX) / mapData.extentWidth;
|
||||
const percentZ = (posZ - mapData.minZ) / mapData.extentHeight;
|
||||
|
||||
const pixelX = percentX * mapData.mapWidth;
|
||||
const pixelY = - (mapData.mapHeight - (percentZ * mapData.mapHeight));
|
||||
|
||||
return [pixelX, pixelY];
|
||||
}
|
||||
|
||||
function createPolygonFromPos_%%current_world_key%%(pos1, pos2) {
|
||||
const p1 = convertMinetestToOpenLayers_%%current_world_key%%(pos1.x, pos1.z);
|
||||
const p2 = convertMinetestToOpenLayers_%%current_world_key%%(pos2.x, pos2.z);
|
||||
if (!p1 || !p2) return null;
|
||||
const minX = Math.min(p1[0], p2[0]);
|
||||
const minY = Math.min(p1[1], p2[1]);
|
||||
const maxX = Math.max(p1[0], p2[0]);
|
||||
const maxY = Math.max(p1[1], p2[1]);
|
||||
const coordinates = [[ [minX, minY], [maxX, minY], [maxX, maxY], [minX, maxY], [minX, minY] ]];
|
||||
return new ol.geom.Polygon(coordinates);
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const mapContainer = document.getElementById('ol-map-container-%%current_world_key%%');
|
||||
const mapInfoPath = '/%%web_map_info_rel_path%%?v=' + new Date().getTime();
|
||||
const areasPath = '/%%web_areas_json_rel_path%%?v=' + new Date().getTime();
|
||||
|
||||
fetch(mapInfoPath)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('map_info.txt nicht gefunden (Status: ' + response.status + ')');
|
||||
return response.text();
|
||||
})
|
||||
.then(mapInfoText => {
|
||||
Promise.all([
|
||||
fetch(mapInfoPath).then(res => { if (!res.ok) throw new Error('map_info.txt'); return res.text(); }),
|
||||
fetch(areasPath).then(res => { if (!res.ok) return {}; return res.json(); })
|
||||
]).then(([mapInfoText, areaData]) => {
|
||||
|
||||
const mapInfo = (text => {
|
||||
const data = {};
|
||||
text.split('\n').forEach(line => {
|
||||
|
|
@ -60,14 +72,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
});
|
||||
return data;
|
||||
})(mapInfoText);
|
||||
|
||||
if (!mapInfo.map_dimension || !mapInfo.map_extent) throw new Error('map_info.txt ist unvollständig.');
|
||||
|
||||
const mapData = window.mapData_%%current_world_key%%;
|
||||
const mapDim = mapInfo.map_dimension.split('x');
|
||||
mapData.mapWidth = parseInt(mapDim[0], 10);
|
||||
mapData.mapHeight = parseInt(mapDim[1], 10);
|
||||
|
||||
const mapExt = mapInfo.map_extent.split(/[+:]/);
|
||||
mapData.minX = parseInt(mapExt[0], 10);
|
||||
mapData.minZ = parseInt(mapExt[1], 10);
|
||||
|
|
@ -78,62 +88,60 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
mapContainer.innerHTML = '';
|
||||
|
||||
const extent = [0, -mapData.mapHeight, mapData.mapWidth, 0];
|
||||
|
||||
// KORREKTUR: Wir trennen die echten von den virtuellen Auflösungen
|
||||
const sourceResolutions = %%RESOLUTIONS_JS_ARRAY%%;
|
||||
|
||||
const viewResolutions = [...sourceResolutions]; // Kopie für die Ansicht
|
||||
const digitalZoomLevels = 3;
|
||||
// HINZUGEFÜGT: Logik für digitalen Zoom wiederhergestellt
|
||||
const maxNativeZoom = sourceResolutions.length -1;
|
||||
const viewResolutions = [...sourceResolutions];
|
||||
const digitalZoomLevels = 3;
|
||||
let lastResolution = viewResolutions[viewResolutions.length - 1];
|
||||
for (let i = 0; i < digitalZoomLevels; i++) {
|
||||
lastResolution = lastResolution / 2;
|
||||
viewResolutions.push(lastResolution);
|
||||
}
|
||||
|
||||
const projection = new ol.proj.Projection({
|
||||
code: 'pixel-map-%%current_world_key%%',
|
||||
units: 'pixels',
|
||||
extent: extent
|
||||
});
|
||||
|
||||
const tileGrid = new ol.tilegrid.TileGrid({
|
||||
origin: ol.extent.getTopLeft(extent),
|
||||
resolutions: sourceResolutions, // Das TileGrid kennt NUR die echten Auflösungen
|
||||
});
|
||||
const projection = new ol.proj.Projection({ code: 'pixel-map-%%current_world_key%%', units: 'pixels', extent: extent });
|
||||
const tileGrid = new ol.tilegrid.TileGrid({ origin: ol.extent.getTopLeft(extent), resolutions: sourceResolutions });
|
||||
|
||||
const tileSource = new ol.source.TileImage({
|
||||
projection: projection,
|
||||
tileGrid: tileGrid,
|
||||
tileUrlFunction: function(tileCoord) {
|
||||
if (!tileCoord) return '';
|
||||
// OpenLayers wählt jetzt selbst die beste verfügbare Kachel aus.
|
||||
// Die URL-Funktion kann wieder einfach sein.
|
||||
return ('/%%web_tiles_rel_path%%/' + tileCoord[0] + '/' + tileCoord[1] + '/' + tileCoord[2] + '.png?v=%%CACHE_BUSTER%%');
|
||||
let z = tileCoord[0]; let x = tileCoord[1]; let y = tileCoord[2];
|
||||
if (z > maxNativeZoom) {
|
||||
const zoomDiff = z - maxNativeZoom;
|
||||
const scale = Math.pow(2, zoomDiff);
|
||||
x = Math.floor(x / scale);
|
||||
y = Math.floor(y / scale);
|
||||
z = maxNativeZoom;
|
||||
}
|
||||
return ('/%%web_tiles_rel_path%%/' + z + '/' + x + '/' + y + '.png?v=%%CACHE_BUSTER%%');
|
||||
}
|
||||
});
|
||||
|
||||
const tileLayer = new ol.layer.Tile({ source: tileSource });
|
||||
const tileLayer = new ol.layer.Tile({ source: tileSource, title: 'Basiskarte', type: 'base' });
|
||||
|
||||
const playerLayer = new ol.layer.Vector({ source: window.playerMarkerSource_%%current_world_key%%, style: f => f.get('style') });
|
||||
const parentAreaStyle = new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(0, 100, 255, 0.8)', width: 2 }), fill: new ol.style.Fill({ color: 'rgba(0, 100, 255, 0.2)' }) });
|
||||
const subAreaStyle = new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(0, 100, 255, 0.8)', width: 1.5, lineDash: [6, 6] }), fill: new ol.style.Fill({ color: 'transparent' }) });
|
||||
const parentAreaLayer = new ol.layer.Vector({ source: window.parentAreaLayerSource_%%current_world_key%%, style: parentAreaStyle, title: 'Grundstücke' });
|
||||
const subAreaLayer = new ol.layer.Vector({ source: window.subAreaLayerSource_%%current_world_key%%, style: subAreaStyle, title: 'Parzellen', visible: false });
|
||||
const playerLayer = new ol.layer.Vector({ source: window.playerMarkerSource_%%current_world_key%%, style: f => f.get('style'), title: 'Spielerpositionen' });
|
||||
|
||||
const view = new ol.View({
|
||||
projection: projection,
|
||||
center: ol.extent.getCenter(extent),
|
||||
resolutions: viewResolutions, // Die View kennt ALLE Auflösungen (echte + digitale)
|
||||
});
|
||||
const overlayLayers = new ol.layer.Group({ title: 'Overlays', layers: [subAreaLayer, parentAreaLayer, playerLayer] });
|
||||
const view = new ol.View({ projection: projection, center: ol.extent.getCenter(extent), resolutions: viewResolutions });
|
||||
|
||||
const popupContainer = document.getElementById('popup-%%current_world_key%%');
|
||||
const popupContent = document.getElementById('popup-content-%%current_world_key%%');
|
||||
const popupCloser = document.getElementById('popup-closer-%%current_world_key%%');
|
||||
|
||||
const overlay = new ol.Overlay({ element: popupContainer, autoPan: { animation: { duration: 250 } } });
|
||||
popupCloser.onclick = () => { overlay.setPosition(undefined); popupCloser.blur(); return false; };
|
||||
|
||||
const mapControls = [ new ol.control.Zoom(), new ol.control.Rotate(), new ol.control.Attribution({ collapsible: false }), new ol.control.FullScreen() ];
|
||||
const mapControls = [ new ol.control.Zoom(), new ol.control.Rotate(), new ol.control.Attribution({ collapsible: false }), new ol.control.FullScreen(), new ol.control.LayerSwitcher() ];
|
||||
|
||||
window.olMap_%%current_world_key%% = new ol.Map({
|
||||
controls: mapControls,
|
||||
layers: [tileLayer, playerLayer],
|
||||
layers: [tileLayer, overlayLayers],
|
||||
overlays: [overlay],
|
||||
target: mapContainer,
|
||||
view: view
|
||||
|
|
@ -142,17 +150,31 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const map = window.olMap_%%current_world_key%%;
|
||||
map.getView().fit(extent, { size: map.getSize() });
|
||||
|
||||
for (const id in areaData) {
|
||||
const area = areaData[id];
|
||||
const polygon = createPolygonFromPos_%%current_world_key%%(area.pos1, area.pos2);
|
||||
if (polygon) {
|
||||
const feature = new ol.Feature({ geometry: polygon, areaData: area });
|
||||
window.parentAreaLayerSource_%%current_world_key%%.addFeature(feature);
|
||||
}
|
||||
}
|
||||
|
||||
// KORREKTUR: Klick-Handler mit korrekter Priorisierung
|
||||
map.on('click', function(evt) {
|
||||
const feature = map.forEachFeatureAtPixel(evt.pixel, f => f);
|
||||
|
||||
// Reset
|
||||
overlay.setPosition(undefined);
|
||||
popupCloser.blur();
|
||||
|
||||
// Priorität 1: Spieler-Marker
|
||||
const playerFeature = map.forEachFeatureAtPixel(evt.pixel, (f, l) => l === playerLayer ? f : undefined);
|
||||
if (playerFeature) {
|
||||
window.subAreaLayerSource_%%current_world_key%%.clear(); // Sub-Areas ausblenden, wenn ein Spieler geklickt wird
|
||||
subAreaLayer.setVisible(false);
|
||||
|
||||
if (feature) {
|
||||
const playerData = feature.get('playerData');
|
||||
const playerData = playerFeature.get('playerData');
|
||||
if (playerData) {
|
||||
const statusDotClass = feature.get('statusDotClass');
|
||||
const lastLoginFormatted = feature.get('lastLoginFormatted');
|
||||
const statusDotClass = playerFeature.get('statusDotClass');
|
||||
const lastLoginFormatted = playerFeature.get('lastLoginFormatted');
|
||||
const popupHTML = `
|
||||
<div class='popup-player-box'>
|
||||
<div class="player-header">
|
||||
|
|
@ -172,8 +194,39 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
</div>
|
||||
</div>`;
|
||||
popupContent.innerHTML = popupHTML;
|
||||
overlay.setPosition(feature.getGeometry().getCoordinates());
|
||||
overlay.setPosition(playerFeature.getGeometry().getCoordinates());
|
||||
}
|
||||
return; // Verarbeitung hier beenden
|
||||
}
|
||||
|
||||
// Priorität 2: Hauptgrundstück (nur wenn kein Spieler geklickt wurde)
|
||||
window.subAreaLayerSource_%%current_world_key%%.clear(); // Alte Sub-Areas immer entfernen
|
||||
const areaFeature = map.forEachFeatureAtPixel(evt.pixel, (f, l) => l === parentAreaLayer ? f : undefined);
|
||||
if (areaFeature) {
|
||||
const areaData = areaFeature.get('areaData');
|
||||
if (areaData.sub_areas && areaData.sub_areas.length > 0) {
|
||||
const sub_features = [];
|
||||
areaData.sub_areas.forEach(sub => {
|
||||
const sub_polygon = createPolygonFromPos_%%current_world_key%%(sub.pos1, sub.pos2);
|
||||
if(sub_polygon) sub_features.push(new ol.Feature({ geometry: sub_polygon }));
|
||||
});
|
||||
window.subAreaLayerSource_%%current_world_key%%.addFeatures(sub_features);
|
||||
subAreaLayer.setVisible(true);
|
||||
} else {
|
||||
subAreaLayer.setVisible(false);
|
||||
}
|
||||
|
||||
let popupHTML = `<strong>${areaData.name}</strong><br>Besitzer: ${areaData.owner}`;
|
||||
if (areaData.sub_areas && areaData.sub_areas.length > 0) {
|
||||
popupHTML += `<hr class="privilege-separator"><p><strong>Parzellen:</strong></p><ul>`;
|
||||
areaData.sub_areas.forEach(sub => { popupHTML += `<li>${sub.name} (${sub.owner})</li>`; });
|
||||
popupHTML += `</ul>`;
|
||||
}
|
||||
popupContent.innerHTML = popupHTML;
|
||||
overlay.setPosition(evt.coordinate);
|
||||
} else {
|
||||
// Wenn nichts geklickt wurde, auch die sub-areas ausblenden
|
||||
subAreaLayer.setVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -183,7 +236,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Fehler beim Initialisieren der Karte:", error);
|
||||
console.error("Fehler beim Initialisieren der Karte oder der Grundstücksdaten:", error);
|
||||
mapContainer.innerHTML = "<p><em>Karte konnte nicht initialisiert werden: " + error.message + "</em></p>";
|
||||
});
|
||||
|
||||
|
|
@ -191,19 +244,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const archiveBtn = document.getElementById('toggle-archive-btn-%%current_world_key%%');
|
||||
const liveContainer = document.getElementById('live-map-container-%%current_world_key%%');
|
||||
const archiveContainer = document.getElementById('archive-view-container-%%current_world_key%%');
|
||||
|
||||
if (liveBtn && archiveBtn && liveContainer && archiveContainer) {
|
||||
liveBtn.addEventListener('click', () => {
|
||||
liveBtn.classList.add('active'); archiveBtn.classList.remove('active');
|
||||
liveContainer.style.display = 'block'; archiveContainer.style.display = 'none';
|
||||
if (window.olMap_%%current_world_key%%) {
|
||||
setTimeout(() => window.olMap_%%current_world_key%%.updateSize(), 10);
|
||||
}
|
||||
});
|
||||
archiveBtn.addEventListener('click', () => {
|
||||
archiveBtn.classList.add('active'); liveBtn.classList.remove('active');
|
||||
archiveContainer.style.display = 'block'; liveContainer.style.display = 'none';
|
||||
});
|
||||
liveBtn.addEventListener('click', () => { liveContainer.style.display = 'block'; archiveContainer.style.display = 'none'; if (window.olMap_%%current_world_key%%) { setTimeout(() => window.olMap_%%current_world_key%%.updateSize(), 10); } });
|
||||
archiveBtn.addEventListener('click', () => { archiveContainer.style.display = 'block'; liveContainer.style.display = 'none'; });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue