-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathAbstractPolylineDecoder.cs
More file actions
287 lines (245 loc) · 11.7 KB
/
AbstractPolylineDecoder.cs
File metadata and controls
287 lines (245 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
//
// Copyright © Pete Sramek. All rights reserved.
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace PolylineAlgorithm.Abstraction;
using Microsoft.Extensions.Logging;
using PolylineAlgorithm;
using PolylineAlgorithm.Internal;
using PolylineAlgorithm.Internal.Logging;
using PolylineAlgorithm.Properties;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Decodes encoded polyline strings into sequences of geographic coordinates.
/// Implements the <see cref="IPolylineDecoder{TPolyline, TCoordinate}"/> interface.
/// </summary>
/// <remarks>
/// This abstract class provides a base implementation for decoding polylines, allowing subclasses to define how to handle specific polyline formats.
/// </remarks>
public abstract class AbstractPolylineDecoder<TPolyline, TCoordinate> : IPolylineDecoder<TPolyline, TCoordinate>, IAsyncPolylineDecoder<TPolyline, TCoordinate>, IPolylinePipeDecoder<TCoordinate> {
/// <summary>
/// Initializes a new instance of the <see cref="AbstractPolylineDecoder{TPolyline, TCoordinate}"/> class with default encoding options.
/// </summary>
protected AbstractPolylineDecoder()
: this(new PolylineEncodingOptions()) { }
/// <summary>
/// Initializes a new instance of the <see cref="AbstractPolylineDecoder{TPolyline, TCoordinate}"/> class with the specified encoding options.
/// </summary>
/// <param name="options">
/// The <see cref="PolylineEncodingOptions"/> to use for encoding operations.
/// </param>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="options"/> is <see langword="null" />
/// </exception>
protected AbstractPolylineDecoder(PolylineEncodingOptions options) {
Options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <summary>
/// Gets the encoding options used by this polyline encoder.
/// </summary>
public PolylineEncodingOptions Options { get; }
/// <summary>
/// Decodes an encoded <typeparamref name="TPolyline"/> into a sequence of <typeparamref name="TCoordinate"/> instances.
/// </summary>
/// <param name="polyline">
/// The <typeparamref name="TPolyline"/> instance containing the encoded polyline string to decode.
/// </param>
/// <returns>
/// An <see cref="IEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded latitude and longitude pairs.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="polyline"/> is <see langword="null"/>.
/// </exception>"
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="polyline"/> is empty.
/// </exception>
/// <exception cref="InvalidPolylineException">
/// Thrown when the polyline format is invalid or malformed at a specific position.
/// </exception>
public IEnumerable<TCoordinate> Decode(TPolyline polyline) {
var logger = Options
.LoggerFactory
.CreateLogger<AbstractPolylineDecoder<TPolyline, TCoordinate>>();
logger.
LogOperationStartedInfo(nameof(Decode));
ValidateNullPolyline(polyline, logger);
ReadOnlyMemory<char> sequence = GetReadOnlyMemory(polyline);
ValidateEmptySequence(logger, sequence);
int position = 0;
int latitude = 0;
int longitude = 0;
while (true) {
// Check if we have reached the end of the sequence
if (sequence.Length == position) {
break;
}
// Read the next value from the polyline encoding
if (!PolylineEncoding.TryReadValue(ref latitude, ref sequence, ref position)
|| !PolylineEncoding.TryReadValue(ref longitude, ref sequence, ref position)
) {
logger
.LogInvalidPolylineWarning(position);
logger.
LogOperationFailedInfo(nameof(Decode));
InvalidPolylineException.Throw(position);
}
yield return CreateCoordinate(PolylineEncoding.Denormalize(latitude, CoordinateValueType.Latitude), PolylineEncoding.Denormalize(longitude, CoordinateValueType.Longitude));
}
logger
.LogOperationFinishedInfo(nameof(Decode));
static void ValidateNullPolyline(TPolyline polyline, ILogger<AbstractPolylineDecoder<TPolyline, TCoordinate>> logger) {
if (polyline is null) {
logger
.LogNullArgumentWarning(nameof(polyline));
throw new ArgumentNullException(nameof(polyline));
}
}
static void ValidateEmptySequence(ILogger<AbstractPolylineDecoder<TPolyline, TCoordinate>> logger, ReadOnlyMemory<char> sequence) {
if (sequence.Length < Defaults.Polyline.Block.Length.Min) {
logger
.LogPolylineCannotBeShorterThanWarning(nameof(sequence), sequence.Length, Defaults.Polyline.Block.Length.Min);
logger.
LogOperationFailedInfo(nameof(Decode));
throw new ArgumentException(string.Format(ExceptionMessageResource.PolylineCannotBeShorterThanExceptionMessage, sequence.Length), nameof(polyline));
}
}
}
/// <summary>
/// Converts the provided polyline instance into a <see cref="ReadOnlyMemory{T}"/> for decoding.
/// </summary>
/// <param name="polyline">
/// The <typeparamref name="TPolyline"/> instance containing the encoded polyline data to decode.
/// </param>
/// <returns>
/// A <see cref="ReadOnlyMemory{T}"/> representing the encoded polyline data.
/// </returns>
protected abstract ReadOnlyMemory<char> GetReadOnlyMemory(TPolyline polyline);
/// <summary>
/// Asynchronously decodes the specified encoded polyline into a sequence of geographic coordinates by
/// iterating the synchronous <see cref="Decode"/> implementation and checking the cancellation token
/// between each yielded coordinate.
/// </summary>
/// <param name="polyline">
/// The <typeparamref name="TPolyline"/> instance containing the encoded polyline string to decode.
/// </param>
/// <param name="cancellationToken">
/// A <see cref="CancellationToken"/> to observe while iterating.
/// </param>
/// <returns>
/// An <see cref="IAsyncEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded
/// latitude and longitude pairs.
/// </returns>
public async IAsyncEnumerable<TCoordinate> DecodeAsync(
TPolyline polyline,
[EnumeratorCancellation] CancellationToken cancellationToken) {
foreach (TCoordinate coordinate in Decode(polyline)) {
cancellationToken.ThrowIfCancellationRequested();
yield return coordinate;
}
}
/// <summary>
/// Asynchronously decodes encoded polyline bytes read from <paramref name="reader"/> into a sequence of
/// geographic coordinates with zero intermediate allocations.
/// </summary>
/// <remarks>
/// The method processes the pipe in chunks using <see cref="SequenceReader{T}"/> to handle multi-segment
/// <see cref="ReadOnlySequence{T}"/> buffers transparently. The pipe reader is not completed by this method.
/// </remarks>
/// <param name="reader">
/// The <see cref="PipeReader"/> from which the encoded polyline bytes are consumed.
/// </param>
/// <param name="cancellationToken">
/// A <see cref="CancellationToken"/> to observe while waiting for data from the pipe.
/// </param>
/// <returns>
/// An <see cref="IAsyncEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded
/// latitude and longitude pairs.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="reader"/> is <see langword="null"/>.
/// </exception>
public async IAsyncEnumerable<TCoordinate> DecodeAsync(
PipeReader reader,
[EnumeratorCancellation] CancellationToken cancellationToken) {
if (reader is null) {
throw new ArgumentNullException(nameof(reader));
}
int latitude = 0;
int longitude = 0;
bool firstRead = true;
while (true) {
ReadResult result = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
ReadOnlySequence<byte> buffer = result.Buffer;
if (firstRead && buffer.IsEmpty && result.IsCompleted) {
throw new ArgumentException(
string.Format(ExceptionMessageResource.PolylineCannotBeShorterThanExceptionMessage, 0),
nameof(reader));
}
firstRead = false;
// Process the buffer synchronously so that the SequenceReader<byte> (a ref struct) never lives
// across a yield boundary.
var decoded = new List<TCoordinate>();
(SequencePosition consumed, latitude, longitude) = ProcessPipeBuffer(buffer, latitude, longitude, decoded);
foreach (TCoordinate coordinate in decoded) {
yield return coordinate;
}
// Tell the pipe how far we have consumed and examined.
reader.AdvanceTo(consumed, buffer.End);
if (result.IsCompleted) {
break;
}
}
}
/// <summary>
/// Synchronously processes a <see cref="ReadOnlySequence{T}"/> pipe buffer, decoding as many complete
/// coordinate pairs as possible and returning the updated variance state and the consumed position.
/// <see cref="System.Buffers.SequenceReader{T}"/> is used here because this method is not an async iterator
/// and therefore the ref-struct constraint does not apply.
/// </summary>
private (SequencePosition consumed, int latitude, int longitude) ProcessPipeBuffer(
ReadOnlySequence<byte> buffer,
int latitude,
int longitude,
List<TCoordinate> results) {
var sequenceReader = new SequenceReader<byte>(buffer);
SequencePosition consumed = buffer.Start;
while (!sequenceReader.End) {
// Save state before attempting to decode a coordinate pair so we can roll back if only
// part of the pair is available in the current buffer.
SequencePosition pairStart = sequenceReader.Position;
int savedLatitude = latitude;
int savedLongitude = longitude;
if (!PolylineEncoding.TryReadValue(ref latitude, ref sequenceReader)
|| !PolylineEncoding.TryReadValue(ref longitude, ref sequenceReader)) {
latitude = savedLatitude;
longitude = savedLongitude;
break;
}
consumed = sequenceReader.Position;
results.Add(CreateCoordinate(
PolylineEncoding.Denormalize(latitude, CoordinateValueType.Latitude),
PolylineEncoding.Denormalize(longitude, CoordinateValueType.Longitude)));
}
return (consumed, latitude, longitude);
}
/// <summary>
/// Creates a coordinate instance from the given latitude and longitude values.
/// </summary>
/// <param name="latitude">
/// The latitude value.
/// </param>
/// <param name="longitude">
/// The longitude value.
/// </param>
/// <returns>
/// A coordinate instance of type <typeparamref name="TCoordinate"/>.
/// </returns>
protected abstract TCoordinate CreateCoordinate(double latitude, double longitude);
}