zh4rif commited on
Commit
a65f40f
·
verified ·
1 Parent(s): d2a7bbf

Upload 2 files

Browse files
Files changed (2) hide show
  1. split_panel_map.html +566 -0
  2. style2.css +306 -0
split_panel_map.html ADDED
@@ -0,0 +1,566 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>MSPO Deforestation</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css" />
8
+ <link rel="stylesheet" href="style2.css">
9
+ <style>
10
+
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <div class="header">
15
+ <div class="logo"></div>
16
+ <img src="https://images.squarespace-cdn.com/content/v1/604db3a6dad32a12b2415387/1636475927545-PK57PVLIB7AEKX1AJQJ8/Logo_MSPO_2020.png" alt="MSPO Logo" style="height: 60px;">
17
+ <div class="logo" "textalign=left">MSPO Deforestation Mapping</div>
18
+ <div class="logo"></div>
19
+ <div class="controls">
20
+ <div class="control-group">
21
+ <label>Search Location</label>
22
+ <input type="text" class="search-box" id="searchBox" placeholder="Enter city or address...">
23
+ </div>
24
+ <div class="control-group">
25
+ <label>Left Panel</label>
26
+ <select class="layer-select" id="leftLayer">
27
+ <option value="osm">OpenStreetMap</option>
28
+ <option value="satellite">Satellite</option>
29
+ <option value="terrain">Terrain</option>
30
+ <option value="sentinel2">Sentinel-2</option>
31
+ <option value="satelogic">satelogic</option>
32
+ </select>
33
+ </div>
34
+ <div class="control-group">
35
+ <label>Right Panel</label>
36
+ <select class="layer-select" id="rightLayer">
37
+ <option value="satellite" selected>Satellite</option>
38
+ <option value="osm">OpenStreetMap</option>
39
+ <option value="terrain">Terrain</option>
40
+ <option value="sentinel2">SENTINEL-2</option>
41
+ <option value="satelogic">Satelogic</option>
42
+ </select>
43
+ </div>
44
+ <input type="file" class="file-input" id="importFile" accept=".geojson,.json,.kml,.gpx" style="display: none;">
45
+ <button class="search-btn" id="searchBtn">Search</button>
46
+ </div>
47
+ </div>
48
+
49
+ <div class="map-container">
50
+ <div class="map-panel left-panel" id="leftPanel">
51
+ <div class="panel-label">OpenStreetMap</div>
52
+ <div class="map" id="leftMap"></div>
53
+ <div class="coordinates" id="leftCoords">Lat: 0.0000, Lng: 0.0000</div>
54
+ </div>
55
+
56
+ <div class="map-panel right-panel" id="rightPanel">
57
+ <div class="panel-label">Satellite</div>
58
+ <div class="map" id="rightMap"></div>
59
+ <div class="coordinates" id="rightCoords">Lat: 0.0000, Lng: 0.0000</div>
60
+ </div>
61
+
62
+ <div class="slider-container" id="sliderContainer">
63
+ <div class="slider-handle" id="sliderHandle"></div>
64
+ </div>
65
+
66
+ <button class="sync-toggle active" id="syncToggle" title="Toggle map synchronization">
67
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
68
+ <path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/>
69
+ </svg>
70
+ </button>
71
+ </div>
72
+
73
+ <div class="loading-indicator" id="loadingIndicator">
74
+ <div>Loading...</div>
75
+ </div>
76
+
77
+ <div class="error-message" id="errorMessage">
78
+ <div id="errorText"></div>
79
+ </div>
80
+
81
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
82
+ <script>
83
+ // Configuration
84
+ const DEFAULT_LOCATION = [40.7128, -74.0060]; // New York City
85
+ const DEFAULT_ZOOM = 10;
86
+ const SEARCH_ZOOM = 12;
87
+ const MIN_SLIDER_POSITION = 15; // 15% from left
88
+ const MAX_SLIDER_POSITION = 85; // 85% from left
89
+
90
+
91
+ // Map layer definitions
92
+ const tileLayers = {
93
+ osm: {
94
+ url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
95
+ attribution: '© OpenStreetMap contributors',
96
+ name: 'OpenStreetMap'
97
+ },
98
+ satellite: {
99
+ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
100
+ attribution: 'Tiles © Esri',
101
+ name: 'Satellite'
102
+ },
103
+ terrain: {
104
+ url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
105
+ attribution: '© OpenTopoMap contributors',
106
+ name: 'Terrain'
107
+ },
108
+ sentinel2: {
109
+
110
+ url: 'https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2020_3857/default/g/{z}/{y}/{x}.jpg',
111
+ attribution: '© SENTINEL-2 Cloudless 2020, EOX IT Services GmbH',
112
+ name: 'sentinel2 Mode'
113
+ },
114
+ satelogic: {
115
+ url: 'http://localhost:8080/data/forest/{z}/{x}/{y}.png',
116
+ attribution: '© Satelogic',
117
+ name: 'satelogic Mode'
118
+ }
119
+
120
+ };
121
+
122
+ // Global variables
123
+ let leftMap, rightMap;
124
+ let leftLayer, rightLayer;
125
+ let isSynced = true;
126
+ let mapSyncLock = false;
127
+ let isSliderActive = false;
128
+ let searchMarkers = [];
129
+
130
+ // UI elements
131
+ const elements = {
132
+ leftPanel: document.getElementById('leftPanel'),
133
+ rightPanel: document.getElementById('rightPanel'),
134
+ sliderContainer: document.getElementById('sliderContainer'),
135
+ sliderHandle: document.getElementById('sliderHandle'),
136
+ mapContainer: document.querySelector('.map-container'),
137
+ searchBox: document.getElementById('searchBox'),
138
+ searchBtn: document.getElementById('searchBtn'),
139
+ leftLayer: document.getElementById('leftLayer'),
140
+ rightLayer: document.getElementById('rightLayer'),
141
+ syncToggle: document.getElementById('syncToggle'),
142
+ leftCoords: document.getElementById('leftCoords'),
143
+ rightCoords: document.getElementById('rightCoords'),
144
+ loadingIndicator: document.getElementById('loadingIndicator'),
145
+ errorMessage: document.getElementById('errorMessage'),
146
+ errorText: document.getElementById('errorText')
147
+ };
148
+
149
+ // Utility functions
150
+ function showError(message) {
151
+ elements.errorText.textContent = message;
152
+ elements.errorMessage.style.display = 'block';
153
+ setTimeout(() => {
154
+ elements.errorMessage.style.display = 'none';
155
+ }, 5000);
156
+ }
157
+
158
+ function showLoading() {
159
+ elements.loadingIndicator.style.display = 'block';
160
+ }
161
+
162
+ function hideLoading() {
163
+ elements.loadingIndicator.style.display = 'none';
164
+ }
165
+
166
+ function updatePanelLabels() {
167
+ const leftLabel = document.querySelector('.left-panel .panel-label');
168
+ const rightLabel = document.querySelector('.right-panel .panel-label');
169
+
170
+ if (leftLabel) {
171
+ leftLabel.textContent = tileLayers[elements.leftLayer.value].name;
172
+ }
173
+ if (rightLabel) {
174
+ rightLabel.textContent = tileLayers[elements.rightLayer.value].name;
175
+ }
176
+ }
177
+
178
+ // Map initialization
179
+ function initializeMaps() {
180
+ try {
181
+ // Initialize left map
182
+ leftMap = L.map('leftMap', {
183
+ zoomControl: false,
184
+ attributionControl: true
185
+ }).setView(DEFAULT_LOCATION, DEFAULT_ZOOM);
186
+
187
+ // Initialize right map
188
+ rightMap = L.map('rightMap', {
189
+ zoomControl: false,
190
+ attributionControl: true
191
+ }).setView(DEFAULT_LOCATION, DEFAULT_ZOOM);
192
+
193
+ // Add zoom controls
194
+ L.control.zoom({
195
+ position: 'topright'
196
+ }).addTo(leftMap);
197
+
198
+ L.control.zoom({
199
+ position: 'topright'
200
+ }).addTo(rightMap);
201
+
202
+ // Add initial tile layers
203
+ leftLayer = L.tileLayer(tileLayers.osm.url, {
204
+ attribution: tileLayers.osm.attribution,
205
+ maxZoom: 18
206
+ }).addTo(leftMap);
207
+
208
+ rightLayer = L.tileLayer(tileLayers.satellite.url, {
209
+ attribution: tileLayers.satellite.attribution,
210
+ maxZoom: 18
211
+ }).addTo(rightMap);
212
+
213
+ // Set up map event listeners
214
+ setupMapEvents();
215
+
216
+ // Initial coordinate update
217
+ updateCoordinates(leftMap, elements.leftCoords);
218
+ updateCoordinates(rightMap, elements.rightCoords);
219
+
220
+ console.log('Maps initialized successfully');
221
+
222
+ } catch (error) {
223
+ console.error('Error initializing maps:', error);
224
+ showError('Failed to initialize maps. Please refresh the page.');
225
+ }
226
+ }
227
+
228
+ // Map event setup
229
+ function setupMapEvents() {
230
+ leftMap.on('moveend', () => {
231
+ updateCoordinates(leftMap, elements.leftCoords);
232
+ if (isSynced && !mapSyncLock && !isSliderActive) {
233
+ syncMaps(leftMap, rightMap);
234
+ }
235
+ });
236
+
237
+ rightMap.on('moveend', () => {
238
+ updateCoordinates(rightMap, elements.rightCoords);
239
+ if (isSynced && !mapSyncLock && !isSliderActive) {
240
+ syncMaps(rightMap, leftMap);
241
+ }
242
+ });
243
+
244
+ // Handle map loading errors
245
+ leftMap.on('tileerror', (e) => {
246
+ console.warn('Left map tile error:', e);
247
+ });
248
+
249
+ rightMap.on('tileerror', (e) => {
250
+ console.warn('Right map tile error:', e);
251
+ });
252
+ }
253
+
254
+ // Coordinates update
255
+ function updateCoordinates(map, coordsElement) {
256
+ if (!map || !coordsElement) return;
257
+
258
+ const center = map.getCenter();
259
+ const zoom = map.getZoom();
260
+ coordsElement.textContent = `Lat: ${center.lat.toFixed(4)}, Lng: ${center.lng.toFixed(4)}, Zoom: ${zoom}`;
261
+ }
262
+
263
+ // Map synchronization
264
+ function syncMaps(sourceMap, targetMap) {
265
+ if (!isSynced || !sourceMap || !targetMap || mapSyncLock) return;
266
+
267
+ mapSyncLock = true;
268
+
269
+ try {
270
+ const center = sourceMap.getCenter();
271
+ const zoom = sourceMap.getZoom();
272
+ targetMap.setView(center, zoom);
273
+ } catch (error) {
274
+ console.error('Error syncing maps:', error);
275
+ }
276
+
277
+ // Release lock after a short delay
278
+ setTimeout(() => {
279
+ mapSyncLock = false;
280
+ }, 100);
281
+ }
282
+
283
+ // Slider functionality - Now with overlapping panels
284
+ let isSliderDragging = false;
285
+ let startMouseX = 0;
286
+ let startSliderPosition = 50;
287
+
288
+ function updateSliderPosition(positionPercentage) {
289
+ // Clamp between MIN_SLIDER_POSITION and MAX_SLIDER_POSITION
290
+ positionPercentage = Math.max(MIN_SLIDER_POSITION, Math.min(MAX_SLIDER_POSITION, positionPercentage));
291
+
292
+ // Move the slider
293
+ elements.sliderContainer.style.left = `${positionPercentage}%`;
294
+
295
+ // Update clip-path for overlapping effect
296
+ elements.leftPanel.style.clipPath = `polygon(0 0, ${positionPercentage}% 0, ${positionPercentage}% 100%, 0 100%)`;
297
+ elements.rightPanel.style.clipPath = `polygon(${positionPercentage}% 0, 100% 0, 100% 100%, ${positionPercentage}% 100%)`;
298
+ }
299
+
300
+ function getMouseX(e) {
301
+ return e.clientX || (e.touches && e.touches[0] ? e.touches[0].clientX : 0);
302
+ }
303
+
304
+ function startSliderDrag(e) {
305
+ e.preventDefault();
306
+ e.stopPropagation();
307
+
308
+ isSliderDragging = true;
309
+ isSliderActive = true;
310
+ startMouseX = getMouseX(e);
311
+
312
+ // Get current slider position as percentage
313
+ const containerRect = elements.mapContainer.getBoundingClientRect();
314
+ const sliderRect = elements.sliderContainer.getBoundingClientRect();
315
+ startSliderPosition = ((sliderRect.left - containerRect.left) / containerRect.width) * 100;
316
+
317
+ // Add visual feedback
318
+ elements.sliderContainer.classList.add('dragging');
319
+ elements.sliderHandle.classList.add('dragging');
320
+
321
+ // Add global event listeners
322
+ document.addEventListener('mousemove', onSliderDrag, { passive: false });
323
+ document.addEventListener('mouseup', stopSliderDrag);
324
+ document.addEventListener('touchmove', onSliderDrag, { passive: false });
325
+ document.addEventListener('touchend', stopSliderDrag);
326
+
327
+ // Prevent text selection and set cursor
328
+ document.body.style.cursor = 'ew-resize';
329
+ document.body.style.userSelect = 'none';
330
+ }
331
+
332
+ function onSliderDrag(e) {
333
+ if (!isSliderDragging) return;
334
+
335
+ e.preventDefault();
336
+ e.stopPropagation();
337
+
338
+ const currentMouseX = getMouseX(e);
339
+ const deltaX = currentMouseX - startMouseX;
340
+ const containerWidth = elements.mapContainer.offsetWidth;
341
+ const deltaPercentage = (deltaX / containerWidth) * 100;
342
+
343
+ const newPosition = startSliderPosition + deltaPercentage;
344
+ updateSliderPosition(newPosition);
345
+ }
346
+
347
+ function stopSliderDrag(e) {
348
+ if (!isSliderDragging) return;
349
+
350
+ e.preventDefault();
351
+ e.stopPropagation();
352
+
353
+ isSliderDragging = false;
354
+
355
+ // Remove visual feedback
356
+ elements.sliderContainer.classList.remove('dragging');
357
+ elements.sliderHandle.classList.remove('dragging');
358
+
359
+ // Remove global event listeners
360
+ document.removeEventListener('mousemove', onSliderDrag);
361
+ document.removeEventListener('mouseup', stopSliderDrag);
362
+ document.removeEventListener('touchmove', onSliderDrag);
363
+ document.removeEventListener('touchend', stopSliderDrag);
364
+
365
+ // Reset cursor and selection
366
+ document.body.style.cursor = '';
367
+ document.body.style.userSelect = '';
368
+
369
+ // Reset slider active state after a short delay
370
+ setTimeout(() => {
371
+ isSliderActive = false;
372
+ }, 300);
373
+ }
374
+
375
+ // Layer switching
376
+ function switchLayer(map, currentLayer, layerType) {
377
+ if (!map || !currentLayer) return null;
378
+
379
+ try {
380
+ map.removeLayer(currentLayer);
381
+ const selectedLayer = tileLayers[layerType];
382
+ const newLayer = L.tileLayer(selectedLayer.url, {
383
+ attribution: selectedLayer.attribution,
384
+ maxZoom: 18
385
+ }).addTo(map);
386
+
387
+ return newLayer;
388
+ } catch (error) {
389
+ console.error('Error switching layer:', error);
390
+ showError('Failed to switch map layer');
391
+ return currentLayer;
392
+ }
393
+ }
394
+
395
+ // Search functionality
396
+ function clearSearchMarkers() {
397
+ searchMarkers.forEach(marker => {
398
+ if (leftMap && leftMap.hasLayer(marker.left)) {
399
+ leftMap.removeLayer(marker.left);
400
+ }
401
+ if (rightMap && rightMap.hasLayer(marker.right)) {
402
+ rightMap.removeLayer(marker.right);
403
+ }
404
+ });
405
+ searchMarkers = [];
406
+ }
407
+
408
+ async function searchLocation(query) {
409
+ if (!query || query.trim() === '') {
410
+ showError('Please enter a search term');
411
+ return false;
412
+ }
413
+
414
+ showLoading();
415
+ elements.searchBtn.disabled = true;
416
+
417
+ try {
418
+ const response = await fetch(
419
+ `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=1&addressdetails=1`,
420
+ {
421
+ headers: {
422
+ 'User-Agent': 'Split Panel Maps'
423
+ }
424
+ }
425
+ );
426
+
427
+ if (!response.ok) {
428
+ throw new Error('Search service unavailable');
429
+ }
430
+
431
+ const data = await response.json();
432
+
433
+ if (data.length > 0) {
434
+ const lat = parseFloat(data[0].lat);
435
+ const lng = parseFloat(data[0].lon);
436
+
437
+ if (isNaN(lat) || isNaN(lng)) {
438
+ throw new Error('Invalid coordinates received');
439
+ }
440
+
441
+ // Clear existing markers
442
+ clearSearchMarkers();
443
+
444
+ // Add markers to both maps
445
+ const markerLeft = L.marker([lat, lng]).addTo(leftMap)
446
+ .bindPopup(`<b>${data[0].display_name}</b>`)
447
+ .openPopup();
448
+
449
+ const markerRight = L.marker([lat, lng]).addTo(rightMap)
450
+ .bindPopup(`<b>${data[0].display_name}</b>`)
451
+ .openPopup();
452
+
453
+ searchMarkers.push({ left: markerLeft, right: markerRight });
454
+
455
+ // Move maps to location
456
+ leftMap.setView([lat, lng], SEARCH_ZOOM);
457
+ if (isSynced) {
458
+ rightMap.setView([lat, lng], SEARCH_ZOOM);
459
+ }
460
+
461
+ return true;
462
+ } else {
463
+ showError('Location not found. Please try a different search term.');
464
+ return false;
465
+ }
466
+ } catch (error) {
467
+ console.error('Search error:', error);
468
+ showError('Search failed. Please try again.');
469
+ return false;
470
+ } finally {
471
+ hideLoading();
472
+ elements.searchBtn.disabled = false;
473
+ }
474
+ }
475
+
476
+ // Event listeners setup
477
+ function setupEventListeners() {
478
+ // Slider events
479
+ elements.sliderContainer.addEventListener('mousedown', startSliderDrag);
480
+ elements.sliderHandle.addEventListener('mousedown', startSliderDrag);
481
+ elements.sliderContainer.addEventListener('touchstart', startSliderDrag);
482
+ elements.sliderHandle.addEventListener('touchstart', startSliderDrag);
483
+
484
+ // Prevent context menu on slider
485
+ elements.sliderContainer.addEventListener('contextmenu', (e) => e.preventDefault());
486
+ elements.sliderHandle.addEventListener('contextmenu', (e) => e.preventDefault());
487
+
488
+ // Sync toggle
489
+ elements.syncToggle.addEventListener('click', () => {
490
+ isSynced = !isSynced;
491
+ elements.syncToggle.classList.toggle('active', isSynced);
492
+
493
+ if (isSynced && leftMap && rightMap) {
494
+ syncMaps(leftMap, rightMap);
495
+ }
496
+ });
497
+
498
+ // Layer switching
499
+ elements.leftLayer.addEventListener('change', (e) => {
500
+ if (leftMap && leftLayer) {
501
+ leftLayer = switchLayer(leftMap, leftLayer, e.target.value);
502
+ updatePanelLabels();
503
+ }
504
+ });
505
+
506
+ elements.rightLayer.addEventListener('change', (e) => {
507
+ if (rightMap && rightLayer) {
508
+ rightLayer = switchLayer(rightMap, rightLayer, e.target.value);
509
+ updatePanelLabels();
510
+ }
511
+ });
512
+
513
+ // Search functionality
514
+ elements.searchBtn.addEventListener('click', async () => {
515
+ const query = elements.searchBox.value.trim();
516
+ await searchLocation(query);
517
+ });
518
+
519
+ elements.searchBox.addEventListener('keypress', async (e) => {
520
+ if (e.key === 'Enter') {
521
+ const query = e.target.value.trim();
522
+ await searchLocation(query);
523
+ }
524
+ });
525
+
526
+ // Window resize
527
+ window.addEventListener('resize', () => {
528
+ setTimeout(() => {
529
+ if (leftMap) leftMap.invalidateSize();
530
+ if (rightMap) rightMap.invalidateSize();
531
+ }, 300);
532
+ });
533
+
534
+ // Error message click to dismiss
535
+ elements.errorMessage.addEventListener('click', () => {
536
+ elements.errorMessage.style.display = 'none';
537
+ });
538
+ }
539
+
540
+ // Initialize everything when DOM is ready
541
+ document.addEventListener('DOMContentLoaded', () => {
542
+ console.log('DOM loaded, initializing application...');
543
+
544
+ // Set up event listeners first
545
+ setupEventListeners();
546
+
547
+ // Initialize maps after a short delay
548
+ setTimeout(() => {
549
+ initializeMaps();
550
+ updatePanelLabels();
551
+ }, 100);
552
+ });
553
+
554
+ // Handle page visibility changes
555
+ document.addEventListener('visibilitychange', () => {
556
+ if (!document.hidden) {
557
+ // Page became visible, invalidate map sizes
558
+ setTimeout(() => {
559
+ if (leftMap) leftMap.invalidateSize();
560
+ if (rightMap) rightMap.invalidateSize();
561
+ }, 100);
562
+ }
563
+ });
564
+ </script>
565
+ </body>
566
+ </html>
style2.css ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ height: 100vh;
11
+ overflow: hidden;
12
+ }
13
+
14
+ .header {
15
+ background: #FFD200;
16
+ backdrop-filter: blur(10px);
17
+ padding: 15px 30px;
18
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
19
+ display: flex;
20
+ justify-content: space-between;
21
+ align-items: center;
22
+ z-index: 1000;
23
+ position: relative;
24
+
25
+ }
26
+
27
+ .logo {
28
+ font-size: 24px;
29
+ font-weight: bold;
30
+ color: #333;
31
+ background: linear-gradient(45deg, #667eea, #764ba2);
32
+ -webkit-background-clip: text;
33
+ -webkit-text-fill-color: transparent;
34
+ background-clip: text;
35
+ justify-content: flex-start; /* aligns everything to the left */
36
+ }
37
+
38
+ .controls {
39
+ display: flex;
40
+ gap: 15px;
41
+ align-items: center;
42
+ }
43
+
44
+ .control-group {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 5px;
48
+ }
49
+
50
+ .control-group label {
51
+ font-size: 12px;
52
+ color: #666;
53
+ font-weight: 500;
54
+ }
55
+
56
+ .search-box, .layer-select {
57
+ padding: 8px 12px;
58
+ border: 2px solid #e0e0e0;
59
+ border-radius: 8px;
60
+ font-size: 14px;
61
+ transition: all 0.3s ease;
62
+ background: white;
63
+ min-width: 150px;
64
+ }
65
+
66
+ .search-box:focus, .layer-select:focus {
67
+ outline: none;
68
+ border-color: #667eea;
69
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
70
+ }
71
+
72
+ .search-btn {
73
+ background: linear-gradient(45deg, #667eea, #764ba2);
74
+ color: white;
75
+ border: none;
76
+ padding: 8px 16px;
77
+ border-radius: 8px;
78
+ cursor: pointer;
79
+ font-weight: 500;
80
+ transition: all 0.3s ease;
81
+ margin-top: 20px;
82
+ }
83
+
84
+ .search-btn:hover {
85
+ transform: translateY(-2px);
86
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
87
+ }
88
+
89
+ .search-btn:disabled {
90
+ opacity: 0.6;
91
+ cursor: not-allowed;
92
+ transform: none;
93
+ }
94
+
95
+ .map-container {
96
+ position: relative;
97
+ height: calc(100vh - 80px);
98
+ overflow: hidden;
99
+ }
100
+
101
+ .map-panel {
102
+ position: absolute;
103
+ top: 0;
104
+ bottom: 0;
105
+ overflow: hidden;
106
+ width: 100%;
107
+ transition: clip-path 0.3s ease;
108
+ }
109
+
110
+ .left-panel {
111
+ left: 0;
112
+ z-index: 1;
113
+ clip-path: polygon(0 0, 50% 0, 50% 100%, 0 100%);
114
+ }
115
+
116
+ .right-panel {
117
+ right: 0;
118
+ z-index: 2;
119
+ clip-path: polygon(50% 0, 100% 0, 100% 100%, 50% 100%);
120
+ }
121
+
122
+ .map {
123
+ width: 100%;
124
+ height: 100%;
125
+ }
126
+
127
+ .panel-label {
128
+ position: absolute;
129
+ top: 15px;
130
+ left: 15px;
131
+ background: rgba(255, 255, 255, 0.9);
132
+ backdrop-filter: blur(10px);
133
+ padding: 8px 16px;
134
+ border-radius: 20px;
135
+ font-weight: 600;
136
+ color: #333;
137
+ z-index: 1000;
138
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
139
+ }
140
+
141
+ .slider-container {
142
+ position: absolute;
143
+ top: 0;
144
+ bottom: 0;
145
+ width: 4px;
146
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.6));
147
+ z-index: 1002;
148
+ cursor: ew-resize;
149
+ transition: all 0.3s ease;
150
+ left: 50%;
151
+ transform: translateX(-50%);
152
+ }
153
+
154
+ .slider-container:hover {
155
+ width: 6px;
156
+ background: linear-gradient(to bottom, rgba(102, 126, 234, 0.8), rgba(102, 126, 234, 0.6));
157
+ }
158
+
159
+ .slider-container.dragging {
160
+ width: 6px;
161
+ background: linear-gradient(to bottom, rgba(102, 126, 234, 0.9), rgba(102, 126, 234, 0.7));
162
+ }
163
+
164
+ .slider-handle {
165
+ position: absolute;
166
+ top: 50%;
167
+ left: 50%;
168
+ width: 36px;
169
+ height: 36px;
170
+ background: linear-gradient(45deg, #667eea, #764ba2);
171
+ border: 2px solid white;
172
+ border-radius: 50%;
173
+ transform: translate(-50%, -50%);
174
+ cursor: ew-resize;
175
+ display: flex;
176
+ align-items: center;
177
+ justify-content: center;
178
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
179
+ transition: all 0.3s ease;
180
+ color: white;
181
+ }
182
+
183
+ .slider-handle:hover {
184
+ transform: translate(-50%, -50%) scale(1.1);
185
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
186
+ }
187
+
188
+ .slider-handle.dragging {
189
+ transform: translate(-50%, -50%) scale(1.1);
190
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
191
+ }
192
+
193
+ .slider-handle::before {
194
+ content: '⟷';
195
+ font-size: 14px;
196
+ font-weight: bold;
197
+ }
198
+
199
+ .coordinates {
200
+ position: absolute;
201
+ bottom: 15px;
202
+ right: 15px;
203
+ background: rgba(0, 0, 0, 0.8);
204
+ color: white;
205
+ padding: 8px 12px;
206
+ border-radius: 6px;
207
+ font-size: 12px;
208
+ font-family: 'Courier New', monospace;
209
+ z-index: 1000;
210
+ backdrop-filter: blur(10px);
211
+ }
212
+
213
+ .sync-toggle {
214
+ position: absolute;
215
+ top: 15px;
216
+ right: 15px;
217
+ background: rgba(255, 255, 255, 0.95);
218
+ backdrop-filter: blur(10px);
219
+ border: 2px solid #e0e0e0;
220
+ padding: 12px;
221
+ border-radius: 50%;
222
+ cursor: pointer;
223
+ z-index: 1001;
224
+ transition: all 0.3s ease;
225
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
226
+ }
227
+
228
+ .sync-toggle:hover {
229
+ transform: scale(1.05);
230
+ box-shadow: 0 6px 25px rgba(0, 0, 0, 0.15);
231
+ }
232
+
233
+ .sync-toggle.active {
234
+ background: linear-gradient(45deg, #667eea, #764ba2);
235
+ color: white;
236
+ border-color: #667eea;
237
+ }
238
+
239
+ .loading-indicator {
240
+ position: absolute;
241
+ top: 50%;
242
+ left: 50%;
243
+ transform: translate(-50%, -50%);
244
+ background: rgba(255, 255, 255, 0.9);
245
+ padding: 20px;
246
+ border-radius: 10px;
247
+ z-index: 2000;
248
+ display: none;
249
+ }
250
+ .logo {
251
+ display: flex;
252
+ align-items: left;
253
+ gap: 12px;
254
+ font-size: 24px;
255
+ font-weight: bold;
256
+ }
257
+
258
+ .error-message {
259
+ position: fixed;
260
+ top: 100px;
261
+ left: 50%;
262
+ transform: translateX(-50%);
263
+ background: #ff4444;
264
+ color: white;
265
+ padding: 15px 25px;
266
+ border-radius: 8px;
267
+ z-index: 3000;
268
+ display: none;
269
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
270
+ }
271
+
272
+ @media (max-width: 768px) {
273
+ .header {
274
+ flex-direction: column;
275
+ gap: 15px;
276
+ padding: 10px 15px;
277
+ }
278
+
279
+ .controls {
280
+ flex-direction: column;
281
+ gap: 10px;
282
+ width: 100%;
283
+ }
284
+
285
+ .control-group {
286
+ width: 100%;
287
+ }
288
+
289
+ .search-box, .layer-select {
290
+ width: 100%;
291
+ min-width: unset;
292
+ }
293
+
294
+ .slider-container {
295
+ width: 6px;
296
+ }
297
+
298
+ .slider-handle {
299
+ width: 32px;
300
+ height: 32px;
301
+ }
302
+
303
+ .map-container {
304
+ height: calc(100vh - 140px);
305
+ }
306
+ }