Skip to content

Commit 7ea10cf

Browse files
Copilotpetesramek
andauthored
Add formatter interfaces, PolylineValueFormatter, PolylineFormatter static class, update base classes and options
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/66b5b819-3735-47b2-a2ec-372ae483e46b Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
1 parent 2ebebaf commit 7ea10cf

11 files changed

Lines changed: 662 additions & 134 deletions

src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs

Lines changed: 110 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,24 @@ namespace PolylineAlgorithm.Abstraction;
1414
/// Provides a base implementation for decoding encoded polyline strings into sequences of geographic coordinates.
1515
/// </summary>
1616
/// <remarks>
17-
/// Derive from this class to implement a decoder for a specific polyline type. Override <see cref="GetReadOnlyMemory"/>
18-
/// and <see cref="CreateCoordinate"/> to provide type-specific behavior.
17+
/// <para>
18+
/// <b>Formatter-based use (no subclassing required):</b>
19+
/// Supply a <see cref="PolylineOptions{TPolyline, TCoordinate}"/> via the
20+
/// <see cref="AbstractPolylineDecoder{TPolyline, TCoordinate}(PolylineOptions{TPolyline, TCoordinate})"/>
21+
/// constructor. The formatters handle all type-specific concerns; override nothing.
22+
/// </para>
23+
/// <para>
24+
/// <b>Legacy override-based use:</b>
25+
/// Derive from this class and override <see cref="GetReadOnlyMemory"/> and <see cref="CreateCoordinate"/>
26+
/// to provide type-specific behaviour. These overrides take priority over any registered formatter.
27+
/// </para>
1928
/// </remarks>
2029
/// <typeparam name="TPolyline">The type that represents the encoded polyline input.</typeparam>
2130
/// <typeparam name="TCoordinate">The type that represents a decoded geographic coordinate.</typeparam>
22-
public abstract class AbstractPolylineDecoder<TPolyline, TCoordinate> : IPolylineDecoder<TPolyline, TCoordinate> {
31+
public class AbstractPolylineDecoder<TPolyline, TCoordinate> : IPolylineDecoder<TPolyline, TCoordinate> {
2332
private readonly ILogger<AbstractPolylineDecoder<TPolyline, TCoordinate>> _logger;
33+
private readonly IPolylineValueFormatter<TCoordinate>? _valueFormatter;
34+
private readonly IPolylineFormatter<TPolyline>? _polylineFormatter;
2435

2536
/// <summary>
2637
/// Initializes a new instance of the <see cref="AbstractPolylineDecoder{TPolyline, TCoordinate}"/> class with default encoding options.
@@ -48,6 +59,35 @@ protected AbstractPolylineDecoder(PolylineEncodingOptions options) {
4859
.CreateLogger<AbstractPolylineDecoder<TPolyline, TCoordinate>>();
4960
}
5061

62+
/// <summary>
63+
/// Initializes a new instance of the <see cref="AbstractPolylineDecoder{TPolyline, TCoordinate}"/> class
64+
/// using the supplied <see cref="PolylineOptions{TCoordinate, TPolyline}"/>.
65+
/// </summary>
66+
/// <remarks>
67+
/// Use this constructor when you want formatter-driven decoding without subclassing.
68+
/// The <see cref="GetReadOnlyMemory"/> and <see cref="CreateCoordinate"/> hooks are not called;
69+
/// all type-specific logic is delegated to the formatters.
70+
/// </remarks>
71+
/// <param name="options">
72+
/// A <see cref="PolylineOptions{TCoordinate, TPolyline}"/> that carries both the value formatter and
73+
/// the polyline formatter together with the underlying <see cref="PolylineEncodingOptions"/>.
74+
/// </param>
75+
/// <exception cref="ArgumentNullException">
76+
/// Thrown when <paramref name="options"/> is <see langword="null"/>.
77+
/// </exception>
78+
public AbstractPolylineDecoder(PolylineOptions<TCoordinate, TPolyline> options) {
79+
if (options is null) {
80+
ExceptionGuard.ThrowArgumentNull(nameof(options));
81+
}
82+
83+
Options = options.Encoding;
84+
_polylineFormatter = options.PolylineFormatter;
85+
_valueFormatter = options.ValueFormatter;
86+
_logger = Options
87+
.LoggerFactory
88+
.CreateLogger<AbstractPolylineDecoder<TPolyline, TCoordinate>>();
89+
}
90+
5191
/// <summary>
5292
/// Gets the encoding options used by this polyline decoder.
5393
/// </summary>
@@ -78,43 +118,75 @@ protected AbstractPolylineDecoder(PolylineEncodingOptions options) {
78118
/// <exception cref="OperationCanceledException">
79119
/// Thrown when <paramref name="cancellationToken"/> is canceled during decoding.
80120
/// </exception>
121+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0051:Method is too long", Justification = "Method contains two path implementations.")]
81122
public IEnumerable<TCoordinate> Decode(TPolyline polyline, CancellationToken cancellationToken = default) {
82123
const string OperationName = nameof(Decode);
83124

84125
_logger?.LogOperationStartedDebug(OperationName);
85126

86127
ValidateNullPolyline(polyline, _logger);
87128

88-
ReadOnlyMemory<char> sequence = GetReadOnlyMemory(in polyline);
129+
ReadOnlyMemory<char> sequence = (_valueFormatter is not null && _polylineFormatter is not null)
130+
? _polylineFormatter.Read(polyline)
131+
: GetReadOnlyMemory(in polyline);
89132

90133
ValidateSequence(sequence, _logger);
91134
ValidateFormat(sequence, _logger);
92135

93136
int position = 0;
94-
int encodedLatitude = 0;
95-
int encodedLongitude = 0;
96137

97-
try {
98-
while (position < sequence.Length) {
99-
cancellationToken.ThrowIfCancellationRequested();
138+
if (_valueFormatter is not null && _polylineFormatter is not null) {
139+
int width = _valueFormatter.Width;
140+
int[] accumulated = new int[width];
141+
long[] longValues = new long[width];
142+
143+
try {
144+
while (position < sequence.Length) {
145+
cancellationToken.ThrowIfCancellationRequested();
100146

101-
if (!PolylineEncoding.TryReadValue(ref encodedLatitude, sequence, ref position)
102-
|| !PolylineEncoding.TryReadValue(ref encodedLongitude, sequence, ref position)) {
103-
_logger?.LogOperationFailedDebug(OperationName);
104-
_logger?.LogInvalidPolylineWarning(position);
147+
for (int j = 0; j < width; j++) {
148+
if (!PolylineEncoding.TryReadValue(ref accumulated[j], sequence, ref position)) {
149+
_logger?.LogOperationFailedDebug(OperationName);
150+
_logger?.LogInvalidPolylineWarning(position);
151+
ExceptionGuard.ThrowInvalidPolylineFormat(position);
152+
}
153+
}
105154

106-
ExceptionGuard.ThrowInvalidPolylineFormat(position);
155+
for (int j = 0; j < width; j++) {
156+
longValues[j] = accumulated[j];
157+
}
158+
159+
yield return _valueFormatter.CreateItem(longValues.AsSpan());
107160
}
161+
} finally {
162+
_logger?.LogOperationFinishedDebug(OperationName);
163+
}
164+
} else {
165+
int encodedLatitude = 0;
166+
int encodedLongitude = 0;
167+
168+
try {
169+
while (position < sequence.Length) {
170+
cancellationToken.ThrowIfCancellationRequested();
108171

109-
double decodedLatitude = PolylineEncoding.Denormalize(encodedLatitude, Options.Precision);
110-
double decodedLongitude = PolylineEncoding.Denormalize(encodedLongitude, Options.Precision);
172+
if (!PolylineEncoding.TryReadValue(ref encodedLatitude, sequence, ref position)
173+
|| !PolylineEncoding.TryReadValue(ref encodedLongitude, sequence, ref position)) {
174+
_logger?.LogOperationFailedDebug(OperationName);
175+
_logger?.LogInvalidPolylineWarning(position);
111176

112-
_logger?.LogDecodedCoordinateDebug(decodedLatitude, decodedLongitude, position);
177+
ExceptionGuard.ThrowInvalidPolylineFormat(position);
178+
}
113179

114-
yield return CreateCoordinate(decodedLatitude, decodedLongitude);
180+
double decodedLatitude = PolylineEncoding.Denormalize(encodedLatitude, Options.Precision);
181+
double decodedLongitude = PolylineEncoding.Denormalize(encodedLongitude, Options.Precision);
182+
183+
_logger?.LogDecodedCoordinateDebug(decodedLatitude, decodedLongitude, position);
184+
185+
yield return CreateCoordinate(decodedLatitude, decodedLongitude);
186+
}
187+
} finally {
188+
_logger?.LogOperationFinishedDebug(OperationName);
115189
}
116-
} finally {
117-
_logger?.LogOperationFinishedDebug(OperationName);
118190
}
119191
}
120192

@@ -184,8 +256,16 @@ protected virtual void ValidateFormat(ReadOnlyMemory<char> sequence, ILogger? lo
184256
/// <returns>
185257
/// A <see cref="ReadOnlyMemory{T}"/> of <see cref="char"/> representing the encoded polyline characters.
186258
/// </returns>
259+
/// <exception cref="NotSupportedException">
260+
/// Thrown by the default implementation when no polyline formatter is registered and the method
261+
/// has not been overridden in a derived class.
262+
/// </exception>
187263
[MethodImpl(MethodImplOptions.AggressiveInlining)]
188-
protected abstract ReadOnlyMemory<char> GetReadOnlyMemory(in TPolyline polyline);
264+
protected virtual ReadOnlyMemory<char> GetReadOnlyMemory(in TPolyline polyline) {
265+
throw new NotSupportedException(
266+
$"Override {nameof(GetReadOnlyMemory)} in a derived class, or provide a " +
267+
$"{nameof(PolylineOptions<TPolyline, TCoordinate>)} with a polyline formatter.");
268+
}
189269

190270
/// <summary>
191271
/// Creates a <typeparamref name="TCoordinate"/> instance from the specified latitude and longitude values.
@@ -199,6 +279,14 @@ protected virtual void ValidateFormat(ReadOnlyMemory<char> sequence, ILogger? lo
199279
/// <returns>
200280
/// A <typeparamref name="TCoordinate"/> instance representing the specified geographic coordinate.
201281
/// </returns>
282+
/// <exception cref="NotSupportedException">
283+
/// Thrown by the default implementation when no value formatter is registered and the method
284+
/// has not been overridden in a derived class.
285+
/// </exception>
202286
[MethodImpl(MethodImplOptions.AggressiveInlining)]
203-
protected abstract TCoordinate CreateCoordinate(double latitude, double longitude);
287+
protected virtual TCoordinate CreateCoordinate(double latitude, double longitude) {
288+
throw new NotSupportedException(
289+
$"Override {nameof(CreateCoordinate)} in a derived class, or provide a " +
290+
$"{nameof(PolylineOptions<TPolyline, TCoordinate>)} with a value formatter.");
291+
}
204292
}

0 commit comments

Comments
 (0)