pretix-map 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,105 +1,117 @@
1
- // Wait for the DOM to be fully loaded before running map code
2
1
  document.addEventListener('DOMContentLoaded', function () {
3
- console.log("Sales Map JS Loaded (FINAL TEST - INCLUDING TOGGLE)");
2
+ console.log("Sales Map JS Loaded (Cluster Toggle & Heatmap Opts)");
3
+
4
+ // --- L.Icon.Default setup ---
5
+ // Explicitly set the path to Leaflet's default icon images
6
+ try {
7
+ delete L.Icon.Default.prototype._getIconUrl;
8
+ L.Icon.Default.mergeOptions({
9
+ iconRetinaUrl: '/static/leaflet/images/marker-icon-2x.png', // Path within your collected static files
10
+ iconUrl: '/static/leaflet/images/marker-icon.png', // Path within your collected static files
11
+ shadowUrl: '/static/leaflet/images/marker-shadow.png', // Path within your collected static files
12
+ iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34],
13
+ tooltipAnchor: [16, -28], shadowSize: [41, 41]
14
+ });
15
+ console.log("Set Leaflet default icon image paths explicitly.");
16
+ } catch (e) {
17
+ console.error("Error setting Leaflet default icon path:", e);
18
+ // Fallback might be needed for very old Leaflet versions
19
+ // L.Icon.Default.imagePath = '/static/leaflet/images/';
20
+ }
21
+ // --- End L.Icon.Default setup ---
22
+
4
23
 
5
24
  // --- Configuration ---
6
25
  const mapContainerId = 'sales-map-container';
7
26
  const statusOverlayId = 'map-status-overlay';
8
- const toggleButtonId = 'view-toggle-btn';
9
- const initialZoom = 5;
10
- const defaultMapView = 'pins'; // Can be 'pins' or 'heatmap'
11
- const heatmapOptions = {
27
+ const viewToggleButtonId = 'view-toggle-btn'; // Changed from toggleButtonId for clarity
28
+ const clusterToggleButtonId = 'cluster-toggle-btn';
29
+ const heatmapOptionsPanelId = 'heatmap-options-panel';
30
+ const initialZoom = 5; // Start more zoomed out
31
+ const defaultMapView = 'pins'; // Start with pins
32
+
33
+ // --- Globals & State ---
34
+ let map = null;
35
+ let coordinateData = []; // Stores {lat, lon, tooltip, order_url}
36
+ let pinLayer = null; // Holds L.markerClusterGroup OR L.layerGroup
37
+ let heatmapLayer = null;
38
+ let currentView = defaultMapView;
39
+ let dataUrl = null;
40
+ let isClusteringEnabled = true; // Default Cluster State: ON
41
+ let heatmapOptions = { // Default Heatmap Options (state variable)
12
42
  radius: 25,
13
43
  blur: 15,
14
44
  maxZoom: 18,
15
- // max: 1.0, // Let leaflet-heat calculate max based on data density
16
- minOpacity: 0.2
45
+ minOpacity: 0.2 // Example optional parameter
17
46
  };
18
47
 
19
- // --- Globals ---
20
- let map = null; // Leaflet map instance
21
- let coordinateData = []; // To store fetched [{lat: Y, lon: X, tooltip: Z}, ...] objects
22
- let pinLayer = null; // Layer for markers/clusters
23
- let heatmapLayer = null; // Layer for heatmap
24
- let currentView = defaultMapView; // Track current view state
25
- let dataUrl = null; // To store the API endpoint URL
26
-
48
+ // --- DOM Elements ---
27
49
  const mapElement = document.getElementById(mapContainerId);
28
50
  const statusOverlay = document.getElementById(statusOverlayId);
29
- const toggleButton = document.getElementById(toggleButtonId);
30
-
31
- // --- Helper to update status overlay ---
51
+ const viewToggleButton = document.getElementById(viewToggleButtonId);
52
+ const clusterToggleButton = document.getElementById(clusterToggleButtonId);
53
+ const heatmapOptionsPanel = document.getElementById(heatmapOptionsPanelId);
54
+ // Heatmap control elements
55
+ const heatmapRadiusInput = document.getElementById('heatmap-radius');
56
+ const heatmapBlurInput = document.getElementById('heatmap-blur');
57
+ const heatmapMaxZoomInput = document.getElementById('heatmap-maxZoom');
58
+ const radiusValueSpan = document.getElementById('radius-value');
59
+ const blurValueSpan = document.getElementById('blur-value');
60
+ const maxZoomValueSpan = document.getElementById('maxzoom-value');
61
+
62
+ // --- Status Update Helpers ---
32
63
  function updateStatus(message, isError = false) {
33
64
  if (statusOverlay) {
34
65
  const p = statusOverlay.querySelector('p');
35
- if (p) {
36
- p.textContent = message;
37
- p.className = isError ? 'text-danger' : ''; // Apply error class if needed
38
- }
39
- statusOverlay.style.display = 'flex'; // Make sure it's visible
40
- } else {
41
- console.warn("Status overlay element not found.");
42
- }
66
+ if (p) { p.textContent = message; p.className = isError ? 'text-danger' : ''; }
67
+ statusOverlay.style.display = 'flex'; // Show overlay when status updates
68
+ } else { console.warn("Status overlay element not found."); }
43
69
  }
44
-
45
- // --- Helper to hide status overlay ---
46
70
  function hideStatus() {
47
- if (statusOverlay) {
48
- statusOverlay.style.display = 'none'; // Hide the overlay
49
- }
71
+ if (statusOverlay) { statusOverlay.style.display = 'none'; } // Hide overlay
50
72
  }
73
+ // --- End Status Helpers ---
74
+
51
75
 
52
76
  // --- Initialization ---
53
77
  function initializeMap() {
54
- console.log("Initializing Leaflet map...");
55
-
56
- if (!mapElement) {
57
- console.error(`Map container element #${mapContainerId} not found.`);
58
- return; // Stop initialization if container is missing
59
- }
60
- if (!statusOverlay) {
61
- console.warn("Status overlay not found");
62
- }
63
- if (!toggleButton) {
64
- // Log a warning but don't necessarily stop if button is missing
65
- console.warn(`Toggle button #${toggleButtonId} not found.`);
66
- }
67
-
68
- // --- Get the data URL from the container's data attribute ---
78
+ // Check required elements
79
+ if (!mapElement) { console.error(`Map container #${mapContainerId} not found.`); return; }
80
+ if (!statusOverlay) { console.warn("Status overlay element not found."); }
81
+ // Check control elements (warn but continue if missing)
82
+ if (!viewToggleButton) console.warn("View toggle button not found.");
83
+ if (!clusterToggleButton) console.warn("Cluster toggle button not found.");
84
+ if (!heatmapOptionsPanel) console.warn("Heatmap options panel not found.");
85
+ if (!heatmapRadiusInput || !heatmapBlurInput || !heatmapMaxZoomInput) console.warn("Heatmap input elements missing.");
86
+
87
+ // Get Data URL
69
88
  dataUrl = mapElement.dataset.dataUrl;
70
89
  if (!dataUrl) {
71
- console.error("Data URL not found in container's data-data-url attribute! Cannot fetch data.");
72
90
  updateStatus("Configuration Error: Missing data source URL.", true);
73
- return; // Stop initialization if data URL is missing
91
+ return;
74
92
  }
75
93
  console.log(`Data URL found: ${dataUrl}`);
76
- // --- End data URL retrieval ---
77
-
78
- // Set Leaflet default image path (if needed, depends on static file setup)
79
- L.Icon.Default.imagePath = '/static/leaflet/images/'; // Ensure this path is correct
80
- console.log("Set Leaflet default imagePath to:", L.Icon.Default.imagePath);
81
-
82
- console.log("Initializing Leaflet map...");
83
94
  updateStatus("Initializing map...");
95
+
96
+ // Create Leaflet Map
84
97
  try {
85
- map = L.map(mapContainerId).setView([48.85, 2.35], initialZoom); // Default center
98
+ map = L.map(mapContainerId).setView([48.85, 2.35], initialZoom); // Centered somewhat on Europe
86
99
  console.log("L.map() called successfully.");
87
100
 
88
- console.log("Adding Tile Layer...");
101
+ // Add Base Tile Layer
89
102
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
90
- attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
91
- }).addTo(map);
103
+ attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
104
+ maxZoom: 18, // Standard max zoom
105
+ }).on('load', function() { console.log('Base tiles loaded.'); }).addTo(map);
92
106
  console.log("Tile layer added successfully.");
93
107
 
94
- // Setup toggle button listener if it exists
95
- if (toggleButton) {
96
- setupToggleButton(); // Call setup function
97
- } else {
98
- console.log("Toggle button not found, skipping listener setup.");
99
- }
108
+ // Setup Controls (Event Listeners)
109
+ if (viewToggleButton) setupViewToggleButton();
110
+ if (clusterToggleButton) setupClusterToggleButton();
111
+ setupHeatmapControls(); // Setup listeners even if panel hidden
100
112
 
101
- // Fetch data and populate the map layers
102
- fetchCoordinateData();
113
+ // Fetch data to populate layers
114
+ fetchDataAndDraw();
103
115
 
104
116
  } catch (error) {
105
117
  console.error("ERROR during Leaflet initialization:", error);
@@ -107,272 +119,324 @@ document.addEventListener('DOMContentLoaded', function () {
107
119
  }
108
120
  }
109
121
 
110
- // --- Data Fetching ---
111
- function fetchCoordinateData() {
112
- // dataUrl should be set during initialization
113
- if (!dataUrl) {
114
- console.error("Cannot fetch data: dataUrl is not set.");
115
- return;
116
- }
117
122
 
123
+ // --- Data Fetching & Initial Drawing ---
124
+ function fetchDataAndDraw() {
125
+ if (!dataUrl) return;
118
126
  console.log("Fetching coordinates from:", dataUrl);
119
- updateStatus("Loading ticket locations..."); // Update status
127
+ updateStatus("Loading ticket locations...");
128
+ // Disable controls while loading
129
+ if(viewToggleButton) viewToggleButton.disabled = true;
130
+ if(clusterToggleButton) clusterToggleButton.disabled = true;
131
+ disableHeatmapControls(true);
120
132
 
121
133
  fetch(dataUrl)
122
134
  .then(response => {
123
- if (!response.ok) {
124
- // Throw an error with status text to be caught below
125
- throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
126
- }
127
- return response.json();
128
- })
135
+ if (!response.ok) throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
136
+ return response.json();
137
+ })
129
138
  .then(data => {
130
- if (data.error) { // Check for application-level errors from the backend
131
- throw new Error(`API Error: ${data.error}`);
132
- }
133
- // --- Adjust check for the new data structure ---
139
+ if (data.error) throw new Error(`API Error: ${data.error}`);
134
140
  if (!data || !data.locations || !Array.isArray(data.locations)) {
135
- console.warn("Invalid or empty data format received:", data);
136
- updateStatus("No valid geocoded ticket locations found.", false); // Inform user
137
- if (toggleButton) toggleButton.disabled = true; // Disable button if no data
138
- coordinateData = []; // Ensure it's empty
139
- return; // Stop processing if data is missing/invalid
141
+ console.warn("Invalid or empty data format received:", data);
142
+ updateStatus("No valid geocoded ticket locations found.", false);
143
+ coordinateData = []; hideStatus(); return;
140
144
  }
141
145
  if (data.locations.length === 0) {
142
146
  console.log("No coordinate data received (empty list).");
143
147
  updateStatus("No geocoded ticket locations found for this event.", false);
144
- if (toggleButton) toggleButton.disabled = true;
145
- coordinateData = [];
146
- return;
148
+ coordinateData = []; hideStatus(); return;
147
149
  }
148
- // --- End structure check ---
149
150
 
150
- coordinateData = data.locations; // Store the [{lat: Y, lon: X, tooltip: Z}, ...] array
151
+ coordinateData = data.locations;
151
152
  console.log(`Received ${coordinateData.length} coordinates.`);
152
153
 
153
- // --- Create layers (but don't add to map yet) ---
154
- createMapLayers();
154
+ // Enable controls now that we have data
155
+ if(viewToggleButton) viewToggleButton.disabled = false;
156
+ // Only enable cluster button if in pins view initially and data exists
157
+ if(clusterToggleButton) clusterToggleButton.disabled = (currentView !== 'pins');
158
+ // Only enable heatmap controls if data exists
159
+ disableHeatmapControls(false);
155
160
 
156
- // --- Show the default view ---
161
+ // Create both layers based on fetched data
162
+ createAllLayers();
163
+ // Display the default layer
157
164
  showCurrentView();
158
-
159
- // Adjust map bounds to fit markers if coordinates were found
165
+ // Adjust bounds to fit the data
160
166
  adjustMapBounds();
161
167
 
162
- // Enable button if it was disabled and we got data
163
- if (toggleButton) toggleButton.disabled = false;
164
- hideStatus();
168
+ hideStatus(); // Hide loading overlay
165
169
 
166
- // Force redraw just in case (sometimes needed after dynamic content/bounds changes)
170
+ // Force redraw after a short delay
167
171
  setTimeout(function () {
168
172
  console.log("Forcing map.invalidateSize() after data load...");
169
- console.log("Map container element just before invalidateSize:", document.getElementById(mapContainerId)); // Check if it exists here
170
- if (map && document.getElementById(mapContainerId)) { // Add check before calling
171
- map.invalidateSize();
173
+ const container = document.getElementById(mapContainerId);
174
+ if (map && container && container.offsetWidth > 0) {
175
+ map.invalidateSize();
172
176
  } else {
173
- console.warn("Skipping invalidateSize because map or container is missing.");
177
+ console.warn(`Skipping invalidateSize. Map: ${!!map}, Container: ${!!container}, OffsetWidth: ${container ? container.offsetWidth : 'N/A'}`);
174
178
  }
175
179
  }, 100);
176
-
177
180
  })
178
181
  .catch(error => {
179
182
  console.error('Error fetching or processing coordinate data:', error);
180
- console.log("Map container element during fetch error:", document.getElementById(mapContainerId)); // Check if it exists here
181
- updateStatus(`Error loading map data: ${error.message}. Please try again later.`, true); // Show error in overlay
182
- if (toggleButton) toggleButton.disabled = true;
183
+ updateStatus(`Error loading map data: ${error.message}. Please try again later.`, true);
184
+ // Keep controls disabled on error
183
185
  });
184
186
  }
185
187
 
186
- // --- Layer Creation ---
187
- function createMapLayers() {
188
- if (!map || coordinateData.length === 0) {
189
- console.log("Skipping layer creation (no map or data).");
190
- return;
191
- }
192
188
 
193
- // 1. Create Pin Layer (using MarkerCluster)
194
- console.log("Creating pin layer instance (marker cluster)...");
195
- pinLayer = L.markerClusterGroup(); // Initialize cluster group
196
- coordinateData.forEach((loc, index) => { // loc is now {lat, lon, tooltip, order_url}
189
+ // --- Layer Creation Functions ---
190
+ function createAllLayers() {
191
+ // Orchestrates creation of both layer types
192
+ createPinLayer();
193
+ createHeatmapLayer();
194
+ console.log("Map layer instances created/updated.");
195
+ }
196
+
197
+ function createPinLayer() {
198
+ // Creates/recreates the pin layer based on isClusteringEnabled state
199
+ console.log(`Creating pin layer instance (Clustering: ${isClusteringEnabled})...`);
200
+ pinLayer = null; // Clear previous instance
201
+ if (coordinateData.length === 0) { console.warn("No coordinate data for pin layer."); return; }
202
+
203
+ const markers = [];
204
+ coordinateData.forEach((loc, index) => {
197
205
  try {
198
- if (loc.lat == null || loc.lon == null) { /* ... skip invalid ... */
199
- return;
200
- }
206
+ if (loc.lat == null || loc.lon == null || isNaN(loc.lat) || isNaN(loc.lon)) return;
201
207
  const latLng = L.latLng(loc.lat, loc.lon);
202
- if (isNaN(latLng.lat) || isNaN(latLng.lng)) { /* ... skip invalid ... */
203
- return;
204
- }
205
-
206
208
  const marker = L.marker(latLng);
207
-
208
- // --- Use the enhanced tooltip from backend ---
209
- // Leaflet tooltips handle HTML content by default
210
- if (loc.tooltip) {
211
- marker.bindTooltip(loc.tooltip);
212
- }
213
- // --- End Tooltip ---
214
-
215
- // --- Add Click Listener to open order URL ---
216
- if (loc.order_url) { // Only add listener if URL was successfully generated
217
- marker.on('click', function () {
218
- console.log(`Marker clicked, opening URL: ${loc.order_url}`);
219
- // Open in a new tab, which is usually better for control panel links
220
- window.open(loc.order_url, '_blank');
221
- // If you prefer opening in the same tab:
222
- // window.location.href = loc.order_url;
223
- });
224
- } else {
225
- // Log if URL is missing for a marker, maybe backend issue
226
- console.warn(`Order URL missing for coordinate index ${index}, click disabled for this marker.`);
227
- }
228
- // --- End Click Listener ---
229
-
230
- pinLayer.addLayer(marker); // Add marker to cluster group
231
-
232
- } catch (e) {
233
- console.error(`Error creating marker for coordinate ${index}:`, loc, e);
234
- }
209
+ if (loc.tooltip) marker.bindTooltip(loc.tooltip);
210
+ if (loc.order_url) { marker.on('click', () => window.open(loc.order_url, '_blank')); }
211
+ markers.push(marker);
212
+ } catch (e) { console.error(`Error creating marker ${index}:`, e); }
235
213
  });
236
- console.log("Pin layer instance created with markers (incl. tooltips and clicks).");
237
214
 
215
+ if (markers.length === 0) { console.warn("No valid markers created for pin layer."); return; }
238
216
 
239
- // 2. Create Heatmap Layer (No changes needed here)
217
+ if (isClusteringEnabled) {
218
+ pinLayer = L.markerClusterGroup(); // Use clustering
219
+ pinLayer.addLayers(markers);
220
+ console.log("Marker cluster group created and populated.");
221
+ } else {
222
+ pinLayer = L.layerGroup(markers); // Use simple layer group
223
+ console.log("Simple layer group created and populated.");
224
+ }
225
+ }
226
+
227
+ function createHeatmapLayer() {
228
+ // Creates/recreates the heatmap layer using current heatmapOptions
240
229
  console.log("Creating heatmap layer instance...");
230
+ heatmapLayer = null; // Clear previous instance
231
+ if (coordinateData.length === 0) { console.warn("No coordinate data for heatmap layer."); return; }
232
+
241
233
  try {
242
234
  const heatPoints = coordinateData.map(loc => {
243
235
  if (loc.lat != null && loc.lon != null && !isNaN(loc.lat) && !isNaN(loc.lon)) {
244
- return [loc.lat, loc.lon, 1.0];
245
- }
246
- return null;
236
+ return [loc.lat, loc.lon, 1.0]; // Intensity 1.0
237
+ } return null;
247
238
  }).filter(p => p !== null);
248
239
 
249
240
  if (heatPoints.length > 0) {
250
- heatmapLayer = L.heatLayer(heatPoints, heatmapOptions);
251
- console.log("Heatmap layer instance created.");
252
- } else { /* ... handle no valid points ... */
253
- heatmapLayer = null;
254
- }
255
- } catch (e) { /* ... error handling ... */
256
- heatmapLayer = null;
241
+ heatmapLayer = L.heatLayer(heatPoints, heatmapOptions); // Use state variable
242
+ console.log("Heatmap layer instance created with options:", heatmapOptions);
243
+ } else { console.warn("No valid points for heatmap layer."); }
244
+ } catch (e) { console.error("Error creating heatmap layer instance:", e); }
245
+ }
246
+
247
+
248
+ // --- Layer Update Functions ---
249
+ function redrawPinLayer() {
250
+ // Removes existing pin layer, calls createPinLayer, adds new one if current view is 'pins'
251
+ if (!map) return;
252
+ console.log("Redrawing pin layer...");
253
+ if (pinLayer && map.hasLayer(pinLayer)) {
254
+ map.removeLayer(pinLayer);
255
+ console.log("Removed existing pin layer before redraw.");
256
+ }
257
+ pinLayer = null; // Ensure it's cleared
258
+ createPinLayer(); // Recreate based on current clustering state
259
+ if (currentView === 'pins' && pinLayer) {
260
+ console.log("Adding newly created pin layer to map.");
261
+ map.addLayer(pinLayer);
257
262
  }
263
+ }
258
264
 
259
- console.log("Map layer instances created/updated.");
265
+ function updateHeatmap() {
266
+ // Updates options on the existing heatmap layer if it's present
267
+ if (!map || !heatmapLayer) {
268
+ console.warn("Cannot update heatmap: map or heatmap layer missing.");
269
+ return;
270
+ }
271
+ console.log("Updating heatmap options:", heatmapOptions);
272
+ try {
273
+ // Use Leaflet.heat's setOptions method
274
+ heatmapLayer.setOptions(heatmapOptions);
275
+ console.log("Heatmap options updated.");
276
+ } catch(e) {
277
+ console.error("Error setting heatmap options:", e);
278
+ }
260
279
  }
261
280
 
262
- // --- Adjust Map Bounds ---
281
+
282
+ // --- Adjust Map Bounds ---
263
283
  function adjustMapBounds() {
284
+ // (Keep the working version from previous steps)
264
285
  if (!map || coordinateData.length === 0) return;
265
-
266
286
  try {
267
287
  let bounds = null;
268
- // Prefer using marker cluster bounds if available and valid
269
- if (pinLayer && typeof pinLayer.getBounds === 'function') {
270
- bounds = pinLayer.getBounds();
271
- console.log("Attempting to get bounds from pin layer (marker cluster).");
288
+ if (currentView === 'pins' && pinLayer && typeof pinLayer.getBounds === 'function') {
289
+ bounds = pinLayer.getBounds();
290
+ console.log("Attempting bounds from pin layer.");
272
291
  }
273
-
274
- // If no valid bounds from cluster, or only heatmap exists, calculate from raw data
275
292
  if (!bounds || !bounds.isValid()) {
276
- console.log("Pin layer bounds invalid or unavailable, calculating bounds from raw coordinates.");
277
- const latLngs = coordinateData
278
- .map(loc => {
279
- if (loc.lat != null && loc.lon != null && !isNaN(loc.lat) && !isNaN(loc.lon)) {
280
- return [loc.lat, loc.lon];
281
- }
282
- return null;
283
- })
284
- .filter(p => p !== null);
285
-
286
- if (latLngs.length > 0) {
287
- bounds = L.latLngBounds(latLngs);
288
- }
293
+ console.log("Calculating bounds from raw coordinates.");
294
+ const latLngs = coordinateData.map(loc => { /* ... filter valid ... */ return [loc.lat, loc.lon]; }).filter(p => p !== null);
295
+ if (latLngs.length > 0) bounds = L.latLngBounds(latLngs);
289
296
  }
290
-
291
- // Fit map to bounds if valid bounds were found
292
297
  if (bounds && bounds.isValid()) {
293
- console.log("Fitting map to calculated bounds...");
294
- map.fitBounds(bounds, {padding: [50, 50]}); // Add padding
295
- console.log("Bounds fitted.");
298
+ console.log("Fitting map to bounds..."); map.fitBounds(bounds, { padding: [50, 50] }); console.log("Bounds fitted.");
296
299
  } else if (coordinateData.length === 1) {
297
- // Special case for a single point
298
- console.log("Only one valid coordinate, setting view directly.");
299
- const singleCoord = coordinateData.find(loc => loc.lat != null && loc.lon != null && !isNaN(loc.lat) && !isNaN(loc.lon));
300
- if (singleCoord) {
301
- map.setView([singleCoord.lat, singleCoord.lon], 13); // Zoom level 13 for single point
302
- } else {
303
- console.warn("Could not find the single valid coordinate to set view.");
304
- }
305
- } else {
306
- console.warn("Could not determine valid bounds to fit the map.");
307
- }
308
- } catch (e) {
309
- console.error("Error fitting map bounds:", e);
310
- }
300
+ console.log("Setting view for single coordinate.");
301
+ const singleCoord = coordinateData.find(loc => /* ... valid ... */);
302
+ if (singleCoord) map.setView([singleCoord.lat, singleCoord.lon], 13);
303
+ else console.warn("Could not find single valid coordinate.");
304
+ } else { console.warn("Could not determine valid bounds."); }
305
+ } catch (e) { console.error("Error fitting map bounds:", e); }
311
306
  }
312
307
 
313
- // --- View Toggling ---
314
- function setupToggleButton() {
315
- updateButtonText(); // Set initial text
316
- toggleButton.addEventListener('click', () => {
317
- console.log("Toggle button clicked!");
308
+
309
+ // --- Control Setup Functions ---
310
+ function setupViewToggleButton() {
311
+ updateViewToggleButtonText(); // Initial text
312
+ viewToggleButton.addEventListener('click', () => {
313
+ console.log("View toggle button clicked!");
318
314
  currentView = (currentView === 'pins') ? 'heatmap' : 'pins';
319
- showCurrentView(); // Update the map layers
320
- updateButtonText(); // Update the button text
315
+ showCurrentView(); // Update map display
316
+ updateViewToggleButtonText(); // Update button text
317
+ // Enable/disable cluster button based on view
318
+ if (clusterToggleButton) clusterToggleButton.disabled = (currentView !== 'pins');
321
319
  });
322
- console.log("Toggle button listener setup complete.");
320
+ console.log("View toggle button listener setup complete.");
323
321
  }
324
322
 
325
- function showCurrentView() {
326
- console.log(`Showing view: ${currentView}`);
327
- if (!map) {
328
- console.warn("Map not initialized, cannot show view.");
323
+ function setupClusterToggleButton() {
324
+ updateClusterToggleButtonText(); // Initial text
325
+ // Start disabled if not in pins view
326
+ clusterToggleButton.disabled = (currentView !== 'pins');
327
+ clusterToggleButton.addEventListener('click', () => {
328
+ if (currentView !== 'pins') return; // Safety check
329
+ console.log("Cluster toggle button clicked!");
330
+ isClusteringEnabled = !isClusteringEnabled; // Toggle state
331
+ redrawPinLayer(); // Recreate and potentially re-add the pin layer
332
+ updateClusterToggleButtonText(); // Update button text
333
+ });
334
+ console.log("Cluster toggle button listener setup complete.");
335
+ }
336
+
337
+ function setupHeatmapControls() {
338
+ // Check if all elements exist before adding listeners
339
+ if (!heatmapRadiusInput || !heatmapBlurInput || !heatmapMaxZoomInput || !radiusValueSpan || !blurValueSpan || !maxZoomValueSpan) {
340
+ console.error("One or more heatmap control elements not found. Cannot setup listeners.");
329
341
  return;
330
342
  }
331
343
 
332
- // --- Safely remove existing layers ---
344
+ // Set initial display values from defaults
345
+ radiusValueSpan.textContent = heatmapOptions.radius;
346
+ heatmapRadiusInput.value = heatmapOptions.radius;
347
+ blurValueSpan.textContent = heatmapOptions.blur;
348
+ heatmapBlurInput.value = heatmapOptions.blur;
349
+ maxZoomValueSpan.textContent = heatmapOptions.maxZoom;
350
+ heatmapMaxZoomInput.value = heatmapOptions.maxZoom;
351
+
352
+ // Add 'input' listeners for real-time updates
353
+ heatmapRadiusInput.addEventListener('input', (e) => {
354
+ const value = parseFloat(e.target.value);
355
+ heatmapOptions.radius = value; // Update state
356
+ radiusValueSpan.textContent = value; // Update display
357
+ updateHeatmap(); // Apply change to map
358
+ });
359
+ heatmapBlurInput.addEventListener('input', (e) => {
360
+ const value = parseFloat(e.target.value);
361
+ heatmapOptions.blur = value; // Update state
362
+ blurValueSpan.textContent = value; // Update display
363
+ updateHeatmap(); // Apply change to map
364
+ });
365
+ heatmapMaxZoomInput.addEventListener('input', (e) => {
366
+ const value = parseInt(e.target.value, 10);
367
+ heatmapOptions.maxZoom = value; // Update state
368
+ maxZoomValueSpan.textContent = value; // Update display
369
+ updateHeatmap(); // Apply change to map
370
+ });
371
+ console.log("Heatmap control listeners setup complete.");
372
+ }
373
+
374
+ function disableHeatmapControls(disabled) {
375
+ // Helper to enable/disable all heatmap inputs
376
+ if (heatmapRadiusInput) heatmapRadiusInput.disabled = disabled;
377
+ if (heatmapBlurInput) heatmapBlurInput.disabled = disabled;
378
+ if (heatmapMaxZoomInput) heatmapMaxZoomInput.disabled = disabled;
379
+ }
380
+
381
+
382
+ // --- View Switching Logic ---
383
+ function showCurrentView() {
384
+ console.log(`Showing view: ${currentView}`);
385
+ if (!map) { console.warn("Map not initialized."); return; }
386
+
387
+ // Remove existing layers from map
333
388
  console.log("Removing existing layers (if present)...");
334
- if (pinLayer && map.hasLayer(pinLayer)) {
335
- map.removeLayer(pinLayer);
336
- console.log("Removed pin layer");
337
- }
338
- if (heatmapLayer && map.hasLayer(heatmapLayer)) {
339
- map.removeLayer(heatmapLayer);
340
- console.log("Removed heatmap layer");
341
- }
342
- // --- End removal ---
389
+ if (pinLayer && map.hasLayer(pinLayer)) map.removeLayer(pinLayer);
390
+ if (heatmapLayer && map.hasLayer(heatmapLayer)) map.removeLayer(heatmapLayer);
343
391
 
344
- // --- Add the selected layer ---
392
+ // Add the selected layer and show/hide controls
345
393
  console.log(`Adding ${currentView} layer...`);
346
- try {
347
- if (currentView === 'pins' && pinLayer) {
348
- map.addLayer(pinLayer);
349
- console.log("Added pin layer to map.");
350
- } else if (currentView === 'heatmap' && heatmapLayer) {
351
- map.addLayer(heatmapLayer);
352
- console.log("Added heatmap layer to map.");
353
- } else {
354
- console.warn(`Cannot add layer for view "${currentView}": Corresponding layer instance is missing or null.`);
355
- // Maybe display a message if no layers could be shown?
356
- mapElement.innerHTML += '<p style="position: absolute; top: 10px; left: 50px; background: yellow; padding: 5px; z-index: 1000;">No data to display for this view.</p>';
357
- setTimeout(() => { // Clear message after a few seconds
358
- const msgElement = mapElement.querySelector('p[style*="yellow"]');
359
- if (msgElement) msgElement.remove();
360
- }, 3000);
394
+ let layerToAdd = null;
395
+ if (currentView === 'pins') {
396
+ layerToAdd = pinLayer;
397
+ if(heatmapOptionsPanel) heatmapOptionsPanel.style.display = 'none';
398
+ if(clusterToggleButton) clusterToggleButton.style.display = 'inline-block'; // Show cluster btn
399
+ if(clusterToggleButton) clusterToggleButton.disabled = false; // Ensure enabled
400
+ } else { // Heatmap view
401
+ layerToAdd = heatmapLayer;
402
+ if(heatmapOptionsPanel) heatmapOptionsPanel.style.display = 'block';
403
+ if(clusterToggleButton) clusterToggleButton.style.display = 'none'; // Hide cluster btn
404
+ if(clusterToggleButton) clusterToggleButton.disabled = true; // Ensure disabled
405
+ }
406
+
407
+ // Add the layer to the map if it exists
408
+ if (layerToAdd) {
409
+ try {
410
+ map.addLayer(layerToAdd);
411
+ console.log(`Added ${currentView} layer to map.`);
412
+ } catch (e) {
413
+ console.error(`Error adding ${currentView} layer:`, e);
414
+ updateStatus(`Error displaying ${currentView} layer.`, true);
361
415
  }
362
- } catch (e) {
363
- console.error(`Error adding ${currentView} layer:`, e);
416
+ } else {
417
+ // This happens if createLayer failed (e.g., no valid data)
418
+ console.warn(`Cannot add layer for view "${currentView}": Layer instance missing.`);
419
+ updateStatus(`No data available for ${currentView} view.`, false);
364
420
  }
365
- // --- End adding ---
366
421
  }
367
422
 
368
- function updateButtonText() {
369
- if (!toggleButton) return;
423
+
424
+ // --- Button Text Update Functions ---
425
+ function updateViewToggleButtonText() {
426
+ if (!viewToggleButton) return;
370
427
  const nextViewText = (currentView === 'pins') ? 'Heatmap' : 'Pin';
371
- toggleButton.textContent = `Switch to ${nextViewText} View`;
372
- console.log(`Button text updated to: ${toggleButton.textContent}`);
428
+ viewToggleButton.textContent = `Switch to ${nextViewText} View`;
429
+ console.log(`View Toggle Button text updated to: ${viewToggleButton.textContent}`);
373
430
  }
374
431
 
375
- // --- Start ---
432
+ function updateClusterToggleButtonText() {
433
+ if (!clusterToggleButton) return;
434
+ clusterToggleButton.textContent = isClusteringEnabled ? 'Disable Clustering' : 'Enable Clustering';
435
+ console.log(`Cluster Toggle Button text updated to: ${clusterToggleButton.textContent}`);
436
+ }
437
+
438
+
439
+ // --- Start Initialization ---
376
440
  initializeMap();
377
441
 
378
442
  }); // End DOMContentLoaded