pretix-map 0.1.3__tar.gz → 0.1.4__tar.gz
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.3/pretix_map.egg-info → pretix_map-0.1.4}/PKG-INFO +1 -1
- {pretix_map-0.1.3 → pretix_map-0.1.4/pretix_map.egg-info}/PKG-INFO +1 -1
- pretix_map-0.1.4/pretix_mapplugin/__init__.py +1 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/management/commands/geocode_existing_orders.py +1 -1
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +1 -1
- pretix_map-0.1.4/pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +452 -0
- pretix_map-0.1.4/pretix_mapplugin/templates/pretix_mapplugin/map_page.html +88 -0
- pretix_map-0.1.3/pretix_mapplugin/__init__.py +0 -1
- pretix_map-0.1.3/pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +0 -442
- pretix_map-0.1.3/pretix_mapplugin/templates/pretix_mapplugin/map_page.html +0 -68
- {pretix_map-0.1.3 → pretix_map-0.1.4}/LICENSE +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/MANIFEST.in +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/README.rst +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_map.egg-info/SOURCES.txt +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_map.egg-info/dependency_links.txt +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_map.egg-info/entry_points.txt +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_map.egg-info/requires.txt +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_map.egg-info/top_level.txt +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/apps.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/geocoding.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/locale/de/LC_MESSAGES/django.mo +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/locale/de/LC_MESSAGES/django.po +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/locale/de_Informal/.gitkeep +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.mo +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/management/__init__.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/management/commands/__init__.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/migrations/0001_initial.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/migrations/0002_remove_ordergeocodedata_geocoded_timestamp_and_more.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/migrations/__init__.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/models.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/signals.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/.gitkeep +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers-2x.png +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers.png +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon-2x.png +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon.png +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js.map +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js.map +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.css +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js.map +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js.map +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/tasks.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/templates/pretix_mapplugin/.gitkeep +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/urls.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/views.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/pyproject.toml +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/setup.cfg +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/setup.py +0 -0
- {pretix_map-0.1.3 → pretix_map-0.1.4}/tests/test_main.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pretix-map
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.4
|
4
4
|
Summary: An overview map of the catchment area of previous orders. Measured by postcode
|
5
5
|
Author-email: MarkenJaden <jjsch1410@gmail.com>
|
6
6
|
Maintainer-email: MarkenJaden <jjsch1410@gmail.com>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pretix-map
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.4
|
4
4
|
Summary: An overview map of the catchment area of previous orders. Measured by postcode
|
5
5
|
Author-email: MarkenJaden <jjsch1410@gmail.com>
|
6
6
|
Maintainer-email: MarkenJaden <jjsch1410@gmail.com>
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.1.4"
|
@@ -131,7 +131,7 @@ class Command(BaseCommand):
|
|
131
131
|
with scope(organizer=organizer):
|
132
132
|
# --- Get orders ---
|
133
133
|
orders_qs = Order.objects.filter(status=Order.STATUS_PAID).select_related(
|
134
|
-
'invoice_address', '
|
134
|
+
'invoice_address', 'event'
|
135
135
|
)
|
136
136
|
|
137
137
|
# --- Filter by event ---
|
@@ -0,0 +1,452 @@
|
|
1
|
+
document.addEventListener('DOMContentLoaded', function () {
|
2
|
+
console.log("Sales Map JS Loaded (Cluster Toggle & Heatmap Opts)");
|
3
|
+
|
4
|
+
// --- L.Icon.Default setup ---
|
5
|
+
try {
|
6
|
+
delete L.Icon.Default.prototype._getIconUrl;
|
7
|
+
L.Icon.Default.mergeOptions({
|
8
|
+
iconRetinaUrl: '/static/leaflet/images/marker-icon-2x.png',
|
9
|
+
iconUrl: '/static/leaflet/images/marker-icon.png',
|
10
|
+
shadowUrl: '/static/leaflet/images/marker-shadow.png',
|
11
|
+
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34],
|
12
|
+
tooltipAnchor: [16, -28], shadowSize: [41, 41]
|
13
|
+
});
|
14
|
+
console.log("Set Leaflet default icon image paths explicitly.");
|
15
|
+
} catch (e) {
|
16
|
+
console.error("Error setting icon path:", e);
|
17
|
+
}
|
18
|
+
// --- End L.Icon.Default setup ---
|
19
|
+
|
20
|
+
|
21
|
+
// --- Configuration ---
|
22
|
+
const mapContainerId = 'sales-map-container';
|
23
|
+
const statusOverlayId = 'map-status-overlay';
|
24
|
+
const viewToggleButtonId = 'view-toggle-btn';
|
25
|
+
const clusterToggleButtonId = 'cluster-toggle-btn';
|
26
|
+
const heatmapOptionsPanelId = 'heatmap-options-panel';
|
27
|
+
const initialZoom = 5;
|
28
|
+
const defaultMapView = 'pins';
|
29
|
+
|
30
|
+
// --- Globals & State ---
|
31
|
+
let map = null;
|
32
|
+
let coordinateData = [];
|
33
|
+
let pinLayer = null;
|
34
|
+
let heatmapLayer = null;
|
35
|
+
let currentView = defaultMapView;
|
36
|
+
let dataUrl = null;
|
37
|
+
let isClusteringEnabled = true;
|
38
|
+
let heatmapOptions = {
|
39
|
+
radius: 25, blur: 15, maxZoom: 18, minOpacity: 0.2
|
40
|
+
};
|
41
|
+
|
42
|
+
// --- DOM Elements ---
|
43
|
+
const mapElement = document.getElementById(mapContainerId);
|
44
|
+
const statusOverlay = document.getElementById(statusOverlayId);
|
45
|
+
const viewToggleButton = document.getElementById(viewToggleButtonId);
|
46
|
+
const clusterToggleButton = document.getElementById(clusterToggleButtonId);
|
47
|
+
const heatmapOptionsPanel = document.getElementById(heatmapOptionsPanelId);
|
48
|
+
const heatmapRadiusInput = document.getElementById('heatmap-radius');
|
49
|
+
const heatmapBlurInput = document.getElementById('heatmap-blur');
|
50
|
+
const heatmapMaxZoomInput = document.getElementById('heatmap-maxZoom');
|
51
|
+
const radiusValueSpan = document.getElementById('radius-value');
|
52
|
+
const blurValueSpan = document.getElementById('blur-value');
|
53
|
+
const maxZoomValueSpan = document.getElementById('maxzoom-value');
|
54
|
+
|
55
|
+
// --- Status Update Helpers ---
|
56
|
+
function updateStatus(message, isError = false) {
|
57
|
+
if (statusOverlay) {
|
58
|
+
const p = statusOverlay.querySelector('p');
|
59
|
+
if (p) {
|
60
|
+
p.textContent = message;
|
61
|
+
p.className = isError ? 'text-danger' : '';
|
62
|
+
}
|
63
|
+
statusOverlay.style.display = 'flex';
|
64
|
+
} else {
|
65
|
+
console.warn("Status overlay element not found.");
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
function hideStatus() {
|
70
|
+
if (statusOverlay) {
|
71
|
+
statusOverlay.style.display = 'none';
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
// --- End Status Helpers ---
|
76
|
+
|
77
|
+
|
78
|
+
// --- Initialization ---
|
79
|
+
function initializeMap() {
|
80
|
+
if (!mapElement) {
|
81
|
+
console.error(`Map container #${mapContainerId} not found.`);
|
82
|
+
return;
|
83
|
+
}
|
84
|
+
if (!statusOverlay) {
|
85
|
+
console.warn("Status overlay element not found.");
|
86
|
+
}
|
87
|
+
if (!viewToggleButton) console.warn("View toggle button not found.");
|
88
|
+
if (!clusterToggleButton) console.warn("Cluster toggle button not found.");
|
89
|
+
if (!heatmapOptionsPanel) console.warn("Heatmap options panel not found.");
|
90
|
+
if (!heatmapRadiusInput || !heatmapBlurInput || !heatmapMaxZoomInput) console.warn("Heatmap input elements missing.");
|
91
|
+
|
92
|
+
dataUrl = mapElement.dataset.dataUrl;
|
93
|
+
if (!dataUrl) {
|
94
|
+
updateStatus("Configuration Error: Missing data source URL.", true);
|
95
|
+
return;
|
96
|
+
}
|
97
|
+
console.log(`Data URL found: ${dataUrl}`);
|
98
|
+
updateStatus("Initializing map...");
|
99
|
+
|
100
|
+
try {
|
101
|
+
map = L.map(mapContainerId).setView([48.85, 2.35], initialZoom);
|
102
|
+
console.log("L.map() called successfully.");
|
103
|
+
|
104
|
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
105
|
+
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
106
|
+
maxZoom: 18,
|
107
|
+
}).on('load', function () {
|
108
|
+
console.log('Base tiles loaded.');
|
109
|
+
}).addTo(map);
|
110
|
+
console.log("Tile layer added successfully.");
|
111
|
+
|
112
|
+
if (viewToggleButton) setupViewToggleButton();
|
113
|
+
if (clusterToggleButton) setupClusterToggleButton();
|
114
|
+
setupHeatmapControls();
|
115
|
+
fetchDataAndDraw();
|
116
|
+
|
117
|
+
} catch (error) {
|
118
|
+
console.error("ERROR during Leaflet initialization:", error);
|
119
|
+
updateStatus(`Leaflet Init Failed: ${error.message}`, true);
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
|
124
|
+
// --- Data Fetching & Initial Drawing ---
|
125
|
+
function fetchDataAndDraw() {
|
126
|
+
if (!dataUrl) return;
|
127
|
+
console.log("Fetching coordinates from:", dataUrl);
|
128
|
+
updateStatus("Loading ticket locations...");
|
129
|
+
if (viewToggleButton) viewToggleButton.disabled = true;
|
130
|
+
if (clusterToggleButton) clusterToggleButton.disabled = true;
|
131
|
+
disableHeatmapControls(true);
|
132
|
+
|
133
|
+
fetch(dataUrl)
|
134
|
+
.then(response => {
|
135
|
+
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
|
136
|
+
return response.json();
|
137
|
+
})
|
138
|
+
.then(data => {
|
139
|
+
if (data.error) throw new Error(`API Error: ${data.error}`);
|
140
|
+
if (!data || !data.locations || !Array.isArray(data.locations)) {
|
141
|
+
console.warn("Invalid data format:", data);
|
142
|
+
updateStatus("No valid locations found.", false);
|
143
|
+
coordinateData = [];
|
144
|
+
hideStatus();
|
145
|
+
return;
|
146
|
+
}
|
147
|
+
if (data.locations.length === 0) {
|
148
|
+
console.log("No locations received.");
|
149
|
+
updateStatus("No locations found for event.", false);
|
150
|
+
coordinateData = [];
|
151
|
+
hideStatus();
|
152
|
+
return;
|
153
|
+
}
|
154
|
+
|
155
|
+
coordinateData = data.locations;
|
156
|
+
console.log(`Received ${coordinateData.length} coordinates.`);
|
157
|
+
|
158
|
+
if (viewToggleButton) viewToggleButton.disabled = false;
|
159
|
+
if (clusterToggleButton) clusterToggleButton.disabled = (currentView !== 'pins');
|
160
|
+
disableHeatmapControls(false);
|
161
|
+
|
162
|
+
createAllLayers();
|
163
|
+
showCurrentView();
|
164
|
+
adjustMapBounds();
|
165
|
+
hideStatus();
|
166
|
+
|
167
|
+
setTimeout(function () {
|
168
|
+
console.log("Forcing map.invalidateSize() after data load...");
|
169
|
+
const container = document.getElementById(mapContainerId);
|
170
|
+
if (map && container && container.offsetWidth > 0) {
|
171
|
+
map.invalidateSize();
|
172
|
+
} else {
|
173
|
+
console.warn(`Skipping invalidateSize. Map: ${!!map}, Container: ${!!container}, OffsetWidth: ${container ? container.offsetWidth : 'N/A'}`);
|
174
|
+
}
|
175
|
+
}, 100);
|
176
|
+
})
|
177
|
+
.catch(error => {
|
178
|
+
console.error('Error fetching/processing data:', error);
|
179
|
+
updateStatus(`Error loading map data: ${error.message}.`, true);
|
180
|
+
});
|
181
|
+
}
|
182
|
+
|
183
|
+
|
184
|
+
// --- Layer Creation Functions ---
|
185
|
+
function createAllLayers() {
|
186
|
+
createPinLayer();
|
187
|
+
createHeatmapLayer();
|
188
|
+
console.log("Layers created/updated.");
|
189
|
+
}
|
190
|
+
|
191
|
+
function createPinLayer() {
|
192
|
+
console.log(`Creating pin layer (Clustering: ${isClusteringEnabled})...`);
|
193
|
+
pinLayer = null;
|
194
|
+
if (coordinateData.length === 0) {
|
195
|
+
console.warn("No data for pin layer.");
|
196
|
+
return;
|
197
|
+
}
|
198
|
+
const markers = [];
|
199
|
+
coordinateData.forEach((loc, index) => {
|
200
|
+
try {
|
201
|
+
if (loc.lat == null || loc.lon == null || isNaN(loc.lat) || isNaN(loc.lon)) return;
|
202
|
+
const marker = L.marker(L.latLng(loc.lat, loc.lon));
|
203
|
+
if (loc.tooltip) marker.bindTooltip(loc.tooltip);
|
204
|
+
if (loc.order_url) {
|
205
|
+
marker.on('click', () => window.open(loc.order_url, '_blank'));
|
206
|
+
}
|
207
|
+
markers.push(marker);
|
208
|
+
} catch (e) {
|
209
|
+
console.error(`Error creating marker ${index}:`, e);
|
210
|
+
}
|
211
|
+
});
|
212
|
+
if (markers.length === 0) {
|
213
|
+
console.warn("No valid markers created.");
|
214
|
+
return;
|
215
|
+
}
|
216
|
+
if (isClusteringEnabled) {
|
217
|
+
pinLayer = L.markerClusterGroup();
|
218
|
+
pinLayer.addLayers(markers);
|
219
|
+
console.log("Marker cluster populated.");
|
220
|
+
} else {
|
221
|
+
pinLayer = L.layerGroup(markers);
|
222
|
+
console.log("Simple layer group populated.");
|
223
|
+
}
|
224
|
+
}
|
225
|
+
|
226
|
+
function createHeatmapLayer() {
|
227
|
+
console.log("Creating heatmap layer...");
|
228
|
+
heatmapLayer = null;
|
229
|
+
if (coordinateData.length === 0) {
|
230
|
+
console.warn("No data for heatmap.");
|
231
|
+
return;
|
232
|
+
}
|
233
|
+
try {
|
234
|
+
const heatPoints = coordinateData.map(loc => {
|
235
|
+
if (loc.lat != null && loc.lon != null && !isNaN(loc.lat) && !isNaN(loc.lon)) {
|
236
|
+
return [loc.lat, loc.lon, 1.0];
|
237
|
+
}
|
238
|
+
return null;
|
239
|
+
}).filter(p => p !== null);
|
240
|
+
if (heatPoints.length > 0) {
|
241
|
+
heatmapLayer = L.heatLayer(heatPoints, heatmapOptions);
|
242
|
+
console.log("Heatmap created:", heatmapOptions);
|
243
|
+
} else {
|
244
|
+
console.warn("No valid points for heatmap.");
|
245
|
+
}
|
246
|
+
} catch (e) {
|
247
|
+
console.error("Error creating heatmap:", e);
|
248
|
+
}
|
249
|
+
}
|
250
|
+
|
251
|
+
|
252
|
+
// --- Layer Update Functions ---
|
253
|
+
function redrawPinLayer() {
|
254
|
+
if (!map) return;
|
255
|
+
console.log("Redrawing pin layer...");
|
256
|
+
if (pinLayer && map.hasLayer(pinLayer)) map.removeLayer(pinLayer);
|
257
|
+
pinLayer = null;
|
258
|
+
createPinLayer();
|
259
|
+
if (currentView === 'pins' && pinLayer) {
|
260
|
+
console.log("Adding new pin layer.");
|
261
|
+
map.addLayer(pinLayer);
|
262
|
+
}
|
263
|
+
}
|
264
|
+
|
265
|
+
function updateHeatmap() {
|
266
|
+
if (!map || !heatmapLayer) {
|
267
|
+
console.warn("Cannot update heatmap.");
|
268
|
+
return;
|
269
|
+
}
|
270
|
+
console.log("Updating heatmap opts:", heatmapOptions);
|
271
|
+
try {
|
272
|
+
heatmapLayer.setOptions(heatmapOptions);
|
273
|
+
console.log("Heatmap opts updated.");
|
274
|
+
} catch (e) {
|
275
|
+
console.error("Error setting heatmap opts:", e);
|
276
|
+
}
|
277
|
+
}
|
278
|
+
|
279
|
+
|
280
|
+
// --- Adjust Map Bounds (Corrected) ---
|
281
|
+
function adjustMapBounds() {
|
282
|
+
if (!map || coordinateData.length === 0) return;
|
283
|
+
try {
|
284
|
+
let bounds = null;
|
285
|
+
if (currentView === 'pins' && pinLayer && typeof pinLayer.getBounds === 'function') {
|
286
|
+
bounds = pinLayer.getBounds();
|
287
|
+
console.log("Attempting bounds from pin layer.");
|
288
|
+
}
|
289
|
+
// Calculate from raw if pin layer bounds unavailable/invalid or if in heatmap view
|
290
|
+
if (!bounds || !bounds.isValid()) {
|
291
|
+
console.log("Calculating bounds from raw coordinates.");
|
292
|
+
// Filter valid lat/lon pairs
|
293
|
+
const latLngs = coordinateData
|
294
|
+
.map(loc => {
|
295
|
+
if (loc.lat != null && loc.lon != null && !isNaN(loc.lat) && !isNaN(loc.lon)) {
|
296
|
+
return [loc.lat, loc.lon];
|
297
|
+
}
|
298
|
+
return null;
|
299
|
+
})
|
300
|
+
.filter(p => p !== null);
|
301
|
+
if (latLngs.length > 0) bounds = L.latLngBounds(latLngs);
|
302
|
+
}
|
303
|
+
|
304
|
+
// Apply bounds if valid
|
305
|
+
if (bounds && bounds.isValid()) {
|
306
|
+
console.log("Fitting map to bounds...");
|
307
|
+
map.fitBounds(bounds, {padding: [50, 50]});
|
308
|
+
console.log("Bounds fitted.");
|
309
|
+
// Handle single point case
|
310
|
+
} else if (coordinateData.filter(loc => loc.lat != null && loc.lon != null && !isNaN(loc.lat) && !isNaN(loc.lon)).length === 1) {
|
311
|
+
console.log("Setting view for single coordinate.");
|
312
|
+
// --- Corrected logic to find the single valid coordinate ---
|
313
|
+
const singleCoord = coordinateData.find(loc => loc.lat != null && loc.lon != null && !isNaN(loc.lat) && !isNaN(loc.lon));
|
314
|
+
// --- End Correction ---
|
315
|
+
if (singleCoord) map.setView([singleCoord.lat, singleCoord.lon], 13);
|
316
|
+
else console.warn("Could not find single valid coordinate to set view.");
|
317
|
+
// Removed extra ')' here
|
318
|
+
} else {
|
319
|
+
console.warn("Could not determine valid bounds.");
|
320
|
+
}
|
321
|
+
} catch (e) {
|
322
|
+
console.error("Error fitting map bounds:", e);
|
323
|
+
}
|
324
|
+
} // End adjustMapBounds function
|
325
|
+
|
326
|
+
|
327
|
+
// --- Control Setup Functions ---
|
328
|
+
function setupViewToggleButton() {
|
329
|
+
updateViewToggleButtonText();
|
330
|
+
viewToggleButton.addEventListener('click', () => {
|
331
|
+
console.log("View toggle clicked!");
|
332
|
+
currentView = (currentView === 'pins') ? 'heatmap' : 'pins';
|
333
|
+
showCurrentView();
|
334
|
+
updateViewToggleButtonText();
|
335
|
+
if (clusterToggleButton) clusterToggleButton.disabled = (currentView !== 'pins');
|
336
|
+
});
|
337
|
+
console.log("View toggle listener setup.");
|
338
|
+
}
|
339
|
+
|
340
|
+
function setupClusterToggleButton() {
|
341
|
+
updateClusterToggleButtonText();
|
342
|
+
clusterToggleButton.disabled = (currentView !== 'pins');
|
343
|
+
clusterToggleButton.addEventListener('click', () => {
|
344
|
+
if (currentView !== 'pins') return;
|
345
|
+
console.log("Cluster toggle clicked!");
|
346
|
+
isClusteringEnabled = !isClusteringEnabled;
|
347
|
+
redrawPinLayer();
|
348
|
+
updateClusterToggleButtonText();
|
349
|
+
});
|
350
|
+
console.log("Cluster toggle listener setup.");
|
351
|
+
}
|
352
|
+
|
353
|
+
function setupHeatmapControls() {
|
354
|
+
if (!heatmapRadiusInput || !heatmapBlurInput || !heatmapMaxZoomInput || !radiusValueSpan || !blurValueSpan || !maxZoomValueSpan) {
|
355
|
+
console.error("Heatmap controls missing.");
|
356
|
+
return;
|
357
|
+
}
|
358
|
+
radiusValueSpan.textContent = heatmapOptions.radius;
|
359
|
+
heatmapRadiusInput.value = heatmapOptions.radius;
|
360
|
+
blurValueSpan.textContent = heatmapOptions.blur;
|
361
|
+
heatmapBlurInput.value = heatmapOptions.blur;
|
362
|
+
maxZoomValueSpan.textContent = heatmapOptions.maxZoom;
|
363
|
+
heatmapMaxZoomInput.value = heatmapOptions.maxZoom;
|
364
|
+
heatmapRadiusInput.addEventListener('input', (e) => {
|
365
|
+
const v = parseFloat(e.target.value);
|
366
|
+
heatmapOptions.radius = v;
|
367
|
+
radiusValueSpan.textContent = v;
|
368
|
+
updateHeatmap();
|
369
|
+
});
|
370
|
+
heatmapBlurInput.addEventListener('input', (e) => {
|
371
|
+
const v = parseFloat(e.target.value);
|
372
|
+
heatmapOptions.blur = v;
|
373
|
+
blurValueSpan.textContent = v;
|
374
|
+
updateHeatmap();
|
375
|
+
});
|
376
|
+
heatmapMaxZoomInput.addEventListener('input', (e) => {
|
377
|
+
const v = parseInt(e.target.value, 10);
|
378
|
+
heatmapOptions.maxZoom = v;
|
379
|
+
maxZoomValueSpan.textContent = v;
|
380
|
+
updateHeatmap();
|
381
|
+
});
|
382
|
+
console.log("Heatmap control listeners setup.");
|
383
|
+
}
|
384
|
+
|
385
|
+
function disableHeatmapControls(disabled) {
|
386
|
+
if (heatmapRadiusInput) heatmapRadiusInput.disabled = disabled;
|
387
|
+
if (heatmapBlurInput) heatmapBlurInput.disabled = disabled;
|
388
|
+
if (heatmapMaxZoomInput) heatmapMaxZoomInput.disabled = disabled;
|
389
|
+
}
|
390
|
+
|
391
|
+
|
392
|
+
// --- View Switching Logic ---
|
393
|
+
function showCurrentView() {
|
394
|
+
console.log(`Showing view: ${currentView}`);
|
395
|
+
if (!map) {
|
396
|
+
console.warn("Map not init.");
|
397
|
+
return;
|
398
|
+
}
|
399
|
+
console.log("Removing layers...");
|
400
|
+
if (pinLayer && map.hasLayer(pinLayer)) map.removeLayer(pinLayer);
|
401
|
+
if (heatmapLayer && map.hasLayer(heatmapLayer)) map.removeLayer(heatmapLayer);
|
402
|
+
console.log(`Adding ${currentView} layer...`);
|
403
|
+
let layerToAdd = null;
|
404
|
+
if (currentView === 'pins') {
|
405
|
+
layerToAdd = pinLayer;
|
406
|
+
if (heatmapOptionsPanel) heatmapOptionsPanel.style.display = 'none';
|
407
|
+
if (clusterToggleButton) {
|
408
|
+
clusterToggleButton.style.display = 'inline-block';
|
409
|
+
clusterToggleButton.disabled = false;
|
410
|
+
}
|
411
|
+
} else {
|
412
|
+
layerToAdd = heatmapLayer;
|
413
|
+
if (heatmapOptionsPanel) heatmapOptionsPanel.style.display = 'block';
|
414
|
+
if (clusterToggleButton) {
|
415
|
+
clusterToggleButton.style.display = 'none';
|
416
|
+
clusterToggleButton.disabled = true;
|
417
|
+
}
|
418
|
+
}
|
419
|
+
if (layerToAdd) {
|
420
|
+
try {
|
421
|
+
map.addLayer(layerToAdd);
|
422
|
+
console.log(`Added ${currentView} layer.`);
|
423
|
+
} catch (e) {
|
424
|
+
console.error(`Error adding ${currentView}:`, e);
|
425
|
+
updateStatus(`Error display ${currentView}.`, true);
|
426
|
+
}
|
427
|
+
} else {
|
428
|
+
console.warn(`Layer instance missing for ${currentView}.`);
|
429
|
+
updateStatus(`No data for ${currentView}.`, false);
|
430
|
+
}
|
431
|
+
}
|
432
|
+
|
433
|
+
|
434
|
+
// --- Button Text Update Functions ---
|
435
|
+
function updateViewToggleButtonText() {
|
436
|
+
if (!viewToggleButton) return;
|
437
|
+
const next = (currentView === 'pins') ? 'Heatmap' : 'Pin';
|
438
|
+
viewToggleButton.textContent = `Switch to ${next} View`;
|
439
|
+
console.log(`View Btn text: ${viewToggleButton.textContent}`);
|
440
|
+
}
|
441
|
+
|
442
|
+
function updateClusterToggleButtonText() {
|
443
|
+
if (!clusterToggleButton) return;
|
444
|
+
clusterToggleButton.textContent = isClusteringEnabled ? 'Disable Clustering' : 'Enable Clustering';
|
445
|
+
console.log(`Cluster Btn text: ${clusterToggleButton.textContent}`);
|
446
|
+
}
|
447
|
+
|
448
|
+
|
449
|
+
// --- Start Initialization ---
|
450
|
+
initializeMap();
|
451
|
+
|
452
|
+
}); // End DOMContentLoaded
|
@@ -0,0 +1,88 @@
|
|
1
|
+
{% extends "pretixcontrol/event/settings_base.html" %}
|
2
|
+
{% load i18n %}
|
3
|
+
{% load static %}
|
4
|
+
{% load eventurl %}
|
5
|
+
|
6
|
+
{% block title %}{% trans "Ticket Sales Map" %}{% endblock %}
|
7
|
+
|
8
|
+
{% block inside %}
|
9
|
+
|
10
|
+
<div class="plugin-map-content-wrapper">
|
11
|
+
|
12
|
+
<h1>{% trans "Ticket Sales Map" %}</h1>
|
13
|
+
|
14
|
+
<div class="form-inline map-controls-row"
|
15
|
+
style="margin-bottom: 1em; display: flex; flex-wrap: wrap; align-items: flex-start; gap: 15px;">
|
16
|
+
|
17
|
+
<div class="map-buttons-group" style="display: flex; flex-wrap: wrap; gap: 10px; align-items: center;">
|
18
|
+
<div class="form-group">
|
19
|
+
<button id="view-toggle-btn" class="btn btn-default" disabled>Switch to Heatmap View</button>
|
20
|
+
</div>
|
21
|
+
<div class="form-group">
|
22
|
+
<button id="cluster-toggle-btn" class="btn btn-default" disabled style="display: inline-block;">
|
23
|
+
Disable Clustering
|
24
|
+
</button>
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
<div id="heatmap-options-panel" class="panel panel-default"
|
28
|
+
style="display: none; padding: 10px 15px; border-radius: 4px; min-width: 350px;">
|
29
|
+
<h5 style="margin-top: 0; margin-bottom: 10px;">{% trans "Heatmap Options" %}</h5>
|
30
|
+
<div class="form-horizontal">
|
31
|
+
<div class="form-group form-group-sm" style="margin-bottom: 5px;">
|
32
|
+
<label for="heatmap-radius" class="col-sm-3 control-label"
|
33
|
+
style="padding-top: 5px;">Radius</label>
|
34
|
+
<div class="col-sm-7">
|
35
|
+
<input type="range" id="heatmap-radius" class="form-control" min="1" max="100" value="25"
|
36
|
+
step="1" disabled>
|
37
|
+
</div>
|
38
|
+
<div class="col-sm-2">
|
39
|
+
<span id="radius-value" class="form-control-static">25</span>
|
40
|
+
</div>
|
41
|
+
</div>
|
42
|
+
<div class="form-group form-group-sm" style="margin-bottom: 5px;">
|
43
|
+
<label for="heatmap-blur" class="col-sm-3 control-label" style="padding-top: 5px;">Blur</label>
|
44
|
+
<div class="col-sm-7">
|
45
|
+
<input type="range" id="heatmap-blur" class="form-control" min="1" max="50" value="15"
|
46
|
+
step="1" disabled>
|
47
|
+
</div>
|
48
|
+
<div class="col-sm-2">
|
49
|
+
<span id="blur-value" class="form-control-static">15</span>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
<div class="form-group form-group-sm" style="margin-bottom: 5px;">
|
53
|
+
<label for="heatmap-maxZoom" class="col-sm-3 control-label" style="padding-top: 5px;">Max
|
54
|
+
Zoom</label>
|
55
|
+
<div class="col-sm-7">
|
56
|
+
<input type="range" id="heatmap-maxZoom" class="form-control" min="1" max="18" value="18"
|
57
|
+
step="1" disabled>
|
58
|
+
</div>
|
59
|
+
<div class="col-sm-2">
|
60
|
+
<span id="maxzoom-value" class="form-control-static">18</span>
|
61
|
+
</div>
|
62
|
+
</div>
|
63
|
+
</div>
|
64
|
+
</div>
|
65
|
+
</div>
|
66
|
+
|
67
|
+
<div class="map-wrapper" style="position: relative; border: 1px solid #ccc; flex-grow: 1; min-height: 0;">
|
68
|
+
<div id="sales-map-container"
|
69
|
+
data-data-url="{% url 'plugins:pretix_mapplugin:event.settings.salesmap.data' organizer=request.organizer.slug event=request.event.slug %}">
|
70
|
+
</div>
|
71
|
+
<div id="map-status-overlay"
|
72
|
+
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.8); z-index: 1000; display: flex; justify-content: center; align-items: center; text-align: center;">
|
73
|
+
<p>Loading map data...</p>
|
74
|
+
</div>
|
75
|
+
</div>
|
76
|
+
</div>
|
77
|
+
|
78
|
+
<script src="
|
79
|
+
{% static 'pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js' %}"></script>
|
80
|
+
<link rel="stylesheet" href="{% static 'pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css' %}"/>
|
81
|
+
<link rel="stylesheet"
|
82
|
+
href="{% static 'pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css' %}"/>
|
83
|
+
<script src="{% static 'pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js' %}"></script>
|
84
|
+
|
85
|
+
<script src="{% static 'pretix_mapplugin/js/salesmap.js' %}"></script>
|
86
|
+
<link rel="stylesheet" href="{% static 'pretix_mapplugin/css/salesmap.css' %}"/>
|
87
|
+
|
88
|
+
{% endblock %}
|
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = "0.1.3"
|
@@ -1,442 +0,0 @@
|
|
1
|
-
document.addEventListener('DOMContentLoaded', function () {
|
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
|
-
|
23
|
-
|
24
|
-
// --- Configuration ---
|
25
|
-
const mapContainerId = 'sales-map-container';
|
26
|
-
const statusOverlayId = 'map-status-overlay';
|
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)
|
42
|
-
radius: 25,
|
43
|
-
blur: 15,
|
44
|
-
maxZoom: 18,
|
45
|
-
minOpacity: 0.2 // Example optional parameter
|
46
|
-
};
|
47
|
-
|
48
|
-
// --- DOM Elements ---
|
49
|
-
const mapElement = document.getElementById(mapContainerId);
|
50
|
-
const statusOverlay = document.getElementById(statusOverlayId);
|
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 ---
|
63
|
-
function updateStatus(message, isError = false) {
|
64
|
-
if (statusOverlay) {
|
65
|
-
const p = statusOverlay.querySelector('p');
|
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."); }
|
69
|
-
}
|
70
|
-
function hideStatus() {
|
71
|
-
if (statusOverlay) { statusOverlay.style.display = 'none'; } // Hide overlay
|
72
|
-
}
|
73
|
-
// --- End Status Helpers ---
|
74
|
-
|
75
|
-
|
76
|
-
// --- Initialization ---
|
77
|
-
function initializeMap() {
|
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
|
88
|
-
dataUrl = mapElement.dataset.dataUrl;
|
89
|
-
if (!dataUrl) {
|
90
|
-
updateStatus("Configuration Error: Missing data source URL.", true);
|
91
|
-
return;
|
92
|
-
}
|
93
|
-
console.log(`Data URL found: ${dataUrl}`);
|
94
|
-
updateStatus("Initializing map...");
|
95
|
-
|
96
|
-
// Create Leaflet Map
|
97
|
-
try {
|
98
|
-
map = L.map(mapContainerId).setView([48.85, 2.35], initialZoom); // Centered somewhat on Europe
|
99
|
-
console.log("L.map() called successfully.");
|
100
|
-
|
101
|
-
// Add Base Tile Layer
|
102
|
-
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
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);
|
106
|
-
console.log("Tile layer added successfully.");
|
107
|
-
|
108
|
-
// Setup Controls (Event Listeners)
|
109
|
-
if (viewToggleButton) setupViewToggleButton();
|
110
|
-
if (clusterToggleButton) setupClusterToggleButton();
|
111
|
-
setupHeatmapControls(); // Setup listeners even if panel hidden
|
112
|
-
|
113
|
-
// Fetch data to populate layers
|
114
|
-
fetchDataAndDraw();
|
115
|
-
|
116
|
-
} catch (error) {
|
117
|
-
console.error("ERROR during Leaflet initialization:", error);
|
118
|
-
updateStatus(`Leaflet Init Failed: ${error.message}`, true);
|
119
|
-
}
|
120
|
-
}
|
121
|
-
|
122
|
-
|
123
|
-
// --- Data Fetching & Initial Drawing ---
|
124
|
-
function fetchDataAndDraw() {
|
125
|
-
if (!dataUrl) return;
|
126
|
-
console.log("Fetching coordinates from:", dataUrl);
|
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);
|
132
|
-
|
133
|
-
fetch(dataUrl)
|
134
|
-
.then(response => {
|
135
|
-
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
|
136
|
-
return response.json();
|
137
|
-
})
|
138
|
-
.then(data => {
|
139
|
-
if (data.error) throw new Error(`API Error: ${data.error}`);
|
140
|
-
if (!data || !data.locations || !Array.isArray(data.locations)) {
|
141
|
-
console.warn("Invalid or empty data format received:", data);
|
142
|
-
updateStatus("No valid geocoded ticket locations found.", false);
|
143
|
-
coordinateData = []; hideStatus(); return;
|
144
|
-
}
|
145
|
-
if (data.locations.length === 0) {
|
146
|
-
console.log("No coordinate data received (empty list).");
|
147
|
-
updateStatus("No geocoded ticket locations found for this event.", false);
|
148
|
-
coordinateData = []; hideStatus(); return;
|
149
|
-
}
|
150
|
-
|
151
|
-
coordinateData = data.locations;
|
152
|
-
console.log(`Received ${coordinateData.length} coordinates.`);
|
153
|
-
|
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);
|
160
|
-
|
161
|
-
// Create both layers based on fetched data
|
162
|
-
createAllLayers();
|
163
|
-
// Display the default layer
|
164
|
-
showCurrentView();
|
165
|
-
// Adjust bounds to fit the data
|
166
|
-
adjustMapBounds();
|
167
|
-
|
168
|
-
hideStatus(); // Hide loading overlay
|
169
|
-
|
170
|
-
// Force redraw after a short delay
|
171
|
-
setTimeout(function () {
|
172
|
-
console.log("Forcing map.invalidateSize() after data load...");
|
173
|
-
const container = document.getElementById(mapContainerId);
|
174
|
-
if (map && container && container.offsetWidth > 0) {
|
175
|
-
map.invalidateSize();
|
176
|
-
} else {
|
177
|
-
console.warn(`Skipping invalidateSize. Map: ${!!map}, Container: ${!!container}, OffsetWidth: ${container ? container.offsetWidth : 'N/A'}`);
|
178
|
-
}
|
179
|
-
}, 100);
|
180
|
-
})
|
181
|
-
.catch(error => {
|
182
|
-
console.error('Error fetching or processing coordinate data:', error);
|
183
|
-
updateStatus(`Error loading map data: ${error.message}. Please try again later.`, true);
|
184
|
-
// Keep controls disabled on error
|
185
|
-
});
|
186
|
-
}
|
187
|
-
|
188
|
-
|
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) => {
|
205
|
-
try {
|
206
|
-
if (loc.lat == null || loc.lon == null || isNaN(loc.lat) || isNaN(loc.lon)) return;
|
207
|
-
const latLng = L.latLng(loc.lat, loc.lon);
|
208
|
-
const marker = L.marker(latLng);
|
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); }
|
213
|
-
});
|
214
|
-
|
215
|
-
if (markers.length === 0) { console.warn("No valid markers created for pin layer."); return; }
|
216
|
-
|
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
|
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
|
-
|
233
|
-
try {
|
234
|
-
const heatPoints = coordinateData.map(loc => {
|
235
|
-
if (loc.lat != null && loc.lon != null && !isNaN(loc.lat) && !isNaN(loc.lon)) {
|
236
|
-
return [loc.lat, loc.lon, 1.0]; // Intensity 1.0
|
237
|
-
} return null;
|
238
|
-
}).filter(p => p !== null);
|
239
|
-
|
240
|
-
if (heatPoints.length > 0) {
|
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);
|
262
|
-
}
|
263
|
-
}
|
264
|
-
|
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
|
-
}
|
279
|
-
}
|
280
|
-
|
281
|
-
|
282
|
-
// --- Adjust Map Bounds ---
|
283
|
-
function adjustMapBounds() {
|
284
|
-
// (Keep the working version from previous steps)
|
285
|
-
if (!map || coordinateData.length === 0) return;
|
286
|
-
try {
|
287
|
-
let bounds = null;
|
288
|
-
if (currentView === 'pins' && pinLayer && typeof pinLayer.getBounds === 'function') {
|
289
|
-
bounds = pinLayer.getBounds();
|
290
|
-
console.log("Attempting bounds from pin layer.");
|
291
|
-
}
|
292
|
-
if (!bounds || !bounds.isValid()) {
|
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);
|
296
|
-
}
|
297
|
-
if (bounds && bounds.isValid()) {
|
298
|
-
console.log("Fitting map to bounds..."); map.fitBounds(bounds, { padding: [50, 50] }); console.log("Bounds fitted.");
|
299
|
-
} else if (coordinateData.length === 1) {
|
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); }
|
306
|
-
}
|
307
|
-
|
308
|
-
|
309
|
-
// --- Control Setup Functions ---
|
310
|
-
function setupViewToggleButton() {
|
311
|
-
updateViewToggleButtonText(); // Initial text
|
312
|
-
viewToggleButton.addEventListener('click', () => {
|
313
|
-
console.log("View toggle button clicked!");
|
314
|
-
currentView = (currentView === 'pins') ? 'heatmap' : 'pins';
|
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');
|
319
|
-
});
|
320
|
-
console.log("View toggle button listener setup complete.");
|
321
|
-
}
|
322
|
-
|
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.");
|
341
|
-
return;
|
342
|
-
}
|
343
|
-
|
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
|
388
|
-
console.log("Removing existing layers (if present)...");
|
389
|
-
if (pinLayer && map.hasLayer(pinLayer)) map.removeLayer(pinLayer);
|
390
|
-
if (heatmapLayer && map.hasLayer(heatmapLayer)) map.removeLayer(heatmapLayer);
|
391
|
-
|
392
|
-
// Add the selected layer and show/hide controls
|
393
|
-
console.log(`Adding ${currentView} layer...`);
|
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);
|
415
|
-
}
|
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);
|
420
|
-
}
|
421
|
-
}
|
422
|
-
|
423
|
-
|
424
|
-
// --- Button Text Update Functions ---
|
425
|
-
function updateViewToggleButtonText() {
|
426
|
-
if (!viewToggleButton) return;
|
427
|
-
const nextViewText = (currentView === 'pins') ? 'Heatmap' : 'Pin';
|
428
|
-
viewToggleButton.textContent = `Switch to ${nextViewText} View`;
|
429
|
-
console.log(`View Toggle Button text updated to: ${viewToggleButton.textContent}`);
|
430
|
-
}
|
431
|
-
|
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 ---
|
440
|
-
initializeMap();
|
441
|
-
|
442
|
-
}); // End DOMContentLoaded
|
@@ -1,68 +0,0 @@
|
|
1
|
-
{% extends "pretixcontrol/event/settings_base.html" %}
|
2
|
-
{% load i18n %}
|
3
|
-
{% load static %}
|
4
|
-
{% load eventurl %}
|
5
|
-
|
6
|
-
{% block title %}{% trans "Ticket Sales Map" %}{% endblock %}
|
7
|
-
|
8
|
-
{% block inside %}
|
9
|
-
<h1>{% trans "Ticket Sales Map" %}</h1>
|
10
|
-
|
11
|
-
{# -- Controls Row -- #}
|
12
|
-
<div class="form-inline" style="margin-bottom: 1em;">
|
13
|
-
<div class="form-group" style="margin-right: 10px;">
|
14
|
-
<button id="view-toggle-btn" class="btn btn-default">Switch to Heatmap View</button>
|
15
|
-
</div>
|
16
|
-
{# -- NEW Cluster Toggle Button -- #}
|
17
|
-
<div class="form-group" style="margin-right: 10px;">
|
18
|
-
<button id="cluster-toggle-btn" class="btn btn-default" disabled>Disable Clustering</button>
|
19
|
-
</div>
|
20
|
-
{# -- End Cluster Toggle Button -- #}
|
21
|
-
</div>
|
22
|
-
{# -- End Controls Row -- #}
|
23
|
-
|
24
|
-
{# -- NEW Heatmap Options Panel (Initially hidden for pins view) -- #}
|
25
|
-
<div id="heatmap-options-panel" class="panel panel-default"
|
26
|
-
style="display: none; padding: 15px; margin-bottom: 1em; max-width: 400px;">
|
27
|
-
<h4>{% trans "Heatmap Options" %}</h4>
|
28
|
-
<div class="form-group">
|
29
|
-
<label for="heatmap-radius">Radius: <span id="radius-value">25</span></label>
|
30
|
-
<input type="range" id="heatmap-radius" class="form-control" min="1" max="100" value="25" step="1" disabled>
|
31
|
-
</div>
|
32
|
-
<div class="form-group">
|
33
|
-
<label for="heatmap-blur">Blur: <span id="blur-value">15</span></label>
|
34
|
-
<input type="range" id="heatmap-blur" class="form-control" min="1" max="50" value="15" step="1" disabled>
|
35
|
-
</div>
|
36
|
-
<div class="form-group">
|
37
|
-
<label for="heatmap-maxZoom">Max Zoom: <span id="maxzoom-value">18</span></label>
|
38
|
-
<input type="range" id="heatmap-maxZoom" class="form-control" min="1" max="18" value="18" step="1" disabled>
|
39
|
-
</div>
|
40
|
-
{# Add more options like gradient if desired later #}
|
41
|
-
</div>
|
42
|
-
{# -- End Heatmap Options Panel -- #}
|
43
|
-
|
44
|
-
|
45
|
-
{# -- Map Area -- #}
|
46
|
-
<div style="position: relative; border: 1px solid #ccc;">
|
47
|
-
<div id="sales-map-container"
|
48
|
-
data-data-url="{% url 'plugins:pretix_mapplugin:event.settings.salesmap.data' organizer=request.organizer.slug event=request.event.slug %}">
|
49
|
-
</div>
|
50
|
-
<div id="map-status-overlay"
|
51
|
-
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.8); z-index: 1000; display: flex; justify-content: center; align-items: center; text-align: center;">
|
52
|
-
<p>Loading map data...</p>
|
53
|
-
</div>
|
54
|
-
</div>
|
55
|
-
{# -- End Map Area -- #}
|
56
|
-
|
57
|
-
|
58
|
-
{# === Leaflet & Plugins (Keep as is) === #}
|
59
|
-
<script src="{% static 'pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js' %}"></script>
|
60
|
-
<link rel="stylesheet" href="{% static 'pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css' %}"/>
|
61
|
-
<link rel="stylesheet" href="{% static 'pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css' %}"/>
|
62
|
-
<script src="{% static 'pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js' %}"></script>
|
63
|
-
|
64
|
-
{# === Your JS/CSS (Keep as is) === #}
|
65
|
-
<script src="{% static 'pretix_mapplugin/js/salesmap.js' %}"></script>
|
66
|
-
<link rel="stylesheet" href="{% static 'pretix_mapplugin/css/salesmap.css' %}"/>
|
67
|
-
|
68
|
-
{% endblock %}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.mo
RENAMED
File without changes
|
{pretix_map-0.1.3 → pretix_map-0.1.4}/pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|