Skip to content

Commit 6d02a3d

Browse files
committed
Speed up loading
1 parent 053f133 commit 6d02a3d

1 file changed

Lines changed: 62 additions & 94 deletions

File tree

docs/js/map.js

Lines changed: 62 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ const MapView = (() => {
55
let anchorLayer = null;
66
let portLayer = null;
77
let portMarkers = {};
8-
let globalCanvas = null;
8+
let _globalLayer = null;
9+
let _globalRenderer = null;
10+
let _portRenderer = null;
911
let _heatLayer = null;
1012

1113
// ── Land mask ──────────────────────────────────────────────────────────────
@@ -20,7 +22,6 @@ const MapView = (() => {
2022
const ctx = c.getContext("2d");
2123
ctx.drawImage(img, 0, 0, _LM_W, _LM_H);
2224
_landMaskData = ctx.getImageData(0, 0, _LM_W, _LM_H).data;
23-
if (globalCanvas) globalCanvas._requestDraw();
2425
};
2526
img.src = "data/land_mask.png";
2627
}
@@ -189,94 +190,55 @@ const MapView = (() => {
189190
}
190191
function clearAnchorZones() { anchorLayer.clearLayers(); }
191192

192-
// ── Global overlay — L.canvas renderer lives inside leaflet-overlay-pane,
193-
// so it participates in Leaflet's CSS zoom animation with zero lag.
194-
const GlobalCanvasOverlay = L.Layer.extend({
195-
_data: [], _filters: {}, _renderer: null, _layer: null,
196-
197-
setData(data, filters) {
198-
this._data = data;
199-
this._filters = filters;
200-
if (this._map) this._redraw();
201-
},
202-
203-
onAdd(map) {
204-
this._map = map;
205-
this._renderer = L.canvas({ padding: 0.5 });
206-
this._layer = L.layerGroup().addTo(map);
207-
this._redraw();
208-
},
209-
210-
onRemove(map) {
211-
if (this._layer) { this._layer.clearLayers(); map.removeLayer(this._layer); }
212-
if (this._renderer) { this._renderer.remove(); }
213-
this._layer = this._renderer = this._map = null;
214-
},
215-
216-
_redraw() {
217-
if (!this._layer || !this._renderer) return;
218-
this._layer.clearLayers();
219-
220-
const { activeTypes, currentTimeOfDay, timeWindow, currentMonth, dateRange } = this._filters;
221-
222-
for (const traj of this._data) {
223-
if (activeTypes && !activeTypes.has(traj.vessel_type)) continue;
224-
if (!_passesTodFilter(traj, currentTimeOfDay, timeWindow ?? 30)) continue;
225-
if (!_passesMonthFilter(traj, currentMonth)) continue;
226-
if (!_passesDateFilter(traj, dateRange)) continue;
227-
228-
const pts = traj.points;
229-
if (!pts || pts.length < 2) continue;
230-
if (pts.reduce((s, p) => s + (p.sog || 0), 0) / pts.length > 50) continue;
231-
232-
// Accumulate consecutive same-color points into one polyline.
233-
// Break on: distance gap, land crossing, or SOG color change.
234-
let seg = [pts[0]];
235-
let segColor = _sogColor(pts[0].sog || 0);
236-
237-
const flush = (color) => {
238-
if (seg.length >= 2) {
239-
L.polyline(seg.map(p => [p.lat, p.lon]), {
240-
renderer: this._renderer,
241-
interactive: false,
242-
bubblingMouseEvents: false,
243-
color,
244-
weight: 1.1,
245-
opacity: 0.45,
246-
smoothFactor: 1.0,
247-
}).addTo(this._layer);
248-
}
249-
};
250-
251-
for (let i = 1; i < pts.length; i++) {
252-
const a = pts[i - 1], b = pts[i];
253-
const tooFar = haversineKm(a, b) > GLOBAL_MAX_STEP_KM;
254-
const onLand = _crossesLand(a, b);
255-
const col = _sogColor(((a.sog || 0) + (b.sog || 0)) / 2);
256-
257-
if (tooFar || onLand) {
258-
flush(segColor);
259-
seg = [b];
260-
segColor = _sogColor(b.sog || 0);
261-
} else if (col !== segColor) {
262-
seg.push(b); // include b for visual continuity
263-
flush(segColor);
264-
seg = [b];
265-
segColor = col;
266-
} else {
267-
seg.push(b);
268-
}
269-
}
270-
flush(segColor);
271-
}
272-
},
273-
});
193+
// ── Global trajectory layer ────────────────────────────────────────────────
194+
// Six L.polyline multipolylines — one per SOG colour — each holding ALL
195+
// visible segments of that colour as an array of 2-point sub-arrays.
196+
// Leaflet renders each as a single canvas/SVG path (M…L M…L …).
197+
// Clearing costs 6 removeLayer calls ≈ zero. L.canvas renderer keeps the
198+
// drawing canvas in overlayPane, so CSS zoom-animation syncs natively.
274199

275-
function _ensureGlobalCanvas() {
276-
if (!globalCanvas) { globalCanvas = new GlobalCanvasOverlay(); globalCanvas.addTo(map); }
200+
function _ensureGlobalLayer() {
201+
if (!_globalRenderer) _globalRenderer = L.canvas({ padding: 0.5 });
202+
if (!_globalLayer) _globalLayer = L.layerGroup().addTo(map);
277203
}
278-
function _removeGlobalCanvas() {
279-
if (globalCanvas) { map.removeLayer(globalCanvas); globalCanvas = null; }
204+
205+
function _removeGlobalLayer() {
206+
if (_globalLayer) {
207+
map.removeLayer(_globalLayer); // only 6 multipolylines — near-instant
208+
_globalLayer = null;
209+
_globalRenderer = null;
210+
}
211+
}
212+
213+
function _buildGlobalPolylines(trajectories, f) {
214+
const { activeTypes, currentTimeOfDay, timeWindow, currentMonth, dateRange } = f;
215+
const byColor = {};
216+
217+
for (const traj of trajectories) {
218+
if (activeTypes && !activeTypes.has(traj.vessel_type)) continue;
219+
if (!_passesTodFilter(traj, currentTimeOfDay, timeWindow ?? 30)) continue;
220+
if (!_passesMonthFilter(traj, currentMonth)) continue;
221+
if (!_passesDateFilter(traj, dateRange)) continue;
222+
223+
const pts = traj.points;
224+
if (!pts || pts.length < 2) continue;
225+
if (pts.reduce((s, p) => s + (p.sog || 0), 0) / pts.length > 50) continue;
226+
227+
for (let i = 0; i < pts.length - 1; i++) {
228+
const a = pts[i], b = pts[i + 1];
229+
if (haversineKm(a, b) > GLOBAL_MAX_STEP_KM || _crossesLand(a, b)) continue;
230+
const col = _sogColor(((a.sog || 0) + (b.sog || 0)) / 2);
231+
if (!byColor[col]) byColor[col] = [];
232+
byColor[col].push([[a.lat, a.lon], [b.lat, b.lon]]);
233+
}
234+
}
235+
236+
for (const [col, segs] of Object.entries(byColor)) {
237+
L.polyline(segs, {
238+
renderer: _globalRenderer, color: col,
239+
weight: 1.1, opacity: 0.45, interactive: false, smoothFactor: 0,
240+
}).addTo(_globalLayer);
241+
}
280242
}
281243

282244
function _drawPortTraj(traj, f) {
@@ -302,34 +264,40 @@ const MapView = (() => {
302264
<div class="vp-row">Avg SOG <span>${avgSog} kn</span></div>
303265
</div>`;
304266

267+
if (!_portRenderer) _portRenderer = L.canvas();
305268
segs.forEach(seg => {
306269
L.polyline(seg.map(p => [p.lat, p.lon]), {
270+
renderer: _portRenderer,
307271
color: style.color, weight: style.weight, opacity: style.opacity, smoothFactor: 1.2,
308272
}).addTo(trajectoryLayer).bindPopup(popup, { maxWidth: 200 });
309273
});
310274
}
311275

312276
function drawTrajectories(trajectories, filters = {}, isGlobal = false) {
313-
trajectoryLayer.clearLayers();
314277
const f = {
315278
activeTypes: filters.activeTypes || null,
316279
activePatterns: filters.activePatterns || null,
317280
currentTimeOfDay: filters.currentTimeOfDay != null ? filters.currentTimeOfDay : null,
318281
timeWindow: filters.timeWindow != null ? filters.timeWindow : 30,
319282
dateRange: filters.dateRange || null,
320283
};
284+
321285
if (isGlobal) {
322-
_ensureGlobalCanvas();
323-
globalCanvas.setData(trajectories, f);
286+
trajectoryLayer.clearLayers();
287+
_removeGlobalLayer();
288+
if (!trajectories.length) return;
289+
_ensureGlobalLayer();
290+
_buildGlobalPolylines(trajectories, f); // synchronous: builds 6 multipolylines
324291
} else {
325-
_removeGlobalCanvas();
326-
trajectories.forEach(t => _drawPortTraj(t, f));
292+
_removeGlobalLayer();
293+
trajectoryLayer.clearLayers();
294+
trajectories.forEach(traj => _drawPortTraj(traj, f)); // synchronous: L.Canvas, no DOM
327295
}
328296
}
329297

330298
function clearTrajectories() {
331299
trajectoryLayer.clearLayers();
332-
_removeGlobalCanvas();
300+
_removeGlobalLayer();
333301
}
334302

335303
function setHeatData(points) {

0 commit comments

Comments
 (0)