feat(admin): enhance areas map interface with global view
Added mainMap to visualize all configured areas. Included reference layers for existing areas when editing or creating a new polygon to prevent overlap.
This commit is contained in:
+116
-7
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, nextTick, onBeforeUnmount, computed } from 'vue'
|
import { ref, reactive, nextTick, onBeforeUnmount, computed, watch } from 'vue'
|
||||||
import { BARI_BOUNDS, BARI_CENTER, isWithinBariBounds } from '~/utils/bari'
|
import { BARI_BOUNDS, BARI_CENTER, isWithinBariBounds } from '~/utils/bari'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -24,8 +24,8 @@ const actionError = ref('')
|
|||||||
const deletingAreaId = ref<number | null>(null)
|
const deletingAreaId = ref<number | null>(null)
|
||||||
const areaPendingDelete = ref<any | null>(null)
|
const areaPendingDelete = ref<any | null>(null)
|
||||||
|
|
||||||
// Map variables
|
|
||||||
let map: any = null
|
let map: any = null
|
||||||
|
let mainMap: any = null
|
||||||
let polygon: any = null
|
let polygon: any = null
|
||||||
const mapPoints = ref<[number, number][]>([])
|
const mapPoints = ref<[number, number][]>([])
|
||||||
|
|
||||||
@@ -37,6 +37,95 @@ const areaDeleteMessage = computed(() =>
|
|||||||
areaPendingDelete.value ? `Vuoi eliminare area "${areaPendingDelete.value.name}"? Operazione irreversibile.` : ''
|
areaPendingDelete.value ? `Vuoi eliminare area "${areaPendingDelete.value.name}"? Operazione irreversibile.` : ''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const initMainMap = async () => {
|
||||||
|
const mapElement = document.getElementById('mainMap')
|
||||||
|
if (!mapElement) return
|
||||||
|
|
||||||
|
const L = (await import('leaflet')).default
|
||||||
|
|
||||||
|
if (mainMap) {
|
||||||
|
mainMap.remove()
|
||||||
|
mainMap = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const bariBounds = L.latLngBounds(
|
||||||
|
[BARI_BOUNDS.south, BARI_BOUNDS.west],
|
||||||
|
[BARI_BOUNDS.north, BARI_BOUNDS.east]
|
||||||
|
)
|
||||||
|
|
||||||
|
mainMap = L.map('mainMap', {
|
||||||
|
maxBounds: bariBounds,
|
||||||
|
maxBoundsViscosity: 1,
|
||||||
|
minZoom: 11
|
||||||
|
}).setView(BARI_CENTER, 12)
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© OpenStreetMap'
|
||||||
|
}).addTo(mainMap)
|
||||||
|
|
||||||
|
const featureGroup = L.featureGroup().addTo(mainMap)
|
||||||
|
let hasLayers = false
|
||||||
|
|
||||||
|
areas.value.forEach((area: any) => {
|
||||||
|
if (area.boundary_geojson) {
|
||||||
|
try {
|
||||||
|
const layer = L.geoJSON(JSON.parse(area.boundary_geojson), {
|
||||||
|
style: {
|
||||||
|
color: '#0d6efd',
|
||||||
|
weight: 2,
|
||||||
|
fillOpacity: 0.2
|
||||||
|
}
|
||||||
|
}).bindPopup(`<strong>${area.name}</strong><br/>${area.description || ''}`)
|
||||||
|
|
||||||
|
layer.addTo(featureGroup)
|
||||||
|
hasLayers = true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid geojson for area', area.id, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasLayers) {
|
||||||
|
mainMap.fitBounds(featureGroup.getBounds())
|
||||||
|
} else {
|
||||||
|
mainMap.fitBounds(bariBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([areas, pending], () => {
|
||||||
|
if (!pending.value) {
|
||||||
|
if (areas.value.length > 0) {
|
||||||
|
nextTick(() => {
|
||||||
|
initMainMap()
|
||||||
|
})
|
||||||
|
} else if (mainMap) {
|
||||||
|
mainMap.remove()
|
||||||
|
mainMap = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
const renderExistingAreasInModal = (L: any, currentMap: any) => {
|
||||||
|
areas.value.forEach((area: any) => {
|
||||||
|
if (area.id === editingAreaId.value) return
|
||||||
|
|
||||||
|
if (area.boundary_geojson) {
|
||||||
|
try {
|
||||||
|
L.geoJSON(JSON.parse(area.boundary_geojson), {
|
||||||
|
style: {
|
||||||
|
color: '#6c757d',
|
||||||
|
weight: 2,
|
||||||
|
dashArray: '5, 5',
|
||||||
|
fillOpacity: 0.1
|
||||||
|
},
|
||||||
|
interactive: false
|
||||||
|
}).addTo(currentMap)
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const openCreateModal = () => {
|
const openCreateModal = () => {
|
||||||
actionError.value = ''
|
actionError.value = ''
|
||||||
editingAreaId.value = null
|
editingAreaId.value = null
|
||||||
@@ -48,7 +137,6 @@ const openCreateModal = () => {
|
|||||||
isCreating.value = true
|
isCreating.value = true
|
||||||
|
|
||||||
nextTick(async () => {
|
nextTick(async () => {
|
||||||
// Import leaflet solo client-side
|
|
||||||
const L = (await import('leaflet')).default
|
const L = (await import('leaflet')).default
|
||||||
|
|
||||||
if (map) {
|
if (map) {
|
||||||
@@ -74,6 +162,8 @@ const openCreateModal = () => {
|
|||||||
polygon = L.polygon([], { color: 'red' }).addTo(map)
|
polygon = L.polygon([], { color: 'red' }).addTo(map)
|
||||||
map.fitBounds(bariBounds)
|
map.fitBounds(bariBounds)
|
||||||
|
|
||||||
|
renderExistingAreasInModal(L, map)
|
||||||
|
|
||||||
map.on('click', (e: any) => {
|
map.on('click', (e: any) => {
|
||||||
if (!isWithinBariBounds(e.latlng.lat, e.latlng.lng)) {
|
if (!isWithinBariBounds(e.latlng.lat, e.latlng.lng)) {
|
||||||
jsonError.value = 'Puoi disegnare aree solo entro confini del Comune di Bari.'
|
jsonError.value = 'Puoi disegnare aree solo entro confini del Comune di Bari.'
|
||||||
@@ -110,7 +200,7 @@ const openEditModal = (area: any) => {
|
|||||||
editingAreaId.value = area.id
|
editingAreaId.value = area.id
|
||||||
formData.name = area.name || ''
|
formData.name = area.name || ''
|
||||||
formData.description = area.description || ''
|
formData.description = area.description || ''
|
||||||
formData.geojson = area.boundary_geojson ? JSON.stringify(area.boundary_geojson, null, 2) : ''
|
formData.geojson = area.boundary_geojson || ''
|
||||||
jsonError.value = ''
|
jsonError.value = ''
|
||||||
mapPoints.value = []
|
mapPoints.value = []
|
||||||
isCreating.value = true
|
isCreating.value = true
|
||||||
@@ -137,9 +227,20 @@ const openEditModal = (area: any) => {
|
|||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
attribution: '© OpenStreetMap'
|
attribution: '© OpenStreetMap'
|
||||||
}).addTo(map)
|
}).addTo(map)
|
||||||
|
console.log(JSON.parse(area.boundary_geojson))
|
||||||
|
L.geoJSON(JSON.parse(area.boundary_geojson), {
|
||||||
|
style: {
|
||||||
|
color: '#0d6efd',
|
||||||
|
weight: 2,
|
||||||
|
fillOpacity: 0.2
|
||||||
|
}
|
||||||
|
}).addTo(map)
|
||||||
|
|
||||||
polygon = L.polygon([], { color: 'red' }).addTo(map)
|
polygon = L.polygon([], { color: 'red' }).addTo(map)
|
||||||
map.fitBounds(bariBounds)
|
map.fitBounds(bariBounds)
|
||||||
|
|
||||||
|
renderExistingAreasInModal(L, map)
|
||||||
|
|
||||||
loadPolygonFromGeoJson(area.boundary_geojson)
|
loadPolygonFromGeoJson(area.boundary_geojson)
|
||||||
|
|
||||||
map.on('click', (e: any) => {
|
map.on('click', (e: any) => {
|
||||||
@@ -171,7 +272,7 @@ const updateGeojson = () => {
|
|||||||
}
|
}
|
||||||
const coords = mapPoints.value.map(p => [p[1], p[0]] as [number, number]) // Lng, Lat
|
const coords = mapPoints.value.map(p => [p[1], p[0]] as [number, number]) // Lng, Lat
|
||||||
if (coords[0]) {
|
if (coords[0]) {
|
||||||
coords.push([...coords[0]]) // Chiudi poligono
|
coords.push([...coords[0]])
|
||||||
}
|
}
|
||||||
|
|
||||||
formData.geojson = JSON.stringify({
|
formData.geojson = JSON.stringify({
|
||||||
@@ -191,6 +292,7 @@ const closeCreateModal = () => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (map) map.remove()
|
if (map) map.remove()
|
||||||
|
if (mainMap) mainMap.remove()
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
@@ -291,6 +393,12 @@ const handleDeleteArea = async () => {
|
|||||||
Nessuna area configurata.
|
Nessuna area configurata.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<!-- Main view map showing all areas -->
|
||||||
|
<div class="card mb-4 shadow-sm border-0">
|
||||||
|
<div id="mainMap" class="rounded" style="height: 400px; width: 100%; z-index: 1;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="actionError" class="alert alert-danger">
|
<div v-if="actionError" class="alert alert-danger">
|
||||||
{{ actionError }}
|
{{ actionError }}
|
||||||
</div>
|
</div>
|
||||||
@@ -332,8 +440,9 @@ const handleDeleteArea = async () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Modale Creazione -->
|
<!-- Modale Creazione/Modifica -->
|
||||||
<div v-if="isCreating" class="modal-backdrop fade show d-block" style="background: rgba(0,0,0,0.5); z-index: 1040;"></div>
|
<div v-if="isCreating" class="modal-backdrop fade show d-block" style="background: rgba(0,0,0,0.5); z-index: 1040;"></div>
|
||||||
<div v-if="isCreating" class="modal fade show d-block" tabindex="-1" style="z-index: 1050;">
|
<div v-if="isCreating" class="modal fade show d-block" tabindex="-1" style="z-index: 1050;">
|
||||||
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||||||
@@ -407,7 +516,7 @@ const handleDeleteArea = async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
#areaMap {
|
#areaMap, #mainMap {
|
||||||
background: #e9ecef;
|
background: #e9ecef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user