geoproject-app/public/site/plugins/map-editor/src/components/map/MapPreview.vue

390 lines
8.8 KiB
Vue
Raw Normal View History

<template>
<div class="map-preview">
<div ref="mapContainer" class="map-container"></div>
<div v-if="loading" class="map-loading">
<div class="spinner"></div>
<span>Loading map...</span>
</div>
</div>
</template>
<script>
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
import maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
export default {
props: {
center: {
type: Object,
required: true
},
zoom: {
type: Number,
required: true
},
markers: {
type: Array,
default: () => []
},
selectedMarkerId: {
type: String,
default: null
}
},
setup(props, { emit }) {
// State
const mapContainer = ref(null);
const map = ref(null);
const loading = ref(true);
const markerElements = ref(new Map());
const isDragging = ref(false);
// Lifecycle
onMounted(async () => {
await nextTick();
initMap();
});
onBeforeUnmount(() => {
if (map.value) {
map.value.remove();
map.value = null;
}
markerElements.value.clear();
});
// Watchers - only for updates FROM parent, not TO parent
watch(() => props.center, (newCenter) => {
if (map.value && map.value.loaded() && !isDragging.value) {
const currentCenter = map.value.getCenter();
// Only update if significantly different to avoid render loops
if (Math.abs(currentCenter.lat - newCenter.lat) > 0.00001 ||
Math.abs(currentCenter.lng - newCenter.lon) > 0.00001) {
map.value.setCenter([newCenter.lon, newCenter.lat]);
}
}
}, { deep: true });
watch(() => props.zoom, (newZoom) => {
if (map.value && map.value.loaded()) {
const currentZoom = map.value.getZoom();
// Only update if significantly different
if (Math.abs(currentZoom - newZoom) > 0.01) {
map.value.setZoom(newZoom);
}
}
});
watch(() => props.markers, () => {
updateMarkers();
}, { deep: true });
watch(() => props.selectedMarkerId, (newId) => {
updateMarkerSelection(newId);
});
// Methods
function initMap() {
if (!mapContainer.value) {
console.error("Map container not found");
return;
}
loading.value = true;
try {
map.value = new maplibregl.Map({
container: mapContainer.value,
style: {
version: 8,
sources: {
osm: {
type: "raster",
tiles: [
"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
"https://b.tile.openstreetmap.org/{z}/{x}/{y}.png",
"https://c.tile.openstreetmap.org/{z}/{x}/{y}.png"
],
tileSize: 256,
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}
},
layers: [
{
id: "osm",
type: "raster",
source: "osm",
minzoom: 0,
maxzoom: 19
}
]
},
center: [props.center.lon, props.center.lat],
zoom: props.zoom
});
map.value.on("load", () => {
loading.value = false;
updateMarkers();
});
// Handle map clicks to add markers
map.value.on("click", (e) => {
// Check if click was on a marker element
const target = e.originalEvent.target;
const isMarkerClick = target.closest('.custom-marker');
if (!isMarkerClick) {
emit("map-click", {
lat: e.lngLat.lat,
lng: e.lngLat.lng
});
}
});
} catch (error) {
console.error("Error initializing map:", error);
loading.value = false;
}
}
function updateMarkers() {
if (!map.value || !map.value.loaded()) {
return;
}
// Remove existing markers
markerElements.value.forEach(({ marker }) => {
if (marker) {
marker.remove();
}
});
markerElements.value.clear();
// Add markers
if (props.markers && Array.isArray(props.markers)) {
props.markers.forEach((markerData, index) => {
addMarkerToMap(markerData, index);
});
}
}
function addMarkerToMap(markerData, index) {
if (!map.value || !markerData || !markerData.position) {
return;
}
// Create marker element
const el = document.createElement("div");
el.className = "custom-marker";
if (props.selectedMarkerId === markerData.id) {
el.classList.add("selected");
}
// Add marker number
const numberEl = document.createElement("div");
numberEl.className = "marker-number";
numberEl.textContent = index + 1;
el.appendChild(numberEl);
try {
// Create MapLibre marker
const marker = new maplibregl.Marker({
element: el,
draggable: true
})
.setLngLat([markerData.position.lon, markerData.position.lat])
.addTo(map.value);
// Handle marker drag
marker.on("dragstart", () => {
isDragging.value = true;
});
marker.on("dragend", () => {
const lngLat = marker.getLngLat();
emit("marker-moved", {
markerId: markerData.id,
position: {
lat: lngLat.lat,
lng: lngLat.lng
}
});
setTimeout(() => {
isDragging.value = false;
}, 100);
});
// Handle marker click
el.addEventListener("click", (e) => {
e.stopPropagation();
emit("marker-click", markerData.id);
});
// Store marker reference
markerElements.value.set(markerData.id, {
marker,
element: el
});
} catch (error) {
console.error("Error adding marker to map:", error);
}
}
function updateMarkerSelection(selectedId) {
markerElements.value.forEach(({ element }, markerId) => {
if (element) {
if (markerId === selectedId) {
element.classList.add("selected");
} else {
element.classList.remove("selected");
}
}
});
}
function getCurrentCenter() {
if (map.value && map.value.loaded()) {
const center = map.value.getCenter();
return {
lat: center.lat,
lon: center.lng
};
}
return {
lat: props.center.lat,
lon: props.center.lon
};
}
return {
mapContainer,
loading,
getCurrentCenter
};
}
};
</script>
<style>
.map-preview {
position: relative;
width: 100%;
height: 100%;
}
.map-container {
width: 100%;
height: 100%;
}
.map-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
background: rgba(255, 255, 255, 0.9);
z-index: 1000;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Custom marker styles */
.custom-marker {
width: 40px;
height: 40px;
cursor: grab;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.custom-marker:active {
cursor: grabbing;
}
.custom-marker::before {
content: "";
position: absolute;
width: 40px;
height: 40px;
background: #e74c3c;
border: 3px solid white;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
}
.custom-marker:hover::before {
background: #c0392b;
transform: rotate(-45deg) scale(1.1);
}
.custom-marker.selected::before {
background: #3498db;
border-color: #2980b9;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.6);
}
.marker-number {
position: relative;
z-index: 1;
color: white;
font-weight: 700;
font-size: 14px;
line-height: 1;
transform: translateY(-4px);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
pointer-events: none;
}
/* MapLibre controls styling */
.maplibregl-ctrl-group {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.maplibregl-ctrl-group button {
width: 30px;
height: 30px;
}
.maplibregl-ctrl-attrib {
font-size: 11px;
background: rgba(255, 255, 255, 0.8);
}
.maplibregl-canvas-container {
cursor: crosshair;
}
.maplibregl-canvas-container.maplibregl-interactive {
cursor: crosshair;
}
</style>