RoyAalekh commited on
Commit
264ecd0
·
1 Parent(s): aa47527

Add immersive interactive map view with mobile optimization

Browse files

- Created dedicated map.html with full-screen interactive map using Leaflet
- Implemented pin management: tap/click to drop pins, location auto-population
- Added mobile-optimized gestures: pinch zoom, pan, long press support
- Created floating controls and bottom info panel with smooth animations
- Added location sharing between map and form via localStorage
- Implemented PWA service worker for offline capabilities
- Added navigation between map and form views
- Mobile-responsive design with touch-friendly controls
- GPS location detection and user location marker
- Real-time tree markers with popups showing tree information
- Deep linking support with URL hash for location sharing
- Haptic feedback on mobile devices

Files changed (5) hide show
  1. static/app.js +21 -0
  2. static/index.html +10 -2
  3. static/map.html +398 -0
  4. static/map.js +501 -0
  5. static/sw.js +159 -0
static/app.js CHANGED
@@ -14,6 +14,7 @@ class TreeTrackApp {
14
  this.loadFormOptions();
15
  this.setupEventListeners();
16
  this.loadTrees();
 
17
  }
18
 
19
  async loadFormOptions() {
@@ -94,6 +95,26 @@ class TreeTrackApp {
94
  this.setupDragAndDrop();
95
  }
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  setupPhotoUploads() {
98
  document.querySelectorAll('.photo-upload').forEach(upload => {
99
  upload.addEventListener('click', (e) => {
 
14
  this.loadFormOptions();
15
  this.setupEventListeners();
16
  this.loadTrees();
17
+ this.loadSelectedLocation();
18
  }
19
 
20
  async loadFormOptions() {
 
95
  this.setupDragAndDrop();
96
  }
97
 
98
+ loadSelectedLocation() {
99
+ // Load location from map selection
100
+ const selectedLocation = localStorage.getItem('selectedLocation');
101
+ if (selectedLocation) {
102
+ try {
103
+ const location = JSON.parse(selectedLocation);
104
+ document.getElementById('latitude').value = location.lat.toFixed(6);
105
+ document.getElementById('longitude').value = location.lng.toFixed(6);
106
+
107
+ // Clear the stored location
108
+ localStorage.removeItem('selectedLocation');
109
+
110
+ // Show success message
111
+ this.showMessage('Location loaded from map!', 'success');
112
+ } catch (error) {
113
+ console.error('Error loading selected location:', error);
114
+ }
115
+ }
116
+ }
117
+
118
  setupPhotoUploads() {
119
  document.querySelectorAll('.photo-upload').forEach(upload => {
120
  upload.addEventListener('click', (e) => {
static/index.html CHANGED
@@ -398,8 +398,13 @@
398
  </head>
399
  <body>
400
  <div class="header">
401
- <h1>🌳 TreeTrack</h1>
402
- <p>Comprehensive Field Research & Documentation Tool</p>
 
 
 
 
 
403
  </div>
404
 
405
  <div class="container">
@@ -421,6 +426,9 @@
421
  <input type="number" id="longitude" step="0.0000001" min="-180" max="180" required>
422
  </div>
423
  </div>
 
 
 
424
  </div>
425
 
426
  <!-- Section 2: Identification -->
 
398
  </head>
399
  <body>
400
  <div class="header">
401
+ <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
402
+ <div>
403
+ <h1 style="margin: 0; font-size: 2rem;">🌳 TreeTrack</h1>
404
+ <p style="margin: 5px 0 0 0; font-size: 1rem;">Field Research Tool</p>
405
+ </div>
406
+ <a href="/static/map.html" class="btn" style="background: rgba(255,255,255,0.2); color: white; text-decoration: none;">🗺️ Map View</a>
407
+ </div>
408
  </div>
409
 
410
  <div class="container">
 
426
  <input type="number" id="longitude" step="0.0000001" min="-180" max="180" required>
427
  </div>
428
  </div>
429
+ <div class="form-group">
430
+ <a href="/static/map.html" class="btn btn-secondary" style="width: 100%; text-align: center; display: block; text-decoration: none;">🗺️ Select from Interactive Map</a>
431
+ </div>
432
  </div>
433
 
434
  <!-- Section 2: Identification -->
static/map.html ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🗺️ TreeTrack Map - Interactive Field View</title>
7
+
8
+ <!-- Leaflet CSS -->
9
+ <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
10
+
11
+ <style>
12
+ * {
13
+ margin: 0;
14
+ padding: 0;
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ body {
19
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
20
+ background: #1a1a1a;
21
+ color: white;
22
+ height: 100vh;
23
+ overflow: hidden;
24
+ }
25
+
26
+ .app-container {
27
+ height: 100vh;
28
+ display: flex;
29
+ flex-direction: column;
30
+ }
31
+
32
+ /* Header */
33
+ .header {
34
+ background: linear-gradient(135deg, #2c5530 0%, #1a3a1c 100%);
35
+ padding: 15px 20px;
36
+ display: flex;
37
+ justify-content: space-between;
38
+ align-items: center;
39
+ box-shadow: 0 2px 10px rgba(0,0,0,0.3);
40
+ z-index: 1000;
41
+ }
42
+
43
+ .logo {
44
+ font-size: 1.5rem;
45
+ font-weight: bold;
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 10px;
49
+ }
50
+
51
+ .header-actions {
52
+ display: flex;
53
+ gap: 10px;
54
+ align-items: center;
55
+ }
56
+
57
+ .btn {
58
+ padding: 8px 15px;
59
+ border: none;
60
+ border-radius: 20px;
61
+ cursor: pointer;
62
+ font-size: 14px;
63
+ font-weight: 600;
64
+ transition: all 0.3s ease;
65
+ text-decoration: none;
66
+ display: inline-flex;
67
+ align-items: center;
68
+ gap: 5px;
69
+ }
70
+
71
+ .btn-primary {
72
+ background: #4CAF50;
73
+ color: white;
74
+ }
75
+
76
+ .btn-primary:hover {
77
+ background: #45a049;
78
+ transform: translateY(-1px);
79
+ }
80
+
81
+ .btn-secondary {
82
+ background: rgba(255,255,255,0.2);
83
+ color: white;
84
+ backdrop-filter: blur(10px);
85
+ }
86
+
87
+ .btn-secondary:hover {
88
+ background: rgba(255,255,255,0.3);
89
+ }
90
+
91
+ /* Map Container */
92
+ .map-container {
93
+ flex: 1;
94
+ position: relative;
95
+ overflow: hidden;
96
+ }
97
+
98
+ #map {
99
+ width: 100%;
100
+ height: 100%;
101
+ z-index: 1;
102
+ }
103
+
104
+ /* Floating Controls */
105
+ .floating-controls {
106
+ position: absolute;
107
+ top: 20px;
108
+ right: 20px;
109
+ z-index: 1000;
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: 10px;
113
+ }
114
+
115
+ .control-panel {
116
+ background: rgba(0,0,0,0.8);
117
+ backdrop-filter: blur(10px);
118
+ border-radius: 15px;
119
+ padding: 15px;
120
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
121
+ border: 1px solid rgba(255,255,255,0.1);
122
+ }
123
+
124
+ .location-info {
125
+ position: absolute;
126
+ bottom: 20px;
127
+ left: 20px;
128
+ right: 20px;
129
+ z-index: 1000;
130
+ }
131
+
132
+ .info-panel {
133
+ background: rgba(0,0,0,0.9);
134
+ backdrop-filter: blur(15px);
135
+ border-radius: 15px;
136
+ padding: 20px;
137
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
138
+ border: 1px solid rgba(255,255,255,0.1);
139
+ transform: translateY(100%);
140
+ transition: transform 0.3s ease;
141
+ }
142
+
143
+ .info-panel.active {
144
+ transform: translateY(0);
145
+ }
146
+
147
+ .coordinates {
148
+ display: flex;
149
+ gap: 20px;
150
+ margin-bottom: 15px;
151
+ }
152
+
153
+ .coord-item {
154
+ flex: 1;
155
+ text-align: center;
156
+ }
157
+
158
+ .coord-label {
159
+ font-size: 12px;
160
+ opacity: 0.7;
161
+ margin-bottom: 5px;
162
+ }
163
+
164
+ .coord-value {
165
+ font-size: 18px;
166
+ font-weight: bold;
167
+ color: #4CAF50;
168
+ }
169
+
170
+ .quick-actions {
171
+ display: flex;
172
+ gap: 10px;
173
+ margin-top: 15px;
174
+ }
175
+
176
+ .quick-actions .btn {
177
+ flex: 1;
178
+ }
179
+
180
+ /* Tree Markers Info */
181
+ .tree-counter {
182
+ background: rgba(76, 175, 80, 0.9);
183
+ color: white;
184
+ padding: 10px 15px;
185
+ border-radius: 20px;
186
+ font-weight: bold;
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 8px;
190
+ }
191
+
192
+ /* Mobile Optimizations */
193
+ @media (max-width: 768px) {
194
+ .header {
195
+ padding: 10px 15px;
196
+ }
197
+
198
+ .logo {
199
+ font-size: 1.3rem;
200
+ }
201
+
202
+ .floating-controls {
203
+ top: 10px;
204
+ right: 10px;
205
+ }
206
+
207
+ .control-panel {
208
+ padding: 10px;
209
+ }
210
+
211
+ .location-info {
212
+ bottom: 10px;
213
+ left: 10px;
214
+ right: 10px;
215
+ }
216
+
217
+ .coordinates {
218
+ flex-direction: column;
219
+ gap: 10px;
220
+ }
221
+
222
+ .quick-actions {
223
+ flex-direction: column;
224
+ }
225
+
226
+ .btn {
227
+ padding: 12px 20px;
228
+ font-size: 16px;
229
+ }
230
+ }
231
+
232
+ /* Custom Pin Styles */
233
+ .tree-pin {
234
+ background: #4CAF50;
235
+ border: 3px solid white;
236
+ border-radius: 50%;
237
+ box-shadow: 0 2px 10px rgba(0,0,0,0.3);
238
+ }
239
+
240
+ .temp-pin {
241
+ background: #ff6b35;
242
+ border: 3px solid white;
243
+ border-radius: 50%;
244
+ box-shadow: 0 2px 10px rgba(0,0,0,0.3);
245
+ animation: pulse 2s infinite;
246
+ }
247
+
248
+ @keyframes pulse {
249
+ 0% { transform: scale(1); opacity: 1; }
250
+ 50% { transform: scale(1.1); opacity: 0.7; }
251
+ 100% { transform: scale(1); opacity: 1; }
252
+ }
253
+
254
+ /* Loading States */
255
+ .loading {
256
+ position: absolute;
257
+ top: 50%;
258
+ left: 50%;
259
+ transform: translate(-50%, -50%);
260
+ background: rgba(0,0,0,0.8);
261
+ padding: 20px 30px;
262
+ border-radius: 10px;
263
+ z-index: 2000;
264
+ }
265
+
266
+ .spinner {
267
+ border: 3px solid rgba(255,255,255,0.3);
268
+ border-top: 3px solid #4CAF50;
269
+ border-radius: 50%;
270
+ width: 30px;
271
+ height: 30px;
272
+ animation: spin 1s linear infinite;
273
+ margin: 0 auto 10px;
274
+ }
275
+
276
+ @keyframes spin {
277
+ 0% { transform: rotate(0deg); }
278
+ 100% { transform: rotate(360deg); }
279
+ }
280
+
281
+ /* Success/Error Messages */
282
+ .message {
283
+ position: absolute;
284
+ top: 80px;
285
+ left: 50%;
286
+ transform: translateX(-50%);
287
+ padding: 15px 25px;
288
+ border-radius: 25px;
289
+ font-weight: 600;
290
+ z-index: 1500;
291
+ opacity: 0;
292
+ transition: opacity 0.3s ease;
293
+ }
294
+
295
+ .message.show {
296
+ opacity: 1;
297
+ }
298
+
299
+ .message.success {
300
+ background: linear-gradient(45deg, #4CAF50, #45a049);
301
+ color: white;
302
+ }
303
+
304
+ .message.error {
305
+ background: linear-gradient(45deg, #f44336, #d32f2f);
306
+ color: white;
307
+ }
308
+
309
+ /* Gesture Instructions */
310
+ .gesture-hint {
311
+ position: absolute;
312
+ bottom: 120px;
313
+ left: 50%;
314
+ transform: translateX(-50%);
315
+ background: rgba(0,0,0,0.7);
316
+ color: white;
317
+ padding: 10px 20px;
318
+ border-radius: 20px;
319
+ font-size: 14px;
320
+ z-index: 1000;
321
+ animation: fadeInOut 4s ease-in-out;
322
+ }
323
+
324
+ @keyframes fadeInOut {
325
+ 0%, 100% { opacity: 0; }
326
+ 20%, 80% { opacity: 1; }
327
+ }
328
+ </style>
329
+ </head>
330
+ <body>
331
+ <div class="app-container">
332
+ <!-- Header -->
333
+ <div class="header">
334
+ <div class="logo">
335
+ 🌳 TreeTrack Map
336
+ </div>
337
+ <div class="header-actions">
338
+ <div class="tree-counter">
339
+ <span>🌳</span>
340
+ <span id="treeCount">0</span>
341
+ </div>
342
+ <a href="/static/index.html" class="btn btn-secondary">📝 Add Tree</a>
343
+ </div>
344
+ </div>
345
+
346
+ <!-- Map Container -->
347
+ <div class="map-container">
348
+ <div id="map"></div>
349
+
350
+ <!-- Floating Controls -->
351
+ <div class="floating-controls">
352
+ <div class="control-panel">
353
+ <button id="myLocationBtn" class="btn btn-primary">📍 My Location</button>
354
+ <button id="clearPinsBtn" class="btn btn-secondary" style="margin-top: 8px;">🗑️ Clear Pins</button>
355
+ </div>
356
+ </div>
357
+
358
+ <!-- Location Info Panel -->
359
+ <div class="location-info">
360
+ <div class="info-panel" id="infoPanel">
361
+ <div class="coordinates">
362
+ <div class="coord-item">
363
+ <div class="coord-label">Latitude</div>
364
+ <div class="coord-value" id="latValue">--</div>
365
+ </div>
366
+ <div class="coord-item">
367
+ <div class="coord-label">Longitude</div>
368
+ <div class="coord-value" id="lngValue">--</div>
369
+ </div>
370
+ </div>
371
+ <div class="quick-actions">
372
+ <button id="useLocationBtn" class="btn btn-primary">✅ Use This Location</button>
373
+ <button id="cancelBtn" class="btn btn-secondary">❌ Cancel</button>
374
+ </div>
375
+ </div>
376
+ </div>
377
+
378
+ <!-- Loading -->
379
+ <div id="loading" class="loading" style="display: none;">
380
+ <div class="spinner"></div>
381
+ <div>Loading map...</div>
382
+ </div>
383
+
384
+ <!-- Messages -->
385
+ <div id="message" class="message"></div>
386
+
387
+ <!-- Gesture Hint -->
388
+ <div class="gesture-hint">
389
+ 👆 Tap anywhere on map to drop a pin
390
+ </div>
391
+ </div>
392
+ </div>
393
+
394
+ <!-- Leaflet JS -->
395
+ <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
396
+ <script src="/static/map.js"></script>
397
+ </body>
398
+ </html>
static/map.js ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // TreeTrack Map - Interactive Map with Pin Management
2
+ class TreeTrackMap {
3
+ constructor() {
4
+ this.map = null;
5
+ this.currentMarker = null;
6
+ this.treeMarkers = [];
7
+ this.userLocation = null;
8
+ this.selectedLocation = null;
9
+
10
+ this.init();
11
+ }
12
+
13
+ init() {
14
+ this.showLoading(true);
15
+ this.initializeMap();
16
+ this.setupEventListeners();
17
+ this.loadExistingTrees();
18
+ this.attemptGeolocation();
19
+ }
20
+
21
+ initializeMap() {
22
+ // Initialize Leaflet map
23
+ this.map = L.map('map', {
24
+ zoomControl: false,
25
+ attributionControl: false,
26
+ tap: true,
27
+ touchZoom: true,
28
+ doubleClickZoom: false // Disable double click to prevent conflicts
29
+ }).setView([26.2006, 92.9376], 13); // Default to Guwahati, Assam
30
+
31
+ // Add tile layer - Using OpenStreetMap with satellite option
32
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
33
+ attribution: '',
34
+ maxZoom: 19
35
+ }).addTo(this.map);
36
+
37
+ // Add custom zoom controls
38
+ L.control.zoom({
39
+ position: 'bottomleft'
40
+ }).addTo(this.map);
41
+
42
+ // Add scale
43
+ L.control.scale({
44
+ position: 'bottomleft',
45
+ metric: true,
46
+ imperial: false
47
+ }).addTo(this.map);
48
+
49
+ this.showLoading(false);
50
+ }
51
+
52
+ setupEventListeners() {
53
+ // Map click event for dropping pins
54
+ this.map.on('click', (e) => {
55
+ this.handleMapClick(e);
56
+ });
57
+
58
+ // Button event listeners
59
+ document.getElementById('myLocationBtn').addEventListener('click', () => {
60
+ this.goToMyLocation();
61
+ });
62
+
63
+ document.getElementById('clearPinsBtn').addEventListener('click', () => {
64
+ this.clearTemporaryPin();
65
+ });
66
+
67
+ document.getElementById('useLocationBtn').addEventListener('click', () => {
68
+ this.useSelectedLocation();
69
+ });
70
+
71
+ document.getElementById('cancelBtn').addEventListener('click', () => {
72
+ this.cancelLocationSelection();
73
+ });
74
+
75
+ // Handle browser back/forward
76
+ window.addEventListener('popstate', (e) => {
77
+ if (e.state && e.state.location) {
78
+ this.selectedLocation = e.state.location;
79
+ this.showLocationPanel(true);
80
+ }
81
+ });
82
+ }
83
+
84
+ handleMapClick(e) {
85
+ const lat = e.latlng.lat;
86
+ const lng = e.latlng.lng;
87
+
88
+ // Clear any existing temporary marker
89
+ this.clearTemporaryPin();
90
+
91
+ // Create new temporary marker
92
+ this.currentMarker = L.circleMarker([lat, lng], {
93
+ radius: 15,
94
+ fillColor: '#ff6b35',
95
+ color: 'white',
96
+ weight: 3,
97
+ opacity: 1,
98
+ fillOpacity: 0.8,
99
+ className: 'temp-pin'
100
+ }).addTo(this.map);
101
+
102
+ // Store selected location
103
+ this.selectedLocation = { lat, lng };
104
+
105
+ // Update info panel
106
+ this.updateLocationDisplay(lat, lng);
107
+ this.showLocationPanel(true);
108
+
109
+ // Add to browser history
110
+ history.pushState(
111
+ { location: this.selectedLocation },
112
+ '',
113
+ `#lat=${lat.toFixed(6)}&lng=${lng.toFixed(6)}`
114
+ );
115
+
116
+ // Haptic feedback on mobile
117
+ this.vibrate(50);
118
+ }
119
+
120
+ clearTemporaryPin() {
121
+ if (this.currentMarker) {
122
+ this.map.removeLayer(this.currentMarker);
123
+ this.currentMarker = null;
124
+ }
125
+ this.selectedLocation = null;
126
+ this.showLocationPanel(false);
127
+
128
+ // Clear URL hash
129
+ history.pushState('', document.title, window.location.pathname);
130
+ }
131
+
132
+ async loadExistingTrees() {
133
+ try {
134
+ const response = await fetch('/trees');
135
+ if (response.ok) {
136
+ const trees = await response.json();
137
+ this.displayTreeMarkers(trees);
138
+ this.updateTreeCounter(trees.length);
139
+ }
140
+ } catch (error) {
141
+ console.error('Error loading trees:', error);
142
+ }
143
+ }
144
+
145
+ displayTreeMarkers(trees) {
146
+ // Clear existing tree markers
147
+ this.treeMarkers.forEach(marker => {
148
+ this.map.removeLayer(marker);
149
+ });
150
+ this.treeMarkers = [];
151
+
152
+ // Add tree markers
153
+ trees.forEach(tree => {
154
+ if (tree.latitude && tree.longitude) {
155
+ const marker = L.circleMarker([tree.latitude, tree.longitude], {
156
+ radius: 12,
157
+ fillColor: '#4CAF50',
158
+ color: 'white',
159
+ weight: 3,
160
+ opacity: 1,
161
+ fillOpacity: 0.9,
162
+ className: 'tree-pin'
163
+ }).addTo(this.map);
164
+
165
+ // Create popup content
166
+ const popupContent = this.createTreePopup(tree);
167
+ marker.bindPopup(popupContent, {
168
+ maxWidth: 300,
169
+ className: 'tree-popup'
170
+ });
171
+
172
+ this.treeMarkers.push(marker);
173
+ }
174
+ });
175
+ }
176
+
177
+ createTreePopup(tree) {
178
+ return `
179
+ <div style="padding: 10px; font-family: 'Segoe UI', sans-serif;">
180
+ <h3 style="margin: 0 0 10px 0; color: #2c5530; font-size: 1.1rem;">
181
+ ${tree.local_name || tree.common_name || 'Tree #' + tree.id}
182
+ </h3>
183
+ ${tree.scientific_name ? `<p style="margin: 0 0 8px 0; font-style: italic; color: #666;">${tree.scientific_name}</p>` : ''}
184
+ ${tree.tree_code ? `<p style="margin: 0 0 8px 0;"><strong>Code:</strong> ${tree.tree_code}</p>` : ''}
185
+ ${tree.height ? `<p style="margin: 0 0 5px 0;"><strong>Height:</strong> ${tree.height}m</p>` : ''}
186
+ ${tree.width ? `<p style="margin: 0 0 5px 0;"><strong>Girth:</strong> ${tree.width}cm</p>` : ''}
187
+ <div style="margin-top: 10px; font-size: 0.9rem; color: #888;">
188
+ 📍 ${tree.latitude.toFixed(6)}, ${tree.longitude.toFixed(6)}
189
+ </div>
190
+ </div>
191
+ `;
192
+ }
193
+
194
+ updateTreeCounter(count) {
195
+ document.getElementById('treeCount').textContent = count;
196
+ }
197
+
198
+ updateLocationDisplay(lat, lng) {
199
+ document.getElementById('latValue').textContent = lat.toFixed(6);
200
+ document.getElementById('lngValue').textContent = lng.toFixed(6);
201
+ }
202
+
203
+ showLocationPanel(show) {
204
+ const panel = document.getElementById('infoPanel');
205
+ if (show) {
206
+ panel.classList.add('active');
207
+ } else {
208
+ panel.classList.remove('active');
209
+ }
210
+ }
211
+
212
+ useSelectedLocation() {
213
+ if (this.selectedLocation) {
214
+ // Store in localStorage for form access
215
+ localStorage.setItem('selectedLocation', JSON.stringify(this.selectedLocation));
216
+
217
+ this.showMessage('Location saved! Redirecting to form...', 'success');
218
+
219
+ // Redirect to form page after a short delay
220
+ setTimeout(() => {
221
+ window.location.href = '/static/index.html';
222
+ }, 1500);
223
+ }
224
+ }
225
+
226
+ cancelLocationSelection() {
227
+ this.clearTemporaryPin();
228
+ }
229
+
230
+ async attemptGeolocation() {
231
+ if ('geolocation' in navigator) {
232
+ try {
233
+ const position = await this.getCurrentPosition();
234
+ this.userLocation = {
235
+ lat: position.coords.latitude,
236
+ lng: position.coords.longitude
237
+ };
238
+
239
+ // Add user location marker
240
+ this.addUserLocationMarker();
241
+
242
+ } catch (error) {
243
+ console.log('Geolocation not available or denied');
244
+ }
245
+ }
246
+ }
247
+
248
+ getCurrentPosition() {
249
+ return new Promise((resolve, reject) => {
250
+ navigator.geolocation.getCurrentPosition(resolve, reject, {
251
+ enableHighAccuracy: true,
252
+ timeout: 10000,
253
+ maximumAge: 300000 // 5 minutes
254
+ });
255
+ });
256
+ }
257
+
258
+ addUserLocationMarker() {
259
+ if (this.userLocation) {
260
+ // Add pulsing blue dot for user location
261
+ const userMarker = L.circleMarker([this.userLocation.lat, this.userLocation.lng], {
262
+ radius: 8,
263
+ fillColor: '#2196F3',
264
+ color: 'white',
265
+ weight: 2,
266
+ opacity: 1,
267
+ fillOpacity: 1
268
+ }).addTo(this.map);
269
+
270
+ userMarker.bindPopup('📍 Your Location', {
271
+ className: 'user-location-popup'
272
+ });
273
+ }
274
+ }
275
+
276
+ async goToMyLocation() {
277
+ this.showLoading(true);
278
+
279
+ try {
280
+ const position = await this.getCurrentPosition();
281
+ const lat = position.coords.latitude;
282
+ const lng = position.coords.longitude;
283
+
284
+ this.userLocation = { lat, lng };
285
+
286
+ // Animate to user location
287
+ this.map.flyTo([lat, lng], 16, {
288
+ animate: true,
289
+ duration: 1.5
290
+ });
291
+
292
+ // Update user location marker
293
+ this.addUserLocationMarker();
294
+
295
+ this.showMessage('Found your location!', 'success');
296
+
297
+ } catch (error) {
298
+ this.showMessage('Could not get your location. Please check permissions.', 'error');
299
+ } finally {
300
+ this.showLoading(false);
301
+ }
302
+ }
303
+
304
+ showMessage(text, type = 'success') {
305
+ const messageEl = document.getElementById('message');
306
+ messageEl.textContent = text;
307
+ messageEl.className = `message ${type} show`;
308
+
309
+ setTimeout(() => {
310
+ messageEl.classList.remove('show');
311
+ }, 3000);
312
+ }
313
+
314
+ showLoading(show) {
315
+ const loadingEl = document.getElementById('loading');
316
+ loadingEl.style.display = show ? 'block' : 'none';
317
+ }
318
+
319
+ vibrate(duration) {
320
+ if ('vibrate' in navigator) {
321
+ navigator.vibrate(duration);
322
+ }
323
+ }
324
+ }
325
+
326
+ // Enhanced Mobile Gestures and Touch Handling
327
+ class MobileEnhancements {
328
+ constructor(mapInstance) {
329
+ this.map = mapInstance;
330
+ this.initMobileFeatures();
331
+ }
332
+
333
+ initMobileFeatures() {
334
+ // Prevent iOS bounce/overscroll
335
+ document.addEventListener('touchmove', (e) => {
336
+ if (e.target === document.getElementById('map')) {
337
+ e.preventDefault();
338
+ }
339
+ }, { passive: false });
340
+
341
+ // Handle orientation change
342
+ window.addEventListener('orientationchange', () => {
343
+ setTimeout(() => {
344
+ this.map.map.invalidateSize();
345
+ }, 500);
346
+ });
347
+
348
+ // Long press for pin dropping (alternative to tap)
349
+ this.setupLongPressHandler();
350
+
351
+ // Swipe gestures for panels
352
+ this.setupSwipeGestures();
353
+ }
354
+
355
+ setupLongPressHandler() {
356
+ let longPressTimer;
357
+ let isLongPress = false;
358
+
359
+ this.map.map.on('mousedown touchstart', (e) => {
360
+ isLongPress = false;
361
+ longPressTimer = setTimeout(() => {
362
+ isLongPress = true;
363
+ this.handleLongPress(e);
364
+ }, 500); // 500ms long press
365
+ });
366
+
367
+ this.map.map.on('mouseup touchend mousemove touchmove', () => {
368
+ clearTimeout(longPressTimer);
369
+ });
370
+ }
371
+
372
+ handleLongPress(e) {
373
+ // Vibrate to indicate long press detected
374
+ this.map.vibrate([50, 50, 50]);
375
+
376
+ // Handle the long press as a pin drop
377
+ this.map.handleMapClick(e);
378
+
379
+ // Show different message for long press
380
+ this.map.showMessage('Pin dropped via long press!', 'success');
381
+ }
382
+
383
+ setupSwipeGestures() {
384
+ let startY = 0;
385
+ let startX = 0;
386
+
387
+ document.addEventListener('touchstart', (e) => {
388
+ startY = e.touches[0].clientY;
389
+ startX = e.touches[0].clientX;
390
+ });
391
+
392
+ document.addEventListener('touchend', (e) => {
393
+ if (!e.changedTouches[0]) return;
394
+
395
+ const endY = e.changedTouches[0].clientY;
396
+ const endX = e.changedTouches[0].clientX;
397
+ const diffY = startY - endY;
398
+ const diffX = startX - endX;
399
+
400
+ // Detect swipe up on info panel to dismiss
401
+ if (Math.abs(diffY) > Math.abs(diffX) && diffY > 50) {
402
+ const infoPanel = document.getElementById('infoPanel');
403
+ if (infoPanel.classList.contains('active')) {
404
+ this.map.cancelLocationSelection();
405
+ }
406
+ }
407
+ });
408
+ }
409
+ }
410
+
411
+ // Progressive Web App features
412
+ class PWAFeatures {
413
+ constructor() {
414
+ this.initPWA();
415
+ }
416
+
417
+ initPWA() {
418
+ // Service worker registration
419
+ if ('serviceWorker' in navigator) {
420
+ navigator.serviceWorker.register('/static/sw.js')
421
+ .then(registration => {
422
+ console.log('SW registered: ', registration);
423
+ })
424
+ .catch(registrationError => {
425
+ console.log('SW registration failed: ', registrationError);
426
+ });
427
+ }
428
+
429
+ // Install prompt
430
+ this.setupInstallPrompt();
431
+ }
432
+
433
+ setupInstallPrompt() {
434
+ let deferredPrompt;
435
+
436
+ window.addEventListener('beforeinstallprompt', (e) => {
437
+ e.preventDefault();
438
+ deferredPrompt = e;
439
+
440
+ // Show custom install button (you can add this to UI)
441
+ console.log('PWA install prompt available');
442
+ });
443
+
444
+ window.addEventListener('appinstalled', () => {
445
+ console.log('PWA was installed');
446
+ deferredPrompt = null;
447
+ });
448
+ }
449
+ }
450
+
451
+ // URL handling for deep linking
452
+ class URLHandler {
453
+ constructor(mapInstance) {
454
+ this.map = mapInstance;
455
+ this.handleInitialURL();
456
+ }
457
+
458
+ handleInitialURL() {
459
+ const hash = window.location.hash;
460
+ if (hash) {
461
+ const params = new URLSearchParams(hash.substring(1));
462
+ const lat = parseFloat(params.get('lat'));
463
+ const lng = parseFloat(params.get('lng'));
464
+
465
+ if (!isNaN(lat) && !isNaN(lng)) {
466
+ // Restore location from URL
467
+ setTimeout(() => {
468
+ this.map.handleMapClick({
469
+ latlng: { lat, lng }
470
+ });
471
+ this.map.map.setView([lat, lng], 16);
472
+ }, 1000);
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ // Initialize the application
479
+ document.addEventListener('DOMContentLoaded', () => {
480
+ // Main map instance
481
+ const treeMap = new TreeTrackMap();
482
+
483
+ // Mobile enhancements
484
+ const mobileEnhancements = new MobileEnhancements(treeMap);
485
+
486
+ // PWA features
487
+ const pwaFeatures = new PWAFeatures();
488
+
489
+ // URL handling
490
+ const urlHandler = new URLHandler(treeMap);
491
+
492
+ // Auto-refresh tree data every 30 seconds
493
+ setInterval(() => {
494
+ treeMap.loadExistingTrees();
495
+ }, 30000);
496
+ });
497
+
498
+ // Export for use in other modules
499
+ if (typeof module !== 'undefined' && module.exports) {
500
+ module.exports = { TreeTrackMap, MobileEnhancements, PWAFeatures };
501
+ }
static/sw.js ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // TreeTrack Service Worker - PWA and Offline Support
2
+ const CACHE_NAME = 'treetrack-v1';
3
+ const urlsToCache = [
4
+ '/static/',
5
+ '/static/index.html',
6
+ '/static/map.html',
7
+ '/static/app.js',
8
+ '/static/map.js',
9
+ 'https://unpkg.com/[email protected]/dist/leaflet.css',
10
+ 'https://unpkg.com/[email protected]/dist/leaflet.js',
11
+ 'https://fonts.googleapis.com/css2?family=Segoe+UI:wght@400;600;700&display=swap'
12
+ ];
13
+
14
+ // Install event - cache resources
15
+ self.addEventListener('install', event => {
16
+ event.waitUntil(
17
+ caches.open(CACHE_NAME)
18
+ .then(cache => {
19
+ console.log('Opened cache');
20
+ return cache.addAll(urlsToCache.map(url => new Request(url, {cache: 'reload'})));
21
+ })
22
+ .catch(error => {
23
+ console.log('Cache install failed:', error);
24
+ })
25
+ );
26
+ });
27
+
28
+ // Fetch event - serve cached content when offline
29
+ self.addEventListener('fetch', event => {
30
+ // Skip non-GET requests
31
+ if (event.request.method !== 'GET') {
32
+ return;
33
+ }
34
+
35
+ // Skip requests to API endpoints - let them fail gracefully
36
+ if (event.request.url.includes('/api/') || event.request.url.includes('/trees')) {
37
+ return;
38
+ }
39
+
40
+ event.respondWith(
41
+ caches.match(event.request)
42
+ .then(response => {
43
+ // Cache hit - return response
44
+ if (response) {
45
+ return response;
46
+ }
47
+
48
+ return fetch(event.request).then(response => {
49
+ // Check if we received a valid response
50
+ if (!response || response.status !== 200 || response.type !== 'basic') {
51
+ return response;
52
+ }
53
+
54
+ // Clone the response
55
+ const responseToCache = response.clone();
56
+
57
+ caches.open(CACHE_NAME)
58
+ .then(cache => {
59
+ cache.put(event.request, responseToCache);
60
+ });
61
+
62
+ return response;
63
+ }).catch(() => {
64
+ // If fetch fails, try to return a cached fallback
65
+ if (event.request.destination === 'document') {
66
+ return caches.match('/static/index.html');
67
+ }
68
+ });
69
+ })
70
+ );
71
+ });
72
+
73
+ // Activate event - clean up old caches
74
+ self.addEventListener('activate', event => {
75
+ const cacheWhitelist = [CACHE_NAME];
76
+
77
+ event.waitUntil(
78
+ caches.keys().then(cacheNames => {
79
+ return Promise.all(
80
+ cacheNames.map(cacheName => {
81
+ if (cacheWhitelist.indexOf(cacheName) === -1) {
82
+ return caches.delete(cacheName);
83
+ }
84
+ })
85
+ );
86
+ })
87
+ );
88
+ });
89
+
90
+ // Background sync for offline data submission
91
+ self.addEventListener('sync', event => {
92
+ if (event.tag === 'background-sync') {
93
+ event.waitUntil(doBackgroundSync());
94
+ }
95
+ });
96
+
97
+ async function doBackgroundSync() {
98
+ // Handle any queued tree submissions when back online
99
+ try {
100
+ const cache = await caches.open('offline-data');
101
+ const requests = await cache.keys();
102
+
103
+ for (const request of requests) {
104
+ if (request.url.includes('offline-tree-')) {
105
+ const response = await cache.match(request);
106
+ const treeData = await response.json();
107
+
108
+ // Try to submit the data
109
+ try {
110
+ const submitResponse = await fetch('/api/trees', {
111
+ method: 'POST',
112
+ headers: {
113
+ 'Content-Type': 'application/json',
114
+ },
115
+ body: JSON.stringify(treeData)
116
+ });
117
+
118
+ if (submitResponse.ok) {
119
+ // Remove from cache if successful
120
+ await cache.delete(request);
121
+ console.log('Offline tree data synced successfully');
122
+ }
123
+ } catch (error) {
124
+ console.log('Failed to sync offline data:', error);
125
+ }
126
+ }
127
+ }
128
+ } catch (error) {
129
+ console.log('Background sync failed:', error);
130
+ }
131
+ }
132
+
133
+ // Push notifications (for future enhancement)
134
+ self.addEventListener('push', event => {
135
+ const options = {
136
+ body: event.data ? event.data.text() : 'New tree data available!',
137
+ icon: '/static/icon-192x192.png',
138
+ badge: '/static/badge-72x72.png',
139
+ tag: 'treetrack-notification',
140
+ data: {
141
+ url: '/static/map.html'
142
+ }
143
+ };
144
+
145
+ event.waitUntil(
146
+ self.registration.showNotification('TreeTrack', options)
147
+ );
148
+ });
149
+
150
+ // Handle notification clicks
151
+ self.addEventListener('notificationclick', event => {
152
+ event.notification.close();
153
+
154
+ if (event.notification.data && event.notification.data.url) {
155
+ event.waitUntil(
156
+ clients.openWindow(event.notification.data.url)
157
+ );
158
+ }
159
+ });