iv1071 commited on
Commit
2f1e98a
·
verified ·
1 Parent(s): 992c014

ok, avrei bisogno di fare una app android in grado di registrare ogni rapido movimento del giroscopio del telefono ma lungo un percorso, quindi essere di supporto side by side con un programma di navi

Browse files
Files changed (2) hide show
  1. README.md +9 -6
  2. index.html +605 -19
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Roadbumps Tracker Prototype S3zeh
3
- emoji:
4
- colorFrom: indigo
5
- colorTo: indigo
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: RoadBumps Tracker Prototype 🚧
3
+ colorFrom: yellow
4
+ colorTo: gray
 
5
  sdk: static
6
+ emoji: 🔧
7
+ tags:
8
+ - deepsite-v4
9
  ---
10
 
11
+ # RoadBumps Tracker Prototype 🚧
12
+
13
+ This project has been created with [DeepSite](https://deepsite.hf.co) AI Vibe Coding.
index.html CHANGED
@@ -1,19 +1,605 @@
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="it">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <meta name="theme-color" content="#ef4444">
7
+ <meta name="apple-mobile-web-app-capable" content="yes">
8
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9
+ <title>RoadBumps Tracker</title>
10
+
11
+ <script src="https://cdn.tailwindcss.com"></script>
12
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
13
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
14
+ <script src="https://unpkg.com/lucide@latest"></script>
15
+
16
+ <style>
17
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
18
+
19
+ body {
20
+ font-family: 'Inter', sans-serif;
21
+ overscroll-behavior: none;
22
+ }
23
+
24
+ #map {
25
+ height: 50vh;
26
+ width: 100%;
27
+ z-index: 1;
28
+ }
29
+
30
+ .glass-panel {
31
+ background: rgba(255, 255, 255, 0.95);
32
+ backdrop-filter: blur(10px);
33
+ -webkit-backdrop-filter: blur(10px);
34
+ }
35
+
36
+ .pulse-ring {
37
+ position: absolute;
38
+ border-radius: 50%;
39
+ animation: pulse-ring 2s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
40
+ }
41
+
42
+ @keyframes pulse-ring {
43
+ 0% { transform: scale(0.8); opacity: 0.8; }
44
+ 100% { transform: scale(2); opacity: 0; }
45
+ }
46
+
47
+ .bump-alert {
48
+ animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
49
+ }
50
+
51
+ @keyframes shake {
52
+ 10%, 90% { transform: translate3d(-1px, 0, 0); }
53
+ 20%, 80% { transform: translate3d(2px, 0, 0); }
54
+ 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
55
+ 40%, 60% { transform: translate3d(4px, 0, 0); }
56
+ }
57
+
58
+ .sensor-bar {
59
+ transition: width 0.1s ease;
60
+ }
61
+ </style>
62
+ </head>
63
+ <body class="bg-gray-100 h-screen flex flex-col overflow-hidden">
64
+
65
+ <!-- Header -->
66
+ <div class="glass-panel shadow-lg z-20 px-4 py-3 flex items-center justify-between border-b border-gray-200">
67
+ <div class="flex items-center gap-2">
68
+ <div class="bg-red-500 text-white p-2 rounded-lg">
69
+ <i data-lucide="alert-triangle" class="w-5 h-5"></i>
70
+ </div>
71
+ <div>
72
+ <h1 class="font-bold text-gray-800 text-lg leading-tight">RoadBumps</h1>
73
+ <p class="text-xs text-gray-500">Rilevatore dissesti stradali</p>
74
+ </div>
75
+ </div>
76
+ <div class="flex items-center gap-2">
77
+ <div id="status-indicator" class="w-3 h-3 rounded-full bg-gray-400"></div>
78
+ <span id="status-text" class="text-xs font-medium text-gray-600">Standby</span>
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Mappa -->
83
+ <div id="map" class="shadow-inner"></div>
84
+
85
+ <!-- Pannello Controlli -->
86
+ <div class="flex-1 glass-panel flex flex-col overflow-hidden">
87
+
88
+ <!-- Toolbar -->
89
+ <div class="p-4 border-b border-gray-200 flex gap-2 overflow-x-auto">
90
+ <button id="btn-start" onclick="toggleTracking()" class="flex-1 bg-green-500 hover:bg-green-600 text-white font-semibold py-3 px-4 rounded-xl flex items-center justify-center gap-2 transition-all active:scale-95 shadow-lg shadow-green-500/30">
91
+ <i data-lucide="play" class="w-5 h-5"></i>
92
+ <span>Avvia</span>
93
+ </button>
94
+
95
+ <button onclick="exportData()" class="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-4 rounded-xl flex items-center gap-2 transition-all active:scale-95 shadow-lg shadow-blue-500/30">
96
+ <i data-lucide="download" class="w-5 h-5"></i>
97
+ </button>
98
+
99
+ <button onclick="clearData()" class="bg-gray-500 hover:bg-gray-600 text-white font-semibold py-3 px-4 rounded-xl flex items-center gap-2 transition-all active:scale-95">
100
+ <i data-lucide="trash-2" class="w-5 h-5"></i>
101
+ </button>
102
+ </div>
103
+
104
+ <!-- Sensori Live -->
105
+ <div class="px-4 py-3 bg-gray-50 border-b border-gray-200">
106
+ <div class="flex items-center justify-between mb-2">
107
+ <span class="text-xs font-semibold text-gray-600 uppercase tracking-wider">Intensità Scossa</span>
108
+ <span id="g-force" class="text-sm font-bold text-gray-800">0.0g</span>
109
+ </div>
110
+ <div class="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
111
+ <div id="intensity-bar" class="sensor-bar bg-gradient-to-r from-green-400 via-yellow-400 to-red-500 h-full rounded-full" style="width: 0%"></div>
112
+ </div>
113
+ <div class="flex justify-between mt-1 text-xs text-gray-400">
114
+ <span>Lieve</span>
115
+ <span>Moderata</span>
116
+ <span>Forte</span>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Lista Dissesti -->
121
+ <div class="flex-1 overflow-y-auto p-4 space-y-3" id="bumps-list">
122
+ <div class="text-center text-gray-400 py-8">
123
+ <i data-lucide="map-pin" class="w-12 h-12 mx-auto mb-2 opacity-20"></i>
124
+ <p class="text-sm">Nessun dissesto rilevato</p>
125
+ <p class="text-xs mt-1">Avvia il tracking per iniziare</p>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- Alert Banner (nascosto di default) -->
130
+ <div id="alert-banner" class="hidden bg-red-500 text-white p-4 shadow-lg transform transition-transform">
131
+ <div class="flex items-center gap-3">
132
+ <i data-lucide="alert-octagon" class="w-6 h-6 animate-bounce"></i>
133
+ <div class="flex-1">
134
+ <p class="font-bold">ATTENZIONE! Dosse rilevato</p>
135
+ <p class="text-sm opacity-90">Rallentare nelle prossimità</p>
136
+ </div>
137
+ <button onclick="dismissAlert()" class="p-1 hover:bg-white/20 rounded">
138
+ <i data-lucide="x" class="w-5 h-5"></i>
139
+ </button>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- Modal Info -->
145
+ <div id="info-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
146
+ <div class="bg-white rounded-2xl max-w-md w-full p-6 shadow-2xl">
147
+ <h3 class="font-bold text-lg mb-2">Come usare l'app</h3>
148
+ <ul class="space-y-2 text-sm text-gray-600 mb-4">
149
+ <li class="flex gap-2"><i data-lucide="check-circle" class="w-5 h-5 text-green-500 shrink-0"></i> <span>Fissa il telefono al cruscotto in posizione verticale</span></li>
150
+ <li class="flex gap-2"><i data-lucide="check-circle" class="w-5 h-5 text-green-500 shrink-0"></i> <span>Avvia il tracking prima di partire</span></li>
151
+ <li class="flex gap-2"><i data-lucide="check-circle" class="w-5 h-5 text-green-500 shrink-0"></i> <span>L'app rileva automaticamente scosse verticali (buche/dossi)</span></li>
152
+ <li class="flex gap-2"><i data-lucide="alert-circle" class="w-5 h-5 text-orange-500 shrink-0"></i> <span>Tieni l'app in primo piano (usa lo split screen con Maps)</span></li>
153
+ </ul>
154
+ <button onclick="document.getElementById('info-modal').classList.add('hidden')" class="w-full bg-gray-800 text-white py-3 rounded-xl font-semibold">Ho capito</button>
155
+ </div>
156
+ </div>
157
+
158
+ <script>
159
+ // Inizializza Lucide icons
160
+ lucide.createIcons();
161
+
162
+ // Variabili globali
163
+ let map;
164
+ let userMarker;
165
+ let pathLine;
166
+ let isTracking = false;
167
+ let accelerometer = null;
168
+ let geolocationId = null;
169
+ let detectedBumps = JSON.parse(localStorage.getItem('roadBumps') || '[]');
170
+ let currentPosition = null;
171
+ let pathCoordinates = [];
172
+
173
+ // Costanti
174
+ const BUMP_THRESHOLD = 15; // m/s² (circa 1.5g di accelerazione improvvisa)
175
+ const COOLDOWN_MS = 2000; // 2 secondi tra un rilevamento e l'altro
176
+ let lastBumpTime = 0;
177
+ let proximityCheckInterval;
178
+
179
+ // Inizializzazione
180
+ document.addEventListener('DOMContentLoaded', () => {
181
+ initMap();
182
+ renderBumpsList();
183
+ checkPermissions();
184
+
185
+ // Mostra info al primo avvio
186
+ if (!localStorage.getItem('roadBumps_seenInfo')) {
187
+ document.getElementById('info-modal').classList.remove('hidden');
188
+ document.getElementById('info-modal').classList.add('flex');
189
+ localStorage.setItem('roadBumps_seenInfo', 'true');
190
+ }
191
+ });
192
+
193
+ function initMap() {
194
+ map = L.map('map').setView([41.9028, 12.4964], 13); // Roma default
195
+
196
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
197
+ attribution: '© OpenStreetMap contributors',
198
+ maxZoom: 19
199
+ }).addTo(map);
200
+
201
+ // Ripristina marker esistenti
202
+ detectedBumps.forEach(bump => {
203
+ addBumpMarker(bump);
204
+ });
205
+ }
206
+
207
+ function checkPermissions() {
208
+ // Verifica supporto sensori
209
+ if ('Accelerometer' in window) {
210
+ console.log('Accelerometer supportato');
211
+ } else {
212
+ alert('Il tuo dispositivo non supporta l\'Accelerometer API. Prova con Chrome su Android.');
213
+ }
214
+ }
215
+
216
+ async function toggleTracking() {
217
+ const btn = document.getElementById('btn-start');
218
+ const statusInd = document.getElementById('status-indicator');
219
+ const statusText = document.getElementById('status-text');
220
+
221
+ if (isTracking) {
222
+ stopTracking();
223
+ btn.innerHTML = '<i data-lucide="play" class="w-5 h-5"></i><span>Avvia</span>';
224
+ btn.classList.remove('bg-red-500', 'hover:bg-red-600', 'shadow-red-500/30');
225
+ btn.classList.add('bg-green-500', 'hover:bg-green-600', 'shadow-green-500/30');
226
+ statusInd.classList.remove('bg-green-500', 'animate-pulse');
227
+ statusInd.classList.add('bg-gray-400');
228
+ statusText.textContent = 'Standby';
229
+ lucide.createIcons();
230
+ } else {
231
+ try {
232
+ await startTracking();
233
+ btn.innerHTML = '<i data-lucide="square" class="w-5 h-5"></i><span>Stop</span>';
234
+ btn.classList.remove('bg-green-500', 'hover:bg-green-600', 'shadow-green-500/30');
235
+ btn.classList.add('bg-red-500', 'hover:bg-red-600', 'shadow-red-500/30');
236
+ statusInd.classList.remove('bg-gray-400');
237
+ statusInd.classList.add('bg-green-500', 'animate-pulse');
238
+ statusText.textContent = 'Registrazione...';
239
+ lucide.createIcons();
240
+ } catch (e) {
241
+ alert('Errore nell\'avvio: ' + e.message);
242
+ }
243
+ }
244
+ }
245
+
246
+ async function startTracking() {
247
+ // 1. Avvia Geolocalizzazione
248
+ if (!navigator.geolocation) {
249
+ throw new Error('Geolocalizzazione non supportata');
250
+ }
251
+
252
+ geolocationId = navigator.geolocation.watchPosition(
253
+ (position) => {
254
+ currentPosition = {
255
+ lat: position.coords.latitude,
256
+ lng: position.coords.longitude,
257
+ accuracy: position.coords.accuracy,
258
+ timestamp: Date.now()
259
+ };
260
+
261
+ updateUserPosition(currentPosition);
262
+ pathCoordinates.push([currentPosition.lat, currentPosition.lng]);
263
+
264
+ if (pathLine) {
265
+ pathLine.setLatLngs(pathCoordinates);
266
+ } else {
267
+ pathLine = L.polyline(pathCoordinates, {color: 'blue', weight: 4, opacity: 0.7}).addTo(map);
268
+ }
269
+ },
270
+ (err) => console.error('GPS Error:', err),
271
+ { enableHighAccuracy: true, maximumAge: 1000, timeout: 5000 }
272
+ );
273
+
274
+ // 2. Avvia Accelerometro
275
+ if ('Accelerometer' in window) {
276
+ try {
277
+ // Richiedi permesso su Android/Chrome
278
+ if (navigator.permissions) {
279
+ const result = await navigator.permissions.query({ name: 'accelerometer' });
280
+ if (result.state === 'denied') {
281
+ throw new Error('Permesso accelerometro negato');
282
+ }
283
+ }
284
+
285
+ accelerometer = new Accelerometer({ frequency: 60 });
286
+
287
+ accelerometer.addEventListener('reading', () => {
288
+ processAccelerometerData(accelerometer.x, accelerometer.y, accelerometer.z);
289
+ });
290
+
291
+ accelerometer.start();
292
+ } catch (e) {
293
+ console.warn('Accelerometro non disponibile, uso mock per test:', e);
294
+ // Per testing su desktop: simula con dati random
295
+ // startMockSensors();
296
+ }
297
+ } else {
298
+ alert('API Accelerometro non supportata. Usa Chrome su Android con HTTPS.');
299
+ }
300
+
301
+ // 3. Avvia controllo prossimità dissesti
302
+ proximityCheckInterval = setInterval(checkProximity, 3000);
303
+
304
+ isTracking = true;
305
+ }
306
+
307
+ function stopTracking() {
308
+ isTracking = false;
309
+
310
+ if (geolocationId) {
311
+ navigator.geolocation.clearWatch(geolocationId);
312
+ }
313
+
314
+ if (accelerometer) {
315
+ accelerometer.stop();
316
+ accelerometer = null;
317
+ }
318
+
319
+ if (proximityCheckInterval) {
320
+ clearInterval(proximityCheckInterval);
321
+ }
322
+ }
323
+
324
+ function processAccelerometerData(x, y, z) {
325
+ if (!currentPosition) return;
326
+
327
+ // Calcola accelerazione totale (rimuovendo la gravità ~9.8 m/s² sull'asse Z)
328
+ // Consideriamo scosse sull'asse Z (su-giù) come dissesti stradali
329
+ const verticalAccel = Math.abs(z - 9.8); // Sottraiamo gravità standard
330
+ const totalAccel = Math.sqrt(x*x + y*y + z*z);
331
+
332
+ // Aggiorna UI barra
333
+ const intensity = Math.min((verticalAccel / 20) * 100, 100);
334
+ document.getElementById('intensity-bar').style.width = intensity + '%';
335
+ document.getElementById('g-force').textContent = (verticalAccel / 9.8).toFixed(1) + 'g';
336
+
337
+ // Cambia colore in base all'intensità
338
+ const bar = document.getElementById('intensity-bar');
339
+ if (verticalAccel > BUMP_THRESHOLD) {
340
+ bar.classList.remove('from-green-400', 'via-yellow-400', 'to-red-500');
341
+ bar.classList.add('bg-red-600');
342
+ } else {
343
+ bar.classList.add('from-green-400', 'via-yellow-400', 'to-red-500');
344
+ bar.classList.remove('bg-red-600');
345
+ }
346
+
347
+ // Rileva dissesto
348
+ const now = Date.now();
349
+ if (verticalAccel > BUMP_THRESHOLD && (now - lastBumpTime > COOLDOWN_MS)) {
350
+ lastBumpTime = now;
351
+ registerBump(currentPosition, verticalAccel);
352
+ }
353
+ }
354
+
355
+ function registerBump(position, intensity) {
356
+ const bump = {
357
+ id: Date.now(),
358
+ lat: position.lat,
359
+ lng: position.lng,
360
+ intensity: intensity,
361
+ timestamp: new Date().toISOString(),
362
+ type: intensity > 25 ? 'buca_profonda' : 'dosso'
363
+ };
364
+
365
+ detectedBumps.push(bump);
366
+ localStorage.setItem('roadBumps', JSON.stringify(detectedBumps));
367
+
368
+ // Aggiungi a mappa
369
+ addBumpMarker(bump);
370
+
371
+ // Aggiorna lista
372
+ renderBumpsList();
373
+
374
+ // Feedback tattile/vibrazione se disponibile
375
+ if (navigator.vibrate) {
376
+ navigator.vibrate([100, 50, 100]);
377
+ }
378
+
379
+ // Animazione UI
380
+ const list = document.getElementById('bumps-list');
381
+ list.classList.add('bump-alert');
382
+ setTimeout(() => list.classList.remove('bump-alert'), 500);
383
+ }
384
+
385
+ function addBumpMarker(bump) {
386
+ const color = bump.intensity > 25 ? 'red' : 'orange';
387
+ const icon = L.divIcon({
388
+ className: 'custom-div-icon',
389
+ html: `<div style="background-color: ${color}; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
390
+ iconSize: [12, 12],
391
+ iconAnchor: [6, 6]
392
+ });
393
+
394
+ const marker = L.marker([bump.lat, bump.lng], { icon }).addTo(map);
395
+
396
+ const popupContent = `
397
+ <div class="text-sm">
398
+ <strong>${bump.type === 'buca_profonda' ? 'Buca Profonda' : 'Dosso/Dissesto'}</strong><br>
399
+ Intensità: ${(bump.intensity/9.8).toFixed(1)}g<br>
400
+ <small>${new Date(bump.timestamp).toLocaleString()}</small>
401
+ </div>
402
+ `;
403
+
404
+ marker.bindPopup(popupContent);
405
+ }
406
+
407
+ function checkProximity() {
408
+ if (!currentPosition || detectedBumps.length === 0) return;
409
+
410
+ const WARNING_DISTANCE = 200; // metri
411
+
412
+ let nearestBump = null;
413
+ let minDistance = Infinity;
414
+
415
+ detectedBumps.forEach(bump => {
416
+ const dist = calculateDistance(
417
+ currentPosition.lat, currentPosition.lng,
418
+ bump.lat, bump.lng
419
+ );
420
+
421
+ if (dist < minDistance && dist < WARNING_DISTANCE) {
422
+ minDistance = dist;
423
+ nearestBump = bump;
424
+ }
425
+ });
426
+
427
+ if (nearestBump) {
428
+ showAlert(nearestBump, minDistance);
429
+ } else {
430
+ hideAlert();
431
+ }
432
+ }
433
+
434
+ function calculateDistance(lat1, lon1, lat2, lon2) {
435
+ const R = 6371000; // Raggio terra in metri
436
+ const φ1 = lat1 * Math.PI/180;
437
+ const φ2 = lat2 * Math.PI/180;
438
+ const Δφ = (lat2-lat1) * Math.PI/180;
439
+ const Δλ = (lon2-lon1) * Math.PI/180;
440
+
441
+ const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
442
+ Math.cos(φ1) * Math.cos(φ2) *
443
+ Math.sin(Δλ/2) * Math.sin(Δλ/2);
444
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
445
+
446
+ return R * c;
447
+ }
448
+
449
+ function showAlert(bump, distance) {
450
+ const banner = document.getElementById('alert-banner');
451
+ const text = banner.querySelector('p.font-bold');
452
+
453
+ text.textContent = `ATTENZIONE! ${bump.type === 'buca_profonda' ? 'Buca' : 'Dosso'} tra ${Math.round(distance)}m`;
454
+
455
+ if (banner.classList.contains('hidden')) {
456
+ banner.classList.remove('hidden');
457
+ banner.classList.remove('translate-y-full');
458
+
459
+ // Vibrazione allarme
460
+ if (navigator.vibrate) {
461
+ navigator.vibrate([200, 100, 200, 100, 400]);
462
+ }
463
+ }
464
+ }
465
+
466
+ function hideAlert() {
467
+ const banner = document.getElementById('alert-banner');
468
+ if (!banner.classList.contains('hidden')) {
469
+ banner.classList.add('hidden');
470
+ }
471
+ }
472
+
473
+ function dismissAlert() {
474
+ hideAlert();
475
+ // Snooze per 30 secondi
476
+ setTimeout(() => {
477
+ // Riaabiliterà automaticamente al prossimo check se ancora vicino
478
+ }, 30000);
479
+ }
480
+
481
+ function updateUserPosition(pos) {
482
+ if (!userMarker) {
483
+ userMarker = L.circleMarker([pos.lat, pos.lng], {
484
+ radius: 8,
485
+ fillColor: "#3b82f6",
486
+ color: "#fff",
487
+ weight: 2,
488
+ opacity: 1,
489
+ fillOpacity: 0.8
490
+ }).addTo(map);
491
+ } else {
492
+ userMarker.setLatLng([pos.lat, pos.lng]);
493
+ }
494
+
495
+ // Centra la mappa solo se è la prima volta o se l'utente lo richiede (auto-center opzionale)
496
+ if (pathCoordinates.length < 2) {
497
+ map.setView([pos.lat, pos.lng], 17);
498
+ }
499
+ }
500
+
501
+ function renderBumpsList() {
502
+ const container = document.getElementById('bumps-list');
503
+
504
+ if (detectedBumps.length === 0) {
505
+ container.innerHTML = `
506
+ <div class="text-center text-gray-400 py-8">
507
+ <i data-lucide="map-pin" class="w-12 h-12 mx-auto mb-2 opacity-20"></i>
508
+ <p class="text-sm">Nessun dissesto rilevato</p>
509
+ <p class="text-xs mt-1">Avvia il tracking per iniziare</p>
510
+ </div>
511
+ `;
512
+ lucide.createIcons();
513
+ return;
514
+ }
515
+
516
+ // Ordina per data decrescente
517
+ const sorted = [...detectedBumps].sort((a, b) => b.id - a.id);
518
+
519
+ container.innerHTML = sorted.map(bump => `
520
+ <div class="bg-white rounded-xl p-3 shadow-sm border border-gray-200 flex items-center gap-3">
521
+ <div class="w-10 h-10 rounded-full ${bump.intensity > 25 ? 'bg-red-100 text-red-600' : 'bg-orange-100 text-orange-600'} flex items-center justify-center shrink-0">
522
+ <i data-lucide="${bump.intensity > 25 ? 'alert-triangle' : 'alert-circle'}" class="w-5 h-5"></i>
523
+ </div>
524
+ <div class="flex-1 min-w-0">
525
+ <p class="font-semibold text-gray-800 text-sm truncate">
526
+ ${bump.type === 'buca_profonda' ? 'Buca Profonda' : 'Dosso Stradale'}
527
+ </p>
528
+ <p class="text-xs text-gray-500">
529
+ ${new Date(bump.timestamp).toLocaleString()} • ${(bump.intensity/9.8).toFixed(1)}g
530
+ </p>
531
+ </div>
532
+ <button onclick="centerOnBump(${bump.lat}, ${bump.lng})" class="p-2 hover:bg-gray-100 rounded-lg text-gray-400 hover:text-blue-500">
533
+ <i data-lucide="map-pin" class="w-4 h-4"></i>
534
+ </button>
535
+ </div>
536
+ `).join('');
537
+
538
+ lucide.createIcons();
539
+ }
540
+
541
+ function centerOnBump(lat, lng) {
542
+ map.setView([lat, lng], 18);
543
+ }
544
+
545
+ function exportData() {
546
+ if (detectedBumps.length === 0) {
547
+ alert('Nessun dato da esportare');
548
+ return;
549
+ }
550
+
551
+ const dataStr = JSON.stringify(detectedBumps, null, 2);
552
+ const dataBlob = new Blob([dataStr], {type: 'application/json'});
553
+ const url = URL.createObjectURL(dataBlob);
554
+ const link = document.createElement('a');
555
+ link.href = url;
556
+ link.download = `roadbumps_${new Date().toISOString().split('T')[0]}.json`;
557
+ link.click();
558
+
559
+ // Condivisione nativa se disponibile (Web Share API)
560
+ if (navigator.share) {
561
+ const file = new File([dataBlob], 'dissesti.json', { type: 'application/json' });
562
+ navigator.share({
563
+ title: 'Dati RoadBumps',
564
+ text: `Esportati ${detectedBumps.length} dissesti stradali`,
565
+ files: [file]
566
+ }).catch(console.error);
567
+ }
568
+ }
569
+
570
+ function clearData() {
571
+ if (confirm('Sei sicuro di voler cancellare tutti i dati raccolti?')) {
572
+ detectedBumps = [];
573
+ localStorage.removeItem('roadBumps');
574
+
575
+ // Pulisci mappa
576
+ map.eachLayer((layer) => {
577
+ if (layer instanceof L.Marker && layer !== userMarker) {
578
+ map.removeLayer(layer);
579
+ }
580
+ });
581
+
582
+ renderBumpsList();
583
+ hideAlert();
584
+ }
585
+ }
586
+
587
+ // Gestione visibilità pagina (pausa quando in background)
588
+ document.addEventListener('visibilitychange', () => {
589
+ if (document.hidden && isTracking) {
590
+ console.log('App in background - i sensori potrebbero essere limitati');
591
+ // Nota: in una PWA reale, qui dovremmo usare Service Worker per notifiche
592
+ }
593
+ });
594
+
595
+ // Prevenire chiusura accidentale durante il tracking
596
+ window.addEventListener('beforeunload', (e) => {
597
+ if (isTracking) {
598
+ e.preventDefault();
599
+ e.returnValue = 'Stai registrando dati. Sei sicuro di voler uscire?';
600
+ }
601
+ });
602
+ </script>
603
+ <script src="https://deepsite.hf.co/deepsite-badge.js"></script>
604
+ </body>
605
+ </html>