Skip to content

Commit 7aa34a9

Browse files
Copilotpetesramek
andauthored
feat: add PolylineEncodingOptions/PolylineDecodingOptions with IChunkedPolylineEncoder/Decoder for chunked encoding support
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/49b11574-fbb3-42e8-b1f2-8af7fc403268 Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
1 parent 446f8c3 commit 7aa34a9

11 files changed

Lines changed: 808 additions & 5 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// Copyright © Pete Sramek. All rights reserved.
3+
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace PolylineAlgorithm.Abstraction;
7+
8+
using System.Collections.Generic;
9+
using System.Threading;
10+
11+
/// <summary>
12+
/// Provides per-call options-based chunked (stateless-continuation) decoding without inheriting the
13+
/// covariant <see cref="IPolylineDecoder{TPolyline, TValue}"/>. Implement both interfaces on the
14+
/// concrete decoder class to support both the standard and chunked overloads.
15+
/// </summary>
16+
/// <typeparam name="TPolyline">The encoded polyline type.</typeparam>
17+
/// <typeparam name="TValue">The coordinate type.</typeparam>
18+
/// <remarks>
19+
/// Use this interface when you need to decode a polyline that was produced by chunked encoding.
20+
/// Pass <see cref="PolylineDecodingOptions{TValue}.Previous"/> set to the last coordinate of the
21+
/// preceding decoded chunk to seed the accumulated-delta state correctly.
22+
/// </remarks>
23+
public interface IChunkedPolylineDecoder<TPolyline, TValue> {
24+
/// <summary>
25+
/// Decodes an encoded <typeparamref name="TPolyline"/> into a sequence of coordinates, applying
26+
/// the per-call <paramref name="options"/> to control the accumulated-delta seed.
27+
/// </summary>
28+
/// <param name="polyline">The encoded polyline to decode. Must not be <see langword="null"/>.</param>
29+
/// <param name="options">
30+
/// Per-call options that control the starting accumulated-delta seed. Pass
31+
/// <see langword="null"/> or an instance with <see cref="PolylineDecodingOptions{TValue}.Previous"/>
32+
/// set to <see langword="null"/> to start from zero (the default behaviour).
33+
/// </param>
34+
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
35+
/// <returns>
36+
/// An <see cref="IEnumerable{T}"/> of <typeparamref name="TValue"/> representing the decoded
37+
/// coordinates.
38+
/// </returns>
39+
/// <exception cref="System.ArgumentNullException">
40+
/// Thrown when <paramref name="polyline"/> is <see langword="null"/>.
41+
/// </exception>
42+
/// <exception cref="InvalidPolylineException">
43+
/// Thrown when the polyline format is invalid or malformed.
44+
/// </exception>
45+
/// <exception cref="System.OperationCanceledException">
46+
/// Thrown when <paramref name="cancellationToken"/> is canceled.
47+
/// </exception>
48+
IEnumerable<TValue> Decode(
49+
TPolyline polyline,
50+
PolylineDecodingOptions<TValue>? options,
51+
CancellationToken cancellationToken);
52+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// Copyright © Pete Sramek. All rights reserved.
3+
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace PolylineAlgorithm.Abstraction;
7+
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Threading;
11+
12+
/// <summary>
13+
/// Extends <see cref="IPolylineEncoder{TValue, TPolyline}"/> with a per-call options overload that
14+
/// supports chunked (stateless-continuation) encoding.
15+
/// </summary>
16+
/// <typeparam name="TValue">The coordinate type.</typeparam>
17+
/// <typeparam name="TPolyline">The encoded polyline type.</typeparam>
18+
/// <remarks>
19+
/// Use this interface when you need to encode a large coordinate sequence in independent chunks that
20+
/// can be concatenated into a single valid polyline. Pass
21+
/// <see cref="PolylineEncodingOptions{TValue}.Previous"/> set to the last coordinate of the
22+
/// preceding chunk to seed the delta baseline correctly.
23+
/// </remarks>
24+
public interface IChunkedPolylineEncoder<TValue, TPolyline> : IPolylineEncoder<TValue, TPolyline> {
25+
/// <summary>
26+
/// Encodes a sequence of geographic coordinates into an encoded polyline, applying the per-call
27+
/// <paramref name="options"/> to control the delta baseline.
28+
/// </summary>
29+
/// <param name="coordinates">The collection of coordinates to encode.</param>
30+
/// <param name="options">
31+
/// Per-call options that control the starting delta baseline. Pass
32+
/// <see langword="null"/> or an instance with <see cref="PolylineEncodingOptions{TValue}.Previous"/>
33+
/// set to <see langword="null"/> to use the formatter's default baseline.
34+
/// </param>
35+
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
36+
/// <returns>
37+
/// An instance of <typeparamref name="TPolyline"/> representing the encoded coordinates.
38+
/// </returns>
39+
/// <exception cref="System.ArgumentException">Thrown when <paramref name="coordinates"/> is empty.</exception>
40+
/// <exception cref="System.OperationCanceledException">
41+
/// Thrown when <paramref name="cancellationToken"/> is canceled.
42+
/// </exception>
43+
TPolyline Encode(
44+
ReadOnlySpan<TValue> coordinates,
45+
PolylineEncodingOptions<TValue>? options,
46+
CancellationToken cancellationToken);
47+
}

src/PolylineAlgorithm/Extensions/PolylineDecoderExtensions.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,38 @@ public static IEnumerable<TValue> Decode<TValue>(this IPolylineDecoder<ReadOnlyM
9797

9898
return decoder.Decode(polyline.AsMemory());
9999
}
100+
101+
/// <summary>
102+
/// Decodes an encoded polyline string into a sequence of geographic coordinates, applying per-call
103+
/// <paramref name="options"/> to seed the accumulated-delta state.
104+
/// </summary>
105+
/// <typeparam name="TValue">The coordinate type returned by the decoder.</typeparam>
106+
/// <param name="decoder">The chunked decoder instance.</param>
107+
/// <param name="polyline">The encoded polyline string to decode.</param>
108+
/// <param name="options">
109+
/// Per-call options that control the accumulated-delta seed. Pass <see langword="null"/> to start
110+
/// from zero (the default behaviour).
111+
/// </param>
112+
/// <returns>
113+
/// An <see cref="IEnumerable{T}"/> of <typeparamref name="TValue"/> containing the decoded
114+
/// coordinate pairs.
115+
/// </returns>
116+
/// <exception cref="ArgumentNullException">
117+
/// Thrown when <paramref name="decoder"/> or <paramref name="polyline"/> is <see langword="null"/>.
118+
/// </exception>
119+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Null is verified before use via ExceptionGuard.ThrowArgumentNull, which is annotated [DoesNotReturn]. CA1062 does not recognise custom [DoesNotReturn] helpers as null guards.")]
120+
public static IEnumerable<TValue> Decode<TValue>(
121+
this IChunkedPolylineDecoder<string, TValue> decoder,
122+
string polyline,
123+
PolylineDecodingOptions<TValue>? options) {
124+
if (decoder is null) {
125+
ExceptionGuard.ThrowArgumentNull(nameof(decoder));
126+
}
127+
128+
if (polyline is null) {
129+
ExceptionGuard.ThrowArgumentNull(nameof(polyline));
130+
}
131+
132+
return decoder.Decode(polyline, options, CancellationToken.None);
133+
}
100134
}

src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,78 @@ public static TPolyline Encode<TCoordinate, TPolyline>(this IPolylineEncoder<TCo
8383

8484
return encoder.Encode(coordinates.AsSpan());
8585
}
86+
87+
/// <summary>
88+
/// Encodes a <see cref="List{T}"/> of <typeparamref name="TCoordinate"/> instances into an encoded
89+
/// polyline, applying per-call <paramref name="options"/> to control the delta baseline.
90+
/// </summary>
91+
/// <typeparam name="TCoordinate">The type that represents a geographic coordinate to encode.</typeparam>
92+
/// <typeparam name="TPolyline">The type that represents the encoded polyline output.</typeparam>
93+
/// <param name="encoder">The chunked encoder instance.</param>
94+
/// <param name="coordinates">The list of coordinates to encode.</param>
95+
/// <param name="options">
96+
/// Per-call options that control the starting delta baseline. Pass <see langword="null"/> to use
97+
/// the formatter's default baseline.
98+
/// </param>
99+
/// <returns>
100+
/// A <typeparamref name="TPolyline"/> representing the encoded polyline.
101+
/// </returns>
102+
/// <exception cref="ArgumentNullException">
103+
/// Thrown when <paramref name="encoder"/> or <paramref name="coordinates"/> is <see langword="null"/>.
104+
/// </exception>
105+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "We need a list as we do need to marshal it as span.")]
106+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "MA0016:Prefer using collection abstraction instead of implementation", Justification = "We need a list as we do need to marshal it as span.")]
107+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Null is verified before use via ExceptionGuard.ThrowArgumentNull, which is annotated [DoesNotReturn]. CA1062 does not recognise custom [DoesNotReturn] helpers as null guards.")]
108+
public static TPolyline Encode<TCoordinate, TPolyline>(
109+
this IChunkedPolylineEncoder<TCoordinate, TPolyline> encoder,
110+
List<TCoordinate> coordinates,
111+
PolylineEncodingOptions<TCoordinate>? options) {
112+
if (encoder is null) {
113+
ExceptionGuard.ThrowArgumentNull(nameof(encoder));
114+
}
115+
116+
if (coordinates is null) {
117+
ExceptionGuard.ThrowArgumentNull(nameof(coordinates));
118+
}
119+
120+
#if NET5_0_OR_GREATER
121+
return encoder.Encode(CollectionsMarshal.AsSpan(coordinates), options, CancellationToken.None);
122+
#else
123+
return encoder.Encode([.. coordinates], options, CancellationToken.None);
124+
#endif
125+
}
126+
127+
/// <summary>
128+
/// Encodes an array of <typeparamref name="TCoordinate"/> instances into an encoded polyline,
129+
/// applying per-call <paramref name="options"/> to control the delta baseline.
130+
/// </summary>
131+
/// <typeparam name="TCoordinate">The type that represents a geographic coordinate to encode.</typeparam>
132+
/// <typeparam name="TPolyline">The type that represents the encoded polyline output.</typeparam>
133+
/// <param name="encoder">The chunked encoder instance.</param>
134+
/// <param name="coordinates">The array of coordinates to encode.</param>
135+
/// <param name="options">
136+
/// Per-call options that control the starting delta baseline. Pass <see langword="null"/> to use
137+
/// the formatter's default baseline.
138+
/// </param>
139+
/// <returns>
140+
/// A <typeparamref name="TPolyline"/> representing the encoded polyline.
141+
/// </returns>
142+
/// <exception cref="ArgumentNullException">
143+
/// Thrown when <paramref name="encoder"/> or <paramref name="coordinates"/> is <see langword="null"/>.
144+
/// </exception>
145+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Null is verified before use via ExceptionGuard.ThrowArgumentNull, which is annotated [DoesNotReturn]. CA1062 does not recognise custom [DoesNotReturn] helpers as null guards.")]
146+
public static TPolyline Encode<TCoordinate, TPolyline>(
147+
this IChunkedPolylineEncoder<TCoordinate, TPolyline> encoder,
148+
TCoordinate[] coordinates,
149+
PolylineEncodingOptions<TCoordinate>? options) {
150+
if (encoder is null) {
151+
ExceptionGuard.ThrowArgumentNull(nameof(encoder));
152+
}
153+
154+
if (coordinates is null) {
155+
ExceptionGuard.ThrowArgumentNull(nameof(coordinates));
156+
}
157+
158+
return encoder.Encode(coordinates.AsSpan(), options, CancellationToken.None);
159+
}
86160
}

src/PolylineAlgorithm/PolylineDecoder.cs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ namespace PolylineAlgorithm;
2424
/// <see cref="IPolylineFormatter{TCoordinate, TPolyline}"/> to the constructor. The formatter handles
2525
/// all type-specific concerns; no subclassing is required.
2626
/// </remarks>
27-
public class PolylineDecoder<TPolyline, TCoordinate> : IPolylineDecoder<TPolyline, TCoordinate> {
27+
public class PolylineDecoder<TPolyline, TCoordinate> : IPolylineDecoder<TPolyline, TCoordinate>, IChunkedPolylineDecoder<TPolyline, TCoordinate> {
2828
private readonly IPolylineFormatter<TCoordinate, TPolyline> _formatter;
2929
private readonly ILogger<PolylineDecoder<TPolyline, TCoordinate>> _logger;
3030

@@ -114,4 +114,98 @@ public IEnumerable<TCoordinate> Decode(TPolyline polyline, CancellationToken can
114114
_logger.LogOperationFinishedDebug(OperationName);
115115
}
116116
}
117+
118+
/// <summary>
119+
/// Decodes an encoded <typeparamref name="TPolyline"/> into a sequence of
120+
/// <typeparamref name="TCoordinate"/> instances, applying per-call <paramref name="options"/> to
121+
/// seed the accumulated-delta state. Use this overload to decode polylines that were produced by
122+
/// chunked encoding.
123+
/// </summary>
124+
/// <param name="polyline">The encoded polyline to decode. Must not be <see langword="null"/>.</param>
125+
/// <param name="options">
126+
/// Per-call options that control the accumulated-delta seed. Pass <see langword="null"/> or an
127+
/// instance with <see cref="PolylineDecodingOptions{TCoordinate}.Previous"/> set to
128+
/// <see langword="null"/> to start from zero (same as calling
129+
/// <see cref="Decode(TPolyline, CancellationToken)"/>).
130+
/// </param>
131+
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
132+
/// <returns>
133+
/// An <see cref="IEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded
134+
/// coordinates.
135+
/// </returns>
136+
/// <exception cref="ArgumentNullException">
137+
/// Thrown when <paramref name="polyline"/> is <see langword="null"/>.
138+
/// </exception>
139+
/// <exception cref="InvalidPolylineException">
140+
/// Thrown when the polyline format is invalid or malformed.
141+
/// </exception>
142+
/// <exception cref="OperationCanceledException">
143+
/// Thrown when <paramref name="cancellationToken"/> is canceled during decoding.
144+
/// </exception>
145+
public IEnumerable<TCoordinate> Decode(
146+
TPolyline polyline,
147+
PolylineDecodingOptions<TCoordinate>? options,
148+
CancellationToken cancellationToken) {
149+
const string OperationName = nameof(Decode);
150+
151+
_logger.LogOperationStartedDebug(OperationName);
152+
153+
if (polyline is null) {
154+
_logger.LogNullArgumentWarning(nameof(polyline));
155+
ExceptionGuard.ThrowArgumentNull(nameof(polyline));
156+
}
157+
158+
ReadOnlyMemory<char> sequence = _formatter.Read(polyline);
159+
160+
if (sequence.Length < Defaults.Polyline.Block.Length.Min) {
161+
_logger.LogOperationFailedDebug(OperationName);
162+
_logger.LogPolylineCannotBeShorterThanWarning(sequence.Length, Defaults.Polyline.Block.Length.Min);
163+
ExceptionGuard.ThrowInvalidPolylineLength(sequence.Length, Defaults.Polyline.Block.Length.Min);
164+
}
165+
166+
try {
167+
PolylineEncoding.ValidateFormat(sequence.Span);
168+
} catch (ArgumentException ex) {
169+
_logger.LogInvalidPolylineFormatWarning(ex);
170+
throw;
171+
}
172+
173+
int width = _formatter.Width;
174+
long[] accumulated = new long[width];
175+
int position = 0;
176+
177+
SeedAccumulated(accumulated, options);
178+
179+
try {
180+
while (position < sequence.Length) {
181+
cancellationToken.ThrowIfCancellationRequested();
182+
183+
for (int j = 0; j < width; j++) {
184+
if (!PolylineEncoding.TryReadValue(ref accumulated[j], sequence, ref position)) {
185+
_logger.LogOperationFailedDebug(OperationName);
186+
_logger.LogInvalidPolylineWarning(position);
187+
ExceptionGuard.ThrowInvalidPolylineFormat(position);
188+
}
189+
}
190+
191+
yield return _formatter.CreateItem(accumulated.AsSpan());
192+
}
193+
} finally {
194+
_logger.LogOperationFinishedDebug(OperationName);
195+
}
196+
}
197+
198+
private void SeedAccumulated(long[] accumulated, PolylineDecodingOptions<TCoordinate>? options) {
199+
if (options is not { HasPrevious: true }) {
200+
return;
201+
}
202+
203+
int width = _formatter.Width;
204+
long[] scaled = new long[width];
205+
_formatter.GetValues(options.Previous, scaled.AsSpan());
206+
207+
for (int j = 0; j < width; j++) {
208+
accumulated[j] = scaled[j] - _formatter.GetBaseline(j);
209+
}
210+
}
117211
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// Copyright © Pete Sramek. All rights reserved.
3+
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace PolylineAlgorithm;
7+
8+
/// <summary>
9+
/// Per-call options for a chunked decoding operation.
10+
/// </summary>
11+
/// <typeparam name="TCoordinate">The coordinate type understood by the formatter.</typeparam>
12+
/// <remarks>
13+
/// Pass an instance of this class to the chunked
14+
/// <see cref="Abstraction.IChunkedPolylineDecoder{TPolyline, TValue}.Decode"/> overload to control
15+
/// the accumulated-delta seed used at the start of each chunk. When <see cref="HasPrevious"/> is
16+
/// <see langword="false"/> zero-initialisation is used, which is the existing default behaviour.
17+
/// </remarks>
18+
public sealed class PolylineDecodingOptions<TCoordinate> {
19+
private readonly TCoordinate _previous;
20+
21+
/// <summary>
22+
/// Initializes a new instance of <see cref="PolylineDecodingOptions{TCoordinate}"/> with no
23+
/// previous coordinate (zero-initialised baseline will be used).
24+
/// </summary>
25+
public PolylineDecodingOptions() { }
26+
27+
/// <summary>
28+
/// Initializes a new instance of <see cref="PolylineDecodingOptions{TCoordinate}"/> with the
29+
/// specified previous coordinate used to seed the accumulated-delta state.
30+
/// </summary>
31+
/// <param name="previous">
32+
/// The last coordinate of the previous chunk, used to seed the accumulated-delta state.
33+
/// </param>
34+
public PolylineDecodingOptions(TCoordinate previous) {
35+
_previous = previous;
36+
HasPrevious = true;
37+
}
38+
39+
/// <summary>
40+
/// Gets a value indicating whether a previous coordinate has been supplied to seed the
41+
/// accumulated-delta state. When <see langword="false"/> zero-initialisation is used, which is
42+
/// the existing default.
43+
/// </summary>
44+
public bool HasPrevious { get; }
45+
46+
/// <summary>
47+
/// Gets the last coordinate of the previous chunk, used to seed the accumulated-delta state.
48+
/// Only meaningful when <see cref="HasPrevious"/> is <see langword="true"/>.
49+
/// </summary>
50+
public TCoordinate Previous => _previous;
51+
}

0 commit comments

Comments
 (0)