Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace PolylineAlgorithm.Benchmarks;
using BenchmarkDotNet.Engines;
using PolylineAlgorithm;
using PolylineAlgorithm.Utility;
using System.Collections.Generic;

/// <summary>
/// Benchmarks for <see cref="PolylineEncoder{TValue, TPolyline}"/>.
Expand All @@ -24,9 +25,9 @@ public class PolylineEncoderBenchmark {
public (double Latitude, double Longitude)[] Array { get; private set; }

/// <summary>
/// Coordinates as read-only memory.
/// Coordinates as list.
/// </summary>
public ReadOnlyMemory<(double Latitude, double Longitude)> Memory { get; private set; }
public List<(double Latitude, double Longitude)> List { get; private set; }

#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.

Expand All @@ -53,15 +54,15 @@ public class PolylineEncoderBenchmark {
[GlobalSetup]
public void SetupData() {
Array = [.. RandomValueProvider.GetCoordinates(CoordinatesCount)];
Memory = Array.AsMemory();
List = [.. RandomValueProvider.GetCoordinates(CoordinatesCount)];
}

/// <summary>
/// Benchmark: encode coordinates from span.
/// Benchmark: encode coordinates from list.
/// </summary>
[Benchmark]
public void PolylineEncoder_Encode_Span() {
var polyline = _encoder.Encode(Memory.Span);
public void PolylineEncoder_Encode_List() {
var polyline = _encoder.Encode(List);
_consumer.Consume(polyline);
}

Expand Down
16 changes: 11 additions & 5 deletions src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
//

namespace PolylineAlgorithm.Abstraction;

using System.Collections.Generic;
using System.Threading;

/// <summary>
/// Contract for encoding a sequence of values into an encoded polyline representation.
/// Implementations interpret the generic <typeparamref name="TValue"/> type and produce an encoded
Expand All @@ -30,18 +34,17 @@
/// - Value precision and rounding rules (for example 1e-5 for 5-decimal precision).
/// - Value ordering and whether altitude or additional dimensions are supported.
/// - Thread-safety guarantees: whether instances are safe to reuse concurrently or must be instantiated per-call.
/// - Implementations are encouraged to be memory-efficient; the API accepts a <see cref="ReadOnlySpan{T}"/>
/// to avoid forced allocations when callers already have contiguous memory.
/// </remarks>
public interface IPolylineEncoder<TValue, TPolyline> {

Check warning on line 38 in src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs

View workflow job for this annotation

GitHub Actions / Compile source code

Symbol 'PolylineAlgorithm.Abstraction.IPolylineEncoder<TValue, TPolyline>' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check warning on line 38 in src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs

View workflow job for this annotation

GitHub Actions / Package binaries

Symbol 'PolylineAlgorithm.Abstraction.IPolylineEncoder<TValue, TPolyline>' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check warning on line 38 in src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs

View workflow job for this annotation

GitHub Actions / Run tests

Symbol 'PolylineAlgorithm.Abstraction.IPolylineEncoder<TValue, TPolyline>' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)
/// <summary>
/// Encodes a sequence of values into an encoded polyline representation.
/// The order of values in <paramref name="coordinates"/> is preserved in the encoded result.
/// </summary>
/// <param name="coordinates">
/// The collection of <typeparamref name="TValue"/> instances to encode into a polyline.
/// The span may be empty; implementations should return an appropriate empty encoded representation
/// The sequence of <typeparamref name="TValue"/> instances to encode into a polyline.
/// The sequence may be empty; implementations should return an appropriate empty encoded representation
/// (for example an empty string or an empty memory slice) rather than <see langword="null"/>.
/// Must not be <see langword="null"/>.
/// </param>
/// <param name="cancellationToken">
/// A <see cref="System.Threading.CancellationToken"/> that can be used to cancel the encoding operation.
Expand Down Expand Up @@ -70,8 +73,11 @@
/// - For large input sequences, implementations may provide streaming or incremental encoders; those
/// variants can still implement this interface by materializing the final encoded result.
/// </remarks>
/// <exception cref="System.ArgumentNullException">
/// Thrown if <paramref name="coordinates"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// Thrown if the operation is canceled via <paramref name="cancellationToken"/>.
/// </exception>
TPolyline Encode(ReadOnlySpan<TValue> coordinates, PolylineEncodingOptions<TValue>? options = null, CancellationToken cancellationToken = default);
TPolyline Encode(IEnumerable<TValue> coordinates, PolylineEncodingOptions<TValue>? options = null, CancellationToken cancellationToken = default);

Check warning on line 82 in src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs

View workflow job for this annotation

GitHub Actions / Compile source code

Parameter 'options' has no matching param tag in the XML comment for 'IPolylineEncoder<TValue, TPolyline>.Encode(IEnumerable<TValue>, PolylineEncodingOptions<TValue>?, CancellationToken)' (but other parameters do)

Check warning on line 82 in src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs

View workflow job for this annotation

GitHub Actions / Package binaries

Parameter 'options' has no matching param tag in the XML comment for 'IPolylineEncoder<TValue, TPolyline>.Encode(IEnumerable<TValue>, PolylineEncodingOptions<TValue>?, CancellationToken)' (but other parameters do)

Check warning on line 82 in src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs

View workflow job for this annotation

GitHub Actions / Run tests

Parameter 'options' has no matching param tag in the XML comment for 'IPolylineEncoder<TValue, TPolyline>.Encode(IEnumerable<TValue>, PolylineEncodingOptions<TValue>?, CancellationToken)' (but other parameters do)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

using PolylineAlgorithm.Abstraction;
using PolylineAlgorithm.Internal.Diagnostics;
using System;
using System.Collections.Generic;
using System.Threading;

/// <summary>
/// Provides extension methods for the <see cref="IPolylineEncoder{TValue, TPolyline}"/> interface to facilitate encoding values into polylines.
Expand All @@ -27,14 +28,14 @@
/// <returns>
/// A <typeparamref name="TPolyline"/> instance representing the encoded polyline for the provided values.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <exception cref="System.ArgumentNullException">
/// Thrown when <paramref name="encoder"/> or <paramref name="values"/> is <see langword="null"/>.
/// </exception>
public static TPolyline Encode<TValue, TPolyline>(
this IPolylineEncoder<TValue, TPolyline> encoder,
TValue[] values,
PolylineEncodingOptions<TValue>? options = null,

Check warning on line 37 in src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs

View workflow job for this annotation

GitHub Actions / Compile source code

Parameter 'options' has no matching param tag in the XML comment for 'PolylineEncoderExtensions.Encode<TValue, TPolyline>(IPolylineEncoder<TValue, TPolyline>, TValue[], PolylineEncodingOptions<TValue>?, CancellationToken)' (but other parameters do)

Check warning on line 37 in src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs

View workflow job for this annotation

GitHub Actions / Package binaries

Parameter 'options' has no matching param tag in the XML comment for 'PolylineEncoderExtensions.Encode<TValue, TPolyline>(IPolylineEncoder<TValue, TPolyline>, TValue[], PolylineEncodingOptions<TValue>?, CancellationToken)' (but other parameters do)

Check warning on line 37 in src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs

View workflow job for this annotation

GitHub Actions / Run tests

Parameter 'options' has no matching param tag in the XML comment for 'PolylineEncoderExtensions.Encode<TValue, TPolyline>(IPolylineEncoder<TValue, TPolyline>, TValue[], PolylineEncodingOptions<TValue>?, CancellationToken)' (but other parameters do)
CancellationToken cancellationToken = default) {

Check warning on line 38 in src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs

View workflow job for this annotation

GitHub Actions / Compile source code

Parameter 'cancellationToken' has no matching param tag in the XML comment for 'PolylineEncoderExtensions.Encode<TValue, TPolyline>(IPolylineEncoder<TValue, TPolyline>, TValue[], PolylineEncodingOptions<TValue>?, CancellationToken)' (but other parameters do)

Check warning on line 38 in src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs

View workflow job for this annotation

GitHub Actions / Package binaries

Parameter 'cancellationToken' has no matching param tag in the XML comment for 'PolylineEncoderExtensions.Encode<TValue, TPolyline>(IPolylineEncoder<TValue, TPolyline>, TValue[], PolylineEncodingOptions<TValue>?, CancellationToken)' (but other parameters do)

Check warning on line 38 in src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs

View workflow job for this annotation

GitHub Actions / Run tests

Parameter 'cancellationToken' has no matching param tag in the XML comment for 'PolylineEncoderExtensions.Encode<TValue, TPolyline>(IPolylineEncoder<TValue, TPolyline>, TValue[], PolylineEncodingOptions<TValue>?, CancellationToken)' (but other parameters do)
if (encoder is null) {
ExceptionGuard.ThrowArgumentNull(nameof(encoder));
}
Expand All @@ -43,6 +44,6 @@
ExceptionGuard.ThrowArgumentNull(nameof(values));
}

return encoder.Encode(values.AsSpan(), options, cancellationToken);
return encoder.Encode(values, options, cancellationToken);
}
}
167 changes: 58 additions & 109 deletions src/PolylineAlgorithm/PolylineEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ namespace PolylineAlgorithm;

using Microsoft.Extensions.Logging;
using PolylineAlgorithm.Abstraction;
using PolylineAlgorithm.Internal;
using PolylineAlgorithm.Internal.Diagnostics;
using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Collections.Generic;
using System.Threading;

/// <summary>
Expand Down Expand Up @@ -60,73 +58,24 @@ public PolylineEncoder(PolylineOptions<TValue, TPolyline> options) {
/// <returns>
/// An instance of <typeparamref name="TPolyline"/> representing the encoded values.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="coordinates"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="coordinates"/> is empty.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Thrown when the internal encoding buffer cannot accommodate the encoded value.
/// </exception>
/// <exception cref="OperationCanceledException">
/// Thrown when <paramref name="cancellationToken"/> is canceled.
/// </exception>
public TPolyline Encode(ReadOnlySpan<TValue> coordinates, CancellationToken cancellationToken = default) {
const string OperationName = nameof(Encode);

_logger.LogOperationStartedDebug(OperationName);

Debug.Assert(coordinates.Length >= 0, "Count must be non-negative.");
[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.")]
public TPolyline Encode(IEnumerable<TValue> coordinates, CancellationToken cancellationToken = default) {
_logger.LogOperationStartedDebug(nameof(Encode));

if (coordinates.Length < 1) {
_logger.LogOperationFailedDebug(OperationName);
_logger.LogEmptyArgumentWarning(nameof(coordinates));
ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates));
if (coordinates is null) {
ExceptionGuard.ThrowArgumentNull(nameof(coordinates));
}

int width = _formatter.Width;
int length = GetMaxBufferLength(coordinates.Length, width);

char[]? temp = length <= _options.StackAllocLimit
? null
: ArrayPool<char>.Shared.Rent(length);

Span<char> buffer = temp is null ? stackalloc char[length] : temp.AsSpan(0, length);

int position = 0;
long[] previous = new long[width];
long[] values = new long[width];

SeedPrevious(previous, null);

try {
for (int i = 0; i < coordinates.Length; i++) {
cancellationToken.ThrowIfCancellationRequested();

_formatter.GetValues(coordinates[i], values.AsSpan());

for (int j = 0; j < width; j++) {
long current = values[j];
long delta = current - previous[j];
previous[j] = current;

if (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) {
_logger.LogOperationFailedDebug(OperationName);
_logger.LogCannotWriteValueToBufferWarning(position, i);
ExceptionGuard.ThrowCouldNotWriteEncodedValueToBuffer();
}
}
}

// Convert to string inside the try block so the buffer is still valid.
string encodedResult = buffer[..position].ToString();

_logger.LogOperationFinishedDebug(OperationName);

return _formatter.Write(encodedResult.AsMemory());
} finally {
if (temp is not null) {
ArrayPool<char>.Shared.Return(temp);
}
}
return EncodeCore(coordinates, null, cancellationToken);
}

/// <summary>
Expand All @@ -140,101 +89,101 @@ public TPolyline Encode(ReadOnlySpan<TValue> coordinates, CancellationToken canc
/// Per-call options that control the starting delta baseline. Pass <see langword="null"/> or an
/// instance with <see cref="PolylineEncodingOptions{TValue}.Previous"/> set to
/// <see langword="null"/> to use the formatter's default baseline (same as calling
/// <see cref="Encode(ReadOnlySpan{TValue}, CancellationToken)"/>).
/// <see cref="Encode(IEnumerable{TValue}, CancellationToken)"/>).
/// </param>
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
/// <returns>
/// An instance of <typeparamref name="TPolyline"/> representing the encoded values.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="coordinates"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="coordinates"/> is empty.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Thrown when the internal encoding buffer cannot accommodate the encoded value.
/// </exception>
/// <exception cref="OperationCanceledException">
/// Thrown when <paramref name="cancellationToken"/> is canceled.
/// </exception>
public TPolyline Encode(ReadOnlySpan<TValue> coordinates, PolylineEncodingOptions<TValue>? options, CancellationToken cancellationToken) {
const string OperationName = nameof(Encode);
[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.")]
public TPolyline Encode(IEnumerable<TValue> coordinates, PolylineEncodingOptions<TValue>? options, CancellationToken cancellationToken) {
_logger.LogOperationStartedDebug(nameof(Encode));

_logger.LogOperationStartedDebug(OperationName);
if (coordinates is null) {
ExceptionGuard.ThrowArgumentNull(nameof(coordinates));
}

Debug.Assert(coordinates.Length >= 0, "Count must be non-negative.");
return EncodeCore(coordinates, options, cancellationToken);
}

if (coordinates.Length < 1) {
_logger.LogOperationFailedDebug(OperationName);
_logger.LogEmptyArgumentWarning(nameof(coordinates));
ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates));
}
private TPolyline EncodeCore(IEnumerable<TValue> coordinates, PolylineEncodingOptions<TValue>? options, CancellationToken cancellationToken) {
const string OperationName = nameof(Encode);
const int MaxStackWidth = 8;

int width = _formatter.Width;
int length = GetMaxBufferLength(coordinates.Length, width);

char[]? temp = length <= _options.StackAllocLimit
? null
: ArrayPool<char>.Shared.Rent(length);
Span<long> previous = width <= MaxStackWidth ? stackalloc long[MaxStackWidth] : new long[width];
Span<long> values = width <= MaxStackWidth ? stackalloc long[MaxStackWidth] : new long[width];
previous = previous[..width];
values = values[..width];

Span<char> buffer = temp is null ? stackalloc char[length] : temp.AsSpan(0, length);
SeedPrevious(previous, options);

int position = 0;
long[] previous = new long[width];
long[] values = new long[width];
int stackLimit = _options.StackAllocLimit;
Span<char> buffer = stackalloc char[stackLimit];
char[]? rented = null;

SeedPrevious(previous, options);
int position = 0;
bool anyItemProcessed = false;

try {
for (int i = 0; i < coordinates.Length; i++) {
foreach (TValue item in coordinates) {
cancellationToken.ThrowIfCancellationRequested();

_formatter.GetValues(coordinates[i], values.AsSpan());
anyItemProcessed = true;
_formatter.GetValues(item, values);

for (int j = 0; j < width; j++) {
long current = values[j];
long delta = current - previous[j];
previous[j] = current;

if (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) {
_logger.LogOperationFailedDebug(OperationName);
_logger.LogCannotWriteValueToBufferWarning(position, i);
ExceptionGuard.ThrowCouldNotWriteEncodedValueToBuffer();
while (!PolylineEncoding.TryWriteValue(delta, buffer, ref position)) {
int newSize = rented is null ? stackLimit * 2 : rented.Length * 2;
char[] newRented = ArrayPool<char>.Shared.Rent(newSize);
buffer[..position].CopyTo(newRented);
if (rented is not null) {
ArrayPool<char>.Shared.Return(rented);
}
rented = newRented;
buffer = rented.AsSpan();
}
}
}

if (!anyItemProcessed) {
_logger.LogOperationFailedDebug(OperationName);
_logger.LogEmptyArgumentWarning(nameof(coordinates));
ExceptionGuard.ThrowArgumentCannotBeEmptyEnumerationMessage(nameof(coordinates));
}

string encodedResult = buffer[..position].ToString();

_logger.LogOperationFinishedDebug(OperationName);

return _formatter.Write(encodedResult.AsMemory());
} finally {
if (temp is not null) {
ArrayPool<char>.Shared.Return(temp);
if (rented is not null) {
ArrayPool<char>.Shared.Return(rented);
}
}
}

private void SeedPrevious(long[] previous, PolylineEncodingOptions<TValue>? options) {
int width = _formatter.Width;

private void SeedPrevious(Span<long> previous, PolylineEncodingOptions<TValue>? options) {
if (options is { HasPrevious: true }) {
_formatter.GetValues(options.Previous, previous.AsSpan());
_formatter.GetValues(options.Previous, previous);
} else {
for (int j = 0; j < width; j++) {
for (int j = 0; j < previous.Length; j++) {
previous[j] = _formatter.GetBaseline(j);
}
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetMaxBufferLength(int count, int valuesPerItem) {
Debug.Assert(count > 0, "Count must be greater than zero.");
Debug.Assert(valuesPerItem > 0, "Values per item must be greater than zero.");

int requestedBufferLength = count * valuesPerItem * Defaults.Polyline.Block.Length.Max;

Debug.Assert(requestedBufferLength > 0, "Requested buffer length must be greater than zero.");

return requestedBufferLength;
}
}
3 changes: 3 additions & 0 deletions src/PolylineAlgorithm/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#nullable enable
PolylineAlgorithm.PolylineEncoder<TValue, TPolyline>.Encode(System.Collections.Generic.IEnumerable<TValue>! coordinates, PolylineAlgorithm.PolylineEncodingOptions<TValue>? options, System.Threading.CancellationToken cancellationToken) -> TPolyline
PolylineAlgorithm.PolylineEncoder<TValue, TPolyline>.Encode(System.Collections.Generic.IEnumerable<TValue>! coordinates, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> TPolyline
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ public void Decode_With_Previous_Seeds_Accumulated_State() {
(double Lat, double Lon)[] chunkB = [(43.252, -126.453)];

string polylineB = encoder.Encode(
chunkB.AsSpan(),
chunkB,
new PolylineEncodingOptions<(double Lat, double Lon)>(chunkA[^1]),
CancellationToken.None);

Expand Down Expand Up @@ -269,9 +269,9 @@ public void Decode_RoundTrip_Reproduces_Full_Sequence() {
(double Lat, double Lon)[] chunkB = all[2..];

// Encode chunked
string polylineA = encoder.Encode(chunkA.AsSpan());
string polylineA = encoder.Encode(chunkA);
string polylineB = encoder.Encode(
chunkB.AsSpan(),
chunkB,
new PolylineEncodingOptions<(double Lat, double Lon)>(chunkA[^1]),
CancellationToken.None);

Expand Down
Loading
Loading