pretix-map 0.0.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.
Files changed (45) hide show
  1. pretix_map-0.0.3.dist-info/METADATA +193 -0
  2. pretix_map-0.0.3.dist-info/RECORD +45 -0
  3. pretix_map-0.0.3.dist-info/WHEEL +5 -0
  4. pretix_map-0.0.3.dist-info/entry_points.txt +5 -0
  5. pretix_map-0.0.3.dist-info/licenses/LICENSE +15 -0
  6. pretix_map-0.0.3.dist-info/top_level.txt +1 -0
  7. pretix_mapplugin/__init__.py +1 -0
  8. pretix_mapplugin/apps.py +28 -0
  9. pretix_mapplugin/geocoding.py +113 -0
  10. pretix_mapplugin/locale/de/LC_MESSAGES/django.mo +0 -0
  11. pretix_mapplugin/locale/de/LC_MESSAGES/django.po +12 -0
  12. pretix_mapplugin/locale/de_Informal/.gitkeep +0 -0
  13. pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.mo +0 -0
  14. pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po +12 -0
  15. pretix_mapplugin/management/__init__.py +0 -0
  16. pretix_mapplugin/management/commands/__init__.py +0 -0
  17. pretix_mapplugin/management/commands/geocode_existing_orders.py +167 -0
  18. pretix_mapplugin/migrations/__init__.py +0 -0
  19. pretix_mapplugin/models.py +27 -0
  20. pretix_mapplugin/signals.py +73 -0
  21. pretix_mapplugin/static/pretix_mapplugin/.gitkeep +0 -0
  22. pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +19 -0
  23. pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +378 -0
  24. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css +60 -0
  25. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css +14 -0
  26. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers-2x.png +0 -0
  27. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers.png +0 -0
  28. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon-2x.png +0 -0
  29. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon.png +0 -0
  30. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png +0 -0
  31. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js +11 -0
  32. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js +14419 -0
  33. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js.map +1 -0
  34. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js +14512 -0
  35. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js.map +1 -0
  36. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.css +661 -0
  37. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js +6 -0
  38. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js.map +1 -0
  39. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js +3 -0
  40. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js.map +1 -0
  41. pretix_mapplugin/tasks.py +74 -0
  42. pretix_mapplugin/templates/pretix_mapplugin/.gitkeep +0 -0
  43. pretix_mapplugin/templates/pretix_mapplugin/map_page.html +45 -0
  44. pretix_mapplugin/urls.py +21 -0
  45. pretix_mapplugin/views.py +163 -0
@@ -0,0 +1,73 @@
1
+ import logging
2
+ from django.dispatch import receiver
3
+ from django.http import HttpRequest # For type hinting
4
+ from django.urls import NoReverseMatch, reverse # Import reverse and NoReverseMatch
5
+ from django.utils.translation import gettext_lazy as _ # For translatable labels
6
+
7
+ # --- Pretix Signals ---
8
+ from pretix.base.signals import order_paid
9
+ from pretix.control.signals import nav_event # Import the navigation signal
10
+
11
+ # --- Tasks ---
12
+ from .tasks import geocode_order_task
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # --- Constants ---
17
+ MAP_VIEW_URL_NAME = 'plugins:pretix_mapplugin:event.settings.salesmap.show'
18
+ # Define the permission required to see the map link
19
+ REQUIRED_MAP_PERMISSION = 'can_view_orders'
20
+
21
+
22
+ # --- Signal Receiver for Geocoding (Keep As Is) ---
23
+ @receiver(order_paid, dispatch_uid="sales_mapper_order_paid_geocode")
24
+ def trigger_geocoding_on_payment(sender, order, **kwargs):
25
+ # ... (keep your existing geocoding logic) ...
26
+ try:
27
+ geocode_order_task.apply_async(args=[order.pk])
28
+ logger.info(f"Geocoding task queued for paid order {order.code} (PK: {order.pk}).")
29
+ except NameError:
30
+ logger.error("geocode_order_task not found. Make sure it's imported correctly.")
31
+ except Exception as e:
32
+ logger.exception(f"Failed to queue geocoding task for order {order.code}: {e}")
33
+
34
+
35
+ # --- Signal Receiver for Adding Navigation Item ---
36
+ @receiver(nav_event, dispatch_uid="sales_mapper_nav_event_add_map")
37
+ def add_map_nav_item(sender, request: HttpRequest, **kwargs):
38
+ """
39
+ Adds a navigation item for the Sales Map to the event control panel sidebar.
40
+ """
41
+ # Check if the user has the required permission for the current event
42
+ has_permission = request.user.has_event_permission(
43
+ request.organizer, request.event, REQUIRED_MAP_PERMISSION, request=request
44
+ )
45
+ if not has_permission:
46
+ return [] # Return empty list if user lacks permission
47
+
48
+ # Try to generate the URL for the map view
49
+ try:
50
+ map_url = reverse(MAP_VIEW_URL_NAME, kwargs={
51
+ 'organizer': request.organizer.slug,
52
+ 'event': request.event.slug,
53
+ })
54
+ except NoReverseMatch:
55
+ logger.error(f"Could not reverse URL for map view '{MAP_VIEW_URL_NAME}'. Check urls.py.")
56
+ return [] # Return empty list if URL cannot be generated
57
+
58
+ # Check if the current page *is* the map page to set the 'active' state
59
+ is_active = False
60
+ if hasattr(request, 'resolver_match') and request.resolver_match:
61
+ is_active = request.resolver_match.view_name == MAP_VIEW_URL_NAME
62
+
63
+ # Define the navigation item dictionary
64
+ nav_item = {
65
+ 'label': _('Sales Map'), # Translatable label
66
+ 'url': map_url,
67
+ 'active': is_active,
68
+ 'icon': 'map-o', # Font Awesome icon name (fa-map-o) - adjust if needed
69
+ # 'category': _('Orders'), # Optional: Suggests category, placement might vary
70
+ }
71
+
72
+ # Return the item in a list
73
+ return [nav_item]
File without changes
@@ -0,0 +1,19 @@
1
+ #sales-map-container {
2
+ height: 500px;
3
+ width: 100%;
4
+ position: relative; /* Needed for Leaflet internal positioning */
5
+ background: #eee; /* Optional: Light background while tiles load */
6
+ }
7
+
8
+ #map-status-overlay p {
9
+ padding: 1em;
10
+ background: #fff;
11
+ border-radius: 5px;
12
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
13
+ }
14
+
15
+ #map-status-overlay p.text-danger { /* Style for error messages */
16
+ color: #a94442; /* Bootstrap danger color */
17
+ background-color: #f2dede; /* Bootstrap danger background */
18
+ border-color: #ebccd1; /* Bootstrap danger border */
19
+ }
@@ -0,0 +1,378 @@
1
+ // Wait for the DOM to be fully loaded before running map code
2
+ document.addEventListener('DOMContentLoaded', function () {
3
+ console.log("Sales Map JS Loaded (FINAL TEST - INCLUDING TOGGLE)");
4
+
5
+ // --- Configuration ---
6
+ const mapContainerId = 'sales-map-container';
7
+ 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 = {
12
+ radius: 25,
13
+ blur: 15,
14
+ maxZoom: 18,
15
+ // max: 1.0, // Let leaflet-heat calculate max based on data density
16
+ minOpacity: 0.2
17
+ };
18
+
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
+
27
+ const mapElement = document.getElementById(mapContainerId);
28
+ const statusOverlay = document.getElementById(statusOverlayId);
29
+ const toggleButton = document.getElementById(toggleButtonId);
30
+
31
+ // --- Helper to update status overlay ---
32
+ function updateStatus(message, isError = false) {
33
+ if (statusOverlay) {
34
+ 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
+ }
43
+ }
44
+
45
+ // --- Helper to hide status overlay ---
46
+ function hideStatus() {
47
+ if (statusOverlay) {
48
+ statusOverlay.style.display = 'none'; // Hide the overlay
49
+ }
50
+ }
51
+
52
+ // --- Initialization ---
53
+ 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 ---
69
+ dataUrl = mapElement.dataset.dataUrl;
70
+ if (!dataUrl) {
71
+ console.error("Data URL not found in container's data-data-url attribute! Cannot fetch data.");
72
+ updateStatus("Configuration Error: Missing data source URL.", true);
73
+ return; // Stop initialization if data URL is missing
74
+ }
75
+ 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
+ updateStatus("Initializing map...");
84
+ try {
85
+ map = L.map(mapContainerId).setView([48.85, 2.35], initialZoom); // Default center
86
+ console.log("L.map() called successfully.");
87
+
88
+ console.log("Adding Tile Layer...");
89
+ 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);
92
+ console.log("Tile layer added successfully.");
93
+
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
+ }
100
+
101
+ // Fetch data and populate the map layers
102
+ fetchCoordinateData();
103
+
104
+ } catch (error) {
105
+ console.error("ERROR during Leaflet initialization:", error);
106
+ updateStatus(`Leaflet Init Failed: ${error.message}`, true);
107
+ }
108
+ }
109
+
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
+
118
+ console.log("Fetching coordinates from:", dataUrl);
119
+ updateStatus("Loading ticket locations..."); // Update status
120
+
121
+ fetch(dataUrl)
122
+ .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
+ })
129
+ .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 ---
134
+ 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
140
+ }
141
+ if (data.locations.length === 0) {
142
+ console.log("No coordinate data received (empty list).");
143
+ updateStatus("No geocoded ticket locations found for this event.", false);
144
+ if (toggleButton) toggleButton.disabled = true;
145
+ coordinateData = [];
146
+ return;
147
+ }
148
+ // --- End structure check ---
149
+
150
+ coordinateData = data.locations; // Store the [{lat: Y, lon: X, tooltip: Z}, ...] array
151
+ console.log(`Received ${coordinateData.length} coordinates.`);
152
+
153
+ // --- Create layers (but don't add to map yet) ---
154
+ createMapLayers();
155
+
156
+ // --- Show the default view ---
157
+ showCurrentView();
158
+
159
+ // Adjust map bounds to fit markers if coordinates were found
160
+ adjustMapBounds();
161
+
162
+ // Enable button if it was disabled and we got data
163
+ if (toggleButton) toggleButton.disabled = false;
164
+ hideStatus();
165
+
166
+ // Force redraw just in case (sometimes needed after dynamic content/bounds changes)
167
+ setTimeout(function () {
168
+ 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();
172
+ } else {
173
+ console.warn("Skipping invalidateSize because map or container is missing.");
174
+ }
175
+ }, 100);
176
+
177
+ })
178
+ .catch(error => {
179
+ 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
+ });
184
+ }
185
+
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
+
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}
197
+ try {
198
+ if (loc.lat == null || loc.lon == null) { /* ... skip invalid ... */
199
+ return;
200
+ }
201
+ const latLng = L.latLng(loc.lat, loc.lon);
202
+ if (isNaN(latLng.lat) || isNaN(latLng.lng)) { /* ... skip invalid ... */
203
+ return;
204
+ }
205
+
206
+ 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
+ }
235
+ });
236
+ console.log("Pin layer instance created with markers (incl. tooltips and clicks).");
237
+
238
+
239
+ // 2. Create Heatmap Layer (No changes needed here)
240
+ console.log("Creating heatmap layer instance...");
241
+ try {
242
+ const heatPoints = coordinateData.map(loc => {
243
+ 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;
247
+ }).filter(p => p !== null);
248
+
249
+ 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;
257
+ }
258
+
259
+ console.log("Map layer instances created/updated.");
260
+ }
261
+
262
+ // --- Adjust Map Bounds ---
263
+ function adjustMapBounds() {
264
+ if (!map || coordinateData.length === 0) return;
265
+
266
+ try {
267
+ 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).");
272
+ }
273
+
274
+ // If no valid bounds from cluster, or only heatmap exists, calculate from raw data
275
+ 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
+ }
289
+ }
290
+
291
+ // Fit map to bounds if valid bounds were found
292
+ 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.");
296
+ } 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
+ }
311
+ }
312
+
313
+ // --- View Toggling ---
314
+ function setupToggleButton() {
315
+ updateButtonText(); // Set initial text
316
+ toggleButton.addEventListener('click', () => {
317
+ console.log("Toggle button clicked!");
318
+ currentView = (currentView === 'pins') ? 'heatmap' : 'pins';
319
+ showCurrentView(); // Update the map layers
320
+ updateButtonText(); // Update the button text
321
+ });
322
+ console.log("Toggle button listener setup complete.");
323
+ }
324
+
325
+ function showCurrentView() {
326
+ console.log(`Showing view: ${currentView}`);
327
+ if (!map) {
328
+ console.warn("Map not initialized, cannot show view.");
329
+ return;
330
+ }
331
+
332
+ // --- Safely remove existing layers ---
333
+ 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 ---
343
+
344
+ // --- Add the selected layer ---
345
+ 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);
361
+ }
362
+ } catch (e) {
363
+ console.error(`Error adding ${currentView} layer:`, e);
364
+ }
365
+ // --- End adding ---
366
+ }
367
+
368
+ function updateButtonText() {
369
+ if (!toggleButton) return;
370
+ const nextViewText = (currentView === 'pins') ? 'Heatmap' : 'Pin';
371
+ toggleButton.textContent = `Switch to ${nextViewText} View`;
372
+ console.log(`Button text updated to: ${toggleButton.textContent}`);
373
+ }
374
+
375
+ // --- Start ---
376
+ initializeMap();
377
+
378
+ }); // End DOMContentLoaded
@@ -0,0 +1,60 @@
1
+ .marker-cluster-small {
2
+ background-color: rgba(181, 226, 140, 0.6);
3
+ }
4
+ .marker-cluster-small div {
5
+ background-color: rgba(110, 204, 57, 0.6);
6
+ }
7
+
8
+ .marker-cluster-medium {
9
+ background-color: rgba(241, 211, 87, 0.6);
10
+ }
11
+ .marker-cluster-medium div {
12
+ background-color: rgba(240, 194, 12, 0.6);
13
+ }
14
+
15
+ .marker-cluster-large {
16
+ background-color: rgba(253, 156, 115, 0.6);
17
+ }
18
+ .marker-cluster-large div {
19
+ background-color: rgba(241, 128, 23, 0.6);
20
+ }
21
+
22
+ /* IE 6-8 fallback colors */
23
+ .leaflet-oldie .marker-cluster-small {
24
+ background-color: rgb(181, 226, 140);
25
+ }
26
+ .leaflet-oldie .marker-cluster-small div {
27
+ background-color: rgb(110, 204, 57);
28
+ }
29
+
30
+ .leaflet-oldie .marker-cluster-medium {
31
+ background-color: rgb(241, 211, 87);
32
+ }
33
+ .leaflet-oldie .marker-cluster-medium div {
34
+ background-color: rgb(240, 194, 12);
35
+ }
36
+
37
+ .leaflet-oldie .marker-cluster-large {
38
+ background-color: rgb(253, 156, 115);
39
+ }
40
+ .leaflet-oldie .marker-cluster-large div {
41
+ background-color: rgb(241, 128, 23);
42
+ }
43
+
44
+ .marker-cluster {
45
+ background-clip: padding-box;
46
+ border-radius: 20px;
47
+ }
48
+ .marker-cluster div {
49
+ width: 30px;
50
+ height: 30px;
51
+ margin-left: 5px;
52
+ margin-top: 5px;
53
+
54
+ text-align: center;
55
+ border-radius: 15px;
56
+ font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
57
+ }
58
+ .marker-cluster span {
59
+ line-height: 30px;
60
+ }
@@ -0,0 +1,14 @@
1
+ .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
2
+ -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
3
+ -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
4
+ -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
5
+ transition: transform 0.3s ease-out, opacity 0.3s ease-in;
6
+ }
7
+
8
+ .leaflet-cluster-spider-leg {
9
+ /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
10
+ -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
11
+ -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
12
+ -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
13
+ transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
14
+ }
@@ -0,0 +1,11 @@
1
+ /*
2
+ (c) 2014, Vladimir Agafonkin
3
+ simpleheat, a tiny JavaScript library for drawing heatmaps with Canvas
4
+ https://github.com/mourner/simpleheat
5
+ */
6
+ !function(){"use strict";function t(i){return this instanceof t?(this._canvas=i="string"==typeof i?document.getElementById(i):i,this._ctx=i.getContext("2d"),this._width=i.width,this._height=i.height,this._max=1,void this.clear()):new t(i)}t.prototype={defaultRadius:25,defaultGradient:{.4:"blue",.6:"cyan",.7:"lime",.8:"yellow",1:"red"},data:function(t,i){return this._data=t,this},max:function(t){return this._max=t,this},add:function(t){return this._data.push(t),this},clear:function(){return this._data=[],this},radius:function(t,i){i=i||15;var a=this._circle=document.createElement("canvas"),s=a.getContext("2d"),e=this._r=t+i;return a.width=a.height=2*e,s.shadowOffsetX=s.shadowOffsetY=200,s.shadowBlur=i,s.shadowColor="black",s.beginPath(),s.arc(e-200,e-200,t,0,2*Math.PI,!0),s.closePath(),s.fill(),this},gradient:function(t){var i=document.createElement("canvas"),a=i.getContext("2d"),s=a.createLinearGradient(0,0,0,256);i.width=1,i.height=256;for(var e in t)s.addColorStop(e,t[e]);return a.fillStyle=s,a.fillRect(0,0,1,256),this._grad=a.getImageData(0,0,1,256).data,this},draw:function(t){this._circle||this.radius(this.defaultRadius),this._grad||this.gradient(this.defaultGradient);var i=this._ctx;i.clearRect(0,0,this._width,this._height);for(var a,s=0,e=this._data.length;e>s;s++)a=this._data[s],i.globalAlpha=Math.max(a[2]/this._max,t||.05),i.drawImage(this._circle,a[0]-this._r,a[1]-this._r);var n=i.getImageData(0,0,this._width,this._height);return this._colorize(n.data,this._grad),i.putImageData(n,0,0),this},_colorize:function(t,i){for(var a,s=3,e=t.length;e>s;s+=4)a=4*t[s],a&&(t[s-3]=i[a],t[s-2]=i[a+1],t[s-1]=i[a+2])}},window.simpleheat=t}(),/*
7
+ (c) 2014, Vladimir Agafonkin
8
+ Leaflet.heat, a tiny and fast heatmap plugin for Leaflet.
9
+ https://github.com/Leaflet/Leaflet.heat
10
+ */
11
+ L.HeatLayer=(L.Layer?L.Layer:L.Class).extend({initialize:function(t,i){this._latlngs=t,L.setOptions(this,i)},setLatLngs:function(t){return this._latlngs=t,this.redraw()},addLatLng:function(t){return this._latlngs.push(t),this.redraw()},setOptions:function(t){return L.setOptions(this,t),this._heat&&this._updateOptions(),this.redraw()},redraw:function(){return!this._heat||this._frame||this._map._animating||(this._frame=L.Util.requestAnimFrame(this._redraw,this)),this},onAdd:function(t){this._map=t,this._canvas||this._initCanvas(),t._panes.overlayPane.appendChild(this._canvas),t.on("moveend",this._reset,this),t.options.zoomAnimation&&L.Browser.any3d&&t.on("zoomanim",this._animateZoom,this),this._reset()},onRemove:function(t){t.getPanes().overlayPane.removeChild(this._canvas),t.off("moveend",this._reset,this),t.options.zoomAnimation&&t.off("zoomanim",this._animateZoom,this)},addTo:function(t){return t.addLayer(this),this},_initCanvas:function(){var t=this._canvas=L.DomUtil.create("canvas","leaflet-heatmap-layer leaflet-layer"),i=L.DomUtil.testProp(["transformOrigin","WebkitTransformOrigin","msTransformOrigin"]);t.style[i]="50% 50%";var a=this._map.getSize();t.width=a.x,t.height=a.y;var s=this._map.options.zoomAnimation&&L.Browser.any3d;L.DomUtil.addClass(t,"leaflet-zoom-"+(s?"animated":"hide")),this._heat=simpleheat(t),this._updateOptions()},_updateOptions:function(){this._heat.radius(this.options.radius||this._heat.defaultRadius,this.options.blur),this.options.gradient&&this._heat.gradient(this.options.gradient),this.options.max&&this._heat.max(this.options.max)},_reset:function(){var t=this._map.containerPointToLayerPoint([0,0]);L.DomUtil.setPosition(this._canvas,t);var i=this._map.getSize();this._heat._width!==i.x&&(this._canvas.width=this._heat._width=i.x),this._heat._height!==i.y&&(this._canvas.height=this._heat._height=i.y),this._redraw()},_redraw:function(){var t,i,a,s,e,n,h,o,r,d=[],_=this._heat._r,l=this._map.getSize(),m=new L.Bounds(L.point([-_,-_]),l.add([_,_])),c=void 0===this.options.max?1:this.options.max,u=void 0===this.options.maxZoom?this._map.getMaxZoom():this.options.maxZoom,f=1/Math.pow(2,Math.max(0,Math.min(u-this._map.getZoom(),12))),g=_/2,p=[],v=this._map._getMapPanePos(),w=v.x%g,y=v.y%g;for(t=0,i=this._latlngs.length;i>t;t++)if(a=this._map.latLngToContainerPoint(this._latlngs[t]),m.contains(a)){e=Math.floor((a.x-w)/g)+2,n=Math.floor((a.y-y)/g)+2;var x=void 0!==this._latlngs[t].alt?this._latlngs[t].alt:void 0!==this._latlngs[t][2]?+this._latlngs[t][2]:1;r=x*f,p[n]=p[n]||[],s=p[n][e],s?(s[0]=(s[0]*s[2]+a.x*r)/(s[2]+r),s[1]=(s[1]*s[2]+a.y*r)/(s[2]+r),s[2]+=r):p[n][e]=[a.x,a.y,r]}for(t=0,i=p.length;i>t;t++)if(p[t])for(h=0,o=p[t].length;o>h;h++)s=p[t][h],s&&d.push([Math.round(s[0]),Math.round(s[1]),Math.min(s[2],c)]);this._heat.data(d).draw(this.options.minOpacity),this._frame=null},_animateZoom:function(t){var i=this._map.getZoomScale(t.zoom),a=this._map._getCenterOffset(t.center)._multiplyBy(-i).subtract(this._map._getMapPanePos());L.DomUtil.setTransform?L.DomUtil.setTransform(this._canvas,a,i):this._canvas.style[L.DomUtil.TRANSFORM]=L.DomUtil.getTranslateString(a)+" scale("+i+")"}}),L.heatLayer=function(t,i){return new L.HeatLayer(t,i)};