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- README.md +9 -6
- index.html +605 -19
README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
colorTo: indigo
|
| 6 |
sdk: static
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
| 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 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|