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.
- pretix_map-0.0.3.dist-info/METADATA +193 -0
- pretix_map-0.0.3.dist-info/RECORD +45 -0
- pretix_map-0.0.3.dist-info/WHEEL +5 -0
- pretix_map-0.0.3.dist-info/entry_points.txt +5 -0
- pretix_map-0.0.3.dist-info/licenses/LICENSE +15 -0
- pretix_map-0.0.3.dist-info/top_level.txt +1 -0
- pretix_mapplugin/__init__.py +1 -0
- pretix_mapplugin/apps.py +28 -0
- pretix_mapplugin/geocoding.py +113 -0
- pretix_mapplugin/locale/de/LC_MESSAGES/django.mo +0 -0
- pretix_mapplugin/locale/de/LC_MESSAGES/django.po +12 -0
- pretix_mapplugin/locale/de_Informal/.gitkeep +0 -0
- pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.mo +0 -0
- pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po +12 -0
- pretix_mapplugin/management/__init__.py +0 -0
- pretix_mapplugin/management/commands/__init__.py +0 -0
- pretix_mapplugin/management/commands/geocode_existing_orders.py +167 -0
- pretix_mapplugin/migrations/__init__.py +0 -0
- pretix_mapplugin/models.py +27 -0
- pretix_mapplugin/signals.py +73 -0
- pretix_mapplugin/static/pretix_mapplugin/.gitkeep +0 -0
- pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +19 -0
- pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +378 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css +60 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css +14 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers-2x.png +0 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers.png +0 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon-2x.png +0 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon.png +0 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png +0 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js +11 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js +14419 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js.map +1 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js +14512 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js.map +1 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.css +661 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js +6 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js.map +1 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js +3 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js.map +1 -0
- pretix_mapplugin/tasks.py +74 -0
- pretix_mapplugin/templates/pretix_mapplugin/.gitkeep +0 -0
- pretix_mapplugin/templates/pretix_mapplugin/map_page.html +45 -0
- pretix_mapplugin/urls.py +21 -0
- 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
|
+
}
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -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)};
|