| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Linq; |
| | 4 | | using Itinero.Network.Storage; |
| | 5 | | using Itinero.Network.Tiles.Standalone; |
| | 6 | |
|
| | 7 | | namespace Itinero.Network.Tiles; |
| | 8 | |
|
| | 9 | | internal class NetworkTileEnumerator : INetworkTileEdge, IStandaloneNetworkTileEnumerator |
| | 10 | | { |
| | 11 | | private uint _localId; |
| | 12 | | private uint? _nextEdgePointer; |
| | 13 | | private uint? _shapePointer; |
| | 14 | | private uint? _attributesPointer; |
| | 15 | | private byte? _tailOrder; |
| | 16 | | private byte? _headOrder; |
| | 17 | |
|
| | 18 | | /// <summary> |
| | 19 | | /// Creates a new graph tile enumerator. |
| | 20 | | /// </summary> |
| 439 | 21 | | internal NetworkTileEnumerator() |
| 439 | 22 | | { |
| 439 | 23 | | } |
| | 24 | |
|
| 18758 | 25 | | public NetworkTile? Tile { get; private set; } |
| | 26 | |
|
| | 27 | | /// <summary> |
| | 28 | | /// Gets the tile id. |
| | 29 | | /// </summary> |
| 1519 | 30 | | public uint TileId => this.Tile?.TileId ?? uint.MaxValue; |
| | 31 | |
|
| | 32 | | /// <summary> |
| | 33 | | /// Moves to the given tile. |
| | 34 | | /// </summary> |
| | 35 | | /// <param name="graphTile">The graph tile to move to.</param> |
| | 36 | | /// <returns>True if the move succeeds.</returns> |
| | 37 | | public void MoveTo(NetworkTile graphTile) |
| 414 | 38 | | { |
| 414 | 39 | | this.Tile = graphTile; |
| | 40 | |
|
| 414 | 41 | | this.Reset(); |
| 414 | 42 | | } |
| | 43 | |
|
| | 44 | | /// <summary> |
| | 45 | | /// Move to the vertex. |
| | 46 | | /// </summary> |
| | 47 | | /// <param name="vertex">The vertex.</param> |
| | 48 | | /// <returns>True if the move succeeds and the vertex exists.</returns> |
| | 49 | | public bool MoveTo(VertexId vertex) |
| 362 | 50 | | { |
| 362 | 51 | | if (this.Tile == null) |
| 0 | 52 | | { |
| 0 | 53 | | throw new InvalidOperationException("Move to graph tile first."); |
| | 54 | | } |
| | 55 | |
|
| 362 | 56 | | if (vertex.LocalId >= this.Tile.VertexCount) |
| 0 | 57 | | { |
| 0 | 58 | | return false; |
| | 59 | | } |
| | 60 | |
|
| 362 | 61 | | _headLocation = null; |
| 362 | 62 | | _tailLocation = null; |
| 362 | 63 | | _localId = vertex.LocalId; |
| 362 | 64 | | _nextEdgePointer = uint.MaxValue; |
| 362 | 65 | | this.EdgePointer = uint.MaxValue; |
| | 66 | |
|
| 362 | 67 | | this.Tail = vertex; |
| 362 | 68 | | return true; |
| 362 | 69 | | } |
| | 70 | |
|
| | 71 | | /// <summary> |
| | 72 | | /// Move to the given edge. |
| | 73 | | /// </summary> |
| | 74 | | /// <param name="edge">The edge.</param> |
| | 75 | | /// <param name="forward">The forward flag.</param> |
| | 76 | | /// <returns>True if the move succeeds and the edge exists.</returns> |
| | 77 | | public bool MoveTo(EdgeId edge, bool forward) |
| 604 | 78 | | { |
| 604 | 79 | | if (this.Tile == null) throw new InvalidOperationException("Move to graph tile first."); |
| | 80 | |
|
| 604 | 81 | | if (this.TileId != edge.TileId) throw new ArgumentOutOfRangeException(nameof(edge), |
| 0 | 82 | | "Cannot move to edge not in current tile, move to the tile first."); |
| | 83 | |
|
| 604 | 84 | | if (this.Tile.IsEdgeDeleted(edge)) return false; |
| | 85 | |
|
| 604 | 86 | | _headLocation = null; |
| 604 | 87 | | _tailLocation = null; |
| 604 | 88 | | _nextEdgePointer = edge.LocalId; |
| 604 | 89 | | if (edge.LocalId >= EdgeId.MinCrossId) |
| 2 | 90 | | { |
| 2 | 91 | | _nextEdgePointer = this.Tile.GetEdgeCrossPointer(edge.LocalId - EdgeId.MinCrossId); |
| 2 | 92 | | } |
| 604 | 93 | | this.EdgePointer = _nextEdgePointer.Value; |
| | 94 | |
|
| | 95 | | // decode edge data. |
| 604 | 96 | | this.EdgeId = edge; |
| 604 | 97 | | var size = this.Tile.DecodeVertex(_nextEdgePointer.Value, out var localId, out var tileId); |
| 604 | 98 | | var vertex1 = new VertexId(tileId, localId); |
| 604 | 99 | | _nextEdgePointer += size; |
| 604 | 100 | | size = this.Tile.DecodeVertex(_nextEdgePointer.Value, out localId, out tileId); |
| 604 | 101 | | var vertex2 = new VertexId(tileId, localId); |
| 604 | 102 | | _nextEdgePointer += size; |
| 604 | 103 | | size = this.Tile.DecodePointer(_nextEdgePointer.Value, out var vp1); |
| 604 | 104 | | _nextEdgePointer += size; |
| 604 | 105 | | size = this.Tile.DecodePointer(_nextEdgePointer.Value, out var vp2); |
| 604 | 106 | | _nextEdgePointer += size; |
| | 107 | |
|
| 604 | 108 | | if (vertex1.TileId != vertex2.TileId) |
| 2 | 109 | | { |
| 2 | 110 | | size = this.Tile.DecodeEdgeCrossId(_nextEdgePointer.Value, out var edgeCrossId); |
| 2 | 111 | | _nextEdgePointer += size; |
| | 112 | |
|
| 2 | 113 | | this.EdgeId = new EdgeId(vertex1.TileId, edgeCrossId); |
| 2 | 114 | | } |
| | 115 | |
|
| | 116 | | // get edge profile id. |
| 604 | 117 | | size = this.Tile.DecodeEdgePointerId(_nextEdgePointer.Value, out var edgeProfileId); |
| 604 | 118 | | _nextEdgePointer += size; |
| 604 | 119 | | this.EdgeTypeId = edgeProfileId; |
| | 120 | |
|
| | 121 | | // get length. |
| 604 | 122 | | size = this.Tile.DecodeEdgePointerId(_nextEdgePointer.Value, out var length); |
| 604 | 123 | | _nextEdgePointer += size; |
| 604 | 124 | | this.Length = length; |
| | 125 | |
|
| | 126 | | // get tail and head order. |
| 604 | 127 | | this.Tile.GetTailHeadOrder(_nextEdgePointer.Value, ref _tailOrder, ref _headOrder); |
| 604 | 128 | | _nextEdgePointer++; |
| | 129 | |
|
| 604 | 130 | | size = this.Tile.DecodePointer(_nextEdgePointer.Value, out _shapePointer); |
| 604 | 131 | | _nextEdgePointer += size; |
| 604 | 132 | | size = this.Tile.DecodePointer(_nextEdgePointer.Value, out _attributesPointer); |
| | 133 | |
|
| 604 | 134 | | if (forward) |
| 436 | 135 | | { |
| 436 | 136 | | this.Tail = vertex1; |
| 436 | 137 | | this.Head = vertex2; |
| 436 | 138 | | this.Forward = true; |
| | 139 | |
|
| 436 | 140 | | _nextEdgePointer = vp1; |
| 436 | 141 | | } |
| | 142 | | else |
| 168 | 143 | | { |
| 168 | 144 | | this.Tail = vertex2; |
| 168 | 145 | | this.Head = vertex1; |
| 168 | 146 | | this.Forward = false; |
| | 147 | |
|
| 168 | 148 | | (_headOrder, _tailOrder) = (_tailOrder, _headOrder); |
| | 149 | |
|
| 168 | 150 | | _nextEdgePointer = vp2; |
| 168 | 151 | | } |
| | 152 | |
|
| 604 | 153 | | return true; |
| 604 | 154 | | } |
| | 155 | |
|
| | 156 | | /// <summary> |
| | 157 | | /// Resets this enumerator. |
| | 158 | | /// </summary> |
| | 159 | | /// <remarks> |
| | 160 | | /// Reset this enumerator to: |
| | 161 | | /// - the first vertex for the currently selected edge. |
| | 162 | | /// - the first vertex for the graph tile if there is no selected edge. |
| | 163 | | /// </remarks> |
| | 164 | | public void Reset() |
| 427 | 165 | | { |
| 427 | 166 | | if (this.Tile == null) |
| 0 | 167 | | { |
| 0 | 168 | | throw new InvalidOperationException("Cannot reset an empty enumerator."); |
| | 169 | | } |
| | 170 | |
|
| 427 | 171 | | _headLocation = null; |
| 427 | 172 | | _tailLocation = null; |
| 427 | 173 | | this.EdgePointer = uint.MaxValue; |
| 427 | 174 | | _nextEdgePointer = uint.MaxValue; |
| 427 | 175 | | } |
| | 176 | |
|
| 0 | 177 | | public bool IsEmpty => this.Tile == null; |
| | 178 | |
|
| 3132 | 179 | | internal uint EdgePointer { get; private set; } = uint.MaxValue; |
| | 180 | |
|
| | 181 | | /// <summary> |
| | 182 | | /// Moves to the next edge for the current vertex. |
| | 183 | | /// </summary> |
| | 184 | | /// <returns>True when there is a new edge.</returns> |
| | 185 | | public bool MoveNext() |
| 773 | 186 | | { |
| 777 | 187 | | while (true) |
| 777 | 188 | | { |
| 777 | 189 | | _headLocation = null; |
| 777 | 190 | | _tailLocation = null; |
| 777 | 191 | | _headOrder = null; |
| 777 | 192 | | _tailOrder = null; |
| 777 | 193 | | this.EdgePointer = uint.MaxValue; |
| | 194 | |
|
| 777 | 195 | | if (this.Tile == null) throw new InvalidOperationException("Move to graph tile first."); |
| | 196 | |
|
| 777 | 197 | | if (_nextEdgePointer == uint.MaxValue) |
| 374 | 198 | | { |
| | 199 | | // move to the first edge. |
| 374 | 200 | | _nextEdgePointer = this.Tile.VertexEdgePointer(_localId).DecodeNullableData(); |
| 374 | 201 | | } |
| | 202 | |
|
| 777 | 203 | | if (_nextEdgePointer == null) |
| 276 | 204 | | { |
| | 205 | | // no more data available. |
| 276 | 206 | | return false; |
| | 207 | | } |
| | 208 | |
|
| | 209 | | // decode edge data. |
| 501 | 210 | | this.EdgePointer = _nextEdgePointer.Value; |
| 501 | 211 | | this.EdgeId = new EdgeId(this.Tile.TileId, _nextEdgePointer.Value); |
| 501 | 212 | | var size = this.Tile.DecodeVertex(_nextEdgePointer.Value, out var localId, out var tileId); |
| 501 | 213 | | var vertex1 = new VertexId(tileId, localId); |
| 501 | 214 | | _nextEdgePointer += size; |
| 501 | 215 | | size = this.Tile.DecodeVertex(_nextEdgePointer.Value, out localId, out tileId); |
| 501 | 216 | | var vertex2 = new VertexId(tileId, localId); |
| 501 | 217 | | _nextEdgePointer += size; |
| 501 | 218 | | size = this.Tile.DecodePointer(_nextEdgePointer.Value, out var vp1); |
| 501 | 219 | | _nextEdgePointer += size; |
| 501 | 220 | | size = this.Tile.DecodePointer(_nextEdgePointer.Value, out var vp2); |
| 501 | 221 | | _nextEdgePointer += size; |
| | 222 | |
|
| 501 | 223 | | if (vertex1.TileId != vertex2.TileId) |
| 7 | 224 | | { |
| 7 | 225 | | size = this.Tile.DecodeEdgeCrossId(_nextEdgePointer.Value, out var edgeCrossId); |
| 7 | 226 | | _nextEdgePointer += size; |
| | 227 | |
|
| 7 | 228 | | this.EdgeId = new EdgeId(vertex1.TileId, edgeCrossId); |
| 7 | 229 | | } |
| | 230 | |
|
| | 231 | | // get edge profile id. |
| 501 | 232 | | size = this.Tile.DecodeEdgePointerId(_nextEdgePointer.Value, out var edgeProfileId); |
| 501 | 233 | | _nextEdgePointer += size; |
| 501 | 234 | | this.EdgeTypeId = edgeProfileId; |
| | 235 | |
|
| | 236 | | // get length. |
| 501 | 237 | | size = this.Tile.DecodeEdgePointerId(_nextEdgePointer.Value, out var length); |
| 501 | 238 | | _nextEdgePointer += size; |
| 501 | 239 | | this.Length = length; |
| | 240 | |
|
| | 241 | | // get tail and head order. |
| 501 | 242 | | this.Tile.GetTailHeadOrder(_nextEdgePointer.Value, ref _tailOrder, ref _headOrder); |
| 501 | 243 | | _nextEdgePointer++; |
| | 244 | |
|
| | 245 | | // get shape and attribute pointers. |
| 501 | 246 | | size = this.Tile.DecodePointer(_nextEdgePointer.Value, out _shapePointer); |
| 501 | 247 | | _nextEdgePointer += size; |
| 501 | 248 | | size = this.Tile.DecodePointer(_nextEdgePointer.Value, out _attributesPointer); |
| | 249 | |
|
| 501 | 250 | | if (vertex1.TileId == this.Tile.TileId && vertex1.LocalId == _localId) |
| 278 | 251 | | { |
| 278 | 252 | | _nextEdgePointer = vp1; |
| | 253 | |
|
| 278 | 254 | | this.Head = vertex2; |
| 278 | 255 | | this.Forward = true; |
| 278 | 256 | | } |
| | 257 | | else |
| 223 | 258 | | { |
| 223 | 259 | | _nextEdgePointer = vp2; |
| | 260 | |
|
| 223 | 261 | | this.Head = vertex1; |
| 223 | 262 | | this.Forward = false; |
| | 263 | |
|
| 223 | 264 | | (_headOrder, _tailOrder) = (_tailOrder, _headOrder); |
| 223 | 265 | | } |
| | 266 | |
|
| 501 | 267 | | if (this.Tile.IsEdgeDeleted(this.EdgeId)) |
| 4 | 268 | | { |
| 4 | 269 | | continue; |
| | 270 | | } |
| | 271 | |
|
| 497 | 272 | | return true; |
| | 273 | | } |
| 773 | 274 | | } |
| | 275 | |
|
| | 276 | | /// <summary> |
| | 277 | | /// Gets the shape of the given edge (not including vertex locations). |
| | 278 | | /// </summary> |
| | 279 | | public IEnumerable<(double longitude, double latitude, float? e)> Shape |
| | 280 | | { |
| | 281 | | get |
| 363 | 282 | | { |
| 363 | 283 | | if (this.Tile == null) |
| 0 | 284 | | { |
| 0 | 285 | | throw new InvalidOperationException("Move to graph tile first."); |
| | 286 | | } |
| | 287 | |
|
| 363 | 288 | | if (!this.Forward) |
| 123 | 289 | | { |
| 123 | 290 | | return this.Tile.GetShape(_shapePointer).Reverse(); |
| | 291 | | } |
| | 292 | |
|
| 240 | 293 | | return this.Tile.GetShape(_shapePointer); |
| 363 | 294 | | } |
| | 295 | | } |
| | 296 | |
|
| | 297 | | /// <summary> |
| | 298 | | /// Gets the attributes of the given edge. |
| | 299 | | /// </summary> |
| | 300 | | public IEnumerable<(string key, string value)> Attributes |
| | 301 | | { |
| | 302 | | get |
| 283 | 303 | | { |
| 283 | 304 | | if (this.Tile == null) |
| 0 | 305 | | { |
| 0 | 306 | | throw new InvalidOperationException("Move to graph tile first."); |
| | 307 | | } |
| | 308 | |
|
| 283 | 309 | | return this.Tile.GetAttributes(_attributesPointer); |
| 283 | 310 | | } |
| | 311 | | } |
| | 312 | |
|
| | 313 | | /// <summary> |
| | 314 | | /// Gets the first vertex. |
| | 315 | | /// </summary> |
| 1521 | 316 | | public VertexId Tail { get; private set; } |
| | 317 | |
|
| | 318 | | private (double longitude, double latitude, float? e)? _tailLocation; |
| | 319 | |
|
| | 320 | | /// <inheritdoc/> |
| | 321 | | public (double longitude, double latitude, float? e) TailLocation |
| | 322 | | { |
| | 323 | | get |
| 0 | 324 | | { |
| 0 | 325 | | _tailLocation ??= this.GetVertex(this.Tail); |
| | 326 | |
|
| 0 | 327 | | return _tailLocation.Value; |
| 0 | 328 | | } |
| | 329 | | } |
| | 330 | |
|
| | 331 | | /// <summary> |
| | 332 | | /// Gets the second vertex. |
| | 333 | | /// </summary> |
| 1661 | 334 | | public VertexId Head { get; private set; } |
| | 335 | |
|
| | 336 | | private (double longitude, double latitude, float? e)? _headLocation; |
| | 337 | |
|
| | 338 | | /// <inheritdoc/> |
| | 339 | | public (double longitude, double latitude, float? e) HeadLocation |
| | 340 | | { |
| | 341 | | get |
| 0 | 342 | | { |
| 0 | 343 | | _headLocation ??= this.GetVertex(this.Head); |
| | 344 | |
|
| 0 | 345 | | return _headLocation.Value; |
| 0 | 346 | | } |
| | 347 | | } |
| | 348 | |
|
| | 349 | | /// <summary> |
| | 350 | | /// Gets the local edge id. |
| | 351 | | /// </summary> |
| 2361 | 352 | | public EdgeId EdgeId { get; private set; } |
| | 353 | |
|
| | 354 | | /// <summary> |
| | 355 | | /// Gets the forward/backward flag. |
| | 356 | | /// </summary> |
| | 357 | | /// <remarks> |
| | 358 | | /// When true the attributes can be interpreted normally, when false they represent the direction from tail -> head. |
| | 359 | | /// </remarks> |
| 1973 | 360 | | public bool Forward { get; private set; } |
| | 361 | |
|
| | 362 | | /// <summary> |
| | 363 | | /// Gets the edge profile id, if any. |
| | 364 | | /// </summary> |
| 1157 | 365 | | public uint? EdgeTypeId { get; private set; } |
| | 366 | |
|
| | 367 | | /// <summary> |
| | 368 | | /// Gets the length in centimeters, if any. |
| | 369 | | /// </summary> |
| 1533 | 370 | | public uint? Length { get; private set; } |
| | 371 | |
|
| | 372 | | /// <summary> |
| | 373 | | /// Gets the head index of this edge in the turn cost table. |
| | 374 | | /// </summary> |
| 141 | 375 | | public byte? HeadOrder => _headOrder; |
| | 376 | |
|
| | 377 | | /// <summary> |
| | 378 | | /// Gets the tail index of this edge in the turn cost table. |
| | 379 | | /// </summary> |
| 108 | 380 | | public byte? TailOrder => _tailOrder; |
| | 381 | |
|
| | 382 | | /// <summary> |
| | 383 | | /// Gets the turn cost at the tail turn (source -> [tail -> head]). |
| | 384 | | /// </summary> |
| | 385 | | /// <param name="sourceOrder">The order of the source edge.</param> |
| | 386 | | /// <returns>The turn costs if any.</returns> |
| | 387 | | public IEnumerable<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnumerable<Ed |
| | 388 | | byte sourceOrder) |
| 18 | 389 | | { |
| 18 | 390 | | if (this.Tile == null) |
| 0 | 391 | | return ArraySegment<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnum |
| | 392 | |
|
| 18 | 393 | | var order = _tailOrder; |
| 18 | 394 | | return order == null |
| 18 | 395 | | ? ArraySegment<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnumerabl |
| 18 | 396 | | : this.Tile.GetTurnCosts(this.Tail, sourceOrder, order.Value); |
| 18 | 397 | | } |
| | 398 | |
|
| | 399 | | /// <summary> |
| | 400 | | /// Gets the turn cost at the tail turn ([head -> tail] -> target). |
| | 401 | | /// </summary> |
| | 402 | | /// <param name="targetOrder">The order of the target edge.</param> |
| | 403 | | /// <returns>The turn costs if any.</returns> |
| | 404 | | public IEnumerable<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnumerable<Ed |
| | 405 | | byte targetOrder) |
| 1 | 406 | | { |
| 1 | 407 | | if (this.Tile == null) |
| 0 | 408 | | return ArraySegment<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnum |
| | 409 | |
|
| 1 | 410 | | var order = _tailOrder; |
| 1 | 411 | | return order == null |
| 1 | 412 | | ? ArraySegment<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnumerabl |
| 1 | 413 | | : this.Tile.GetTurnCosts(this.Tail, order.Value, targetOrder); |
| 1 | 414 | | } |
| | 415 | |
|
| | 416 | | /// <summary> |
| | 417 | | /// Gets the turn cost at the tail turn (source -> [head -> tail]). |
| | 418 | | /// </summary> |
| | 419 | | /// <param name="sourceOrder">The order of the source edge.</param> |
| | 420 | | /// <returns>The turn costs if any.</returns> |
| | 421 | | public IEnumerable<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnumerable<Ed |
| | 422 | | byte sourceOrder) |
| 1 | 423 | | { |
| 1 | 424 | | if (this.Tile == null) |
| 0 | 425 | | return ArraySegment<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnum |
| | 426 | |
|
| 1 | 427 | | var order = _headOrder; |
| 1 | 428 | | return order == null |
| 1 | 429 | | ? ArraySegment<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnumerabl |
| 1 | 430 | | : this.Tile.GetTurnCosts(this.Head, sourceOrder, order.Value); |
| 1 | 431 | | } |
| | 432 | |
|
| | 433 | | /// <summary> |
| | 434 | | /// Gets the turn cost at the tail turn ([tail -> head] -> target). |
| | 435 | | /// </summary> |
| | 436 | | /// <param name="targetOrder">The order of the target edge.</param> |
| | 437 | | /// <returns>The turn costs if any.</returns> |
| | 438 | | public IEnumerable<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnumerable<Ed |
| | 439 | | byte targetOrder) |
| 1 | 440 | | { |
| 1 | 441 | | if (this.Tile == null) |
| 0 | 442 | | return ArraySegment<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnum |
| | 443 | |
|
| 1 | 444 | | var order = _headOrder; |
| 1 | 445 | | return order == null |
| 1 | 446 | | ? ArraySegment<(uint turnCostType, IEnumerable<(string key, string value)> attributes, uint cost, IEnumerabl |
| 1 | 447 | | : this.Tile.GetTurnCosts(this.Head, order.Value, targetOrder); |
| 1 | 448 | | } |
| | 449 | |
|
| | 450 | | private (double longitude, double latitude, float? e) GetVertex(VertexId vertex) |
| 0 | 451 | | { |
| 0 | 452 | | if (this.Tile == null) |
| 0 | 453 | | { |
| 0 | 454 | | throw new ArgumentOutOfRangeException(nameof(vertex), $"Vertex {vertex} not found!"); |
| | 455 | | } |
| | 456 | |
|
| 0 | 457 | | if (!this.Tile.TryGetVertex(vertex, out var longitude, out var latitude, out var e)) |
| 0 | 458 | | { |
| 0 | 459 | | throw new ArgumentOutOfRangeException(nameof(vertex), $"Vertex {vertex} not found!"); |
| | 460 | | } |
| | 461 | |
|
| 0 | 462 | | return (longitude, latitude, e); |
| 0 | 463 | | } |
| | 464 | | } |