Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 134 additions & 1 deletion src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ namespace PolylineAlgorithm.Abstraction;
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.
Expand All @@ -20,7 +26,7 @@ namespace PolylineAlgorithm.Abstraction;
/// <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> {
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>
Expand Down Expand Up @@ -138,6 +144,133 @@ static void ValidateEmptySequence(ILogger<AbstractPolylineDecoder<TPolyline, TCo
/// </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>
Expand Down
91 changes: 90 additions & 1 deletion src/PolylineAlgorithm/Abstraction/AbstractPolylineEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ namespace PolylineAlgorithm.Abstraction;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// Provides functionality to encode a collection of geographic coordinates into an encoded polyline string.
Expand All @@ -22,7 +26,7 @@ namespace PolylineAlgorithm.Abstraction;
/// <remarks>
/// This abstract class serves as a base for specific polyline encoders, allowing customization of the encoding process.
/// </remarks>
public abstract class AbstractPolylineEncoder<TCoordinate, TPolyline> : IPolylineEncoder<TCoordinate, TPolyline> {
public abstract class AbstractPolylineEncoder<TCoordinate, TPolyline> : IPolylineEncoder<TCoordinate, TPolyline>, IAsyncPolylineEncoder<TCoordinate, TPolyline>, IPolylinePipeEncoder<TCoordinate> {
/// <summary>
/// Initializes a new instance of the <see cref="AbstractPolylineEncoder{TCoordinate, TPolyline}"/> class with default encoding options.
/// </summary>
Expand Down Expand Up @@ -217,5 +221,90 @@ static void ValidateBuffer(ILogger<AbstractPolylineDecoder<TPolyline, TCoordinat
/// </returns>

protected abstract double GetLatitude(TCoordinate current);

/// <summary>
/// Asynchronously encodes a sequence of geographic coordinates into an encoded polyline by collecting all
/// coordinates from the <see cref="IAsyncEnumerable{T}"/> and then encoding them synchronously.
/// </summary>
/// <param name="coordinates">
/// The asynchronous collection of <typeparamref name="TCoordinate"/> instances to encode.
/// </param>
/// <param name="cancellationToken">
/// A <see cref="CancellationToken"/> to observe while collecting coordinates.
/// </param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> containing the encoded <typeparamref name="TPolyline"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="coordinates"/> is <see langword="null"/>.
/// </exception>
public async ValueTask<TPolyline> EncodeAsync(
IAsyncEnumerable<TCoordinate> coordinates,
CancellationToken cancellationToken) {

if (coordinates is null) {
throw new ArgumentNullException(nameof(coordinates));
}

var list = new List<TCoordinate>();

await foreach (TCoordinate coordinate in coordinates.WithCancellation(cancellationToken).ConfigureAwait(false)) {
list.Add(coordinate);
}

return Encode(list);
}

/// <summary>
/// Asynchronously encodes a sequence of geographic coordinates and writes the encoded polyline bytes directly
/// into <paramref name="writer"/> with zero intermediate allocations.
/// </summary>
/// <remarks>
/// Each coordinate pair is encoded directly into the <see cref="PipeWriter"/>'s buffer using
/// <see cref="PolylineEncoding.WriteValue(int, System.Buffers.IBufferWriter{byte})"/>,
/// avoiding intermediate string or character-array allocations. The writer is flushed periodically
/// but is not completed by this method.
/// </remarks>
/// <param name="coordinates">
/// The asynchronous collection of <typeparamref name="TCoordinate"/> instances to encode.
/// </param>
/// <param name="writer">
/// The <see cref="PipeWriter"/> to which the encoded bytes are written.
/// </param>
/// <param name="cancellationToken">
/// A <see cref="CancellationToken"/> to observe while iterating coordinates.
/// </param>
/// <returns>
/// A <see cref="ValueTask"/> representing the asynchronous encode-and-write operation.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="coordinates"/> or <paramref name="writer"/> is <see langword="null"/>.
/// </exception>
public async ValueTask EncodeAsync(
IAsyncEnumerable<TCoordinate> coordinates,
PipeWriter writer,
CancellationToken cancellationToken) {

if (coordinates is null) {
throw new ArgumentNullException(nameof(coordinates));
}

if (writer is null) {
throw new ArgumentNullException(nameof(writer));
}

CoordinateVariance variance = new();

await foreach (TCoordinate coordinate in coordinates.WithCancellation(cancellationToken).ConfigureAwait(false)) {
variance.Next(
PolylineEncoding.Normalize(GetLatitude(coordinate), CoordinateValueType.Latitude),
PolylineEncoding.Normalize(GetLongitude(coordinate), CoordinateValueType.Longitude));

PolylineEncoding.WriteValue(variance.Latitude, writer);
PolylineEncoding.WriteValue(variance.Longitude, writer);
}

await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
}
}

29 changes: 29 additions & 0 deletions src/PolylineAlgorithm/Abstraction/IAsyncPolylineDecoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// 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 System.Collections.Generic;
using System.Threading;

/// <summary>
/// Defines a contract for asynchronously decoding an encoded polyline into a sequence of geographic coordinates.
/// </summary>
public interface IAsyncPolylineDecoder<TPolyline, TCoordinate> {
/// <summary>
/// Asynchronously decodes the specified encoded polyline into a sequence of geographic coordinates.
/// </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 waiting for the task to complete.
/// </param>
/// <returns>
/// An <see cref="IAsyncEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded
/// latitude and longitude pairs, streamed asynchronously.
/// </returns>
IAsyncEnumerable<TCoordinate> DecodeAsync(TPolyline polyline, CancellationToken cancellationToken);
}
30 changes: 30 additions & 0 deletions src/PolylineAlgorithm/Abstraction/IAsyncPolylineEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// 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 System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// Defines a contract for asynchronously encoding a sequence of geographic coordinates into an encoded polyline.
/// </summary>
public interface IAsyncPolylineEncoder<TCoordinate, TPolyline> {
/// <summary>
/// Asynchronously encodes a sequence of geographic coordinates into an encoded polyline representation.
/// </summary>
/// <param name="coordinates">
/// The asynchronous collection of <typeparamref name="TCoordinate"/> instances to encode into a polyline.
/// </param>
/// <param name="cancellationToken">
/// A <see cref="CancellationToken"/> to observe while waiting for the task to complete.
/// </param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that represents the asynchronous operation, containing a
/// <typeparamref name="TPolyline"/> with the encoded polyline string that represents the input coordinates.
/// </returns>
ValueTask<TPolyline> EncodeAsync(IAsyncEnumerable<TCoordinate> coordinates, CancellationToken cancellationToken);
}
38 changes: 38 additions & 0 deletions src/PolylineAlgorithm/Abstraction/IPolylinePipeDecoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// 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 System.Collections.Generic;
using System.IO.Pipelines;
using System.Threading;

/// <summary>
/// Defines a contract for zero-allocation decoding of an encoded polyline streamed from a <see cref="PipeReader"/>
/// into a sequence of geographic coordinates.
/// </summary>
/// <remarks>
/// Implementations operate directly on pipe buffers to avoid intermediate string or character-array allocations,
/// making this interface well-suited for high-throughput or memory-constrained scenarios where the encoded
/// polyline arrives over a network stream or another <see cref="System.IO.Pipelines.IDuplexPipe"/> source.
/// </remarks>
public interface IPolylinePipeDecoder<TCoordinate> {
/// <summary>
/// Asynchronously decodes encoded polyline bytes read from <paramref name="reader"/> into a sequence of
/// geographic coordinates, operating with zero intermediate allocations.
/// </summary>
/// <param name="reader">
/// The <see cref="PipeReader"/> from which the encoded polyline bytes are consumed.
/// The reader is not completed by this method; the caller is responsible for its lifetime.
/// </param>
/// <param name="cancellationToken">
/// A <see cref="CancellationToken"/> to observe while waiting for the task to complete.
/// </param>
/// <returns>
/// An <see cref="IAsyncEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded
/// latitude and longitude pairs, streamed asynchronously as they become available from the pipe.
/// </returns>
IAsyncEnumerable<TCoordinate> DecodeAsync(PipeReader reader, CancellationToken cancellationToken);
}
Loading
Loading