| | | 1 | | using System; |
| | | 2 | | using System.Collections.Generic; |
| | | 3 | | using System.Linq; |
| | | 4 | | using System.Threading; |
| | | 5 | | using System.Threading.Tasks; |
| | | 6 | | using Itinero.Geo; |
| | | 7 | | using Itinero.Network.Enumerators.Edges; |
| | | 8 | | using Itinero.Network.Tiles; |
| | | 9 | | using Itinero.Snapping; |
| | | 10 | | |
| | | 11 | | namespace Itinero.Network.Search.Edges; |
| | | 12 | | |
| | | 13 | | internal static class EdgeSearch |
| | | 14 | | { |
| | | 15 | | /// <summary> |
| | | 16 | | /// Returns the closest edge to the center of the given box that has at least one vertex inside the given box. |
| | | 17 | | /// </summary> |
| | | 18 | | /// <param name="network">The network.</param> |
| | | 19 | | /// <param name="searchBox">The box to search in.</param> |
| | | 20 | | /// <param name="maxDistance">The maximum distance of any snap point returned relative to the center of the search b |
| | | 21 | | /// <param name="edgeChecker">Used to determine if an edge is acceptable or not. If null any edge will be accepted.< |
| | | 22 | | /// <param name="cancellationToken">The cancellation token.</param> |
| | | 23 | | /// <returns>The closest edge to the center of the box inside the given box.</returns> |
| | | 24 | | public static async Task<SnapPoint> SnapInBoxAsync(this RoutingNetwork network, |
| | | 25 | | ((double longitude, double, float? e) topLeft, (double longitude, double latitude, float? e) bottomRight) |
| | | 26 | | searchBox, |
| | | 27 | | IEdgeChecker? edgeChecker = null, double maxDistance = double.MaxValue, CancellationToken cancellationToken = de |
| | 96 | 28 | | { |
| | 96 | 29 | | var center = ((double longitude, double latitude, float? e))searchBox.Center(); |
| | 96 | 30 | | var zoom = network.Zoom; |
| | | 31 | | |
| | | 32 | | const double exactTolerance = 1; |
| | 96 | 33 | | var bestDistance = maxDistance; |
| | 96 | 34 | | (EdgeId edgeId, ushort offset) bestSnapPoint = (EdgeId.Empty, ushort.MaxValue); |
| | | 35 | | |
| | | 36 | | // Spiral tile iteration: start at the tile containing Q, expand to |
| | | 37 | | // ring 1 (8 neighbours), ring 2, ..., until rings fall outside the |
| | | 38 | | // search box. Closest-first ordering means the first snap lands fast |
| | | 39 | | // and bestDistance shrinks early, so outer-ring tiles get rejected |
| | | 40 | | // by the per-tile predicate before we ever read their edges. Diff |
| | | 41 | | // computation is lazy per-tile, so cold-path snaps avoid touching |
| | | 42 | | // tiles the inner-ring snap already made irrelevant. |
| | | 43 | | |
| | | 44 | | // Search box tile-range bounds (clamped, can be a single tile). The |
| | | 45 | | // searchBox.topLeft's latitude field is unnamed in the parameter |
| | | 46 | | // signature (legacy quirk), so we access it positionally as Item2. |
| | 96 | 47 | | var tlTile = TileStatic.WorldToTile(searchBox.topLeft.longitude, searchBox.topLeft.Item2, zoom); |
| | 96 | 48 | | var brTile = TileStatic.WorldToTile(searchBox.bottomRight.longitude, searchBox.bottomRight.latitude, zoom); |
| | 192 | 49 | | var minX = tlTile.x; var maxX = brTile.x; |
| | 192 | 50 | | var minY = tlTile.y; var maxY = brTile.y; |
| | | 51 | | |
| | | 52 | | // Start tile = Q's tile. Outermost ring needed = max distance from |
| | | 53 | | // start to any corner of the box, in tile units. |
| | 96 | 54 | | var centerTile = TileStatic.WorldToTile(center.longitude, center.latitude, zoom); |
| | 96 | 55 | | var cx = centerTile.x; |
| | 96 | 56 | | var cy = centerTile.y; |
| | 96 | 57 | | var maxRing = (uint)Math.Max( |
| | 96 | 58 | | Math.Max(SafeDelta(cx, minX), SafeDelta(maxX, cx)), |
| | 96 | 59 | | Math.Max(SafeDelta(cy, minY), SafeDelta(maxY, cy))); |
| | | 60 | | |
| | 96 | 61 | | var edgeEnumerator = network.GetEdgeEnumerator(); |
| | | 62 | | |
| | 390 | 63 | | for (uint ring = 0; ring <= maxRing; ring++) |
| | 102 | 64 | | { |
| | 105 | 65 | | if (bestDistance <= 0) break; |
| | | 66 | | |
| | 537 | 67 | | foreach (var (x, y) in TilesInRing(cx, cy, ring)) |
| | 120 | 68 | | { |
| | 120 | 69 | | if (bestDistance <= 0) break; |
| | 137 | 70 | | if (x < minX || x > maxX || y < minY || y > maxY) continue; |
| | | 71 | | |
| | 103 | 72 | | var tileId = TileStatic.ToLocalId(x, y, zoom); |
| | 103 | 73 | | var tile = network.GetTileForRead(tileId); |
| | 114 | 74 | | if (tile == null) continue; |
| | 92 | 75 | | tile.EnsureDiffs(network); // lazy — only for tiles we actually visit |
| | 92 | 76 | | var bbox = TileStatic.GetTileBoundingBox(zoom, tileId); |
| | 92 | 77 | | if (!TileCanReach(bbox, tile.MaxLonDiff, tile.MaxLatDiff, center, bestDistance)) continue; |
| | | 78 | | |
| | 92 | 79 | | var tileMaxLonDiff = tile.MaxLonDiff; |
| | 92 | 80 | | var tileMaxLatDiff = tile.MaxLatDiff; |
| | | 81 | | |
| | 754 | 82 | | for (uint v = 0; v < tile.VertexCount; v++) |
| | 342 | 83 | | { |
| | 399 | 84 | | if (bestDistance <= 0) break; |
| | 285 | 85 | | var vertexId = new VertexId(tileId, v); |
| | 285 | 86 | | if (!network.TryGetVertex(vertexId, out var vLon, out var vLat, out var vE)) continue; |
| | | 87 | | |
| | | 88 | | // Restore the PR1 candidate set: vertices inside the search |
| | | 89 | | // box. Without this, we'd walk every vertex in every tile |
| | | 90 | | // overlapping the box — including those geometrically outside |
| | | 91 | | // the box. Under bestDistance = ∞ (initial state with |
| | | 92 | | // MaxDistance = ∞) the VertexCanReach predicate is a no-op, |
| | | 93 | | // so without this check we visit ~40% more vertices than PR1 |
| | | 94 | | // did, paying the per-edge bbox iteration cost for each. |
| | 453 | 95 | | if (!searchBox.Overlaps((vLon, vLat, vE))) continue; |
| | | 96 | | |
| | | 97 | | // Per-vertex predicate: an edge from this vertex extends at |
| | | 98 | | // most (tileMaxLonDiff, tileMaxLatDiff) in each axis, so if |
| | | 99 | | // the vertex itself is too far for even that envelope to |
| | | 100 | | // reach the current best, every edge from it is irrelevant. |
| | 124 | 101 | | if (!VertexCanReach(vLon, vLat, tileMaxLonDiff, tileMaxLatDiff, center, bestDistance)) continue; |
| | | 102 | | |
| | 110 | 103 | | if (!edgeEnumerator.MoveTo(vertexId)) continue; |
| | | 104 | | |
| | 241 | 105 | | while (edgeEnumerator.MoveNext()) |
| | 149 | 106 | | { |
| | 167 | 107 | | if (bestDistance <= 0) break; |
| | | 108 | | |
| | | 109 | | // PR1: per-edge MBR prefilter. After the tile and vertex |
| | | 110 | | // predicates above, only edges that *might* beat the |
| | | 111 | | // current best reach this point — but the bbox prefilter |
| | | 112 | | // is still useful to skip edges whose tight bbox can't |
| | | 113 | | // beat best even though their vertex passed the looser |
| | | 114 | | // (per-tile-extent) check. |
| | 153 | 115 | | if (!EdgeBboxCanBeat(edgeEnumerator, center, bestDistance)) continue; |
| | | 116 | | |
| | | 117 | | // search for the local snap point that improves the current best snap point. |
| | 109 | 118 | | (EdgeId edgeId, double offset) localSnapPoint = (EdgeId.Empty, 0); |
| | 109 | 119 | | var isAcceptable = edgeChecker == null ? (bool?)true : null; |
| | 109 | 120 | | var completeShape = edgeEnumerator.GetCompleteShape(); |
| | 109 | 121 | | var length = 0.0; |
| | 109 | 122 | | using (var completeShapeEnumerator = completeShape.GetEnumerator()) |
| | 109 | 123 | | { |
| | 109 | 124 | | completeShapeEnumerator.MoveNext(); |
| | 109 | 125 | | var previous = completeShapeEnumerator.Current; |
| | | 126 | | |
| | | 127 | | // start with the first location. |
| | 109 | 128 | | var distance = previous.DistanceEstimateInMeter(center); |
| | 109 | 129 | | if (distance < bestDistance) |
| | 67 | 130 | | { |
| | 67 | 131 | | isAcceptable ??= edgeChecker!.IsAcceptable(edgeEnumerator) ?? |
| | 67 | 132 | | await edgeChecker.RunCheckAsync(edgeEnumerator |
| | 67 | 133 | | if (!isAcceptable.Value) |
| | 16 | 134 | | { |
| | 16 | 135 | | continue; |
| | | 136 | | } |
| | | 137 | | |
| | 51 | 138 | | if (distance < exactTolerance) |
| | 44 | 139 | | { |
| | 44 | 140 | | distance = 0; |
| | 44 | 141 | | } |
| | | 142 | | |
| | 51 | 143 | | bestDistance = distance; |
| | 51 | 144 | | localSnapPoint = (edgeEnumerator.EdgeId, 0); |
| | 51 | 145 | | } |
| | | 146 | | |
| | | 147 | | // loop over all pairs. |
| | 231 | 148 | | while (completeShapeEnumerator.MoveNext()) |
| | 138 | 149 | | { |
| | 138 | 150 | | var current = completeShapeEnumerator.Current; |
| | | 151 | | |
| | 138 | 152 | | var segmentLength = previous.DistanceEstimateInMeter(current); |
| | | 153 | | |
| | | 154 | | // first check the actual current location, it may be an exact match. |
| | 138 | 155 | | distance = current.DistanceEstimateInMeter(center); |
| | 138 | 156 | | if (distance < bestDistance) |
| | 33 | 157 | | { |
| | 33 | 158 | | isAcceptable ??= edgeChecker!.IsAcceptable(edgeEnumerator) ?? |
| | 33 | 159 | | await edgeChecker.RunCheckAsync(edgeEnumerator, cancellationToken); |
| | 33 | 160 | | if (!isAcceptable.Value) |
| | 0 | 161 | | { |
| | 0 | 162 | | break; |
| | | 163 | | } |
| | | 164 | | |
| | 33 | 165 | | if (distance < exactTolerance) |
| | 22 | 166 | | { |
| | 22 | 167 | | distance = 0; |
| | 22 | 168 | | } |
| | | 169 | | |
| | 33 | 170 | | bestDistance = distance; |
| | 33 | 171 | | localSnapPoint = (edgeEnumerator.EdgeId, length + segmentLength); |
| | 33 | 172 | | } |
| | | 173 | | |
| | | 174 | | // update length. |
| | 138 | 175 | | var startLength = length; |
| | 138 | 176 | | length += segmentLength; |
| | | 177 | | |
| | | 178 | | // TODO: figure this out, there has to be a way to not project every segment. |
| | | 179 | | // // check if we even need to check. |
| | | 180 | | // var previousDistance = previous.DistanceEstimateInMeter(center |
| | | 181 | | // var shapePointDistance = current.DistanceEstimateInMeter(cente |
| | | 182 | | // if (previousDistance + segmentLength > bestDistance && |
| | | 183 | | // shapePointDistance + segmentLength > bestDistance) |
| | | 184 | | // { |
| | | 185 | | // continue; |
| | | 186 | | // } |
| | | 187 | | |
| | | 188 | | // project on line segment. |
| | 138 | 189 | | var line = (previous, current); |
| | 138 | 190 | | var originalPrevious = previous; |
| | 138 | 191 | | previous = current; |
| | 138 | 192 | | if (bestDistance <= 0) |
| | 84 | 193 | | { |
| | | 194 | | // we need to continue, we need the total length. |
| | 84 | 195 | | continue; |
| | | 196 | | } |
| | | 197 | | |
| | 54 | 198 | | var projected = line.ProjectOn(center); |
| | 54 | 199 | | if (!projected.HasValue) |
| | 26 | 200 | | { |
| | 26 | 201 | | continue; |
| | | 202 | | } |
| | | 203 | | |
| | 28 | 204 | | distance = projected.Value.DistanceEstimateInMeter(center); |
| | 28 | 205 | | if (!(distance < bestDistance)) |
| | 14 | 206 | | { |
| | 14 | 207 | | continue; |
| | | 208 | | } |
| | | 209 | | |
| | 14 | 210 | | isAcceptable ??= edgeChecker!.IsAcceptable(edgeEnumerator) ?? |
| | 14 | 211 | | await edgeChecker.RunCheckAsync(edgeEnumerator, cancellationToken); |
| | 14 | 212 | | if (!isAcceptable.Value) |
| | 0 | 213 | | { |
| | 0 | 214 | | break; |
| | | 215 | | } |
| | | 216 | | |
| | 14 | 217 | | if (distance < exactTolerance) |
| | 4 | 218 | | { |
| | 4 | 219 | | distance = 0; |
| | 4 | 220 | | } |
| | | 221 | | |
| | 14 | 222 | | bestDistance = distance; |
| | 14 | 223 | | localSnapPoint = (edgeEnumerator.EdgeId, |
| | 14 | 224 | | startLength + originalPrevious.DistanceEstimateInMeter(projected.Value)); |
| | 14 | 225 | | } |
| | 93 | 226 | | } |
| | | 227 | | |
| | | 228 | | // move to the nex edge if no better point was found. |
| | 93 | 229 | | if (localSnapPoint.edgeId == EdgeId.Empty) |
| | 13 | 230 | | { |
| | 13 | 231 | | continue; |
| | | 232 | | } |
| | | 233 | | |
| | | 234 | | // calculate the actual offset. |
| | 80 | 235 | | var offset = ushort.MaxValue; |
| | 80 | 236 | | if (localSnapPoint.offset < length) |
| | 58 | 237 | | { |
| | 58 | 238 | | if (localSnapPoint.offset <= 0) |
| | 45 | 239 | | { |
| | 45 | 240 | | offset = 0; |
| | 45 | 241 | | } |
| | | 242 | | else |
| | 13 | 243 | | { |
| | 13 | 244 | | offset = (ushort)(localSnapPoint.offset / length * ushort.MaxValue); |
| | 13 | 245 | | } |
| | 58 | 246 | | } |
| | | 247 | | |
| | | 248 | | // invert offset if edge is reversed. |
| | 80 | 249 | | if (!edgeEnumerator.Forward) |
| | 14 | 250 | | { |
| | 14 | 251 | | offset = (ushort)(ushort.MaxValue - offset); |
| | 14 | 252 | | } |
| | | 253 | | |
| | 80 | 254 | | bestSnapPoint = (localSnapPoint.edgeId, offset); |
| | 80 | 255 | | } // while (edgeEnumerator.MoveNext()) |
| | 110 | 256 | | } // for (uint v = 0; v < tile.VertexCount; v++) |
| | 92 | 257 | | } // foreach (var (x, y) in TilesInRing(...)) |
| | 99 | 258 | | } // for (uint ring = 0; ring <= maxRing; ring++) |
| | | 259 | | |
| | 96 | 260 | | return new SnapPoint(bestSnapPoint.edgeId, bestSnapPoint.offset); |
| | 96 | 261 | | } |
| | | 262 | | |
| | | 263 | | /// <summary> |
| | | 264 | | /// Snaps all points in the given box that could potentially be snapping points. |
| | | 265 | | /// </summary> |
| | | 266 | | /// <param name="routerDb"></param> |
| | | 267 | | /// <param name="searchBox">The box to search in.</param> |
| | | 268 | | /// <param name="maxDistance">The maximum distance of any snap point returned relative to the center of the search b |
| | | 269 | | /// <param name="edgeChecker">Used to determine if an edge is acceptable or not. If null any edge will be accepted.< |
| | | 270 | | /// <param name="nonOrthogonalEdges">When true the best potential location on each edge is returned, when false only |
| | | 271 | | /// <param name="cancellationToken">The cancellation token.</param> |
| | | 272 | | /// <returns>All edges that could potentially be relevant snapping points, not only the closest.</returns> |
| | | 273 | | public static async IAsyncEnumerable<SnapPoint> SnapAllInBoxAsync(this RoutingNetwork routerDb, |
| | | 274 | | ((double longitude, double latitude, float? e) topLeft, (double longitude, double latitude, float? e) |
| | | 275 | | bottomRight) searchBox, |
| | | 276 | | IEdgeChecker? edgeChecker = null, bool nonOrthogonalEdges = true, double maxDistance = double.MaxValue, Cancella |
| | 2110 | 277 | | { |
| | 2110 | 278 | | var edges = new HashSet<EdgeId>(); |
| | 2110 | 279 | | var center = ((double longitude, double latitude, float? e))searchBox.Center(); |
| | 2110 | 280 | | var zoom = routerDb.Zoom; |
| | | 281 | | |
| | | 282 | | // Tile-bounded iteration — same structure as SnapInBoxAsync but with |
| | | 283 | | // a fixed maxDistance cutoff (no shrinking-best). Predicates trim |
| | | 284 | | // tiles and vertices that can't possibly contribute any candidate. |
| | | 285 | | // Under unbounded maxDistance they degenerate to "always true" and |
| | | 286 | | // we fall back to the per-edge prefilter alone — same cost as before. |
| | 2110 | 287 | | var candidates = new List<(uint tileId, NetworkTile tile)>(); |
| | 17112 | 288 | | foreach (var (x, y) in searchBox.TileRange(zoom)) |
| | 5391 | 289 | | { |
| | 5391 | 290 | | var tileId = TileStatic.ToLocalId(x, y, zoom); |
| | 5391 | 291 | | var tile = routerDb.GetTileForRead(tileId); |
| | 5481 | 292 | | if (tile == null) continue; |
| | 5301 | 293 | | tile.EnsureDiffs(routerDb); // resolve cross-tile edge endpoints accurately |
| | 5301 | 294 | | var bbox = TileStatic.GetTileBoundingBox(zoom, tileId); |
| | 5462 | 295 | | if (!TileCanReach(bbox, tile.MaxLonDiff, tile.MaxLatDiff, center, maxDistance)) continue; |
| | 5140 | 296 | | candidates.Add((tileId, tile)); |
| | 5140 | 297 | | } |
| | | 298 | | |
| | 2110 | 299 | | var edgeEnumerator = routerDb.GetEdgeEnumerator(); |
| | 16610 | 300 | | foreach (var (tileId, tile) in candidates) |
| | 5140 | 301 | | { |
| | 5140 | 302 | | var tileMaxLonDiff = tile.MaxLonDiff; |
| | 5140 | 303 | | var tileMaxLatDiff = tile.MaxLatDiff; |
| | | 304 | | |
| | 5397502 | 305 | | for (uint v = 0; v < tile.VertexCount; v++) |
| | 2693611 | 306 | | { |
| | 2693611 | 307 | | var vertexId = new VertexId(tileId, v); |
| | 2693611 | 308 | | if (!routerDb.TryGetVertex(vertexId, out var vLon, out var vLat, out var vE)) continue; |
| | | 309 | | |
| | | 310 | | // Preserve the historical contract: candidates come from |
| | | 311 | | // vertices inside the search box. Under maxDistance = ∞ the |
| | | 312 | | // VertexCanReach predicate is a no-op, so without this check |
| | | 313 | | // we'd iterate every vertex in every overlapping tile — |
| | | 314 | | // including those geometrically outside the box — and |
| | | 315 | | // measurably regress ToAllAsync_Drain. |
| | 4510246 | 316 | | if (!searchBox.Overlaps((vLon, vLat, vE))) continue; |
| | | 317 | | |
| | 951264 | 318 | | if (!VertexCanReach(vLon, vLat, tileMaxLonDiff, tileMaxLatDiff, center, maxDistance)) continue; |
| | | 319 | | |
| | 802688 | 320 | | if (!edgeEnumerator.MoveTo(vertexId)) continue; |
| | 3042990 | 321 | | while (edgeEnumerator.MoveNext()) |
| | 2240302 | 322 | | { |
| | 3317043 | 323 | | if (!edges.Add(edgeEnumerator.EdgeId)) continue; |
| | | 324 | | |
| | | 325 | | // PR1 per-edge prefilter — same idea as SnapInBoxAsync. |
| | | 326 | | // Under maxDistance = ∞ this also degenerates, leaving |
| | | 327 | | // SnapAllInBoxAsync at same cost as before this PR. |
| | 2266568 | 328 | | if (!EdgeBboxCanBeat(edgeEnumerator, center, maxDistance)) continue; |
| | | 329 | | |
| | | 330 | | // search for the best snap point for the current edge. |
| | 60554 | 331 | | (EdgeId edgeId, double offset, bool isOrthoganal, double distance) bestEdgeSnapPoint = |
| | 60554 | 332 | | (EdgeId.Empty, 0, false, maxDistance); |
| | 60554 | 333 | | var isAcceptable = edgeChecker == null ? (bool?)true : null; |
| | 60554 | 334 | | var completeShape = edgeEnumerator.GetCompleteShape(); |
| | 60554 | 335 | | var length = 0.0; |
| | 60554 | 336 | | using (var completeShapeEnumerator = completeShape.GetEnumerator()) |
| | 60554 | 337 | | { |
| | 60554 | 338 | | completeShapeEnumerator.MoveNext(); |
| | 60554 | 339 | | var previous = completeShapeEnumerator.Current; |
| | | 340 | | |
| | | 341 | | // start with the first location. |
| | 60554 | 342 | | var distance = previous.DistanceEstimateInMeter(center); |
| | 60554 | 343 | | if (distance < bestEdgeSnapPoint.distance) |
| | 48098 | 344 | | { |
| | 48098 | 345 | | isAcceptable ??= edgeChecker!.IsAcceptable(edgeEnumerator) ?? await edgeChecker.RunCheckAsyn |
| | 48098 | 346 | | if (!isAcceptable.Value) |
| | 22702 | 347 | | { |
| | 22702 | 348 | | continue; |
| | | 349 | | } |
| | | 350 | | |
| | 25396 | 351 | | bestEdgeSnapPoint = (edgeEnumerator.EdgeId, 0, false, distance); |
| | 25396 | 352 | | } |
| | | 353 | | |
| | | 354 | | // loop over all pairs. |
| | 147006 | 355 | | while (completeShapeEnumerator.MoveNext()) |
| | 112365 | 356 | | { |
| | 112365 | 357 | | var current = completeShapeEnumerator.Current; |
| | | 358 | | |
| | 112365 | 359 | | var segmentLength = previous.DistanceEstimateInMeter(current); |
| | | 360 | | |
| | | 361 | | // first check the actual current location, it may be an exact match. |
| | 112365 | 362 | | distance = current.DistanceEstimateInMeter(center); |
| | 112365 | 363 | | if (distance < bestEdgeSnapPoint.distance) |
| | 32098 | 364 | | { |
| | 32098 | 365 | | isAcceptable ??= edgeChecker!.IsAcceptable(edgeEnumerator) ?? await edgeChecker.RunCheck |
| | 32098 | 366 | | if (!isAcceptable.Value) |
| | 3211 | 367 | | { |
| | 3211 | 368 | | break; |
| | | 369 | | } |
| | | 370 | | |
| | 28887 | 371 | | bestEdgeSnapPoint = (edgeEnumerator.EdgeId, length + segmentLength, false, distance); |
| | 28887 | 372 | | } |
| | | 373 | | |
| | | 374 | | // update length. |
| | 109154 | 375 | | var startLength = length; |
| | 109154 | 376 | | length += segmentLength; |
| | | 377 | | |
| | | 378 | | // project on line segment. |
| | 109154 | 379 | | var line = (previous, current); |
| | 109154 | 380 | | var originalPrevious = previous; |
| | 109154 | 381 | | previous = current; |
| | | 382 | | |
| | 109154 | 383 | | var projected = line.ProjectOn(center); |
| | 109154 | 384 | | if (projected.HasValue) |
| | 8170 | 385 | | { |
| | 8170 | 386 | | distance = projected.Value.DistanceEstimateInMeter(center); |
| | 8170 | 387 | | if (distance < bestEdgeSnapPoint.distance) |
| | 6324 | 388 | | { |
| | 6324 | 389 | | isAcceptable ??= edgeChecker!.IsAcceptable(edgeEnumerator) ?? await edgeChecker.RunC |
| | 6324 | 390 | | if (isAcceptable.Value) |
| | 6295 | 391 | | { |
| | 6295 | 392 | | bestEdgeSnapPoint = (edgeEnumerator.EdgeId, |
| | 6295 | 393 | | startLength + originalPrevious.DistanceEstimateInMeter(projected.Value), |
| | 6295 | 394 | | true, distance); |
| | 6295 | 395 | | } |
| | 6324 | 396 | | } |
| | 8170 | 397 | | } |
| | 109154 | 398 | | } |
| | 37852 | 399 | | } |
| | | 400 | | |
| | | 401 | | // move to the nex edge if no snap point was found. |
| | 37852 | 402 | | if (bestEdgeSnapPoint.edgeId == EdgeId.Empty) |
| | 6213 | 403 | | { |
| | 6213 | 404 | | continue; |
| | | 405 | | } |
| | | 406 | | |
| | | 407 | | // check type and return if needed. |
| | 31639 | 408 | | var returnEdge = bestEdgeSnapPoint.isOrthoganal || nonOrthogonalEdges; |
| | 31639 | 409 | | if (!returnEdge) |
| | 1 | 410 | | { |
| | 1 | 411 | | continue; |
| | | 412 | | } |
| | | 413 | | |
| | | 414 | | // calculate the actual offset. |
| | 31638 | 415 | | var offset = ushort.MaxValue; |
| | 31638 | 416 | | if (bestEdgeSnapPoint.offset < length) |
| | 19711 | 417 | | { |
| | 19711 | 418 | | if (bestEdgeSnapPoint.offset <= 0) |
| | 12778 | 419 | | { |
| | 12778 | 420 | | offset = 0; |
| | 12778 | 421 | | } |
| | | 422 | | else |
| | 6933 | 423 | | { |
| | 6933 | 424 | | offset = (ushort)(bestEdgeSnapPoint.offset / length * ushort.MaxValue); |
| | 6933 | 425 | | } |
| | 19711 | 426 | | } |
| | | 427 | | |
| | | 428 | | // invert offset if edge is reversed. |
| | 31638 | 429 | | if (!edgeEnumerator.Forward) |
| | 9629 | 430 | | { |
| | 9629 | 431 | | offset = (ushort)(ushort.MaxValue - offset); |
| | 9629 | 432 | | } |
| | | 433 | | |
| | 31638 | 434 | | yield return new SnapPoint(bestEdgeSnapPoint.edgeId, offset); |
| | 31638 | 435 | | } // while (edgeEnumerator.MoveNext()) |
| | 802688 | 436 | | } // for (uint v = 0; v < tile.VertexCount; v++) |
| | 5140 | 437 | | } // foreach (var (tileId, tile) in candidates) |
| | 2110 | 438 | | } |
| | | 439 | | |
| | | 440 | | /// <summary> |
| | | 441 | | /// Returns the closest vertex to the center of the given box. |
| | | 442 | | /// </summary> |
| | | 443 | | /// <param name="network">The network.</param> |
| | | 444 | | /// <param name="searchBox">The box to search in.</param> |
| | | 445 | | /// <param name="maxDistance">The maximum distance of any vertex returned relative to the center of the search box.< |
| | | 446 | | /// <param name="edgeChecker">Used to determine if an edge is acceptable or not. If null any edge will be accepted.< |
| | | 447 | | /// <param name="cancellationToken">The cancellation token.</param> |
| | | 448 | | /// <returns>The closest edge to the center of the box inside the given box.</returns> |
| | | 449 | | public static async Task<VertexId> SnapToVertexInBoxAsync(this RoutingNetwork network, |
| | | 450 | | ((double longitude, double latitude, float? e) topLeft, (double longitude, double latitude, float? e) |
| | | 451 | | bottomRight) searchBox, IEdgeChecker? edgeChecker = null, double maxDistance = double.MaxValue, Cancellation |
| | 1 | 452 | | { |
| | 1 | 453 | | var center = searchBox.Center(); |
| | 1 | 454 | | var closestDistance = maxDistance; |
| | 1 | 455 | | var closest = VertexId.Empty; |
| | | 456 | | |
| | 1 | 457 | | var vertices = network.SearchVerticesInBox(searchBox); |
| | 1 | 458 | | var edgeEnumerator = network.GetEdgeEnumerator(); |
| | 7 | 459 | | foreach (var (vertex, location) in vertices) |
| | 2 | 460 | | { |
| | 2 | 461 | | var d = center.DistanceEstimateInMeter(location); |
| | 3 | 462 | | if (d > closestDistance) continue; |
| | | 463 | | |
| | 1 | 464 | | if (edgeChecker == null) |
| | 1 | 465 | | { |
| | 1 | 466 | | closest = vertex; |
| | 1 | 467 | | closestDistance = d; |
| | 1 | 468 | | continue; |
| | | 469 | | } |
| | | 470 | | |
| | 0 | 471 | | edgeEnumerator.MoveTo(vertex); |
| | 0 | 472 | | while (edgeEnumerator.MoveNext()) |
| | 0 | 473 | | { |
| | 0 | 474 | | if (!(edgeChecker.IsAcceptable(edgeEnumerator) ?? await edgeChecker.RunCheckAsync(edgeEnumerator, cancel |
| | | 475 | | |
| | 0 | 476 | | closest = vertex; |
| | 0 | 477 | | closestDistance = d; |
| | 0 | 478 | | break; |
| | | 479 | | } |
| | 0 | 480 | | } |
| | | 481 | | |
| | 1 | 482 | | return closest; |
| | 1 | 483 | | } |
| | | 484 | | |
| | | 485 | | /// <summary> |
| | | 486 | | /// Snaps all vertices in the given box that could potentially be snapping vertices. |
| | | 487 | | /// </summary> |
| | | 488 | | /// <param name="network">The network.</param> |
| | | 489 | | /// <param name="searchBox">The box to search in.</param> |
| | | 490 | | /// <param name="maxDistance">The maximum distance of any vertex returned relative to the center of the search box.< |
| | | 491 | | /// <param name="edgeChecker">Used to determine if an edge is acceptable or not. If null any edge will be accepted.< |
| | | 492 | | /// <param name="cancellationToken">The cancellation token.</param> |
| | | 493 | | /// <returns>All the vertices within the box with at least one acceptable edge.</returns> |
| | | 494 | | public static async IAsyncEnumerable<VertexId> SnapToAllVerticesInBoxAsync(this RoutingNetwork network, |
| | | 495 | | ((double longitude, double latitude, float? e) topLeft, (double longitude, double latitude, float? e) |
| | | 496 | | bottomRight) searchBox, |
| | | 497 | | IEdgeChecker? edgeChecker = null, double maxDistance = double.MaxValue, CancellationToken cancellationToken = de |
| | 0 | 498 | | { |
| | 0 | 499 | | var center = searchBox.Center(); |
| | 0 | 500 | | var vertices = network.SearchVerticesInBox(searchBox); |
| | 0 | 501 | | var edgeEnumerator = network.GetEdgeEnumerator(); |
| | 0 | 502 | | foreach (var (vertex, location) in vertices) |
| | 0 | 503 | | { |
| | 0 | 504 | | edgeEnumerator.MoveTo(vertex); |
| | | 505 | | |
| | 0 | 506 | | var d = center.DistanceEstimateInMeter(location); |
| | 0 | 507 | | if (d > maxDistance) continue; |
| | | 508 | | |
| | 0 | 509 | | while (edgeEnumerator.MoveNext()) |
| | 0 | 510 | | { |
| | 0 | 511 | | if (edgeChecker != null && !(edgeChecker.IsAcceptable(edgeEnumerator) ?? await edgeChecker.RunCheckAsync |
| | | 512 | | |
| | 0 | 513 | | yield return vertex; |
| | 0 | 514 | | break; |
| | | 515 | | } |
| | 0 | 516 | | } |
| | 0 | 517 | | } |
| | | 518 | | |
| | | 519 | | /// <summary> |
| | | 520 | | /// Enumerates all edges that have at least one vertex in the given bounding box. |
| | | 521 | | /// </summary> |
| | | 522 | | /// <param name="network">The network.</param> |
| | | 523 | | /// <param name="box">The box to enumerate in.</param> |
| | | 524 | | /// <returns>An enumerator with all the vertices and their location.</returns> |
| | | 525 | | public static IEdgeEnumerator<RoutingNetwork> SearchEdgesInBox(this RoutingNetwork network, |
| | | 526 | | ((double longitude, double latitude, float? e) topLeft, (double longitude, double latitude, float? e) |
| | | 527 | | bottomRight) box) |
| | 2 | 528 | | { |
| | 2 | 529 | | var vertices = network.SearchVerticesInBox(box); |
| | 4 | 530 | | return new VertexEdgeEnumerator(network, vertices.Select((i) => i.vertex)); |
| | 2 | 531 | | } |
| | | 532 | | |
| | | 533 | | /// <summary> |
| | | 534 | | /// Returns true if the edge's bounding box could possibly produce a snap |
| | | 535 | | /// point closer than <paramref name="bestDistance"/> meters from |
| | | 536 | | /// <paramref name="center"/>. Used as a cheap prefilter before the |
| | | 537 | | /// full per-segment projection loop. Returns false → caller can skip |
| | | 538 | | /// the edge entirely. |
| | | 539 | | /// |
| | | 540 | | /// The bbox is built inline by streaming tail + shape + head once and |
| | | 541 | | /// taking min/max on each axis. Intentionally not cached: a per-tile |
| | | 542 | | /// or per-edge cache costs memory in proportion to graph size, which |
| | | 543 | | /// matters for country/continent-scale router DBs. The per-snap cost |
| | | 544 | | /// is one extra shape iteration per candidate edge, all arithmetic and |
| | | 545 | | /// comparisons, no haversines and no allocations beyond the shape |
| | | 546 | | /// enumerator we'd allocate anyway. |
| | | 547 | | /// </summary> |
| | | 548 | | // Sentinel for "effectively unbounded" cutoffs. Earth's circumference is |
| | | 549 | | // ~4×10⁷ m, so any distance above this threshold can never prune a real |
| | | 550 | | // edge — and importantly catches the very common `double.MaxValue` cutoff |
| | | 551 | | // (which is finite by C# rules and so isn't caught by IsInfinity). |
| | | 552 | | private const double UnboundedDistanceCutoff = 1e10; |
| | | 553 | | |
| | | 554 | | private static bool EdgeBboxCanBeat( |
| | | 555 | | IEdgeEnumerator<RoutingNetwork> enumerator, |
| | | 556 | | (double longitude, double latitude, float? e) center, |
| | | 557 | | double bestDistance) |
| | 1163692 | 558 | | { |
| | 1163702 | 559 | | if (bestDistance >= UnboundedDistanceCutoff || double.IsNaN(bestDistance)) return true; |
| | | 560 | | |
| | 1163682 | 561 | | var tail = enumerator.TailLocation; |
| | 1163682 | 562 | | var head = enumerator.HeadLocation; |
| | 1163682 | 563 | | var minLon = tail.longitude < head.longitude ? tail.longitude : head.longitude; |
| | 1163682 | 564 | | var maxLon = tail.longitude > head.longitude ? tail.longitude : head.longitude; |
| | 1163682 | 565 | | var minLat = tail.latitude < head.latitude ? tail.latitude : head.latitude; |
| | 1163682 | 566 | | var maxLat = tail.latitude > head.latitude ? tail.latitude : head.latitude; |
| | | 567 | | |
| | 5905540 | 568 | | foreach (var (sLon, sLat, _) in enumerator.Shape) |
| | 1207247 | 569 | | { |
| | 1275120 | 570 | | if (sLon < minLon) minLon = sLon; |
| | 1199596 | 571 | | else if (sLon > maxLon) maxLon = sLon; |
| | 1272694 | 572 | | if (sLat < minLat) minLat = sLat; |
| | 1199819 | 573 | | else if (sLat > maxLat) maxLat = sLat; |
| | 1207247 | 574 | | } |
| | | 575 | | |
| | | 576 | | // Closest point on the bbox to the query, clamped per axis. |
| | 1163682 | 577 | | var cLon = center.longitude < minLon ? minLon |
| | 1163682 | 578 | | : center.longitude > maxLon ? maxLon : center.longitude; |
| | 1163682 | 579 | | var cLat = center.latitude < minLat ? minLat |
| | 1163682 | 580 | | : center.latitude > maxLat ? maxLat : center.latitude; |
| | 1163682 | 581 | | var closest = (cLon, cLat, (float?)null); |
| | 1163682 | 582 | | return closest.DistanceEstimateInMeter(center) <= bestDistance; |
| | 1163692 | 583 | | } |
| | | 584 | | |
| | | 585 | | /// <summary> |
| | | 586 | | /// Tile-level predicate from snap-algorithm.md. Returns false if no edge |
| | | 587 | | /// with a vertex in this tile can possibly produce a snap within |
| | | 588 | | /// <paramref name="bestDistance"/> of the query, given the tile's own |
| | | 589 | | /// max edge extents. |
| | | 590 | | /// </summary> |
| | | 591 | | private static bool TileCanReach( |
| | | 592 | | (double minLon, double minLat, double maxLon, double maxLat) tileBbox, |
| | | 593 | | double tileMaxLonDiff, |
| | | 594 | | double tileMaxLatDiff, |
| | | 595 | | (double longitude, double latitude, float? e) center, |
| | | 596 | | double bestDistance) |
| | 5393 | 597 | | { |
| | 5399 | 598 | | if (bestDistance >= UnboundedDistanceCutoff || double.IsNaN(bestDistance)) return true; |
| | | 599 | | |
| | 5387 | 600 | | var lonClosest = center.longitude < tileBbox.minLon ? tileBbox.minLon |
| | 5387 | 601 | | : center.longitude > tileBbox.maxLon ? tileBbox.maxLon : center.longitude; |
| | 5387 | 602 | | var latClosest = center.latitude < tileBbox.minLat ? tileBbox.minLat |
| | 5387 | 603 | | : center.latitude > tileBbox.maxLat ? tileBbox.maxLat : center.latitude; |
| | | 604 | | |
| | 5387 | 605 | | var dLon = Math.Abs(center.longitude - lonClosest) - tileMaxLonDiff; |
| | 10549 | 606 | | if (dLon < 0) dLon = 0; |
| | 5387 | 607 | | var dLat = Math.Abs(center.latitude - latClosest) - tileMaxLatDiff; |
| | 10575 | 608 | | if (dLat < 0) dLat = 0; |
| | 10394 | 609 | | if (dLon == 0 && dLat == 0) return true; |
| | | 610 | | |
| | | 611 | | // Convert the (dLon, dLat) offset at center.lat into meters by |
| | | 612 | | // measuring the haversine distance from center to a point offset by |
| | | 613 | | // exactly that much. Direction sign doesn't matter for distance. |
| | 380 | 614 | | var probe = (center.longitude + dLon, center.latitude + dLat, (float?)null); |
| | 380 | 615 | | return probe.DistanceEstimateInMeter(center) <= bestDistance; |
| | 5393 | 616 | | } |
| | | 617 | | |
| | | 618 | | /// <summary> |
| | | 619 | | /// Per-vertex predicate from snap-algorithm.md. Same shape as |
| | | 620 | | /// <see cref="TileCanReach"/> but the "closest point of the tile bbox" |
| | | 621 | | /// becomes the vertex itself (the actual point, not a clamp). |
| | | 622 | | /// </summary> |
| | | 623 | | private static bool VertexCanReach( |
| | | 624 | | double vLon, double vLat, |
| | | 625 | | double tileMaxLonDiff, |
| | | 626 | | double tileMaxLatDiff, |
| | | 627 | | (double longitude, double latitude, float? e) center, |
| | | 628 | | double bestDistance) |
| | 877093 | 629 | | { |
| | 877109 | 630 | | if (bestDistance >= UnboundedDistanceCutoff || double.IsNaN(bestDistance)) return true; |
| | | 631 | | |
| | 877077 | 632 | | var dLon = Math.Abs(center.longitude - vLon) - tileMaxLonDiff; |
| | 1593478 | 633 | | if (dLon < 0) dLon = 0; |
| | 877077 | 634 | | var dLat = Math.Abs(center.latitude - vLat) - tileMaxLatDiff; |
| | 1697781 | 635 | | if (dLat < 0) dLat = 0; |
| | 1559078 | 636 | | if (dLon == 0 && dLat == 0) return true; |
| | | 637 | | |
| | 195076 | 638 | | var probe = (center.longitude + dLon, center.latitude + dLat, (float?)null); |
| | 195076 | 639 | | return probe.DistanceEstimateInMeter(center) <= bestDistance; |
| | 877093 | 640 | | } |
| | | 641 | | |
| | | 642 | | /// <summary> |
| | | 643 | | /// Yields the tile coordinates on the perimeter of the square at |
| | | 644 | | /// <paramref name="ring"/> tiles around (<paramref name="cx"/>, <paramref name="cy"/>). |
| | | 645 | | /// Ring 0 is just the centre tile. Tile coords are unsigned, so the |
| | | 646 | | /// caller must filter against valid (in-box) bounds. We compute via |
| | | 647 | | /// <c>long</c> internally to handle the unsigned underflow safely when |
| | | 648 | | /// the centre tile is near (0, 0). |
| | | 649 | | /// </summary> |
| | | 650 | | private static IEnumerable<(uint x, uint y)> TilesInRing(uint cx, uint cy, uint ring) |
| | 99 | 651 | | { |
| | 99 | 652 | | if (ring == 0) |
| | 96 | 653 | | { |
| | 96 | 654 | | yield return (cx, cy); |
| | 96 | 655 | | yield break; |
| | | 656 | | } |
| | | 657 | | |
| | 3 | 658 | | var startX = (long)cx - ring; |
| | 3 | 659 | | var endX = (long)cx + ring; |
| | 3 | 660 | | var startY = (long)cy - ring; |
| | 3 | 661 | | var endY = (long)cy + ring; |
| | | 662 | | |
| | | 663 | | // Top row (y = startY), full width. |
| | 3 | 664 | | if (startY >= 0) |
| | 3 | 665 | | { |
| | 24 | 666 | | for (var x = startX; x <= endX; x++) |
| | 18 | 667 | | if (x >= 0) yield return ((uint)x, (uint)startY); |
| | 3 | 668 | | } |
| | | 669 | | |
| | | 670 | | // Right column (x = endX), excluding the top-right corner already yielded. |
| | 18 | 671 | | for (var y = startY + 1; y <= endY; y++) |
| | 12 | 672 | | if (y >= 0) yield return ((uint)endX, (uint)y); |
| | | 673 | | |
| | | 674 | | // Bottom row (y = endY), right-to-left, excluding the bottom-right corner. |
| | 18 | 675 | | for (var x = endX - 1; x >= startX; x--) |
| | 12 | 676 | | if (x >= 0) yield return ((uint)x, (uint)endY); |
| | | 677 | | |
| | | 678 | | // Left column (x = startX), bottom-to-top, excluding both corners. |
| | 3 | 679 | | if (startX >= 0) |
| | 3 | 680 | | { |
| | 12 | 681 | | for (var y = endY - 1; y >= startY + 1; y--) |
| | 6 | 682 | | if (y >= 0) yield return ((uint)startX, (uint)y); |
| | 3 | 683 | | } |
| | 3 | 684 | | } |
| | | 685 | | |
| | | 686 | | /// <summary> |
| | | 687 | | /// Distance between two unsigned tile indices, returning 0 if the second |
| | | 688 | | /// would underflow the first. Used to size the outermost ring needed to |
| | | 689 | | /// cover the search box without doing signed arithmetic on uints. |
| | | 690 | | /// </summary> |
| | 384 | 691 | | private static long SafeDelta(uint a, uint b) => a >= b ? (long)(a - b) : 0L; |
| | | 692 | | |
| | | 693 | | /// <summary> |
| | | 694 | | /// Used only as a sort key: distance from the query to the tile bbox's |
| | | 695 | | /// closest point, ignoring the tile's max edge extents. Smaller = closer |
| | | 696 | | /// tile, processed first → bestDistance shrinks faster → more aggressive |
| | | 697 | | /// pruning on subsequent tiles. |
| | | 698 | | /// </summary> |
| | | 699 | | private static double ClosestBoxDistanceMeters( |
| | | 700 | | (double minLon, double minLat, double maxLon, double maxLat) bbox, |
| | | 701 | | (double longitude, double latitude, float? e) center) |
| | 0 | 702 | | { |
| | 0 | 703 | | var lonClosest = center.longitude < bbox.minLon ? bbox.minLon |
| | 0 | 704 | | : center.longitude > bbox.maxLon ? bbox.maxLon : center.longitude; |
| | 0 | 705 | | var latClosest = center.latitude < bbox.minLat ? bbox.minLat |
| | 0 | 706 | | : center.latitude > bbox.maxLat ? bbox.maxLat : center.latitude; |
| | 0 | 707 | | var probe = (lonClosest, latClosest, (float?)null); |
| | 0 | 708 | | return probe.DistanceEstimateInMeter(center); |
| | 0 | 709 | | } |
| | | 710 | | } |