@@ -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