30793079 throw Error ( 'Internal error' ) ;
30803080 }
30813081 }
3082- if ( filteredArcs ) {
3083- // match simplification of unfiltered arcs
3084- filteredArcs . setRetainedInterval ( unfilteredArcs . getRetainedInterval ( ) ) ;
3085- }
3082+ var userInterval = unfilteredArcs . getRetainedInterval ( ) ;
30863083 // switch to filtered version of arcs at small scales
30873084 var unitsPerPixel = 1 / ext . getTransform ( ) . mx ,
30883085 useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5 ;
3089- return useFiltering ? filteredArcs : unfilteredArcs ;
3086+ if ( useFiltering ) {
3087+ // Dynamic LOD: drop any vertex whose Visvalingam triangle-area
3088+ // contribution is smaller than half a square pixel at the current
3089+ // zoom. Visually imperceptible, but can remove a large fraction of
3090+ // vertices on very zoomed-out views of highly detailed arcs.
3091+ // Never go below the user's own simplification setting.
3092+ var displayInterval = unitsPerPixel * unitsPerPixel * 0.5 ;
3093+ filteredArcs . setRetainedInterval ( Math . max ( userInterval , displayInterval ) ) ;
3094+ return filteredArcs ;
3095+ }
3096+ if ( filteredArcs ) {
3097+ // match simplification of unfiltered arcs, in case we switch back
3098+ filteredArcs . setRetainedInterval ( userInterval ) ;
3099+ }
3100+ return unfilteredArcs ;
30903101 } ;
30913102 }
30923103
89318942 var pct = ease ? ease ( e . pct ) : e . pct ,
89328943 val = end * pct + start * ( 1 - pct ) ;
89338944 self . dispatchEvent ( 'change' , { value : val } ) ;
8945+ if ( e . done ) {
8946+ self . dispatchEvent ( 'done' ) ;
8947+ }
89348948 }
89358949 }
89368950
90959109 }
90969110 if ( evt . done ) {
90979111 active = false ;
9112+ self . dispatchEvent ( 'mousewheelend' ) ;
90989113 } else {
90999114 if ( fadeFactor > 0 ) {
91009115 // Decelerate towards the end of the sustain interval (for smoother zooming)
96889703
96899704 gui . on ( 'map_reset' , function ( ) {
96909705 ext . reset ( true ) ;
9706+ // ext.reset() synchronously triggers a 'nav' draw; signal end-of-
9707+ // interaction so the LayerRenderer settles on a sharp frame
9708+ // immediately instead of waiting on the fallback timer.
9709+ gui . dispatchEvent ( 'map_interaction_end' ) ;
96919710 } ) ;
96929711
96939712 zoomTween . on ( 'change' , function ( e ) {
96949713 ext . zoomToExtent ( e . value , _fx , _fy ) ;
96959714 } ) ;
96969715
9716+ // Signal end-of-interaction so downstream modules (e.g. LayerRenderer) can
9717+ // trigger a settle/redraw immediately, instead of waiting on a debounce.
9718+ zoomTween . on ( 'done' , function ( ) {
9719+ gui . dispatchEvent ( 'map_interaction_end' ) ;
9720+ } ) ;
9721+
9722+ wheel . on ( 'mousewheelend' , function ( ) {
9723+ gui . dispatchEvent ( 'map_interaction_end' ) ;
9724+ } ) ;
9725+
96979726 mouse . on ( 'click' , function ( e ) {
96989727 gui . dispatchEvent ( 'map_click' , e ) ;
96999728 } ) ;
97449773 zoomBox . turnOff ( ) ;
97459774 } else {
97469775 El ( 'body' ) . removeClass ( 'panning' ) . removeClass ( 'pan' ) ;
9776+ gui . dispatchEvent ( 'map_interaction_end' ) ;
97479777 }
97489778 } ) ;
97499779
1227912309 }
1228012310
1228112311 function drawStyledLayerToCanvas ( lyr , canv , ext ) {
12282- // TODO: add filter for out-of-view shapes
1228312312 var style = lyr . gui . style ;
1228412313 var layer = lyr . gui . displayLayer ;
1228512314 var arcs , filter ;
1229112320 }
1229212321 } else {
1229312322 arcs = getArcsForRendering ( lyr , ext ) ;
12294- filter = getShapeFilter ( arcs , ext ) ;
12323+ filter = getShapeFilter ( arcs , layer . shapes , ext ) ;
1229512324 canv . drawStyledPaths ( layer . shapes , arcs , style , filter ) ;
1229612325 if ( style . vertices ) {
1229712326 canv . drawVertices ( layer . shapes , arcs , style , filter ) ;
@@ -12300,11 +12329,12 @@
1230012329 canv . clearStyles ( ) ;
1230112330 }
1230212331
12303-
1230412332 // Return a function for testing if an arc should be drawn in the current view
1230512333 function getArcFilter ( arcs , ext , usedFlag , arcCounts ) {
12306- var MIN_PATH_LEN = 0.1 ;
12307- var minPathLen = ext . getPixelSize ( ) * MIN_PATH_LEN , // * 0.5
12334+ // Arcs whose bbox is smaller than this pixel threshold collapse to a dot
12335+ // under roundToPix anyway; skipping them saves per-arc iteration.
12336+ var MIN_PATH_LEN = 0.5 ;
12337+ var minPathLen = ext . getPixelSize ( ) * MIN_PATH_LEN ,
1230812338 geoBounds = ext . getBounds ( ) ,
1230912339 geoBBox = geoBounds . toArray ( ) ,
1231012340 allIn = geoBounds . contains ( arcs . getBounds ( ) ) ,
@@ -12324,15 +12354,20 @@
1232412354 } ;
1232512355 }
1232612356
12327- // Return a function for testing if a shape should be drawn in the current view
12328- function getShapeFilter ( arcs , ext ) {
12329- var viewBounds = ext . getBounds ( ) ;
12330- var bounds = new Bounds ( ) ;
12331- if ( ext . scale ( ) < 1.1 ) return null ; // full or almost-full zoom: no filter
12332- return function ( shape ) {
12333- bounds . empty ( ) ;
12334- arcs . getMultiShapeBounds ( shape , bounds ) ;
12335- return viewBounds . intersects ( bounds ) ;
12357+ // Return a function for testing if a shape should be drawn in the current
12358+ // view. The filter takes a shape index and tests the shape's bbox against
12359+ // the viewport. At nearly full extent the test is a no-op, so we return
12360+ // null to let the caller skip it entirely.
12361+ function getShapeFilter ( arcs , shapes , ext ) {
12362+ if ( ext . scale ( ) < 1.1 ) return null ;
12363+ var view = ext . getBounds ( ) ;
12364+ var b = new Bounds ( ) ;
12365+ return function ( i ) {
12366+ var shp = shapes [ i ] ;
12367+ if ( ! shp ) return false ;
12368+ b . empty ( ) ;
12369+ arcs . getMultiShapeBounds ( shp , b ) ;
12370+ return view . intersects ( b ) ;
1233612371 } ;
1233712372 }
1233812373
1240512440 _ctx . fillStyle = color ;
1240612441 for ( i = 0 ; i < shapes . length ; i ++ ) {
1240712442 var shp = shapes [ i ] ;
12408- if ( ! shp || filter && ! filter ( shp ) ) continue ;
12443+ if ( ! shp || filter && ! filter ( i ) ) continue ;
1240912444 for ( j = 0 ; j < shp . length ; j ++ ) {
1241012445 iter . init ( shp [ j ] ) ;
1241112446 while ( iter . hasNext ( ) ) {
1243812473 var styler = style . styler || null ;
1243912474 for ( var i = 0 ; i < shapes . length ; i ++ ) {
1244012475 shp = shapes [ i ] ;
12441- if ( ! shp || filter && ! filter ( shp ) ) continue ;
12476+ if ( ! shp || filter && ! filter ( i ) ) continue ;
1244212477 if ( styler ) {
1244312478 styler ( style , i ) ;
1244412479 }
1260612641 var startPath = getPathStart ( _ext , getLineScale ( _ext ) ) ,
1260712642 t = getScaledTransform ( _ext ) ,
1260812643 ctx = _ctx ,
12609- batch = 25 , // render paths in batches of this size (an optimization)
12644+ // Larger batches reduce the number of stroke() flushes.
12645+ batch = 100 ,
1261012646 count = 0 ,
1261112647 n = arcs . size ( ) ,
1261212648 i , iter ;
@@ -12963,12 +12999,68 @@
1296312999 _furniture = new SvgDisplayLayer ( gui , ext , null ) . appendTo ( el ) ,
1296413000 _ext = ext ;
1296513001
13002+ // Fast-nav config: when the most recent render cycle exceeded SLOW_FRAME_MS,
13003+ // subsequent 'nav' actions transform the previously rendered bitmap via CSS
13004+ // instead of re-drawing vector content. This trades visible scaling/edge
13005+ // artifacts for smoother panning and zooming on large datasets.
13006+ //
13007+ // Canvas 2D paint ops are normally rasterized asynchronously, so JS usually
13008+ // returns long before the pixels are committed to the compositor. We force
13009+ // a synchronous flush at the end of a full render (see flushCanvas) so that
13010+ // (a) the elapsed time reflects the *true* frame cost (JS + rasterization)
13011+ // rather than just the JS portion, and
13012+ // (b) subsequent fast-nav CSS transforms composite cleanly rather than on
13013+ // top of a still-committing bitmap.
13014+ // We use only the most recent sample because previous renders may have
13015+ // drawn a different scene (e.g. a simpler layer that is no longer active).
13016+ var SLOW_FRAME_MS = 100 ;
13017+ // Fallback delay before triggering a redraw if no explicit end-of-interaction
13018+ // event arrives. The primary trigger is gui 'map_interaction_end' (mouse
13019+ // release, wheel timeout, zoom tween done); this timer only fires if that
13020+ // signal is somehow missed, so it's set generously.
13021+ var FAST_SETTLE_FALLBACK_MS = 2000 ;
13022+ var _lastFrameMs = 0 ; // duration of the most recent full render cycle
13023+ var _snapshot = null ; // {bounds, width, height, pixRatio}
13024+ var _fastActive = false ;
13025+ var _settleTimer = null ;
13026+ var _redrawPending = false ;
13027+ var _settleRequested = false ;
13028+
13029+ // 'map_interaction_end' may arrive before the in-flight 'nav' draw runs,
13030+ // because drawLayers() schedules its work via requestAnimationFrame.
13031+ // We latch the intent so that whichever order things happen in, the
13032+ // settle fires immediately rather than waiting on the fallback timer.
13033+ gui . on ( 'map_interaction_end' , function ( ) {
13034+ if ( _redrawPending ) {
13035+ settleNow ( ) ;
13036+ } else {
13037+ _settleRequested = true ;
13038+ }
13039+ } ) ;
13040+
1296613041 // don't let furniture container block events to symbol layers
1296713042 _furniture . css ( 'pointer-events' , 'none' ) ;
1296813043
1296913044 this . drawMainLayers = function ( layers , action ) {
12970- var needSvgRedraw = action != 'nav' && action != 'hover' ;
1297113045 if ( skipMainLayerRedraw ( action ) ) return ;
13046+ if ( action == 'nav' && shouldUseFastNav ( ) ) {
13047+ applyFastTransform ( ) ;
13048+ // SVG symbol reposition is already cheap; keep labels/symbols accurate
13049+ // while the canvas is being transformed.
13050+ layers . forEach ( function ( lyr ) {
13051+ if ( internal . layerHasSvgSymbols ( lyr ) || internal . layerHasLabels ( lyr ) ) {
13052+ _svg . reposition ( lyr , 'symbol' ) ;
13053+ }
13054+ } ) ;
13055+ markRedrawPending ( ) ;
13056+ return ;
13057+ }
13058+ var startTime = performance . now ( ) ;
13059+ var needSvgRedraw = action != 'nav' && action != 'hover' ;
13060+ cancelSettle ( ) ;
13061+ _redrawPending = false ;
13062+ _settleRequested = false ; // a full render satisfies any pending settle
13063+ clearFastTransform ( ) ;
1297213064 _mainCanv . prep ( _ext ) ;
1297313065 if ( needSvgRedraw ) {
1297413066 _svg . clear ( ) ;
1298313075 drawCanvasLayer ( lyr , _mainCanv ) ;
1298413076 }
1298513077 } ) ;
13078+ // Force synchronous rasterization so performance.now() reflects the true
13079+ // frame cost (including GPU paint-op commit), and so subsequent fast-nav
13080+ // CSS transforms don't composite over a still-pending canvas commit.
13081+ flushCanvas ( _mainCanv ) ;
13082+ _lastFrameMs = performance . now ( ) - startTime ;
13083+ captureSnapshot ( ) ;
1298613084 } ;
1298713085
1298813086 // Draw highlight effect for hover and selection
1303413132 }
1303513133 }
1303613134
13135+ // Reading back a single pixel forces Chrome/Firefox to synchronously complete
13136+ // any queued paint operations before returning. This converts an unbounded
13137+ // deferred "Commit" phase into in-line wall time we can measure, and ensures
13138+ // the canvas pixels are actually on screen before the caller returns.
13139+ function flushCanvas ( canv ) {
13140+ try {
13141+ canv . node ( ) . getContext ( '2d' ) . getImageData ( 0 , 0 , 1 , 1 ) ;
13142+ } catch ( e ) {
13143+ // e.g. cross-origin-tainted canvas (shouldn't happen here, but safe)
13144+ }
13145+ }
13146+
1303713147 function getSvgLayerType ( layer ) {
1303813148 var type = null ;
1303913149 if ( internal . layerHasSvgSymbols ( layer ) ) {
1304313153 }
1304413154 return type ;
1304513155 }
13156+
13157+ function captureSnapshot ( ) {
13158+ _snapshot = {
13159+ bounds : _ext . getBounds ( ) ,
13160+ width : _ext . width ( ) ,
13161+ height : _ext . height ( ) ,
13162+ pixRatio : GUI . getPixelRatio ( )
13163+ } ;
13164+ }
13165+
13166+ function shouldUseFastNav ( ) {
13167+ if ( ! _snapshot ) return false ;
13168+ if ( _lastFrameMs <= SLOW_FRAME_MS ) return false ;
13169+ if ( _snapshot . width != _ext . width ( ) || _snapshot . height != _ext . height ( ) ) return false ;
13170+ if ( _snapshot . pixRatio != GUI . getPixelRatio ( ) ) return false ;
13171+ return true ;
13172+ }
13173+
13174+ // Apply a CSS transform to the main canvas so its previously rendered
13175+ // contents line up with the current map extent. The canvas bitmap is not
13176+ // redrawn.
13177+ function applyFastTransform ( ) {
13178+ var t = _ext . getTransform ( ) ;
13179+ var b = _snapshot . bounds ;
13180+ var tl = t . transform ( b . xmin , b . ymax ) ;
13181+ var br = t . transform ( b . xmax , b . ymin ) ;
13182+ var sx = ( br [ 0 ] - tl [ 0 ] ) / _snapshot . width ;
13183+ var sy = ( br [ 1 ] - tl [ 1 ] ) / _snapshot . height ;
13184+ // On retina the canvas bitmap is already scaled down to CSS pixels via a
13185+ // CSS transform from the .retina class; overriding `transform` here
13186+ // replaces that rule so we must bake the pixRatio scale back in.
13187+ var k = 1 / _snapshot . pixRatio ;
13188+ setFastTransform ( _mainCanv . node ( ) , tl [ 0 ] , tl [ 1 ] , sx * k , sy * k ) ;
13189+ _fastActive = true ;
13190+ }
13191+
13192+ function clearFastTransform ( ) {
13193+ if ( ! _fastActive ) return ;
13194+ var node = _mainCanv . node ( ) ;
13195+ node . style . transform = '' ;
13196+ node . style . transformOrigin = '' ;
13197+ _fastActive = false ;
13198+ }
13199+
13200+ function setFastTransform ( node , tx , ty , sx , sy ) {
13201+ node . style . transformOrigin = 'top left' ;
13202+ node . style . transform = 'translate(' + tx + 'px,' + ty + 'px) scale(' + sx + ',' + sy + ')' ;
13203+ }
13204+
13205+ // Record that a fast-nav frame is showing so that the next end-of-interaction
13206+ // event (or the fallback timer) can trigger a real redraw. Each fast-nav
13207+ // frame resets the fallback timer so it only fires after the user truly
13208+ // stops interacting, as a backstop for any interaction path that doesn't
13209+ // emit 'map_interaction_end'. If 'map_interaction_end' was already
13210+ // received before this frame ran (e.g. the home button reset, which
13211+ // dispatches synchronously while the nav draw is still queued in rAF),
13212+ // settle right now instead of waiting.
13213+ function markRedrawPending ( ) {
13214+ _redrawPending = true ;
13215+ cancelSettle ( ) ;
13216+ if ( _settleRequested ) {
13217+ _settleRequested = false ;
13218+ settleNow ( ) ;
13219+ } else {
13220+ _settleTimer = setTimeout ( settleNow , FAST_SETTLE_FALLBACK_MS ) ;
13221+ }
13222+ }
13223+
13224+ function settleNow ( ) {
13225+ cancelSettle ( ) ;
13226+ if ( ! _redrawPending ) return ;
13227+ _redrawPending = false ;
13228+ gui . dispatchEvent ( 'map-needs-refresh' ) ;
13229+ }
13230+
13231+ function cancelSettle ( ) {
13232+ if ( _settleTimer ) {
13233+ clearTimeout ( _settleTimer ) ;
13234+ _settleTimer = null ;
13235+ }
13236+ }
1304613237 }
1304713238
1304813239 // Controls the shift-drag box editing tool
@@ -13778,6 +13969,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
1377813969
1377913970 // RENDERING
1378013971 // draw main content layers
13972+
1378113973 _renderer . drawMainLayers ( contentLayers , action ) ;
1378213974
1378313975 // draw hover & selection overlay
0 commit comments