| | | 1 | | //======================================================================= |
| | | 2 | | // NavigationMapOverlayDelta.cs |
| | | 3 | | //======================================================================= |
| | | 4 | | // MIT License, Copyright (c) 2024-present David Oravsky (mrdav30) |
| | | 5 | | // See LICENSE file in the project root for full license information. |
| | | 6 | | //======================================================================= |
| | | 7 | | |
| | | 8 | | using System; |
| | | 9 | | using System.Collections.Generic; |
| | | 10 | | using System.Collections.ObjectModel; |
| | | 11 | | using SwiftCollections; |
| | | 12 | | |
| | | 13 | | namespace Trailblazer.Pathing; |
| | | 14 | | |
| | | 15 | | /// <summary> |
| | | 16 | | /// Contains the canonically ordered semantic overlay operations for one source map. |
| | | 17 | | /// </summary> |
| | | 18 | | public sealed class NavigationMapOverlayDelta |
| | | 19 | | { |
| | 1 | 20 | | private static readonly IComparer<NavigationCellOverlayOperation> CellComparer = new CellOperationComparer(); |
| | 1 | 21 | | private static readonly IComparer<NavigationConnectionOverlayOperation> ConnectionComparer = new ConnectionOperation |
| | 1 | 22 | | private static readonly IComparer<TraversalTransitionOverlayOperation> TransitionComparer = new TransitionOperationC |
| | | 23 | | |
| | | 24 | | private readonly NavigationCellOverlayOperation[] _cells; |
| | | 25 | | private readonly NavigationConnectionOverlayOperation[] _connections; |
| | | 26 | | private readonly TraversalTransitionOverlayOperation[] _transitions; |
| | | 27 | | private readonly ReadOnlyCollection<NavigationCellOverlayOperation> _cellView; |
| | | 28 | | private readonly ReadOnlyCollection<NavigationConnectionOverlayOperation> _connectionView; |
| | | 29 | | private readonly ReadOnlyCollection<TraversalTransitionOverlayOperation> _transitionView; |
| | | 30 | | |
| | | 31 | | /// <summary> |
| | | 32 | | /// Initializes an immutable delta and normalizes every operation collection into canonical key order. |
| | | 33 | | /// </summary> |
| | 412 | 34 | | public NavigationMapOverlayDelta( |
| | 412 | 35 | | string mapId, |
| | 412 | 36 | | ReadOnlySpan<NavigationCellOverlayOperation> cells = default, |
| | 412 | 37 | | ReadOnlySpan<NavigationConnectionOverlayOperation> connections = default, |
| | 412 | 38 | | ReadOnlySpan<TraversalTransitionOverlayOperation> transitions = default) |
| | | 39 | | { |
| | 412 | 40 | | SwiftThrowHelper.ThrowIfNull(mapId, nameof(mapId)); |
| | 412 | 41 | | SwiftThrowHelper.ThrowIfArgument( |
| | 412 | 42 | | string.IsNullOrWhiteSpace(mapId), |
| | 412 | 43 | | nameof(mapId), |
| | 412 | 44 | | "Map id cannot be empty or whitespace."); |
| | 412 | 45 | | SwiftThrowHelper.ThrowIfArgument( |
| | 412 | 46 | | cells.IsEmpty && connections.IsEmpty && transitions.IsEmpty, |
| | 412 | 47 | | nameof(cells), |
| | 412 | 48 | | "A map overlay delta must contain at least one operation."); |
| | | 49 | | |
| | 412 | 50 | | MapId = mapId; |
| | 412 | 51 | | _cells = cells.ToArray(); |
| | 412 | 52 | | _connections = connections.ToArray(); |
| | 412 | 53 | | _transitions = transitions.ToArray(); |
| | | 54 | | |
| | 412 | 55 | | Array.Sort(_cells, CellComparer); |
| | 412 | 56 | | Array.Sort(_connections, ConnectionComparer); |
| | 412 | 57 | | Array.Sort(_transitions, TransitionComparer); |
| | | 58 | | |
| | 412 | 59 | | ValidateCells(_cells); |
| | 410 | 60 | | ValidateConnections(_connections); |
| | 410 | 61 | | ValidateTransitions(_transitions); |
| | 410 | 62 | | _cellView = Array.AsReadOnly(_cells); |
| | 410 | 63 | | _connectionView = Array.AsReadOnly(_connections); |
| | 410 | 64 | | _transitionView = Array.AsReadOnly(_transitions); |
| | 410 | 65 | | EstimatedDescriptorBytes = EstimateDescriptorBytes(); |
| | 410 | 66 | | } |
| | | 67 | | |
| | | 68 | | /// <summary>Gets the stable source map identifier.</summary> |
| | | 69 | | public string MapId { get; } |
| | | 70 | | |
| | | 71 | | /// <summary>Gets canonically ordered, unique cell operations.</summary> |
| | 68318 | 72 | | public IReadOnlyList<NavigationCellOverlayOperation> Cells => _cellView; |
| | | 73 | | |
| | | 74 | | /// <summary>Gets canonically ordered, unique connection operations.</summary> |
| | 8823 | 75 | | public IReadOnlyList<NavigationConnectionOverlayOperation> Connections => _connectionView; |
| | | 76 | | |
| | | 77 | | /// <summary>Gets canonically ordered, unique transition operations.</summary> |
| | 8772 | 78 | | public IReadOnlyList<TraversalTransitionOverlayOperation> Transitions => _transitionView; |
| | | 79 | | |
| | 51007 | 80 | | internal ReadOnlySpan<NavigationCellOverlayOperation> CellSpan => _cells; |
| | | 81 | | |
| | 943 | 82 | | internal ReadOnlySpan<NavigationConnectionOverlayOperation> ConnectionSpan => _connections; |
| | | 83 | | |
| | 567 | 84 | | internal ReadOnlySpan<TraversalTransitionOverlayOperation> TransitionSpan => _transitions; |
| | | 85 | | |
| | | 86 | | /// <summary> |
| | | 87 | | /// Gets a deterministic conservative byte count for admission of this submitted descriptor. |
| | | 88 | | /// It is not the retained runtime-overlay size. |
| | | 89 | | /// </summary> |
| | | 90 | | public long EstimatedDescriptorBytes { get; } |
| | | 91 | | |
| | | 92 | | private static void ValidateCells(NavigationCellOverlayOperation[] operations) |
| | | 93 | | { |
| | 35496 | 94 | | for (int i = 0; i < operations.Length; i++) |
| | | 95 | | { |
| | 17338 | 96 | | NavigationCell cell = operations[i].Cell; |
| | 17338 | 97 | | SwiftThrowHelper.ThrowIfArgument( |
| | 17338 | 98 | | operations[i].Kind == NavigationCellOverlayOperationKind.Set |
| | 17338 | 99 | | && (cell.Media == TraversalMedia.None |
| | 17338 | 100 | | || (cell.Media & ~NavigationCell.KnownMedia) != 0 |
| | 17338 | 101 | | || (cell.RequiredCapabilities & ~NavigationCell.KnownCapabilities) != 0 |
| | 17338 | 102 | | || cell.EnterCost < FixedMathSharp.Fixed64.Zero |
| | 17338 | 103 | | || cell.RadiusClearance < FixedMathSharp.Fixed64.Zero |
| | 17338 | 104 | | || cell.HeightClearance < FixedMathSharp.Fixed64.Zero |
| | 17338 | 105 | | || (cell.Flags & ~NavigationCell.KnownFlags) != 0), |
| | 17338 | 106 | | nameof(operations), |
| | 17338 | 107 | | "Cell Set requires one complete valid navigation-cell payload."); |
| | 17337 | 108 | | SwiftThrowHelper.ThrowIfArgument( |
| | 17337 | 109 | | i > 0 && operations[i - 1].Index.CompareTo(operations[i].Index) == 0, |
| | 17337 | 110 | | nameof(operations), |
| | 17337 | 111 | | "Cell overlay addresses must be unique."); |
| | | 112 | | } |
| | 410 | 113 | | } |
| | | 114 | | |
| | | 115 | | private static void ValidateConnections(NavigationConnectionOverlayOperation[] operations) |
| | | 116 | | { |
| | 9152 | 117 | | for (int i = 0; i < operations.Length; i++) |
| | | 118 | | { |
| | 4166 | 119 | | NavigationConnectionOverlayOperation operation = operations[i]; |
| | 4166 | 120 | | SwiftThrowHelper.ThrowIfArgument( |
| | 4166 | 121 | | string.IsNullOrWhiteSpace(operation.Id), |
| | 4166 | 122 | | nameof(operations), |
| | 4166 | 123 | | "Connection overlay ids cannot be empty."); |
| | 4166 | 124 | | if (operation.Kind == NavigationConnectionOverlayOperationKind.Upsert) |
| | | 125 | | { |
| | 4147 | 126 | | ValidateConnectionWitnesses(operation.Connection!, operations); |
| | | 127 | | } |
| | 4166 | 128 | | SwiftThrowHelper.ThrowIfArgument( |
| | 4166 | 129 | | i > 0 && string.Equals(operations[i - 1].Id, operations[i].Id, StringComparison.Ordinal), |
| | 4166 | 130 | | nameof(operations), |
| | 4166 | 131 | | "Connection overlay ids must be unique."); |
| | | 132 | | } |
| | 410 | 133 | | } |
| | | 134 | | |
| | | 135 | | private static void ValidateConnectionWitnesses( |
| | | 136 | | NavigationConnection connection, |
| | | 137 | | NavigationConnectionOverlayOperation[] operations) |
| | | 138 | | { |
| | 4147 | 139 | | var witnesses = new SwiftHashSet<NavigationCellAddress>(connection.Witnesses.Count); |
| | 8860 | 140 | | for (int witness = 0; witness < connection.Witnesses.Count; witness++) |
| | | 141 | | { |
| | 283 | 142 | | SwiftThrowHelper.ThrowIfArgument( |
| | 283 | 143 | | string.IsNullOrWhiteSpace(connection.Witnesses[witness].MapId), |
| | 283 | 144 | | nameof(operations), |
| | 283 | 145 | | "Connection witness map ids cannot be empty."); |
| | 283 | 146 | | SwiftThrowHelper.ThrowIfArgument( |
| | 283 | 147 | | !witnesses.Add(connection.Witnesses[witness]), |
| | 283 | 148 | | nameof(operations), |
| | 283 | 149 | | "Connection witnesses must be unique."); |
| | | 150 | | } |
| | 4147 | 151 | | } |
| | | 152 | | |
| | | 153 | | private static void ValidateTransitions(TraversalTransitionOverlayOperation[] operations) |
| | | 154 | | { |
| | 9062 | 155 | | for (int i = 0; i < operations.Length; i++) |
| | | 156 | | { |
| | 4121 | 157 | | SwiftThrowHelper.ThrowIfArgument( |
| | 4121 | 158 | | string.IsNullOrWhiteSpace(operations[i].Id), |
| | 4121 | 159 | | nameof(operations), |
| | 4121 | 160 | | "Transition overlay ids cannot be empty."); |
| | 4121 | 161 | | SwiftThrowHelper.ThrowIfArgument( |
| | 4121 | 162 | | i > 0 && string.Equals(operations[i - 1].Id, operations[i].Id, StringComparison.Ordinal), |
| | 4121 | 163 | | nameof(operations), |
| | 4121 | 164 | | "Transition overlay ids must be unique."); |
| | | 165 | | } |
| | 410 | 166 | | } |
| | | 167 | | |
| | | 168 | | private long EstimateDescriptorBytes() |
| | | 169 | | { |
| | 410 | 170 | | long bytes = NavigationByteCount.SaturatingAdd( |
| | 410 | 171 | | 32L, |
| | 410 | 172 | | (long)MapId.Length * sizeof(char)); |
| | 410 | 173 | | bytes = NavigationByteCount.SaturatingAdd(bytes, (long)_cells.Length * 64L); |
| | 410 | 174 | | bytes = NavigationByteCount.SaturatingAdd(bytes, EstimateIdOperations(_connections)); |
| | 410 | 175 | | return NavigationByteCount.SaturatingAdd(bytes, EstimateIdOperations(_transitions)); |
| | | 176 | | } |
| | | 177 | | |
| | 17325 | 178 | | internal static long EstimateRetainedPayload(NavigationCellOverlayOperation operation) => 64L; |
| | | 179 | | |
| | | 180 | | private static long EstimateIdOperations(NavigationConnectionOverlayOperation[] operations) |
| | | 181 | | { |
| | 410 | 182 | | long bytes = 0; |
| | 9152 | 183 | | for (int i = 0; i < operations.Length; i++) |
| | 4166 | 184 | | bytes = NavigationByteCount.SaturatingAdd(bytes, EstimateRetainedPayload(operations[i])); |
| | 410 | 185 | | return bytes; |
| | | 186 | | } |
| | | 187 | | |
| | | 188 | | private static long EstimateIdOperations(TraversalTransitionOverlayOperation[] operations) |
| | | 189 | | { |
| | 410 | 190 | | long bytes = 0; |
| | 9062 | 191 | | for (int i = 0; i < operations.Length; i++) |
| | 4121 | 192 | | bytes = NavigationByteCount.SaturatingAdd(bytes, EstimateRetainedPayload(operations[i])); |
| | 410 | 193 | | return bytes; |
| | | 194 | | } |
| | | 195 | | |
| | | 196 | | internal static long EstimateRetainedPayload(NavigationConnectionOverlayOperation operation) |
| | | 197 | | { |
| | 8352 | 198 | | long bytes = NavigationByteCount.SaturatingAdd( |
| | 8352 | 199 | | 96L, |
| | 8352 | 200 | | (long)operation.Id.Length * sizeof(char)); |
| | 8352 | 201 | | if (operation.Kind != NavigationConnectionOverlayOperationKind.Upsert) |
| | 37 | 202 | | return bytes; |
| | | 203 | | |
| | 8315 | 204 | | NavigationConnection connection = operation.Connection!; |
| | 8315 | 205 | | bytes = NavigationByteCount.SaturatingAdd(bytes, 96L); |
| | 8315 | 206 | | bytes = NavigationByteCount.SaturatingAdd( |
| | 8315 | 207 | | bytes, |
| | 8315 | 208 | | (long)connection.Destination.MapId.Length * sizeof(char)); |
| | 17794 | 209 | | for (int witness = 0; witness < connection.Witnesses.Count; witness++) |
| | | 210 | | { |
| | 582 | 211 | | bytes = NavigationByteCount.SaturatingAdd(bytes, 32L); |
| | 582 | 212 | | bytes = NavigationByteCount.SaturatingAdd( |
| | 582 | 213 | | bytes, |
| | 582 | 214 | | (long)connection.Witnesses[witness].MapId.Length * sizeof(char)); |
| | | 215 | | } |
| | 8315 | 216 | | return bytes; |
| | | 217 | | } |
| | | 218 | | |
| | | 219 | | internal static long EstimateRetainedPayload(TraversalTransitionOverlayOperation operation) |
| | | 220 | | { |
| | 8254 | 221 | | long bytes = NavigationByteCount.SaturatingAdd( |
| | 8254 | 222 | | 96L, |
| | 8254 | 223 | | (long)operation.Id.Length * sizeof(char)); |
| | 8254 | 224 | | if (operation.Kind == TraversalTransitionOverlayOperationKind.Upsert) |
| | | 225 | | { |
| | 8235 | 226 | | bytes = NavigationByteCount.SaturatingAdd(bytes, 64L); |
| | 8235 | 227 | | bytes = NavigationByteCount.SaturatingAdd( |
| | 8235 | 228 | | bytes, |
| | 8235 | 229 | | (long)operation.Transition.Destination.MapId.Length * sizeof(char)); |
| | | 230 | | } |
| | 8254 | 231 | | return bytes; |
| | | 232 | | } |
| | | 233 | | |
| | | 234 | | private sealed class CellOperationComparer : IComparer<NavigationCellOverlayOperation> |
| | | 235 | | { |
| | | 236 | | public int Compare(NavigationCellOverlayOperation left, NavigationCellOverlayOperation right) => |
| | 149754 | 237 | | left.Index.CompareTo(right.Index); |
| | | 238 | | } |
| | | 239 | | |
| | | 240 | | private sealed class ConnectionOperationComparer : IComparer<NavigationConnectionOverlayOperation> |
| | | 241 | | { |
| | | 242 | | public int Compare(NavigationConnectionOverlayOperation left, NavigationConnectionOverlayOperation right) => |
| | 28443 | 243 | | string.CompareOrdinal(left.Id, right.Id); |
| | | 244 | | } |
| | | 245 | | |
| | | 246 | | private sealed class TransitionOperationComparer : IComparer<TraversalTransitionOverlayOperation> |
| | | 247 | | { |
| | | 248 | | public int Compare(TraversalTransitionOverlayOperation left, TraversalTransitionOverlayOperation right) => |
| | 28448 | 249 | | string.CompareOrdinal(left.Id, right.Id); |
| | | 250 | | } |
| | | 251 | | } |