@@ -224,8 +224,38 @@ private static void OnIsExpandedChanged(DependencyObject d, DependencyPropertyCh
224224
225225 #region Load, draw
226226
227+ // Guards EnsureMapDataInitialized so the (per-instance) ~900-element label/city build below
228+ // only ever runs once per control, regardless of how many times Loaded or IsVisibleChanged fire.
229+ private bool _mapDataInitialized ;
230+
227231 private void TracerouteMapControl_Loaded ( object sender , RoutedEventArgs e )
228232 {
233+ // TracerouteView binds this control's Visibility to ShowMap, which is false whenever IP
234+ // geolocation checking is disabled - the map can then never show anything (no hop has a
235+ // location to plot), so building ~900 city/country WPF elements for it here would be pure
236+ // waste. Defer until it's actually shown; if it already is, this fires immediately below.
237+ if ( IsVisible )
238+ EnsureMapDataInitialized ( ) ;
239+ else
240+ IsVisibleChanged += TracerouteMapControl_IsVisibleChanged ;
241+ }
242+
243+ private void TracerouteMapControl_IsVisibleChanged ( object sender , DependencyPropertyChangedEventArgs e )
244+ {
245+ if ( ! IsVisible )
246+ return ;
247+
248+ IsVisibleChanged -= TracerouteMapControl_IsVisibleChanged ;
249+ EnsureMapDataInitialized ( ) ;
250+ }
251+
252+ private void EnsureMapDataInitialized ( )
253+ {
254+ if ( _mapDataInitialized )
255+ return ;
256+
257+ _mapDataInitialized = true ;
258+
229259 // The embedded world map is ~1.4 MB of JSON; parsing it and building the (frozen)
230260 // country geometry synchronously here would briefly hitch the UI thread the first time a
231261 // Traceroute tab is opened. Materialize the shared Lazy caches on a background thread
@@ -894,6 +924,13 @@ private Action<bool> DrawArrow((List<TracerouteHopInfo> Hops, ClusteredPoint Pos
894924 ( List < TracerouteHopInfo > Hops , ClusteredPoint Position , IPGeolocationInfo Info ) to ,
895925 Action < bool > highlightFromMarker , Action < bool > highlightToMarker )
896926 {
927+ // A hop between these two groups was dropped for lacking geolocation (see the filter in
928+ // RedrawHops) - the direct line drawn below isn't the actual route then, just the closest
929+ // honest approximation. Rendered in a muted gray (see below) and labelled "Unknown" in the
930+ // tooltip instead of the usual accent color, so it doesn't read as an equally-confident,
931+ // fully observed segment.
932+ var isGap = to . Hops [ 0 ] . Hop != from . Hops [ ^ 1 ] . Hop + 1 ;
933+
897934 // Skip hops that resolve to (nearly) the same on-screen position. Judged on the resolved,
898935 // cluster-displaced positions (at the route's own just-computed fit scale) rather than the
899936 // raw geo anchors - two cluster members of the same location group (see AssignClusterIds)
@@ -909,7 +946,9 @@ private Action<bool> DrawArrow((List<TracerouteHopInfo> Hops, ClusteredPoint Pos
909946 if ( Math . Sqrt ( resolvedDx * resolvedDx + resolvedDy * resolvedDy ) < 0.01 )
910947 return static _ => { } ;
911948
912- var tooltip = $ "{ FormatLocation ( from . Info , includeRegion : true ) } \n ↓\n { FormatLocation ( to . Info , includeRegion : true ) } ";
949+ var tooltip = isGap
950+ ? $ "{ FormatLocation ( from . Info , includeRegion : true ) } \n ↓\n { Strings . Unknown } \n ↓\n { FormatLocation ( to . Info , includeRegion : true ) } "
951+ : $ "{ FormatLocation ( from . Info , includeRegion : true ) } \n ↓\n { FormatLocation ( to . Info , includeRegion : true ) } ";
913952
914953 var geometry = new PathGeometry ( ) ;
915954 var figure = new PathFigure { IsClosed = false } ;
@@ -946,7 +985,7 @@ private Action<bool> DrawArrow((List<TracerouteHopInfo> Hops, ClusteredPoint Pos
946985 IsHitTestVisible = false
947986 } ;
948987
949- path . SetResourceReference ( Shape . StrokeProperty , "MahApps.Brushes.Accent" ) ;
988+ path . SetResourceReference ( Shape . StrokeProperty , isGap ? "MahApps.Brushes.Gray3" : "MahApps.Brushes.Accent" ) ;
950989
951990 HopsCanvas . Children . Add ( path ) ;
952991
@@ -959,7 +998,7 @@ private Action<bool> DrawArrow((List<TracerouteHopInfo> Hops, ClusteredPoint Pos
959998 IsHitTestVisible = false
960999 } ;
9611000
962- arrowHead . SetResourceReference ( Shape . FillProperty , "MahApps.Brushes.Accent" ) ;
1001+ arrowHead . SetResourceReference ( Shape . FillProperty , isGap ? "MahApps.Brushes.Gray3" : "MahApps.Brushes.Accent" ) ;
9631002
9641003 HopsCanvas . Children . Add ( arrowHead ) ;
9651004
@@ -1100,7 +1139,7 @@ private static void UpdateArrowGeometry(ArrowVisual arrow, double inverseScale)
11001139 }
11011140
11021141 /// <summary>
1103- /// Shows the fixed info panel (top-left ) with the given text - see the XAML comment on
1142+ /// Shows the fixed info panel (bottom-right ) with the given text - see the XAML comment on
11041143 /// InfoPanel for why this replaces a WPF ToolTip popup for hop markers/arrows.
11051144 /// </summary>
11061145 private void ShowInfoPanel ( string text )
@@ -1258,9 +1297,13 @@ private void FitToHops(IReadOnlyList<Point> points, double extraPaddingScreenPix
12581297 var centerX = ( rawMinX + rawMaxX ) / 2 ;
12591298 var centerY = ( rawMinY + rawMaxY ) / 2 ;
12601299
1261- return ( scale ,
1262- BorderHost . ActualWidth / 2 - centerX * scale ,
1263- BorderHost . ActualHeight / 2 - centerY * scale ) ;
1300+ // Clamped the same way as FitToView/wheel zoom/drag-pan - otherwise a route near the map's
1301+ // edge (e.g. close to +/-180 deg longitude or a pole) can center a pan that leaves blank
1302+ // canvas past the map's real edge in the viewport.
1303+ var panX = ClampPan ( BorderHost . ActualWidth / 2 - centerX * scale , MapWidth * scale , BorderHost . ActualWidth ) ;
1304+ var panY = ClampPan ( BorderHost . ActualHeight / 2 - centerY * scale , MapHeight * scale , BorderHost . ActualHeight ) ;
1305+
1306+ return ( scale , panX , panY ) ;
12641307 }
12651308
12661309 private void ApplyTransform ( )
0 commit comments