Spaces:
Running
Running
<html lang="es"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Foto a KML</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
@keyframes fadeIn { | |
from { opacity: 0; transform: translateY(10px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
.fade-in { | |
animation: fadeIn 0.5s ease-out forwards; | |
} | |
#preview { | |
transform: scaleX(-1); /* Espejo para selfie */ | |
} | |
.watermark { | |
text-shadow: 1px 1px 2px black, -1px -1px 2px black; | |
} | |
#map { | |
height: 300px; | |
width: 100%; | |
border-radius: 0.5rem; | |
} | |
.leaflet-container { | |
background: #1e293b; | |
} | |
.category-selector { | |
display: grid; | |
grid-template-columns: repeat(3, 1fr); | |
gap: 0.5rem; | |
} | |
.category-option { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
padding: 0.75rem; | |
border-radius: 0.5rem; | |
background-color: #334155; | |
cursor: pointer; | |
transition: all 0.2s; | |
border: 2px solid transparent; | |
} | |
.category-option:hover { | |
background-color: #3b82f6; | |
transform: translateY(-2px); | |
} | |
.category-option.selected { | |
background-color: #3b82f6; | |
border-color: white; | |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2); | |
} | |
.banner-container { | |
width: 100%; | |
height: 150px; | |
overflow: hidden; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
background-color: #0f172a; | |
margin-bottom: 1.5rem; | |
border-radius: 0.5rem; | |
background-image: url('https://huggingface.co./spaces/bytedance-research/UNO-FLUX/discussions/6'); | |
background-size: cover; | |
background-position: center; | |
position: relative; | |
} | |
.banner-container::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background: rgba(0, 0, 0, 0.5); | |
} | |
.banner-content { | |
position: relative; | |
z-index: 1; | |
text-align: center; | |
width: 100%; | |
} | |
.banner-title { | |
font-size: 2.5rem; | |
font-weight: bold; | |
color: white; | |
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); | |
margin-bottom: 0.5rem; | |
} | |
.banner-subtitle { | |
font-size: 1.2rem; | |
color: #e2e8f0; | |
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); | |
} | |
body { | |
background-image: url('https://huggingface.co./spaces/bytedance-research/UNO-FLUX/discussions/6'); | |
background-size: cover; | |
background-attachment: fixed; | |
background-position: center; | |
} | |
.container { | |
background-color: rgba(15, 23, 42, 0.9); | |
border-radius: 0.5rem; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); | |
margin-top: 2rem; | |
margin-bottom: 2rem; | |
} | |
</style> | |
<!-- Leaflet CSS --> | |
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" /> | |
</head> | |
<body class="min-h-screen font-sans"> | |
<div class="container mx-auto px-4 py-8 max-w-3xl"> | |
<!-- Banner Header --> | |
<div class="banner-container fade-in"> | |
<div class="banner-content"> | |
<h1 class="banner-title">Foto a KML</h1> | |
<p class="banner-subtitle">Georreferenciación de imágenes</p> | |
</div> | |
</div> | |
<!-- Header --> | |
<header class="mb-8 fade-in"> | |
<div class="flex justify-between items-center"> | |
<div> | |
<p class="text-slate-400">Registro fotográfico georreferenciado</p> | |
</div> | |
<div class="flex items-center space-x-2"> | |
<div id="gps-status" class="flex items-center text-sm"> | |
<div class="w-3 h-3 rounded-full bg-rose-500 mr-2"></div> | |
<span>GPS: Desconectado</span> | |
</div> | |
</div> | |
</div> | |
</header> | |
<!-- Main Content --> | |
<div class="grid grid-cols-1 gap-8"> | |
<!-- User Input Section --> | |
<div class="bg-slate-800 rounded-xl p-6 shadow-xl fade-in"> | |
<h2 class="text-xl font-semibold mb-4 flex items-center"> | |
<i class="fas fa-user text-blue-400 mr-2"></i> | |
Registrado por | |
</h2> | |
<div class="space-y-4"> | |
<div> | |
<label for="operator-name" class="block text-sm font-medium text-slate-300 mb-1">Nombre</label> | |
<input type="text" id="operator-name" class="w-full bg-slate-700 border border-slate-600 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 transition" placeholder="Ej: Juan Pérez" required> | |
</div> | |
<div> | |
<label for="contract-id" class="block text-sm font-medium text-slate-300 mb-1">Descripción breve</label> | |
<input type="text" id="contract-id" class="w-full bg-slate-700 border border-slate-600 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 transition" placeholder="Ej: Inspección de área"> | |
</div> | |
<div> | |
<label class="block text-sm font-medium text-slate-300 mb-2">Área</label> | |
<div class="category-selector"> | |
<div class="category-option" data-category="ambiente"> | |
<i class="fas fa-leaf category-icon"></i> | |
<span>Medio Ambiente</span> | |
</div> | |
<div class="category-option" data-category="sso"> | |
<i class="fas fa-hard-hat category-icon"></i> | |
<span>SSO</span> | |
</div> | |
<div class="category-option" data-category="calidad"> | |
<i class="fas fa-award category-icon"></i> | |
<span>Calidad</span> | |
</div> | |
</div> | |
<input type="hidden" id="selected-category" value=""> | |
</div> | |
</div> | |
</div> | |
<!-- Camera Section --> | |
<div class="bg-slate-800 rounded-xl p-6 shadow-xl fade-in"> | |
<h2 class="text-xl font-semibold mb-4 flex items-center"> | |
<i class="fas fa-camera text-blue-400 mr-2"></i> | |
Cámara | |
</h2> | |
<div class="relative"> | |
<!-- Camera Preview --> | |
<div id="camera-container" class="relative rounded-lg overflow-hidden bg-slate-900"> | |
<video id="preview" autoplay muted class="w-full h-auto"></video> | |
<canvas id="canvas" class="hidden"></canvas> | |
<!-- Watermark Overlay --> | |
<div id="watermark-overlay" class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent"> | |
<div id="watermark-text" class="text-white text-sm watermark"> | |
<div id="watermark-name"></div> | |
<div id="watermark-coords" class="mt-1"></div> | |
<div id="watermark-date" class="mt-1"></div> | |
</div> | |
</div> | |
</div> | |
<!-- Capture Button --> | |
<div class="flex justify-center mt-4"> | |
<button id="capture-btn" class="w-16 h-16 rounded-full bg-blue-600 hover:bg-blue-700 text-white flex items-center justify-center shadow-lg transition transform hover:scale-105"> | |
<i class="fas fa-camera text-2xl"></i> | |
</button> | |
</div> | |
</div> | |
<!-- Photo Gallery --> | |
<div id="photo-gallery" class="mt-6 grid grid-cols-3 gap-2 hidden"> | |
<h3 class="col-span-3 text-lg font-medium mb-2">Fotos tomadas:</h3> | |
<!-- Photos will be added here dynamically --> | |
</div> | |
</div> | |
<!-- Map Section --> | |
<div class="bg-slate-800 rounded-xl p-6 shadow-xl fade-in"> | |
<h2 class="text-xl font-semibold mb-4 flex items-center"> | |
<i class="fas fa-map-marked-alt text-blue-400 mr-2"></i> | |
Ubicación | |
</h2> | |
<div id="map"></div> | |
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4"> | |
<div class="bg-slate-700/50 p-3 rounded-lg"> | |
<div class="text-sm text-slate-400">Coordenadas UTM</div> | |
<div id="utm-coords" class="font-mono text-blue-400">Esperando GPS...</div> | |
</div> | |
<div class="bg-slate-700/50 p-3 rounded-lg"> | |
<div class="text-sm text-slate-400">Huso</div> | |
<div id="utm-zone" class="font-mono text-blue-400">H18S (WGS84)</div> | |
</div> | |
</div> | |
</div> | |
<!-- Export Section --> | |
<div class="bg-slate-800 rounded-xl p-6 shadow-xl fade-in"> | |
<h2 class="text-xl font-semibold mb-4 flex items-center"> | |
<i class="fas fa-file-export text-blue-400 mr-2"></i> | |
Exportar | |
</h2> | |
<div class="flex flex-wrap gap-3"> | |
<button id="export-kml" class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-medium transition flex items-center disabled:opacity-50" disabled> | |
<i class="fas fa-map-marked-alt mr-2"></i> Exportar KML | |
</button> | |
<button id="export-zip" class="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-lg font-medium transition flex items-center disabled:opacity-50" disabled> | |
<i class="fas fa-file-archive mr-2"></i> Exportar ZIP | |
</button> | |
<button id="clear-all" class="px-4 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-lg font-medium transition flex items-center"> | |
<i class="fas fa-trash-alt mr-2"></i> Limpiar todo | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Photo Modal --> | |
<div id="photo-modal" class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 hidden"> | |
<div class="bg-slate-800 rounded-xl p-4 w-full max-w-2xl mx-4 shadow-2xl fade-in"> | |
<div class="flex justify-between items-center mb-4"> | |
<h3 class="text-xl font-semibold">Vista previa de foto</h3> | |
<button id="close-photo-modal" class="text-slate-400 hover:text-white"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="flex flex-col md:flex-row gap-4"> | |
<div class="flex-1"> | |
<img id="modal-photo" src="" alt="Foto" class="w-full h-auto rounded-lg"> | |
</div> | |
<div class="flex-1 bg-slate-700/50 p-4 rounded-lg"> | |
<h4 class="font-medium mb-2">Metadatos:</h4> | |
<div id="photo-metadata" class="space-y-2 text-sm"> | |
<!-- Metadata will be added here --> | |
</div> | |
<button id="delete-photo" class="mt-4 px-3 py-1 bg-rose-600 hover:bg-rose-700 text-white rounded-lg text-sm font-medium transition"> | |
<i class="fas fa-trash-alt mr-1"></i> Eliminar foto | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Leaflet JS --> | |
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script> | |
<!-- JSZip --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> | |
<!-- FileSaver --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Variables | |
let photos = []; | |
let currentStream = null; | |
let currentPosition = null; | |
let map = null; | |
let marker = null; | |
let watchId = null; | |
let selectedCategory = ''; | |
// DOM Elements | |
const preview = document.getElementById('preview'); | |
const canvas = document.getElementById('canvas'); | |
const captureBtn = document.getElementById('capture-btn'); | |
const photoGallery = document.getElementById('photo-gallery'); | |
const operatorNameInput = document.getElementById('operator-name'); | |
const contractIdInput = document.getElementById('contract-id'); | |
const watermarkName = document.getElementById('watermark-name'); | |
const watermarkCoords = document.getElementById('watermark-coords'); | |
const watermarkDate = document.getElementById('watermark-date'); | |
const gpsStatus = document.getElementById('gps-status'); | |
const utmCoords = document.getElementById('utm-coords'); | |
const utmZone = document.getElementById('utm-zone'); | |
const exportKmlBtn = document.getElementById('export-kml'); | |
const exportZipBtn = document.getElementById('export-zip'); | |
const clearAllBtn = document.getElementById('clear-all'); | |
const photoModal = document.getElementById('photo-modal'); | |
const modalPhoto = document.getElementById('modal-photo'); | |
const photoMetadata = document.getElementById('photo-metadata'); | |
const closePhotoModal = document.getElementById('close-photo-modal'); | |
const deletePhotoBtn = document.getElementById('delete-photo'); | |
const categoryOptions = document.querySelectorAll('.category-option'); | |
const selectedCategoryInput = document.getElementById('selected-category'); | |
// Constants for UTM Zone 18H (Sur) | |
const UTM_ZONE = 18; | |
const UTM_ZONE_LETTER = 'S'; // 'H' para hemisferio sur | |
const FALSE_EASTING = 500000; // Standard false easting for UTM | |
const SCALE_FACTOR = 0.9996; // Standard scale factor for UTM | |
const EQUATORIAL_RADIUS = 6378137.0; // WGS84 equatorial radius in meters | |
const FLATTENING = 1 / 298.257223563; // WGS84 flattening | |
const ECC_SQUARED = FLATTENING * (2 - FLATTENING); // e^2 | |
// Initialize | |
initCamera(); | |
initMap(); | |
startGPS(); | |
setupCategorySelector(); | |
// Event Listeners | |
operatorNameInput.addEventListener('input', updateWatermark); | |
captureBtn.addEventListener('click', capturePhoto); | |
exportKmlBtn.addEventListener('click', exportKML); | |
exportZipBtn.addEventListener('click', exportZIP); | |
clearAllBtn.addEventListener('click', clearAll); | |
closePhotoModal.addEventListener('click', () => photoModal.classList.add('hidden')); | |
// Functions | |
function setupCategorySelector() { | |
categoryOptions.forEach(option => { | |
option.addEventListener('click', function() { | |
// Remove selected class from all options | |
categoryOptions.forEach(opt => opt.classList.remove('selected')); | |
// Add selected class to clicked option | |
this.classList.add('selected'); | |
// Update selected category | |
selectedCategory = this.dataset.category; | |
selectedCategoryInput.value = selectedCategory; | |
}); | |
}); | |
} | |
function initCamera() { | |
navigator.mediaDevices.getUserMedia({ | |
video: { | |
width: { ideal: 1280 }, | |
height: { ideal: 720 }, | |
facingMode: 'environment' | |
} | |
}) | |
.then(stream => { | |
currentStream = stream; | |
preview.srcObject = stream; | |
// Set canvas size to match video | |
preview.addEventListener('loadedmetadata', () => { | |
canvas.width = preview.videoWidth; | |
canvas.height = preview.videoHeight; | |
}); | |
}) | |
.catch(err => { | |
console.error "Error al acceder a la cámara:", err); | |
alert("No se pudo acceder a la cámara. Asegúrate de dar los permisos necesarios."); | |
}); | |
} | |
function initMap() { | |
map = L.map('map').setView([-34.6037, -58.3816], 13); // Default to Buenos Aires | |
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', | |
maxZoom: 19 | |
}).addTo(map); | |
marker = L.marker([0, 0], { | |
icon: L.divIcon({ | |
className: 'custom-marker', | |
html: '<i class="fas fa-map-marker-alt text-3xl text-blue-500"></i>', | |
iconSize: [30, 30], | |
iconAnchor: [15, 30] | |
}), | |
draggable: false | |
}).addTo(map); | |
} | |
function startGPS() { | |
if (navigator.geolocation) { | |
gpsStatus.querySelector('div').classList.remove('bg-rose-500'); | |
gpsStatus.querySelector('div').classList.add('bg-amber-500'); | |
gpsStatus.querySelector('span').textContent = 'GPS: Conectando...'; | |
watchId = navigator.geolocation.watchPosition( | |
position => { | |
currentPosition = position; | |
updatePositionInfo(position); | |
updateWatermark(); | |
gpsStatus.querySelector('div').classList.remove('bg-amber-500'); | |
gpsStatus.querySelector('div').classList.add('bg-emerald-500'); | |
gpsStatus.querySelector('span').textContent = 'GPS: Conectado'; | |
}, | |
error => { | |
console.error("Error de GPS:", error); | |
gpsStatus.querySelector('div').classList.remove('bg-amber-500', 'bg-emerald-500'); | |
gpsStatus.querySelector('div').classList.add('bg-rose-500'); | |
gpsStatus.querySelector('span').textContent = 'GPS: Error'; | |
}, | |
{ | |
enableHighAccuracy: true, | |
maximumAge: 30000, | |
timeout: 27000 | |
} | |
); | |
} else { | |
alert("Geolocalización no soportada por tu navegador"); | |
} | |
} | |
function updatePositionInfo(position) { | |
const lat = position.coords.latitude; | |
const lng = position.coords.longitude; | |
// Update map | |
map.setView([lat, lng], 18); | |
marker.setLatLng([lat, lng]); | |
// Convert to UTM Zone 18H (Sur) | |
const utm = convertToUTMZone18H(lat, lng); | |
// Update UTM display | |
utmCoords.textContent = `Este: ${utm.easting.toFixed(2)}m, Norte: ${utm.northing.toFixed(2)}m`; | |
utmZone.textContent = `H${UTM_ZONE}${UTM_ZONE_LETTER} (WGS84)`; | |
return utm; | |
} | |
function convertToUTMZone18H(lat, lng) { | |
// Verificar que la longitud esté dentro del huso 18 (-78° a -72°) | |
if (lng < -78 || lng >= -72) { | |
console.warn(`Longitud ${lng}° está fuera del huso 18H (-78° a -72°)`); | |
} | |
// Convertir a radianes | |
const latRad = lat * Math.PI / 180; | |
const lngRad = lng * Math.PI / 180; | |
// Meridiano central para huso 18 (-75°) | |
const lng0Rad = (-75) * Math.PI / 180; | |
// Cálculos preliminares | |
const N = EQUATORIAL_RADIUS / Math.sqrt(1 - ECC_SQUARED * Math.sin(latRad) * Math.sin(latRad)); | |
const T = Math.tan(latRad) * Math.tan(latRad); | |
const C = ECC_SQUARED * Math.cos(latRad) * Math.cos(latRad); | |
const A = Math.cos(latRad) * (lngRad - lng0Rad); | |
// Cálculo de M (distancia meridional) | |
const M = EQUATORIAL_RADIUS * ( | |
(1 - ECC_SQUARED/4 - 3*Math.pow(ECC_SQUARED,2)/64 - 5*Math.pow(ECC_SQUARED,3)/256) * latRad | |
- (3*ECC_SQUARED/8 + 3*Math.pow(ECC_SQUARED,2)/32 + 45*Math.pow(ECC_SQUARED,3)/1024) * Math.sin(2*latRad) | |
+ (15*Math.pow(ECC_SQUARED,2)/256 + 45*Math.pow(ECC_SQUARED,3)/1024) * Math.sin(4*latRad) | |
- (35*Math.pow(ECC_SQUARed,3)/3072) * Math.sin(6*latRad) | |
); | |
// Cálculo de coordenadas UTM | |
const easting = FALSE_EASTING + SCALE_FACTOR * N * ( | |
A + (1 - T + C) * Math.pow(A,3)/6 | |
+ (5 - 18*T + Math.pow(T,2) + 72*C - 58*ECC_SQUARED) * Math.pow(A,5)/120 | |
); | |
// Para hemisferio sur, sumamos 10,000,000m al northing | |
let northing = SCALE_FACTOR * (M + N * Math.tan(latRad) * ( | |
Math.pow(A,2)/2 | |
+ (5 - T + 9*C + 4*Math.pow(C,2)) * Math.pow(A,4)/24 | |
+ (61 - 58*T + Math.pow(T,2) + 600*C - 330*ECC_SQUARED) * Math.pow(A,6)/720 | |
)); | |
// Ajuste para hemisferio sur | |
if (lat < 0) { | |
northing += 10000000; // 10,000,000m para hemisferio sur | |
} | |
return { | |
easting: easting, | |
northing: northing, | |
zone: UTM_ZONE, | |
zoneLetter: UTM_ZONE_LETTER | |
}; | |
} | |
function updateWatermark() { | |
const name = operatorNameInput.value || "No especificado"; | |
watermarkName.textContent = `Registrado por: ${name}`; | |
if (currentPosition) { | |
const utm = updatePositionInfo(currentPosition); | |
watermarkCoords.textContent = `UTM H${utm.zone}${utm.zoneLetter}: E ${utm.easting.toFixed(2)}m, N ${utm.northing.toFixed(2)}m`; | |
} else { | |
watermarkCoords.textContent = "Coordenadas no disponibles"; | |
} | |
const now = new Date(); | |
watermarkDate.textContent = now.toLocaleString(); | |
} | |
function capturePhoto() { | |
if (!currentStream) return; | |
// Validar que se haya seleccionado una categoría | |
if (!selectedCategory) { | |
alert("Por favor selecciona un área antes de tomar la foto"); | |
return; | |
} | |
// Get canvas context | |
const context = canvas.getContext('2d'); | |
// Draw current frame to canvas | |
context.drawImage(preview, 0, 0, canvas.width, canvas.height); | |
// Add watermark text directly to canvas | |
context.font = '16px Arial'; | |
context.fillStyle = 'white'; | |
context.textAlign = 'left'; | |
context.textBaseline = 'bottom'; | |
const name = operatorNameInput.value || "No especificado"; | |
const description = contractIdInput.value || "Sin descripción"; | |
if (currentPosition) { | |
const utm = updatePositionInfo(currentPosition); | |
const now = new Date(); | |
// Draw watermark text with shadow effect | |
context.shadowColor = 'black'; | |
context.shadowBlur = 5; | |
context.fillText(`Registrado por: ${name}`, 20, canvas.height - 80); | |
context.fillText(`Descripción: ${description}`, 20, canvas.height - 60); | |
context.fillText(`Área: ${getCategoryName(selectedCategory)}`, 20, canvas.height - 40); | |
context.fillText(`UTM H${utm.zone}${utm.zoneLetter}: E ${utm.easting.toFixed(2)}m, N ${utm.northing.toFixed(2)}m`, 20, canvas.height - 20); | |
context.fillText(now.toLocaleString(), 20, canvas.height - 5); | |
context.shadowBlur = 0; | |
// Create photo object with metadata | |
const photo = { | |
id: Date.now(), | |
dataUrl: canvas.toDataURL('image/jpeg', 0.9), | |
timestamp: now.toISOString(), | |
operator: name, | |
description: description, | |
category: selectedCategory, | |
coordinates: { | |
lat: currentPosition.coords.latitude, | |
lng: currentPosition.coords.longitude, | |
utm: utm | |
} | |
}; | |
photos.push(photo); | |
savePhotos(); | |
renderPhotoGallery(); | |
updateExportButtons(); | |
// Show success animation | |
captureBtn.innerHTML = '<i class="fas fa-check"></i>'; | |
captureBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700'); | |
captureBtn.classList.add('bg-emerald-600', 'hover:bg-emerald-700'); | |
setTimeout(() => { | |
captureBtn.innerHTML = '<i class="fas fa-camera"></i>'; | |
captureBtn.classList.remove('bg-emerald-600', 'hover:bg-emerald-700'); | |
captureBtn.classList.add('bg-blue-600', 'hover:bg-blue-700'); | |
}, 1000); | |
} else { | |
alert("No se pudo obtener la ubicación GPS. Por favor espera a que se conecte."); | |
} | |
} | |
function getCategoryName(categoryCode) { | |
switch(categoryCode) { | |
case 'ambiente': return 'Medio Ambiente'; | |
case 'sso': return 'SSO'; | |
case 'calidad': return 'Calidad'; | |
default: return 'No especificado'; | |
} | |
} | |
function getCategoryIcon(categoryCode) { | |
switch(categoryCode) { | |
case 'ambiente': return 'leaf'; | |
case 'sso': return 'hard-hat'; | |
case 'calidad': return 'award'; | |
default: return 'question-circle'; | |
} | |
} | |
function renderPhotoGallery() { | |
if (photos.length === 0) { | |
photoGallery.classList.add('hidden'); | |
return; | |
} | |
photoGallery.classList.remove('hidden'); | |
photoGallery.innerHTML = '<h3 class="col-span-3 text-lg font-medium mb-2">Fotos tomadas:</h3>'; | |
photos.forEach((photo, index) => { | |
const photoElement = document.createElement('div'); | |
photoElement.className = 'relative group cursor-pointer'; | |
photoElement.innerHTML = ` | |
<img src="${photo.dataUrl}" alt="Foto ${index + 1}" class="w-full h-24 object-cover rounded-lg"> | |
<div class="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition"> | |
<i class="fas fa-${getCategoryIcon(photo.category)} text-white"></i> | |
</div> | |
`; | |
photoElement.addEventListener('click', () => showPhotoModal(photo)); | |
photoGallery.appendChild(photoElement); | |
}); | |
} | |
function showPhotoModal(photo) { | |
modalPhoto.src = photo.dataUrl; | |
// Format metadata | |
const date = new Date(photo.timestamp); | |
photoMetadata.innerHTML = ` | |
<div><strong>Registrado por:</strong> ${photo.operator}</div> | |
<div><strong>Descripción:</strong> ${photo.description || "No especificado"}</div> | |
<div><strong>Área:</strong> ${getCategoryName(photo.category)}</div> | |
<div><strong>Fecha:</strong> ${date.toLocaleString()}</div> | |
<div><strong>Coordenadas:</strong></div> | |
<div class="pl-4">Lat: ${photo.coordinates.lat.toFixed(6)}</div> | |
<div class="pl-4">Lng: ${photo.coordinates.lng.toFixed(6)}</div> | |
<div class="pl-4">UTM H${photo.coordinates.utm.zone}${photo.coordinates.utm.zoneLetter}: E ${photo.coordinates.utm.easting.toFixed(2)}m, N ${photo.coordinates.utm.northing.toFixed(2)}m</div> | |
`; | |
// Set up delete button | |
deletePhotoBtn.onclick = () => { | |
photos = photos.filter(p => p.id !== photo.id); | |
savePhotos(); | |
renderPhotoGallery(); | |
updateExportButtons(); | |
photoModal.classList.add('hidden'); | |
}; | |
photoModal.classList.remove('hidden'); | |
} | |
function exportKML() { | |
if (photos.length === 0) return; | |
// Group photos by category | |
const photosByCategory = {}; | |
photos.forEach(photo => { | |
if (!photosByCategory[photo.category]) { | |
photosByCategory[photo.category] = []; | |
} | |
photosByCategory[photo.category].push(photo); | |
}); | |
// Create KML content for each category | |
let kml = `<?xml version="1.0" encoding="UTF-8"?> | |
<kml xmlns="http://www.opengis.net/kml/2.2"> | |
<Document> | |
<name>Registros Georreferenciados</name> | |
<description>Fotos tomadas con Foto a KML</description>`; | |
// Add a folder for each category | |
Object.keys(photosByCategory).forEach(category => { | |
const categoryName = getCategoryName(category); | |
kml += ` | |
<Folder> | |
<name>${categoryName}</name> | |
<description>Registros de ${categoryName}</description>`; | |
photosByCategory[category].forEach(photo => { | |
// Convert image to base64 without data URL prefix | |
const base64Image = photo.dataUrl.split(',')[1]; | |
const date = new Date(photo.timestamp); | |
const dateStr = `${date.getDate().toString().padStart(2, '0')}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getFullYear()}`; | |
kml += ` | |
<Placemark> | |
<name>${categoryName} - ${photo.description || "Sin descripción"} - ${dateStr}</name> | |
<description> | |
<![CDATA[ | |
<h3>Registro de ${categoryName}</h3> | |
<p><strong>Registrado por:</strong> ${photo.operator}</p> | |
<p><strong>Descripción:</strong> ${photo.description || "No especificado"}</p> | |
<p><strong>Fecha:</strong> ${date.toLocaleString()}</p> | |
<p><strong>Coordenadas UTM:</strong> H${photo.coordinates.utm.zone}${photo.coordinates.utm.zoneLetter} E ${photo.coordinates.utm.easting.toFixed(2)}m, N ${photo.coordinates.utm.northing.toFixed(2)}m</p> | |
<img src="data:image/jpeg;base64,${base64Image}" width="400" /> | |
]]> | |
</description> | |
<Point> | |
<coordinates>${photo.coordinates.lng},${photo.coordinates.lat},0</coordinates> | |
</Point> | |
</Placemark>`; | |
}); | |
kml += ` | |
</Folder>`; | |
}); | |
kml += ` | |
</Document> | |
</kml>`; | |
// Generate filename with current date | |
const now = new Date(); | |
const dateStr = `${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}`; | |
const filename = `Registros_${dateStr}.kml`; | |
// Create blob and download | |
const blob = new Blob([kml], { type: 'application/vnd.google-earth.kml+xml' }); | |
saveAs(blob, filename); | |
} | |
function exportZIP() { | |
if (photos.length === 0) return; | |
const zip = new JSZip(); | |
const imgFolder = zip.folder("fotos"); | |
const kmlFolder = zip.folder("kml"); | |
// Group photos by category | |
const photosByCategory = {}; | |
photos.forEach(photo => { | |
if (!photosByCategory[photo.category]) { | |
photosByCategory[photo.category] = []; | |
} | |
photosByCategory[photo.category].push(photo); | |
}); | |
// Add photos to folders by category | |
Object.keys(photosByCategory).forEach(category => { | |
const categoryFolder = imgFolder.folder(category); | |
photosByCategory[category].forEach((photo, index) => { | |
// Extract base64 data | |
const base64Data = photo.dataUrl.split(',')[1]; | |
categoryFolder.file(`foto_${index + 1}.jpg`, base64Data, { base64: true }); | |
}); | |
}); | |
// Create and add KML for each category | |
Object.keys(photosByCategory).forEach(category => { | |
const categoryName = getCategoryName(category); | |
let kml = `<?xml version="1.0" encoding="UTF-8"?> | |
<kml xmlns="http://www.opengis.net/kml/2.2"> | |
<Document> | |
<name>Registros de ${categoryName}</name> | |
<description>Fotos tomadas con Foto a KML</description>`; | |
photosByCategory[category].forEach((photo, index) => { | |
const date = new Date(photo.timestamp); | |
const dateStr = `${date.getDate().toString().padStart(2, '0')}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getFullYear()}`; | |
kml += ` | |
<Placemark> | |
<name>${categoryName} - ${photo.description || "Sin descripción"} - ${dateStr}</name> | |
<description> | |
<![CDATA[ | |
<h3>Registro de ${categoryName}</h3> | |
<p><strong>Registrado por:</strong> ${photo.operator}</p> | |
<p><strong>Descripción:</strong> ${photo.description || "No especificado"}</p> | |
<p><strong>Fecha:</strong> ${date.toLocaleString()}</p> | |
<p><strong>Coordenadas UTM:</strong> H${photo.coordinates.utm.zone}${photo.coordinates.utm.zoneLetter} E ${photo.coordinates.utm.easting.toFixed(2)}m, N ${photo.coordinates.utm.northing.toFixed(2)}m</p> | |
<img src="../fotos/${category}/foto_${index + 1}.jpg" width="400" /> | |
]]> | |
</description> | |
<Point> | |
<coordinates>${photo.coordinates.lng},${photo.coordinates.lat},0</coordinates> | |
</Point> | |
</Placemark>`; | |
}); | |
kml += ` | |
</Document> | |
</kml>`; | |
kmlFolder.file(`registros_${category}.kml`, kml); | |
}); | |
// Generate ZIP and download | |
const now = new Date(); | |
const dateStr = `${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}`; | |
const filename = `Registros_${dateStr}.zip`; | |
zip.generateAsync({ type: "blob" }).then(content => { | |
saveAs(content, filename); | |
}); | |
} | |
function clearAll() { | |
if (confirm("¿Estás seguro de que deseas eliminar todas las fotos?")) { | |
photos = []; | |
savePhotos(); | |
renderPhotoGallery(); | |
updateExportButtons(); | |
} | |
} | |
function savePhotos() { | |
localStorage.setItem('geoPhotos', JSON.stringify(photos)); | |
} | |
function loadPhotos() { | |
const savedPhotos = localStorage.getItem('geoPhotos'); | |
if (savedPhotos) { | |
photos = JSON.parse(savedPhotos); | |
renderPhotoGallery(); | |
updateExportButtons(); | |
} | |
} | |
function updateExportButtons() { | |
exportKmlBtn.disabled = photos.length === 0; | |
exportZipBtn.disabled = photos.length === 0; | |
} | |
// Initialize from storage | |
loadPhotos(); | |
// Update watermark periodically | |
setInterval(updateWatermark, 1000); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=ignaciomdr/geocam" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |