252 lines
11 KiB
PHP
252 lines
11 KiB
PHP
|
|
<div class="flex flex-col h-screen">
|
||
|
|
{{-- Cabecera fija --}}
|
||
|
|
<div class="flex justify-between items-center mb-4 px-4 pt-4 flex-shrink-0">
|
||
|
|
<h1 class="text-2xl font-bold">Gestión de elementos - {{ $phase->name }}</h1>
|
||
|
|
<a href="{{ route('projects.map', $project) }}" class="btn btn-outline btn-sm">← Volver al mapa</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="flex-1 overflow-hidden px-4 pb-4">
|
||
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2 h-full">
|
||
|
|
{{-- Columna izquierda --}}
|
||
|
|
<div class="space-y-4 overflow-y-auto h-full pr-2">
|
||
|
|
<div class="card bg-base-100 shadow-xl">
|
||
|
|
<div class="card-body">
|
||
|
|
<h2 class="card-title">Importar archivo</h2>
|
||
|
|
<form wire:submit.prevent="importFile">
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label">Nombre de capa</label>
|
||
|
|
<input type="text" wire:model="layerName" class="input input-bordered" required>
|
||
|
|
</div>
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label">Color</label>
|
||
|
|
<input type="color" wire:model="layerColor" class="input input-bordered">
|
||
|
|
</div>
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label">Archivo (GeoJSON, KML, Shapefile.zip, DWG)</label></br>
|
||
|
|
<input type="file" wire:model="uploadFile" class="file-input file-input-bordered">
|
||
|
|
@error('uploadFile') <span class="text-error">{{ $message }}</span> @enderror
|
||
|
|
</div>
|
||
|
|
<button type="submit" class="btn btn-primary w-full mt-2">Subir y convertir</button>
|
||
|
|
</form>
|
||
|
|
<div class="divider"></div>
|
||
|
|
<button wire:click="createEmptyLayer" class="btn btn-secondary w-full">Crear capa vacía</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card bg-base-100 shadow-xl">
|
||
|
|
<div class="card-body">
|
||
|
|
<h2 class="card-title">Capas existentes</h2>
|
||
|
|
<div class="space-y-2">
|
||
|
|
@foreach($layers as $layer)
|
||
|
|
<div class="flex justify-between items-center p-2 border rounded">
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<span class="w-4 h-4 rounded-full" style="background: {{ $layer->geojson_data['style']['color'] ?? '#ccc' }}"></span>
|
||
|
|
{{ $layer->name }}
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<button wire:click="selectLayer({{ $layer->id }})" class="btn btn-xs btn-info"><x-heroicon-s-trash />Editar</button>
|
||
|
|
<button wire:click="deleteLayer({{ $layer->id }})" class="btn btn-xs btn-error"><x-heroicon-s-trash />Eliminar</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
@endforeach
|
||
|
|
@if($layers->isEmpty())
|
||
|
|
<p class="text-center">Sin capas. Crea una o importa.</p>
|
||
|
|
@endif
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{-- Columna derecha: mapa PERSISTENTE --}}
|
||
|
|
<div class="lg:col-span-2 flex flex-col h-full">
|
||
|
|
<div class="card bg-base-100 shadow-xl flex-1 flex flex-col">
|
||
|
|
<div class="card-body flex-1 flex flex-col p-2">
|
||
|
|
<h2 class="card-title">Editor gráfico</h2>
|
||
|
|
@if($selectedLayer)
|
||
|
|
<div class="mt-3 flex gap-2">
|
||
|
|
<button id="saveDrawingBtn" class="btn btn-primary">💾 Guardar cambios</button>
|
||
|
|
<button wire:click="cancelEditing" class="btn btn-outline">Cancelar edición</button>
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
<div id="permanentMap" style="flex: 1; min-height: 500px; width: 100%; background: #e2e8f0;" wire:ignore></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@push('scripts')
|
||
|
|
<script>
|
||
|
|
// Variables globales
|
||
|
|
let mapInitialized = false;
|
||
|
|
let mapInstance = null;
|
||
|
|
let drawnItems = null; // Grupo que contiene todos los elementos editables
|
||
|
|
let currentColor = '#3b82f6';
|
||
|
|
|
||
|
|
function initializeMap() {
|
||
|
|
if (mapInitialized) return;
|
||
|
|
const container = document.getElementById('permanentMap');
|
||
|
|
if (!container) {
|
||
|
|
setTimeout(initializeMap, 200);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const center = [{{ $project->lat }}, {{ $project->lng }}];
|
||
|
|
mapInstance = L.map('permanentMap').setView(center, 16);
|
||
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> & CartoDB'
|
||
|
|
}).addTo(mapInstance);
|
||
|
|
|
||
|
|
// Grupo que contendrá todos los elementos (sin anidar)
|
||
|
|
drawnItems = L.featureGroup().addTo(mapInstance);
|
||
|
|
|
||
|
|
const drawControl = new L.Control.Draw({
|
||
|
|
edit: { featureGroup: drawnItems },
|
||
|
|
draw: {
|
||
|
|
polygon: true,
|
||
|
|
polyline: true,
|
||
|
|
marker: true,
|
||
|
|
circle: false,
|
||
|
|
rectangle: false,
|
||
|
|
circlemarker: false
|
||
|
|
}
|
||
|
|
});
|
||
|
|
mapInstance.addControl(drawControl);
|
||
|
|
|
||
|
|
// Cuando se dibuja un nuevo elemento, lo añadimos al grupo
|
||
|
|
mapInstance.on(L.Draw.Event.CREATED, (e) => {
|
||
|
|
const layer = e.layer;
|
||
|
|
// Asignar propiedades por defecto (necesario para guardar)
|
||
|
|
if (!layer.feature) {
|
||
|
|
layer.feature = {
|
||
|
|
type: 'Feature',
|
||
|
|
properties: { name: 'Nuevo elemento', progress: 0, responsible: '' }
|
||
|
|
};
|
||
|
|
}
|
||
|
|
drawnItems.addLayer(layer);
|
||
|
|
});
|
||
|
|
|
||
|
|
mapInitialized = true;
|
||
|
|
console.log('Mapa inicializado correctamente');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Limpia todas las capas actuales
|
||
|
|
function clearAllLayers() {
|
||
|
|
if (drawnItems) {
|
||
|
|
drawnItems.clearLayers();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Cargar una capa GeoJSON haciendo cada feature editable individualmente
|
||
|
|
function loadGeoJSONLayer(geojson, color) {
|
||
|
|
console.log('Cargando capa GeoJSON:', geojson);
|
||
|
|
if (!mapInstance) return;
|
||
|
|
clearAllLayers();
|
||
|
|
currentColor = color;
|
||
|
|
|
||
|
|
if (!geojson || !geojson.features || geojson.features.length === 0) {
|
||
|
|
console.warn('GeoJSON sin features');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
geojson.features.forEach(feature => {
|
||
|
|
// Crear capa GeoJSON para este feature individual
|
||
|
|
const tempLayer = L.geoJSON(feature, {
|
||
|
|
style: { color: color, weight: 3, opacity: 0.8, fillOpacity: 0.3 },
|
||
|
|
onEachFeature: (f, l) => {
|
||
|
|
l.feature = f; // guardar propiedades originales
|
||
|
|
const props = f.properties;
|
||
|
|
const content = `<b>${props.name || 'Elemento'}</b><br>
|
||
|
|
Progreso: ${props.progress || 0}%<br>
|
||
|
|
Responsable: ${props.responsible || '-'}`;
|
||
|
|
l.bindPopup(content);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
// L.geoJSON devuelve un FeatureGroup, extraemos cada capa hija
|
||
|
|
tempLayer.eachLayer(subLayer => {
|
||
|
|
subLayer.feature = feature;
|
||
|
|
drawnItems.addLayer(subLayer);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Ajustar zoom a los elementos
|
||
|
|
if (drawnItems.getLayers().length > 0) {
|
||
|
|
const bounds = drawnItems.getBounds();
|
||
|
|
if (bounds.isValid()) {
|
||
|
|
mapInstance.fitBounds(bounds);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Guardar todos los elementos (modificados y nuevos)
|
||
|
|
function saveAllLayers() {
|
||
|
|
if (!drawnItems) return;
|
||
|
|
const features = [];
|
||
|
|
drawnItems.eachLayer(layer => {
|
||
|
|
const feature = extractFeatureFromLayer(layer);
|
||
|
|
if (feature) features.push(feature);
|
||
|
|
});
|
||
|
|
if (features.length === 0) {
|
||
|
|
alert('No hay elementos para guardar.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const finalGeojson = {
|
||
|
|
type: 'FeatureCollection',
|
||
|
|
features: features,
|
||
|
|
style: { color: document.querySelector('input[type="color"]')?.value || '#3b82f6' }
|
||
|
|
};
|
||
|
|
console.log('Guardando GeoJSON:', finalGeojson);
|
||
|
|
@this.saveManualGeojson(JSON.stringify(finalGeojson));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extraer geometría y propiedades de una capa Leaflet
|
||
|
|
function extractFeatureFromLayer(layer) {
|
||
|
|
let geojson = layer.toGeoJSON();
|
||
|
|
if (geojson && geojson.geometry) {
|
||
|
|
// Si la capa tiene propiedades asociadas, las conservamos
|
||
|
|
if (!geojson.properties && layer.feature?.properties) {
|
||
|
|
geojson.properties = layer.feature.properties;
|
||
|
|
} else if (!geojson.properties) {
|
||
|
|
geojson.properties = { name: 'Elemento', progress: 0, responsible: '' };
|
||
|
|
}
|
||
|
|
return geojson;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function setupSaveButton() {
|
||
|
|
const btn = document.getElementById('saveDrawingBtn');
|
||
|
|
if (btn && !btn.hasAttribute('data-listener')) {
|
||
|
|
btn.setAttribute('data-listener', 'true');
|
||
|
|
btn.addEventListener('click', saveAllLayers);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Inicialización con Livewire
|
||
|
|
document.addEventListener('livewire:init', () => {
|
||
|
|
console.log('Livewire inicializado');
|
||
|
|
initializeMap();
|
||
|
|
setupSaveButton();
|
||
|
|
// Escuchar selección de capa (Livewire v3)
|
||
|
|
Livewire.on('layerSelectedForEdit', (data) => {
|
||
|
|
const payload = Array.isArray(data) ? data[0] : data;
|
||
|
|
console.log('Evento layerSelectedForEdit recibido:', payload);
|
||
|
|
if (!mapInitialized) {
|
||
|
|
setTimeout(() => Livewire.dispatch('layerSelectedForEdit', payload), 200);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (payload && payload.geojson) {
|
||
|
|
loadGeoJSONLayer(payload.geojson, payload.color);
|
||
|
|
} else {
|
||
|
|
clearAllLayers();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Para navegación SPA
|
||
|
|
document.addEventListener('livewire:navigated', () => {
|
||
|
|
if (!mapInitialized) initializeMap();
|
||
|
|
setupSaveButton();
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
@endpush
|