| | | 1 | | using System; |
| | | 2 | | using System.Collections.Generic; |
| | | 3 | | using System.Globalization; |
| | | 4 | | using System.Threading; |
| | | 5 | | using System.Threading.Tasks; |
| | | 6 | | using Itinero.Geo; |
| | | 7 | | using Itinero.Network; |
| | | 8 | | using Itinero.Profiles; |
| | | 9 | | using Itinero.Routes.Paths; |
| | | 10 | | using Itinero.Routing; |
| | | 11 | | using Itinero.Snapping; |
| | | 12 | | |
| | | 13 | | namespace Itinero.MapMatching.Model; |
| | | 14 | | |
| | | 15 | | /// <summary> |
| | | 16 | | /// The model builder. Builds an HMM factor graph for map matching using the |
| | | 17 | | /// Newson & Krumm (2009) probabilistic model: |
| | | 18 | | /// - Emission probability: Gaussian based on GPS noise (sigma_z) |
| | | 19 | | /// - Transition probability: Exponential based on |great-circle distance - route distance| (beta) |
| | | 20 | | /// </summary> |
| | | 21 | | public class ModelBuilder |
| | | 22 | | { |
| | | 23 | | private readonly RoutingNetwork _routingNetwork; |
| | | 24 | | private readonly ModelBuilderSettings _settings; |
| | | 25 | | |
| | | 26 | | /// <summary> |
| | | 27 | | /// Creates a new model builder. |
| | | 28 | | /// </summary> |
| | | 29 | | /// <param name="routingNetwork">The routing network.</param> |
| | | 30 | | /// <param name="settings">The settings.</param> |
| | 31 | 31 | | public ModelBuilder(RoutingNetwork routingNetwork, ModelBuilderSettings? settings = null) |
| | 31 | 32 | | { |
| | 31 | 33 | | _routingNetwork = routingNetwork; |
| | 31 | 34 | | _settings = settings ?? new ModelBuilderSettings(); |
| | 31 | 35 | | } |
| | | 36 | | |
| | | 37 | | /// <summary> |
| | | 38 | | /// Builds one or more graph models for the given track. |
| | | 39 | | /// </summary> |
| | | 40 | | /// <param name="track">The track.</param> |
| | | 41 | | /// <param name="profile">The profile to match with.</param> |
| | | 42 | | /// <param name="cancellationToken">The cancellation token.</param> |
| | | 43 | | /// <returns>One or more graph models with probabilities as costs.</returns> |
| | | 44 | | public async Task<IEnumerable<GraphModel>> BuildModels(Track track, Profile profile, CancellationToken cancellationT |
| | 31 | 45 | | { |
| | 31 | 46 | | var models = new List<GraphModel>(); |
| | | 47 | | |
| | 31 | 48 | | var start = 0; |
| | 68 | 49 | | while (start < track.Count - 1) |
| | 37 | 50 | | { |
| | 37 | 51 | | var (model, lastUsed) = await this.BuildModel(track, profile, start, cancellationToken); |
| | 37 | 52 | | if (cancellationToken.IsCancellationRequested) return ArraySegment<GraphModel>.Empty; |
| | | 53 | | |
| | 37 | 54 | | if (lastUsed == start) |
| | 4 | 55 | | { |
| | | 56 | | // nothing consumed, move next. |
| | 4 | 57 | | start++; |
| | 4 | 58 | | continue; |
| | | 59 | | } |
| | | 60 | | |
| | 33 | 61 | | models.Add(model); |
| | 33 | 62 | | start = lastUsed; |
| | 33 | 63 | | } |
| | | 64 | | |
| | 31 | 65 | | return models; |
| | 31 | 66 | | } |
| | | 67 | | |
| | | 68 | | private async Task<(GraphModel model, int index)> BuildModel(Track track, Profile profile, int start, CancellationTo |
| | 37 | 69 | | { |
| | 37 | 70 | | var model = new GraphModel(track); |
| | | 71 | | |
| | | 72 | | // Newson & Krumm (2009) HMM parameters. |
| | 37 | 73 | | var sigmaZ = _settings.SigmaZ; |
| | 37 | 74 | | var beta = _settings.Beta; |
| | 37 | 75 | | var searchRadius = _settings.SearchRadius; |
| | 37 | 76 | | var minPointDistance = _settings.MinPointDistance; |
| | 37 | 77 | | var maxPointSkip = _settings.MaxPointSkip; |
| | 37 | 78 | | var breakageDistance = _settings.BreakageDistance; |
| | 37 | 79 | | var maxRouteDistanceFactor = _settings.MaxRouteDistanceFactor; |
| | | 80 | | |
| | | 81 | | // precompute for emission cost: 1 / (2 * sigma_z^2) |
| | 37 | 82 | | var invDoubleSigmaZSq = 1.0 / (2.0 * sigmaZ * sigmaZ); |
| | | 83 | | // precompute for transition cost: 1 / beta |
| | 37 | 84 | | var invBeta = 1.0 / beta; |
| | | 85 | | |
| | 37 | 86 | | var previousLayer = new List<int>(); |
| | 37 | 87 | | var startNode = new GraphNode(); |
| | 37 | 88 | | previousLayer.Add(model.AddNode(startNode)); |
| | | 89 | | |
| | 37 | 90 | | var lastPoint = start; |
| | 37 | 91 | | var lastUsedLocation = start > 0 |
| | 37 | 92 | | ? (track[start].Location.longitude, track[start].Location.latitude, (float?)null) |
| | 37 | 93 | | : ((double, double, float?))default; |
| | 37 | 94 | | var consecutiveSkips = 0; |
| | | 95 | | |
| | | 96 | | // use a snap bounding box larger than the search radius |
| | | 97 | | // to account for tile boundaries and long edges in the spatial index. |
| | 37 | 98 | | var snapBox = Math.Max(searchRadius * 3, 500); |
| | 37 | 99 | | var snapper = _routingNetwork.Snap(profile, s => |
| | 37 | 100 | | { |
| | 37 | 101 | | s.OffsetInMeter = snapBox; |
| | 37 | 102 | | s.OffsetInMeterMax = snapBox; |
| | 74 | 103 | | }); |
| | | 104 | | |
| | 7002 | 105 | | for (var i = start; i < track.Count; i++) |
| | 3470 | 106 | | { |
| | 3470 | 107 | | var trackPoint = track[i]; |
| | 3470 | 108 | | var trackPointLocation = (trackPoint.Location.longitude, trackPoint.Location.latitude, (float?)null); |
| | | 109 | | |
| | | 110 | | // close-point filtering: skip points too close to the last used point. |
| | 3470 | 111 | | if (i > start && lastUsedLocation != default) |
| | 3433 | 112 | | { |
| | 3433 | 113 | | var distToLast = lastUsedLocation.DistanceEstimateInMeter(trackPointLocation); |
| | | 114 | | |
| | | 115 | | // breakage distance: if too far, break the model. |
| | 3433 | 116 | | if (distToLast > breakageDistance) |
| | 1 | 117 | | { |
| | 1 | 118 | | break; |
| | | 119 | | } |
| | | 120 | | |
| | | 121 | | // skip points too close together. |
| | 3432 | 122 | | if (distToLast < minPointDistance) |
| | 1362 | 123 | | { |
| | 1362 | 124 | | continue; |
| | | 125 | | } |
| | 2070 | 126 | | } |
| | | 127 | | |
| | 2107 | 128 | | var trackPointLayer = new List<int>(); |
| | 2107 | 129 | | var isConnected = false; |
| | | 130 | | |
| | | 131 | | // phase 1: collect all snap candidates and add nodes. |
| | 2107 | 132 | | var currentCandidates = new List<(SnapPoint snapPoint, int nodeId, double emissionCost)>(); |
| | 69577 | 133 | | await foreach (var snapPoint in snapper |
| | 2107 | 134 | | .ToAllAsync(trackPointLocation.longitude, trackPointLocation.latitude, cancellationToken: |
| | 31628 | 135 | | { |
| | 31628 | 136 | | if (cancellationToken.IsCancellationRequested) return (new GraphModel(track), start); |
| | | 137 | | |
| | 31628 | 138 | | var distanceToCandidate = trackPointLocation.DistanceEstimateInMeter(snapPoint.LocationOnNetwork(_routin |
| | 50301 | 139 | | if (distanceToCandidate > searchRadius) continue; |
| | | 140 | | |
| | | 141 | | // Emission cost: Gaussian model (negative log probability, ignoring normalization constant). |
| | | 142 | | // cost = distance^2 / (2 * sigma_z^2) |
| | 12955 | 143 | | var emissionCost = distanceToCandidate * distanceToCandidate * invDoubleSigmaZSq; |
| | | 144 | | |
| | | 145 | | // add node. |
| | 12955 | 146 | | var node = new GraphNode() { TrackPoint = i, SnapPoint = snapPoint, Cost = emissionCost }; |
| | 12955 | 147 | | var nodeId = model.AddNode(node); |
| | 12955 | 148 | | currentCandidates.Add((snapPoint, nodeId, emissionCost)); |
| | 12955 | 149 | | } |
| | | 150 | | |
| | 2107 | 151 | | if (currentCandidates.Count == 0) |
| | 3 | 152 | | { |
| | 3 | 153 | | consecutiveSkips++; |
| | 3 | 154 | | if (consecutiveSkips > maxPointSkip) |
| | 0 | 155 | | { |
| | 0 | 156 | | break; |
| | | 157 | | } |
| | 3 | 158 | | continue; |
| | | 159 | | } |
| | | 160 | | |
| | | 161 | | // great-circle distance between consecutive track points. |
| | 2104 | 162 | | var greatCircleDistance = 0.0; |
| | 2104 | 163 | | if (lastPoint > start || i > start) |
| | 2068 | 164 | | { |
| | 2068 | 165 | | var previousTrackPoint = track[lastPoint]; |
| | 2068 | 166 | | var previousTrackPointLocation = (previousTrackPoint.Location.longitude, |
| | 2068 | 167 | | previousTrackPoint.Location.latitude, (float?)null); |
| | 2068 | 168 | | greatCircleDistance = previousTrackPointLocation.DistanceEstimateInMeter(trackPointLocation); |
| | 2068 | 169 | | } |
| | | 170 | | |
| | 2104 | 171 | | var maxRouteDistance = greatCircleDistance * maxRouteDistanceFactor; |
| | 15059 | 172 | | var targetSnapPoints = currentCandidates.ConvertAll(c => c.snapPoint); |
| | | 173 | | |
| | | 174 | | // phase 2: for each previous candidate, route one-to-many to all current candidates. |
| | 2104 | 175 | | var nodeIsConnected = new bool[currentCandidates.Count]; |
| | 28976 | 176 | | foreach (var previousNode in previousLayer) |
| | 11332 | 177 | | { |
| | 11332 | 178 | | var previousSnapPoint = model.GetNode(previousNode).SnapPoint; |
| | | 179 | | |
| | 11332 | 180 | | if (previousSnapPoint == null) |
| | 37 | 181 | | { |
| | | 182 | | // start node: connect to all current candidates with zero transition cost. |
| | 368 | 183 | | for (var c = 0; c < currentCandidates.Count; c++) |
| | 147 | 184 | | { |
| | 147 | 185 | | model.AddEdge(new GraphEdge() |
| | 147 | 186 | | { |
| | 147 | 187 | | Node1 = previousNode, |
| | 147 | 188 | | Node2 = currentCandidates[c].nodeId, |
| | 147 | 189 | | Cost = 0 |
| | 147 | 190 | | }); |
| | 147 | 191 | | isConnected = true; |
| | 147 | 192 | | nodeIsConnected[c] = true; |
| | 147 | 193 | | } |
| | 37 | 194 | | continue; |
| | | 195 | | } |
| | | 196 | | |
| | | 197 | | // check for same snap points (zero transition cost). |
| | 11295 | 198 | | var needsRouting = false; |
| | 397218 | 199 | | for (var c = 0; c < currentCandidates.Count; c++) |
| | 187314 | 200 | | { |
| | 187314 | 201 | | if (previousSnapPoint.Value.EdgeId == currentCandidates[c].snapPoint.EdgeId && |
| | 187314 | 202 | | previousSnapPoint.Value.Offset == currentCandidates[c].snapPoint.Offset) |
| | 5177 | 203 | | { |
| | 5177 | 204 | | model.AddEdge(new GraphEdge() |
| | 5177 | 205 | | { |
| | 5177 | 206 | | Node1 = previousNode, |
| | 5177 | 207 | | Node2 = currentCandidates[c].nodeId, |
| | 5177 | 208 | | Cost = 0 |
| | 5177 | 209 | | }); |
| | 5177 | 210 | | isConnected = true; |
| | 5177 | 211 | | nodeIsConnected[c] = true; |
| | 5177 | 212 | | } |
| | | 213 | | else |
| | 182137 | 214 | | { |
| | 182137 | 215 | | needsRouting = true; |
| | 182137 | 216 | | } |
| | 187314 | 217 | | } |
| | | 218 | | |
| | 11297 | 219 | | if (!needsRouting) continue; |
| | | 220 | | |
| | | 221 | | // one-to-many: single Dijkstra from this previous candidate to all current candidates. |
| | 11293 | 222 | | var paths = await _routingNetwork.Route(new RoutingSettings() { MaxDistance = maxRouteDistance, Profile |
| | 11293 | 223 | | .From(previousSnapPoint.Value).To(targetSnapPoints).Paths(cancellationToken); |
| | | 224 | | |
| | 397210 | 225 | | for (var c = 0; c < currentCandidates.Count; c++) |
| | 187312 | 226 | | { |
| | | 227 | | // skip same-snap-point pairs already handled above. |
| | 187312 | 228 | | if (previousSnapPoint.Value.EdgeId == currentCandidates[c].snapPoint.EdgeId && |
| | 187312 | 229 | | previousSnapPoint.Value.Offset == currentCandidates[c].snapPoint.Offset) |
| | 5175 | 230 | | continue; |
| | | 231 | | |
| | 182137 | 232 | | var pathResult = paths[c]; |
| | 240607 | 233 | | if (pathResult.IsError) continue; |
| | | 234 | | |
| | 123667 | 235 | | var cachedPath = pathResult.Value; |
| | 123667 | 236 | | var routeDistance = cachedPath.LengthInMeters(); |
| | | 237 | | |
| | | 238 | | // Transition cost: Exponential model (negative log probability). |
| | | 239 | | // cost = |greatCircleDistance - routeDistance| / beta |
| | 123667 | 240 | | var dt = Math.Abs(greatCircleDistance - routeDistance); |
| | 123667 | 241 | | var transitionCost = dt * invBeta; |
| | | 242 | | |
| | 123667 | 243 | | var attributes = new List<(string key, string value)> |
| | 123667 | 244 | | { |
| | 123667 | 245 | | ("great_circle_distance", greatCircleDistance.ToString(CultureInfo.InvariantCulture)), |
| | 123667 | 246 | | ("route_distance", routeDistance.ToString(CultureInfo.InvariantCulture)), |
| | 123667 | 247 | | ("dt", dt.ToString(CultureInfo.InvariantCulture)) |
| | 123667 | 248 | | }; |
| | | 249 | | |
| | 123667 | 250 | | model.AddEdge(new GraphEdge() |
| | 123667 | 251 | | { |
| | 123667 | 252 | | Node1 = previousNode, |
| | 123667 | 253 | | Node2 = currentCandidates[c].nodeId, |
| | 123667 | 254 | | Cost = transitionCost, |
| | 123667 | 255 | | Attributes = attributes, |
| | 123667 | 256 | | CachedPath = cachedPath |
| | 123667 | 257 | | }); |
| | 123667 | 258 | | isConnected = true; |
| | 123667 | 259 | | nodeIsConnected[c] = true; |
| | 123667 | 260 | | } |
| | 11293 | 261 | | } |
| | | 262 | | |
| | 30118 | 263 | | for (var c = 0; c < currentCandidates.Count; c++) |
| | 12955 | 264 | | { |
| | 12955 | 265 | | if (nodeIsConnected[c]) |
| | 11312 | 266 | | { |
| | 11312 | 267 | | trackPointLayer.Add(currentCandidates[c].nodeId); |
| | 11312 | 268 | | } |
| | 12955 | 269 | | } |
| | | 270 | | |
| | 2104 | 271 | | if (!isConnected) |
| | 29 | 272 | | { |
| | | 273 | | // try skipping this point before breaking. |
| | 29 | 274 | | consecutiveSkips++; |
| | 29 | 275 | | if (consecutiveSkips > maxPointSkip) |
| | 5 | 276 | | { |
| | 5 | 277 | | break; |
| | | 278 | | } |
| | 24 | 279 | | continue; |
| | | 280 | | } |
| | | 281 | | |
| | | 282 | | // successfully matched this point: reset skip counter and advance. |
| | 2075 | 283 | | consecutiveSkips = 0; |
| | 2075 | 284 | | previousLayer = trackPointLayer; |
| | 2075 | 285 | | lastPoint = i; |
| | 2075 | 286 | | lastUsedLocation = trackPointLocation; |
| | 2075 | 287 | | } |
| | | 288 | | |
| | 37 | 289 | | var endNode = new GraphNode(); |
| | 37 | 290 | | var endNodeId = model.AddNode(endNode); |
| | | 291 | | |
| | | 292 | | // add edges from previous layer to last node. |
| | 309 | 293 | | foreach (var previousNode in previousLayer) |
| | 99 | 294 | | { |
| | 99 | 295 | | model.AddEdge(new GraphEdge() |
| | 99 | 296 | | { |
| | 99 | 297 | | Node1 = previousNode, |
| | 99 | 298 | | Node2 = endNodeId, |
| | 99 | 299 | | Cost = 0 // last edges have cost 0. |
| | 99 | 300 | | }); |
| | 99 | 301 | | } |
| | | 302 | | |
| | 37 | 303 | | return (model, lastPoint); |
| | 37 | 304 | | } |
| | | 305 | | |
| | | 306 | | } |