@@ -11,7 +11,13 @@ namespace PolylineAlgorithm.Abstraction;
1111using PolylineAlgorithm . Internal . Logging ;
1212using PolylineAlgorithm . Properties ;
1313using System ;
14+ using System . Buffers ;
15+ using System . Collections . Generic ;
1416using System . Diagnostics . CodeAnalysis ;
17+ using System . IO . Pipelines ;
18+ using System . Runtime . CompilerServices ;
19+ using System . Threading ;
20+ using System . Threading . Tasks ;
1521
1622/// <summary>
1723/// Decodes encoded polyline strings into sequences of geographic coordinates.
@@ -20,7 +26,7 @@ namespace PolylineAlgorithm.Abstraction;
2026/// <remarks>
2127/// This abstract class provides a base implementation for decoding polylines, allowing subclasses to define how to handle specific polyline formats.
2228/// </remarks>
23- public abstract class AbstractPolylineDecoder < TPolyline , TCoordinate > : IPolylineDecoder < TPolyline , TCoordinate > {
29+ public abstract class AbstractPolylineDecoder < TPolyline , TCoordinate > : IPolylineDecoder < TPolyline , TCoordinate > , IAsyncPolylineDecoder < TPolyline , TCoordinate > , IPolylinePipeDecoder < TCoordinate > {
2430 /// <summary>
2531 /// Initializes a new instance of the <see cref="AbstractPolylineDecoder{TPolyline, TCoordinate}"/> class with default encoding options.
2632 /// </summary>
@@ -138,6 +144,135 @@ static void ValidateEmptySequence(ILogger<AbstractPolylineDecoder<TPolyline, TCo
138144 /// </returns>
139145 protected abstract ReadOnlyMemory < char > GetReadOnlyMemory ( TPolyline polyline ) ;
140146
147+ /// <summary>
148+ /// Asynchronously decodes the specified encoded polyline into a sequence of geographic coordinates by
149+ /// iterating the synchronous <see cref="Decode"/> implementation and checking the cancellation token
150+ /// between each yielded coordinate.
151+ /// </summary>
152+ /// <param name="polyline">
153+ /// The <typeparamref name="TPolyline"/> instance containing the encoded polyline string to decode.
154+ /// </param>
155+ /// <param name="cancellationToken">
156+ /// A <see cref="CancellationToken"/> to observe while iterating.
157+ /// </param>
158+ /// <returns>
159+ /// An <see cref="IAsyncEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded
160+ /// latitude and longitude pairs.
161+ /// </returns>
162+ public async IAsyncEnumerable < TCoordinate > DecodeAsync (
163+ TPolyline polyline ,
164+ [ EnumeratorCancellation ] CancellationToken cancellationToken ) {
165+
166+ foreach ( TCoordinate coordinate in Decode ( polyline ) ) {
167+ cancellationToken . ThrowIfCancellationRequested ( ) ;
168+ yield return coordinate ;
169+ }
170+
171+ await Task . CompletedTask . ConfigureAwait ( false ) ;
172+ }
173+
174+ /// <summary>
175+ /// Asynchronously decodes encoded polyline bytes read from <paramref name="reader"/> into a sequence of
176+ /// geographic coordinates with zero intermediate allocations.
177+ /// </summary>
178+ /// <remarks>
179+ /// The method processes the pipe in chunks using <see cref="SequenceReader{T}"/> to handle multi-segment
180+ /// <see cref="ReadOnlySequence{T}"/> buffers transparently. The pipe reader is not completed by this method.
181+ /// </remarks>
182+ /// <param name="reader">
183+ /// The <see cref="PipeReader"/> from which the encoded polyline bytes are consumed.
184+ /// </param>
185+ /// <param name="cancellationToken">
186+ /// A <see cref="CancellationToken"/> to observe while waiting for data from the pipe.
187+ /// </param>
188+ /// <returns>
189+ /// An <see cref="IAsyncEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded
190+ /// latitude and longitude pairs.
191+ /// </returns>
192+ /// <exception cref="ArgumentNullException">
193+ /// Thrown when <paramref name="reader"/> is <see langword="null"/>.
194+ /// </exception>
195+ public async IAsyncEnumerable < TCoordinate > DecodeAsync (
196+ PipeReader reader ,
197+ [ EnumeratorCancellation ] CancellationToken cancellationToken ) {
198+
199+ if ( reader is null ) {
200+ throw new ArgumentNullException ( nameof ( reader ) ) ;
201+ }
202+
203+ int latitude = 0 ;
204+ int longitude = 0 ;
205+ bool firstRead = true ;
206+
207+ while ( true ) {
208+ ReadResult result = await reader . ReadAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
209+ ReadOnlySequence < byte > buffer = result . Buffer ;
210+
211+ if ( firstRead && buffer . IsEmpty && result . IsCompleted ) {
212+ throw new ArgumentException (
213+ string . Format ( ExceptionMessageResource . PolylineCannotBeShorterThanExceptionMessage , 0 ) ,
214+ nameof ( reader ) ) ;
215+ }
216+
217+ firstRead = false ;
218+
219+ // Process the buffer synchronously so that the SequenceReader<byte> (a ref struct) never lives
220+ // across a yield boundary.
221+ var decoded = new List < TCoordinate > ( ) ;
222+ ( SequencePosition consumed , latitude , longitude ) = ProcessPipeBuffer ( buffer , latitude , longitude , decoded ) ;
223+
224+ foreach ( TCoordinate coordinate in decoded ) {
225+ yield return coordinate ;
226+ }
227+
228+ // Tell the pipe how far we have consumed and examined.
229+ reader . AdvanceTo ( consumed , buffer . End ) ;
230+
231+ if ( result . IsCompleted ) {
232+ break ;
233+ }
234+ }
235+ }
236+
237+ /// <summary>
238+ /// Synchronously processes a <see cref="ReadOnlySequence{T}"/> pipe buffer, decoding as many complete
239+ /// coordinate pairs as possible and returning the updated variance state and the consumed position.
240+ /// <see cref="System.Buffers.SequenceReader{T}"/> is used here because this method is not an async iterator
241+ /// and therefore the ref-struct constraint does not apply.
242+ /// </summary>
243+ private ( SequencePosition consumed , int latitude , int longitude ) ProcessPipeBuffer (
244+ ReadOnlySequence < byte > buffer ,
245+ int latitude ,
246+ int longitude ,
247+ List < TCoordinate > results ) {
248+
249+ var sequenceReader = new SequenceReader < byte > ( buffer ) ;
250+ SequencePosition consumed = buffer . Start ;
251+
252+ while ( ! sequenceReader . End ) {
253+ // Save state before attempting to decode a coordinate pair so we can roll back if only
254+ // part of the pair is available in the current buffer.
255+ SequencePosition pairStart = sequenceReader . Position ;
256+ int savedLatitude = latitude ;
257+ int savedLongitude = longitude ;
258+
259+ if ( ! PolylineEncoding . TryReadValue ( ref latitude , ref sequenceReader )
260+ || ! PolylineEncoding . TryReadValue ( ref longitude , ref sequenceReader ) ) {
261+
262+ latitude = savedLatitude ;
263+ longitude = savedLongitude ;
264+ break ;
265+ }
266+
267+ consumed = sequenceReader . Position ;
268+ results . Add ( CreateCoordinate (
269+ PolylineEncoding . Denormalize ( latitude , CoordinateValueType . Latitude ) ,
270+ PolylineEncoding . Denormalize ( longitude , CoordinateValueType . Longitude ) ) ) ;
271+ }
272+
273+ return ( consumed , latitude , longitude ) ;
274+ }
275+
141276 /// <summary>
142277 /// Creates a coordinate instance from the given latitude and longitude values.
143278 /// </summary>
0 commit comments