ignaciomdr commited on
Commit
65669c9
verified
1 Parent(s): f45d8ed

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +882 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Geocam
3
- emoji: 馃彚
4
- colorFrom: red
5
- colorTo: green
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: geocam
3
+ emoji: 馃惓
4
+ colorFrom: yellow
5
+ colorTo: red
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,882 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Foto a KML</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ @keyframes fadeIn {
11
+ from { opacity: 0; transform: translateY(10px); }
12
+ to { opacity: 1; transform: translateY(0); }
13
+ }
14
+
15
+ .fade-in {
16
+ animation: fadeIn 0.5s ease-out forwards;
17
+ }
18
+
19
+ #preview {
20
+ transform: scaleX(-1); /* Espejo para selfie */
21
+ }
22
+
23
+ .watermark {
24
+ text-shadow: 1px 1px 2px black, -1px -1px 2px black;
25
+ }
26
+
27
+ #map {
28
+ height: 300px;
29
+ width: 100%;
30
+ border-radius: 0.5rem;
31
+ }
32
+
33
+ .leaflet-container {
34
+ background: #1e293b;
35
+ }
36
+
37
+ .category-selector {
38
+ display: grid;
39
+ grid-template-columns: repeat(3, 1fr);
40
+ gap: 0.5rem;
41
+ }
42
+
43
+ .category-option {
44
+ display: flex;
45
+ flex-direction: column;
46
+ align-items: center;
47
+ justify-content: center;
48
+ padding: 0.75rem;
49
+ border-radius: 0.5rem;
50
+ background-color: #334155;
51
+ cursor: pointer;
52
+ transition: all 0.2s;
53
+ border: 2px solid transparent;
54
+ }
55
+
56
+ .category-option:hover {
57
+ background-color: #3b82f6;
58
+ transform: translateY(-2px);
59
+ }
60
+
61
+ .category-option.selected {
62
+ background-color: #3b82f6;
63
+ border-color: white;
64
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
65
+ }
66
+
67
+ .banner-container {
68
+ width: 100%;
69
+ height: 150px;
70
+ overflow: hidden;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ background-color: #0f172a;
75
+ margin-bottom: 1.5rem;
76
+ border-radius: 0.5rem;
77
+ background-image: url('https://huggingface.co/spaces/bytedance-research/UNO-FLUX/discussions/6');
78
+ background-size: cover;
79
+ background-position: center;
80
+ position: relative;
81
+ }
82
+
83
+ .banner-container::before {
84
+ content: '';
85
+ position: absolute;
86
+ top: 0;
87
+ left: 0;
88
+ right: 0;
89
+ bottom: 0;
90
+ background: rgba(0, 0, 0, 0.5);
91
+ }
92
+
93
+ .banner-content {
94
+ position: relative;
95
+ z-index: 1;
96
+ text-align: center;
97
+ width: 100%;
98
+ }
99
+
100
+ .banner-title {
101
+ font-size: 2.5rem;
102
+ font-weight: bold;
103
+ color: white;
104
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
105
+ margin-bottom: 0.5rem;
106
+ }
107
+
108
+ .banner-subtitle {
109
+ font-size: 1.2rem;
110
+ color: #e2e8f0;
111
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
112
+ }
113
+
114
+ body {
115
+ background-image: url('https://huggingface.co/spaces/bytedance-research/UNO-FLUX/discussions/6');
116
+ background-size: cover;
117
+ background-attachment: fixed;
118
+ background-position: center;
119
+ }
120
+
121
+ .container {
122
+ background-color: rgba(15, 23, 42, 0.9);
123
+ border-radius: 0.5rem;
124
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
125
+ margin-top: 2rem;
126
+ margin-bottom: 2rem;
127
+ }
128
+ </style>
129
+ <!-- Leaflet CSS -->
130
+ <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
131
+ </head>
132
+ <body class="min-h-screen font-sans">
133
+ <div class="container mx-auto px-4 py-8 max-w-3xl">
134
+ <!-- Banner Header -->
135
+ <div class="banner-container fade-in">
136
+ <div class="banner-content">
137
+ <h1 class="banner-title">Foto a KML</h1>
138
+ <p class="banner-subtitle">Georreferenciaci贸n de im谩genes</p>
139
+ </div>
140
+ </div>
141
+
142
+ <!-- Header -->
143
+ <header class="mb-8 fade-in">
144
+ <div class="flex justify-between items-center">
145
+ <div>
146
+ <p class="text-slate-400">Registro fotogr谩fico georreferenciado</p>
147
+ </div>
148
+ <div class="flex items-center space-x-2">
149
+ <div id="gps-status" class="flex items-center text-sm">
150
+ <div class="w-3 h-3 rounded-full bg-rose-500 mr-2"></div>
151
+ <span>GPS: Desconectado</span>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </header>
156
+
157
+ <!-- Main Content -->
158
+ <div class="grid grid-cols-1 gap-8">
159
+ <!-- User Input Section -->
160
+ <div class="bg-slate-800 rounded-xl p-6 shadow-xl fade-in">
161
+ <h2 class="text-xl font-semibold mb-4 flex items-center">
162
+ <i class="fas fa-user text-blue-400 mr-2"></i>
163
+ Registrado por
164
+ </h2>
165
+ <div class="space-y-4">
166
+ <div>
167
+ <label for="operator-name" class="block text-sm font-medium text-slate-300 mb-1">Nombre</label>
168
+ <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>
169
+ </div>
170
+
171
+ <div>
172
+ <label for="contract-id" class="block text-sm font-medium text-slate-300 mb-1">Descripci贸n breve</label>
173
+ <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">
174
+ </div>
175
+
176
+ <div>
177
+ <label class="block text-sm font-medium text-slate-300 mb-2">脕rea</label>
178
+ <div class="category-selector">
179
+ <div class="category-option" data-category="ambiente">
180
+ <i class="fas fa-leaf category-icon"></i>
181
+ <span>Medio Ambiente</span>
182
+ </div>
183
+ <div class="category-option" data-category="sso">
184
+ <i class="fas fa-hard-hat category-icon"></i>
185
+ <span>SSO</span>
186
+ </div>
187
+ <div class="category-option" data-category="calidad">
188
+ <i class="fas fa-award category-icon"></i>
189
+ <span>Calidad</span>
190
+ </div>
191
+ </div>
192
+ <input type="hidden" id="selected-category" value="">
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Camera Section -->
198
+ <div class="bg-slate-800 rounded-xl p-6 shadow-xl fade-in">
199
+ <h2 class="text-xl font-semibold mb-4 flex items-center">
200
+ <i class="fas fa-camera text-blue-400 mr-2"></i>
201
+ C谩mara
202
+ </h2>
203
+
204
+ <div class="relative">
205
+ <!-- Camera Preview -->
206
+ <div id="camera-container" class="relative rounded-lg overflow-hidden bg-slate-900">
207
+ <video id="preview" autoplay muted class="w-full h-auto"></video>
208
+ <canvas id="canvas" class="hidden"></canvas>
209
+
210
+ <!-- Watermark Overlay -->
211
+ <div id="watermark-overlay" class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent">
212
+ <div id="watermark-text" class="text-white text-sm watermark">
213
+ <div id="watermark-name"></div>
214
+ <div id="watermark-coords" class="mt-1"></div>
215
+ <div id="watermark-date" class="mt-1"></div>
216
+ </div>
217
+ </div>
218
+ </div>
219
+
220
+ <!-- Capture Button -->
221
+ <div class="flex justify-center mt-4">
222
+ <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">
223
+ <i class="fas fa-camera text-2xl"></i>
224
+ </button>
225
+ </div>
226
+ </div>
227
+
228
+ <!-- Photo Gallery -->
229
+ <div id="photo-gallery" class="mt-6 grid grid-cols-3 gap-2 hidden">
230
+ <h3 class="col-span-3 text-lg font-medium mb-2">Fotos tomadas:</h3>
231
+ <!-- Photos will be added here dynamically -->
232
+ </div>
233
+ </div>
234
+
235
+ <!-- Map Section -->
236
+ <div class="bg-slate-800 rounded-xl p-6 shadow-xl fade-in">
237
+ <h2 class="text-xl font-semibold mb-4 flex items-center">
238
+ <i class="fas fa-map-marked-alt text-blue-400 mr-2"></i>
239
+ Ubicaci贸n
240
+ </h2>
241
+
242
+ <div id="map"></div>
243
+
244
+ <div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
245
+ <div class="bg-slate-700/50 p-3 rounded-lg">
246
+ <div class="text-sm text-slate-400">Coordenadas UTM</div>
247
+ <div id="utm-coords" class="font-mono text-blue-400">Esperando GPS...</div>
248
+ </div>
249
+
250
+ <div class="bg-slate-700/50 p-3 rounded-lg">
251
+ <div class="text-sm text-slate-400">Huso</div>
252
+ <div id="utm-zone" class="font-mono text-blue-400">H18S (WGS84)</div>
253
+ </div>
254
+ </div>
255
+ </div>
256
+
257
+ <!-- Export Section -->
258
+ <div class="bg-slate-800 rounded-xl p-6 shadow-xl fade-in">
259
+ <h2 class="text-xl font-semibold mb-4 flex items-center">
260
+ <i class="fas fa-file-export text-blue-400 mr-2"></i>
261
+ Exportar
262
+ </h2>
263
+
264
+ <div class="flex flex-wrap gap-3">
265
+ <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>
266
+ <i class="fas fa-map-marked-alt mr-2"></i> Exportar KML
267
+ </button>
268
+
269
+ <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>
270
+ <i class="fas fa-file-archive mr-2"></i> Exportar ZIP
271
+ </button>
272
+
273
+ <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">
274
+ <i class="fas fa-trash-alt mr-2"></i> Limpiar todo
275
+ </button>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+
281
+ <!-- Photo Modal -->
282
+ <div id="photo-modal" class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 hidden">
283
+ <div class="bg-slate-800 rounded-xl p-4 w-full max-w-2xl mx-4 shadow-2xl fade-in">
284
+ <div class="flex justify-between items-center mb-4">
285
+ <h3 class="text-xl font-semibold">Vista previa de foto</h3>
286
+ <button id="close-photo-modal" class="text-slate-400 hover:text-white">
287
+ <i class="fas fa-times"></i>
288
+ </button>
289
+ </div>
290
+
291
+ <div class="flex flex-col md:flex-row gap-4">
292
+ <div class="flex-1">
293
+ <img id="modal-photo" src="" alt="Foto" class="w-full h-auto rounded-lg">
294
+ </div>
295
+
296
+ <div class="flex-1 bg-slate-700/50 p-4 rounded-lg">
297
+ <h4 class="font-medium mb-2">Metadatos:</h4>
298
+ <div id="photo-metadata" class="space-y-2 text-sm">
299
+ <!-- Metadata will be added here -->
300
+ </div>
301
+
302
+ <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">
303
+ <i class="fas fa-trash-alt mr-1"></i> Eliminar foto
304
+ </button>
305
+ </div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+
310
+ <!-- Leaflet JS -->
311
+ <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
312
+ <!-- JSZip -->
313
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
314
+ <!-- FileSaver -->
315
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
316
+
317
+ <script>
318
+ document.addEventListener('DOMContentLoaded', function() {
319
+ // Variables
320
+ let photos = [];
321
+ let currentStream = null;
322
+ let currentPosition = null;
323
+ let map = null;
324
+ let marker = null;
325
+ let watchId = null;
326
+ let selectedCategory = '';
327
+
328
+ // DOM Elements
329
+ const preview = document.getElementById('preview');
330
+ const canvas = document.getElementById('canvas');
331
+ const captureBtn = document.getElementById('capture-btn');
332
+ const photoGallery = document.getElementById('photo-gallery');
333
+ const operatorNameInput = document.getElementById('operator-name');
334
+ const contractIdInput = document.getElementById('contract-id');
335
+ const watermarkName = document.getElementById('watermark-name');
336
+ const watermarkCoords = document.getElementById('watermark-coords');
337
+ const watermarkDate = document.getElementById('watermark-date');
338
+ const gpsStatus = document.getElementById('gps-status');
339
+ const utmCoords = document.getElementById('utm-coords');
340
+ const utmZone = document.getElementById('utm-zone');
341
+ const exportKmlBtn = document.getElementById('export-kml');
342
+ const exportZipBtn = document.getElementById('export-zip');
343
+ const clearAllBtn = document.getElementById('clear-all');
344
+ const photoModal = document.getElementById('photo-modal');
345
+ const modalPhoto = document.getElementById('modal-photo');
346
+ const photoMetadata = document.getElementById('photo-metadata');
347
+ const closePhotoModal = document.getElementById('close-photo-modal');
348
+ const deletePhotoBtn = document.getElementById('delete-photo');
349
+ const categoryOptions = document.querySelectorAll('.category-option');
350
+ const selectedCategoryInput = document.getElementById('selected-category');
351
+
352
+ // Constants for UTM Zone 18H (Sur)
353
+ const UTM_ZONE = 18;
354
+ const UTM_ZONE_LETTER = 'S'; // 'H' para hemisferio sur
355
+ const FALSE_EASTING = 500000; // Standard false easting for UTM
356
+ const SCALE_FACTOR = 0.9996; // Standard scale factor for UTM
357
+ const EQUATORIAL_RADIUS = 6378137.0; // WGS84 equatorial radius in meters
358
+ const FLATTENING = 1 / 298.257223563; // WGS84 flattening
359
+ const ECC_SQUARED = FLATTENING * (2 - FLATTENING); // e^2
360
+
361
+ // Initialize
362
+ initCamera();
363
+ initMap();
364
+ startGPS();
365
+ setupCategorySelector();
366
+
367
+ // Event Listeners
368
+ operatorNameInput.addEventListener('input', updateWatermark);
369
+ captureBtn.addEventListener('click', capturePhoto);
370
+ exportKmlBtn.addEventListener('click', exportKML);
371
+ exportZipBtn.addEventListener('click', exportZIP);
372
+ clearAllBtn.addEventListener('click', clearAll);
373
+ closePhotoModal.addEventListener('click', () => photoModal.classList.add('hidden'));
374
+
375
+ // Functions
376
+ function setupCategorySelector() {
377
+ categoryOptions.forEach(option => {
378
+ option.addEventListener('click', function() {
379
+ // Remove selected class from all options
380
+ categoryOptions.forEach(opt => opt.classList.remove('selected'));
381
+
382
+ // Add selected class to clicked option
383
+ this.classList.add('selected');
384
+
385
+ // Update selected category
386
+ selectedCategory = this.dataset.category;
387
+ selectedCategoryInput.value = selectedCategory;
388
+ });
389
+ });
390
+ }
391
+
392
+ function initCamera() {
393
+ navigator.mediaDevices.getUserMedia({
394
+ video: {
395
+ width: { ideal: 1280 },
396
+ height: { ideal: 720 },
397
+ facingMode: 'environment'
398
+ }
399
+ })
400
+ .then(stream => {
401
+ currentStream = stream;
402
+ preview.srcObject = stream;
403
+
404
+ // Set canvas size to match video
405
+ preview.addEventListener('loadedmetadata', () => {
406
+ canvas.width = preview.videoWidth;
407
+ canvas.height = preview.videoHeight;
408
+ });
409
+ })
410
+ .catch(err => {
411
+ console.error "Error al acceder a la c谩mara:", err);
412
+ alert("No se pudo acceder a la c谩mara. Aseg煤rate de dar los permisos necesarios.");
413
+ });
414
+ }
415
+
416
+ function initMap() {
417
+ map = L.map('map').setView([-34.6037, -58.3816], 13); // Default to Buenos Aires
418
+
419
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
420
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
421
+ maxZoom: 19
422
+ }).addTo(map);
423
+
424
+ marker = L.marker([0, 0], {
425
+ icon: L.divIcon({
426
+ className: 'custom-marker',
427
+ html: '<i class="fas fa-map-marker-alt text-3xl text-blue-500"></i>',
428
+ iconSize: [30, 30],
429
+ iconAnchor: [15, 30]
430
+ }),
431
+ draggable: false
432
+ }).addTo(map);
433
+ }
434
+
435
+ function startGPS() {
436
+ if (navigator.geolocation) {
437
+ gpsStatus.querySelector('div').classList.remove('bg-rose-500');
438
+ gpsStatus.querySelector('div').classList.add('bg-amber-500');
439
+ gpsStatus.querySelector('span').textContent = 'GPS: Conectando...';
440
+
441
+ watchId = navigator.geolocation.watchPosition(
442
+ position => {
443
+ currentPosition = position;
444
+ updatePositionInfo(position);
445
+ updateWatermark();
446
+
447
+ gpsStatus.querySelector('div').classList.remove('bg-amber-500');
448
+ gpsStatus.querySelector('div').classList.add('bg-emerald-500');
449
+ gpsStatus.querySelector('span').textContent = 'GPS: Conectado';
450
+ },
451
+ error => {
452
+ console.error("Error de GPS:", error);
453
+ gpsStatus.querySelector('div').classList.remove('bg-amber-500', 'bg-emerald-500');
454
+ gpsStatus.querySelector('div').classList.add('bg-rose-500');
455
+ gpsStatus.querySelector('span').textContent = 'GPS: Error';
456
+ },
457
+ {
458
+ enableHighAccuracy: true,
459
+ maximumAge: 30000,
460
+ timeout: 27000
461
+ }
462
+ );
463
+ } else {
464
+ alert("Geolocalizaci贸n no soportada por tu navegador");
465
+ }
466
+ }
467
+
468
+ function updatePositionInfo(position) {
469
+ const lat = position.coords.latitude;
470
+ const lng = position.coords.longitude;
471
+
472
+ // Update map
473
+ map.setView([lat, lng], 18);
474
+ marker.setLatLng([lat, lng]);
475
+
476
+ // Convert to UTM Zone 18H (Sur)
477
+ const utm = convertToUTMZone18H(lat, lng);
478
+
479
+ // Update UTM display
480
+ utmCoords.textContent = `Este: ${utm.easting.toFixed(2)}m, Norte: ${utm.northing.toFixed(2)}m`;
481
+ utmZone.textContent = `H${UTM_ZONE}${UTM_ZONE_LETTER} (WGS84)`;
482
+
483
+ return utm;
484
+ }
485
+
486
+ function convertToUTMZone18H(lat, lng) {
487
+ // Verificar que la longitud est茅 dentro del huso 18 (-78掳 a -72掳)
488
+ if (lng < -78 || lng >= -72) {
489
+ console.warn(`Longitud ${lng}掳 est谩 fuera del huso 18H (-78掳 a -72掳)`);
490
+ }
491
+
492
+ // Convertir a radianes
493
+ const latRad = lat * Math.PI / 180;
494
+ const lngRad = lng * Math.PI / 180;
495
+
496
+ // Meridiano central para huso 18 (-75掳)
497
+ const lng0Rad = (-75) * Math.PI / 180;
498
+
499
+ // C谩lculos preliminares
500
+ const N = EQUATORIAL_RADIUS / Math.sqrt(1 - ECC_SQUARED * Math.sin(latRad) * Math.sin(latRad));
501
+ const T = Math.tan(latRad) * Math.tan(latRad);
502
+ const C = ECC_SQUARED * Math.cos(latRad) * Math.cos(latRad);
503
+ const A = Math.cos(latRad) * (lngRad - lng0Rad);
504
+
505
+ // C谩lculo de M (distancia meridional)
506
+ const M = EQUATORIAL_RADIUS * (
507
+ (1 - ECC_SQUARED/4 - 3*Math.pow(ECC_SQUARED,2)/64 - 5*Math.pow(ECC_SQUARED,3)/256) * latRad
508
+ - (3*ECC_SQUARED/8 + 3*Math.pow(ECC_SQUARED,2)/32 + 45*Math.pow(ECC_SQUARED,3)/1024) * Math.sin(2*latRad)
509
+ + (15*Math.pow(ECC_SQUARED,2)/256 + 45*Math.pow(ECC_SQUARED,3)/1024) * Math.sin(4*latRad)
510
+ - (35*Math.pow(ECC_SQUARed,3)/3072) * Math.sin(6*latRad)
511
+ );
512
+
513
+ // C谩lculo de coordenadas UTM
514
+ const easting = FALSE_EASTING + SCALE_FACTOR * N * (
515
+ A + (1 - T + C) * Math.pow(A,3)/6
516
+ + (5 - 18*T + Math.pow(T,2) + 72*C - 58*ECC_SQUARED) * Math.pow(A,5)/120
517
+ );
518
+
519
+ // Para hemisferio sur, sumamos 10,000,000m al northing
520
+ let northing = SCALE_FACTOR * (M + N * Math.tan(latRad) * (
521
+ Math.pow(A,2)/2
522
+ + (5 - T + 9*C + 4*Math.pow(C,2)) * Math.pow(A,4)/24
523
+ + (61 - 58*T + Math.pow(T,2) + 600*C - 330*ECC_SQUARED) * Math.pow(A,6)/720
524
+ ));
525
+
526
+ // Ajuste para hemisferio sur
527
+ if (lat < 0) {
528
+ northing += 10000000; // 10,000,000m para hemisferio sur
529
+ }
530
+
531
+ return {
532
+ easting: easting,
533
+ northing: northing,
534
+ zone: UTM_ZONE,
535
+ zoneLetter: UTM_ZONE_LETTER
536
+ };
537
+ }
538
+
539
+ function updateWatermark() {
540
+ const name = operatorNameInput.value || "No especificado";
541
+ watermarkName.textContent = `Registrado por: ${name}`;
542
+
543
+ if (currentPosition) {
544
+ const utm = updatePositionInfo(currentPosition);
545
+ watermarkCoords.textContent = `UTM H${utm.zone}${utm.zoneLetter}: E ${utm.easting.toFixed(2)}m, N ${utm.northing.toFixed(2)}m`;
546
+ } else {
547
+ watermarkCoords.textContent = "Coordenadas no disponibles";
548
+ }
549
+
550
+ const now = new Date();
551
+ watermarkDate.textContent = now.toLocaleString();
552
+ }
553
+
554
+ function capturePhoto() {
555
+ if (!currentStream) return;
556
+
557
+ // Validar que se haya seleccionado una categor铆a
558
+ if (!selectedCategory) {
559
+ alert("Por favor selecciona un 谩rea antes de tomar la foto");
560
+ return;
561
+ }
562
+
563
+ // Get canvas context
564
+ const context = canvas.getContext('2d');
565
+
566
+ // Draw current frame to canvas
567
+ context.drawImage(preview, 0, 0, canvas.width, canvas.height);
568
+
569
+ // Add watermark text directly to canvas
570
+ context.font = '16px Arial';
571
+ context.fillStyle = 'white';
572
+ context.textAlign = 'left';
573
+ context.textBaseline = 'bottom';
574
+
575
+ const name = operatorNameInput.value || "No especificado";
576
+ const description = contractIdInput.value || "Sin descripci贸n";
577
+
578
+ if (currentPosition) {
579
+ const utm = updatePositionInfo(currentPosition);
580
+ const now = new Date();
581
+
582
+ // Draw watermark text with shadow effect
583
+ context.shadowColor = 'black';
584
+ context.shadowBlur = 5;
585
+
586
+ context.fillText(`Registrado por: ${name}`, 20, canvas.height - 80);
587
+ context.fillText(`Descripci贸n: ${description}`, 20, canvas.height - 60);
588
+ context.fillText(`脕rea: ${getCategoryName(selectedCategory)}`, 20, canvas.height - 40);
589
+ context.fillText(`UTM H${utm.zone}${utm.zoneLetter}: E ${utm.easting.toFixed(2)}m, N ${utm.northing.toFixed(2)}m`, 20, canvas.height - 20);
590
+ context.fillText(now.toLocaleString(), 20, canvas.height - 5);
591
+
592
+ context.shadowBlur = 0;
593
+
594
+ // Create photo object with metadata
595
+ const photo = {
596
+ id: Date.now(),
597
+ dataUrl: canvas.toDataURL('image/jpeg', 0.9),
598
+ timestamp: now.toISOString(),
599
+ operator: name,
600
+ description: description,
601
+ category: selectedCategory,
602
+ coordinates: {
603
+ lat: currentPosition.coords.latitude,
604
+ lng: currentPosition.coords.longitude,
605
+ utm: utm
606
+ }
607
+ };
608
+
609
+ photos.push(photo);
610
+ savePhotos();
611
+ renderPhotoGallery();
612
+ updateExportButtons();
613
+
614
+ // Show success animation
615
+ captureBtn.innerHTML = '<i class="fas fa-check"></i>';
616
+ captureBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
617
+ captureBtn.classList.add('bg-emerald-600', 'hover:bg-emerald-700');
618
+
619
+ setTimeout(() => {
620
+ captureBtn.innerHTML = '<i class="fas fa-camera"></i>';
621
+ captureBtn.classList.remove('bg-emerald-600', 'hover:bg-emerald-700');
622
+ captureBtn.classList.add('bg-blue-600', 'hover:bg-blue-700');
623
+ }, 1000);
624
+ } else {
625
+ alert("No se pudo obtener la ubicaci贸n GPS. Por favor espera a que se conecte.");
626
+ }
627
+ }
628
+
629
+ function getCategoryName(categoryCode) {
630
+ switch(categoryCode) {
631
+ case 'ambiente': return 'Medio Ambiente';
632
+ case 'sso': return 'SSO';
633
+ case 'calidad': return 'Calidad';
634
+ default: return 'No especificado';
635
+ }
636
+ }
637
+
638
+ function getCategoryIcon(categoryCode) {
639
+ switch(categoryCode) {
640
+ case 'ambiente': return 'leaf';
641
+ case 'sso': return 'hard-hat';
642
+ case 'calidad': return 'award';
643
+ default: return 'question-circle';
644
+ }
645
+ }
646
+
647
+ function renderPhotoGallery() {
648
+ if (photos.length === 0) {
649
+ photoGallery.classList.add('hidden');
650
+ return;
651
+ }
652
+
653
+ photoGallery.classList.remove('hidden');
654
+ photoGallery.innerHTML = '<h3 class="col-span-3 text-lg font-medium mb-2">Fotos tomadas:</h3>';
655
+
656
+ photos.forEach((photo, index) => {
657
+ const photoElement = document.createElement('div');
658
+ photoElement.className = 'relative group cursor-pointer';
659
+ photoElement.innerHTML = `
660
+ <img src="${photo.dataUrl}" alt="Foto ${index + 1}" class="w-full h-24 object-cover rounded-lg">
661
+ <div class="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition">
662
+ <i class="fas fa-${getCategoryIcon(photo.category)} text-white"></i>
663
+ </div>
664
+ `;
665
+
666
+ photoElement.addEventListener('click', () => showPhotoModal(photo));
667
+ photoGallery.appendChild(photoElement);
668
+ });
669
+ }
670
+
671
+ function showPhotoModal(photo) {
672
+ modalPhoto.src = photo.dataUrl;
673
+
674
+ // Format metadata
675
+ const date = new Date(photo.timestamp);
676
+ photoMetadata.innerHTML = `
677
+ <div><strong>Registrado por:</strong> ${photo.operator}</div>
678
+ <div><strong>Descripci贸n:</strong> ${photo.description || "No especificado"}</div>
679
+ <div><strong>脕rea:</strong> ${getCategoryName(photo.category)}</div>
680
+ <div><strong>Fecha:</strong> ${date.toLocaleString()}</div>
681
+ <div><strong>Coordenadas:</strong></div>
682
+ <div class="pl-4">Lat: ${photo.coordinates.lat.toFixed(6)}</div>
683
+ <div class="pl-4">Lng: ${photo.coordinates.lng.toFixed(6)}</div>
684
+ <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>
685
+ `;
686
+
687
+ // Set up delete button
688
+ deletePhotoBtn.onclick = () => {
689
+ photos = photos.filter(p => p.id !== photo.id);
690
+ savePhotos();
691
+ renderPhotoGallery();
692
+ updateExportButtons();
693
+ photoModal.classList.add('hidden');
694
+ };
695
+
696
+ photoModal.classList.remove('hidden');
697
+ }
698
+
699
+ function exportKML() {
700
+ if (photos.length === 0) return;
701
+
702
+ // Group photos by category
703
+ const photosByCategory = {};
704
+ photos.forEach(photo => {
705
+ if (!photosByCategory[photo.category]) {
706
+ photosByCategory[photo.category] = [];
707
+ }
708
+ photosByCategory[photo.category].push(photo);
709
+ });
710
+
711
+ // Create KML content for each category
712
+ let kml = `<?xml version="1.0" encoding="UTF-8"?>
713
+ <kml xmlns="http://www.opengis.net/kml/2.2">
714
+ <Document>
715
+ <name>Registros Georreferenciados</name>
716
+ <description>Fotos tomadas con Foto a KML</description>`;
717
+
718
+ // Add a folder for each category
719
+ Object.keys(photosByCategory).forEach(category => {
720
+ const categoryName = getCategoryName(category);
721
+
722
+ kml += `
723
+ <Folder>
724
+ <name>${categoryName}</name>
725
+ <description>Registros de ${categoryName}</description>`;
726
+
727
+ photosByCategory[category].forEach(photo => {
728
+ // Convert image to base64 without data URL prefix
729
+ const base64Image = photo.dataUrl.split(',')[1];
730
+ const date = new Date(photo.timestamp);
731
+ const dateStr = `${date.getDate().toString().padStart(2, '0')}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getFullYear()}`;
732
+
733
+ kml += `
734
+ <Placemark>
735
+ <name>${categoryName} - ${photo.description || "Sin descripci贸n"} - ${dateStr}</name>
736
+ <description>
737
+ <![CDATA[
738
+ <h3>Registro de ${categoryName}</h3>
739
+ <p><strong>Registrado por:</strong> ${photo.operator}</p>
740
+ <p><strong>Descripci贸n:</strong> ${photo.description || "No especificado"}</p>
741
+ <p><strong>Fecha:</strong> ${date.toLocaleString()}</p>
742
+ <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>
743
+ <img src="data:image/jpeg;base64,${base64Image}" width="400" />
744
+ ]]>
745
+ </description>
746
+ <Point>
747
+ <coordinates>${photo.coordinates.lng},${photo.coordinates.lat},0</coordinates>
748
+ </Point>
749
+ </Placemark>`;
750
+ });
751
+
752
+ kml += `
753
+ </Folder>`;
754
+ });
755
+
756
+ kml += `
757
+ </Document>
758
+ </kml>`;
759
+
760
+ // Generate filename with current date
761
+ const now = new Date();
762
+ const dateStr = `${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}`;
763
+ const filename = `Registros_${dateStr}.kml`;
764
+
765
+ // Create blob and download
766
+ const blob = new Blob([kml], { type: 'application/vnd.google-earth.kml+xml' });
767
+ saveAs(blob, filename);
768
+ }
769
+
770
+ function exportZIP() {
771
+ if (photos.length === 0) return;
772
+
773
+ const zip = new JSZip();
774
+ const imgFolder = zip.folder("fotos");
775
+ const kmlFolder = zip.folder("kml");
776
+
777
+ // Group photos by category
778
+ const photosByCategory = {};
779
+ photos.forEach(photo => {
780
+ if (!photosByCategory[photo.category]) {
781
+ photosByCategory[photo.category] = [];
782
+ }
783
+ photosByCategory[photo.category].push(photo);
784
+ });
785
+
786
+ // Add photos to folders by category
787
+ Object.keys(photosByCategory).forEach(category => {
788
+ const categoryFolder = imgFolder.folder(category);
789
+
790
+ photosByCategory[category].forEach((photo, index) => {
791
+ // Extract base64 data
792
+ const base64Data = photo.dataUrl.split(',')[1];
793
+ categoryFolder.file(`foto_${index + 1}.jpg`, base64Data, { base64: true });
794
+ });
795
+ });
796
+
797
+ // Create and add KML for each category
798
+ Object.keys(photosByCategory).forEach(category => {
799
+ const categoryName = getCategoryName(category);
800
+
801
+ let kml = `<?xml version="1.0" encoding="UTF-8"?>
802
+ <kml xmlns="http://www.opengis.net/kml/2.2">
803
+ <Document>
804
+ <name>Registros de ${categoryName}</name>
805
+ <description>Fotos tomadas con Foto a KML</description>`;
806
+
807
+ photosByCategory[category].forEach((photo, index) => {
808
+ const date = new Date(photo.timestamp);
809
+ const dateStr = `${date.getDate().toString().padStart(2, '0')}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getFullYear()}`;
810
+
811
+ kml += `
812
+ <Placemark>
813
+ <name>${categoryName} - ${photo.description || "Sin descripci贸n"} - ${dateStr}</name>
814
+ <description>
815
+ <![CDATA[
816
+ <h3>Registro de ${categoryName}</h3>
817
+ <p><strong>Registrado por:</strong> ${photo.operator}</p>
818
+ <p><strong>Descripci贸n:</strong> ${photo.description || "No especificado"}</p>
819
+ <p><strong>Fecha:</strong> ${date.toLocaleString()}</p>
820
+ <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>
821
+ <img src="../fotos/${category}/foto_${index + 1}.jpg" width="400" />
822
+ ]]>
823
+ </description>
824
+ <Point>
825
+ <coordinates>${photo.coordinates.lng},${photo.coordinates.lat},0</coordinates>
826
+ </Point>
827
+ </Placemark>`;
828
+ });
829
+
830
+ kml += `
831
+ </Document>
832
+ </kml>`;
833
+
834
+ kmlFolder.file(`registros_${category}.kml`, kml);
835
+ });
836
+
837
+ // Generate ZIP and download
838
+ const now = new Date();
839
+ const dateStr = `${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}`;
840
+ const filename = `Registros_${dateStr}.zip`;
841
+
842
+ zip.generateAsync({ type: "blob" }).then(content => {
843
+ saveAs(content, filename);
844
+ });
845
+ }
846
+
847
+ function clearAll() {
848
+ if (confirm("驴Est谩s seguro de que deseas eliminar todas las fotos?")) {
849
+ photos = [];
850
+ savePhotos();
851
+ renderPhotoGallery();
852
+ updateExportButtons();
853
+ }
854
+ }
855
+
856
+ function savePhotos() {
857
+ localStorage.setItem('geoPhotos', JSON.stringify(photos));
858
+ }
859
+
860
+ function loadPhotos() {
861
+ const savedPhotos = localStorage.getItem('geoPhotos');
862
+ if (savedPhotos) {
863
+ photos = JSON.parse(savedPhotos);
864
+ renderPhotoGallery();
865
+ updateExportButtons();
866
+ }
867
+ }
868
+
869
+ function updateExportButtons() {
870
+ exportKmlBtn.disabled = photos.length === 0;
871
+ exportZipBtn.disabled = photos.length === 0;
872
+ }
873
+
874
+ // Initialize from storage
875
+ loadPhotos();
876
+
877
+ // Update watermark periodically
878
+ setInterval(updateWatermark, 1000);
879
+ });
880
+ </script>
881
+ <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>
882
+ </html>