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.
- {pretix_map-0.1.1.dist-info → pretix_map-0.1.3.dist-info}/METADATA +1 -1
- {pretix_map-0.1.1.dist-info → pretix_map-0.1.3.dist-info}/RECORD +15 -14
- pretix_mapplugin/__init__.py +1 -1
- pretix_mapplugin/management/commands/geocode_existing_orders.py +149 -70
- pretix_mapplugin/migrations/0002_remove_ordergeocodedata_geocoded_timestamp_and_more.py +32 -0
- pretix_mapplugin/models.py +30 -10
- pretix_mapplugin/signals.py +27 -22
- pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +40 -7
- pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +321 -257
- pretix_mapplugin/tasks.py +41 -18
- pretix_mapplugin/templates/pretix_mapplugin/map_page.html +40 -17
- {pretix_map-0.1.1.dist-info → pretix_map-0.1.3.dist-info}/WHEEL +0 -0
- {pretix_map-0.1.1.dist-info → pretix_map-0.1.3.dist-info}/entry_points.txt +0 -0
- {pretix_map-0.1.1.dist-info → pretix_map-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {pretix_map-0.1.1.dist-info → pretix_map-0.1.3.dist-info}/top_level.txt +0 -0
@@ -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 (
|
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
|
9
|
-
const
|
10
|
-
const
|
11
|
-
const
|
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
|
-
|
16
|
-
minOpacity: 0.2
|
45
|
+
minOpacity: 0.2 // Example optional parameter
|
17
46
|
};
|
18
47
|
|
19
|
-
// ---
|
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
|
30
|
-
|
31
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
55
|
-
|
56
|
-
if (!
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
if (!
|
61
|
-
|
62
|
-
|
63
|
-
|
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;
|
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); //
|
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
|
-
|
101
|
+
// Add Base Tile Layer
|
89
102
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
90
|
-
|
91
|
-
|
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
|
95
|
-
if (
|
96
|
-
|
97
|
-
|
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
|
102
|
-
|
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...");
|
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
|
-
|
124
|
-
|
125
|
-
|
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)
|
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
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
145
|
-
coordinateData = [];
|
146
|
-
return;
|
148
|
+
coordinateData = []; hideStatus(); return;
|
147
149
|
}
|
148
|
-
// --- End structure check ---
|
149
150
|
|
150
|
-
coordinateData = data.locations;
|
151
|
+
coordinateData = data.locations;
|
151
152
|
console.log(`Received ${coordinateData.length} coordinates.`);
|
152
153
|
|
153
|
-
//
|
154
|
-
|
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
|
-
//
|
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
|
-
//
|
163
|
-
if (toggleButton) toggleButton.disabled = false;
|
164
|
-
hideStatus();
|
168
|
+
hideStatus(); // Hide loading overlay
|
165
169
|
|
166
|
-
// Force redraw
|
170
|
+
// Force redraw after a short delay
|
167
171
|
setTimeout(function () {
|
168
172
|
console.log("Forcing map.invalidateSize() after data load...");
|
169
|
-
|
170
|
-
if (map &&
|
171
|
-
|
173
|
+
const container = document.getElementById(mapContainerId);
|
174
|
+
if (map && container && container.offsetWidth > 0) {
|
175
|
+
map.invalidateSize();
|
172
176
|
} else {
|
173
|
-
|
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
|
-
|
181
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
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
|
-
|
209
|
-
|
210
|
-
|
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
|
-
|
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 {
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
269
|
-
|
270
|
-
|
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("
|
277
|
-
const latLngs = coordinateData
|
278
|
-
|
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
|
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
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
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
|
320
|
-
|
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("
|
320
|
+
console.log("View toggle button listener setup complete.");
|
323
321
|
}
|
324
322
|
|
325
|
-
function
|
326
|
-
|
327
|
-
if
|
328
|
-
|
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
|
-
//
|
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
|
-
|
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
|
-
//
|
392
|
+
// Add the selected layer and show/hide controls
|
345
393
|
console.log(`Adding ${currentView} layer...`);
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
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
|
-
}
|
363
|
-
|
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
|
-
|
369
|
-
|
423
|
+
|
424
|
+
// --- Button Text Update Functions ---
|
425
|
+
function updateViewToggleButtonText() {
|
426
|
+
if (!viewToggleButton) return;
|
370
427
|
const nextViewText = (currentView === 'pins') ? 'Heatmap' : 'Pin';
|
371
|
-
|
372
|
-
console.log(`Button text updated to: ${
|
428
|
+
viewToggleButton.textContent = `Switch to ${nextViewText} View`;
|
429
|
+
console.log(`View Toggle Button text updated to: ${viewToggleButton.textContent}`);
|
373
430
|
}
|
374
431
|
|
375
|
-
|
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
|