Skip to content

Commit a94f7c7

Browse files
Copilotpetesramek
andauthored
refactor: replace fixed lat/lon pair contract with N-value GetValues/CreateItem/ValuesPerItem
- AbstractPolylineEncoder: remove GetLatitude/GetLongitude, add ValuesPerItem + GetValues(item, Span<double>) - AbstractPolylineDecoder: remove CreateCoordinate(lat, lon), add ValuesPerItem + CreateItem(ReadOnlyMemory<double>) - CoordinateDelta: generalize from lat/lon pair to N-value array with Next(ReadOnlySpan<int>) and Deltas property - LogDebugExtensions: rename LogDecodedCoordinateDebug → LogDecodedValuesDebug(count, position) - Update all tests, samples, benchmarks, and utilities to new API - Declare new protected abstract members in PublicAPI.Unshipped.txt Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/b28a38c3-82b5-4aea-87ac-240d0503f978 Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
1 parent 0b16dcd commit a94f7c7

16 files changed

Lines changed: 349 additions & 206 deletions

File tree

benchmarks/PolylineAlgorithm.Benchmarks/PolylineDecoderBenchmark.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,10 @@ public void PolylineDecoder_Decode_Memory() {
9393
}
9494

9595
private sealed class StringPolylineDecoder : AbstractPolylineDecoder<string, (double Latitude, double Longitude)> {
96-
protected override (double Latitude, double Longitude) CreateCoordinate(double latitude, double longitude) {
97-
return (latitude, longitude);
96+
protected override int ValuesPerItem => 2;
97+
protected override (double Latitude, double Longitude) CreateItem(ReadOnlyMemory<double> values) {
98+
ReadOnlySpan<double> span = values.Span;
99+
return (span[0], span[1]);
98100
}
99101

100102
protected override ReadOnlyMemory<char> GetReadOnlyMemory(in string polyline) {
@@ -103,8 +105,10 @@ protected override ReadOnlyMemory<char> GetReadOnlyMemory(in string polyline) {
103105
}
104106

105107
private sealed class CharArrayPolylineDecoder : AbstractPolylineDecoder<char[], (double Latitude, double Longitude)> {
106-
protected override (double Latitude, double Longitude) CreateCoordinate(double latitude, double longitude) {
107-
return (latitude, longitude);
108+
protected override int ValuesPerItem => 2;
109+
protected override (double Latitude, double Longitude) CreateItem(ReadOnlyMemory<double> values) {
110+
ReadOnlySpan<double> span = values.Span;
111+
return (span[0], span[1]);
108112
}
109113

110114
protected override ReadOnlyMemory<char> GetReadOnlyMemory(in char[] polyline) {
@@ -113,8 +117,10 @@ protected override ReadOnlyMemory<char> GetReadOnlyMemory(in char[] polyline) {
113117
}
114118

115119
private sealed class MemoryCharPolylineDecoder : AbstractPolylineDecoder<ReadOnlyMemory<char>, (double Latitude, double Longitude)> {
116-
protected override (double Latitude, double Longitude) CreateCoordinate(double latitude, double longitude) {
117-
return (latitude, longitude);
120+
protected override int ValuesPerItem => 2;
121+
protected override (double Latitude, double Longitude) CreateItem(ReadOnlyMemory<double> values) {
122+
ReadOnlySpan<double> span = values.Span;
123+
return (span[0], span[1]);
118124
}
119125

120126
protected override ReadOnlyMemory<char> GetReadOnlyMemory(in ReadOnlyMemory<char> polyline) {

benchmarks/PolylineAlgorithm.Benchmarks/PolylineEncoderBenchmark.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,11 @@ public void PolylineEncoder_Encode_List() {
8585
}
8686

8787
private sealed class StringPolylineEncoder : AbstractPolylineEncoder<(double Latitude, double Longitude), string> {
88+
protected override int ValuesPerItem => 2;
8889
protected override string CreatePolyline(ReadOnlyMemory<char> polyline) => polyline.ToString();
89-
protected override double GetLatitude((double Latitude, double Longitude) current) => current.Latitude;
90-
protected override double GetLongitude((double Latitude, double Longitude) current) => current.Longitude;
90+
protected override void GetValues((double Latitude, double Longitude) item, Span<double> destination) {
91+
destination[0] = item.Latitude;
92+
destination[1] = item.Longitude;
93+
}
9194
}
9295
}

samples/PolylineAlgorithm.NetTopologySuite.Sample/NetTopologyPolylineDecoder.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@ namespace PolylineAlgorithm.NetTopologySuite.Sample;
1414
/// </summary>
1515
internal sealed class NetTopologyPolylineDecoder : AbstractPolylineDecoder<string, Point> {
1616
/// <summary>
17-
/// Creates a NetTopologySuite point from latitude and longitude.
17+
/// Gets the number of encoded values per item: latitude (index 0) and longitude (index 1).
1818
/// </summary>
19-
/// <param name="latitude">Latitude value.</param>
20-
/// <param name="longitude">Longitude value.</param>
19+
protected override int ValuesPerItem => 2;
20+
21+
/// <summary>
22+
/// Creates a NetTopologySuite point from the decoded values.
23+
/// </summary>
24+
/// <param name="values">
25+
/// A memory region containing two values: index 0 is latitude, index 1 is longitude.
26+
/// </param>
2127
/// <returns>Point instance.</returns>
22-
protected override Point CreateCoordinate(double latitude, double longitude) {
28+
protected override Point CreateItem(ReadOnlyMemory<double> values) {
29+
ReadOnlySpan<double> span = values.Span;
2330
// NetTopologySuite Point: x = longitude, y = latitude
24-
return new Point(longitude, latitude);
31+
return new Point(span[1], span[0]);
2532
}
2633

2734
/// <summary>

samples/PolylineAlgorithm.NetTopologySuite.Sample/NetTopologyPolylineEncoder.cs

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ namespace PolylineAlgorithm.NetTopologySuite.Sample;
77

88
using global::NetTopologySuite.Geometries;
99
using PolylineAlgorithm.Abstraction;
10+
using System;
1011

1112
/// <summary>
1213
/// Polyline encoder using NetTopologySuite's Point type.
1314
/// </summary>
1415
internal sealed class NetTopologyPolylineEncoder : AbstractPolylineEncoder<Point, string> {
16+
/// <summary>
17+
/// Gets the number of values per item: latitude (index 0) and longitude (index 1).
18+
/// </summary>
19+
protected override int ValuesPerItem => 2;
20+
1521
/// <summary>
1622
/// Creates encoded polyline string from memory.
1723
/// </summary>
@@ -26,26 +32,15 @@ protected override string CreatePolyline(ReadOnlyMemory<char> polyline) {
2632
}
2733

2834
/// <summary>
29-
/// Gets latitude from point.
35+
/// Fills destination with latitude (index 0) and longitude (index 1) from the point.
3036
/// </summary>
31-
/// <param name="current">Point instance.</param>
32-
/// <returns>Latitude value.</returns>
33-
protected override double GetLatitude(Point current) {
34-
ArgumentNullException.ThrowIfNull(current);
35-
36-
// NetTopologySuite Point: Y = latitude
37-
return current.Y;
38-
}
39-
40-
/// <summary>
41-
/// Gets longitude from point.
42-
/// </summary>
43-
/// <param name="current">Point instance.</param>
44-
/// <returns>Longitude value.</returns>
45-
protected override double GetLongitude(Point current) {
46-
ArgumentNullException.ThrowIfNull(current);
47-
48-
// NetTopologySuite Point: X = longitude
49-
return current.X;
37+
/// <param name="item">Point instance.</param>
38+
/// <param name="destination">Span of length 2 to fill.</param>
39+
protected override void GetValues(Point item, Span<double> destination) {
40+
ArgumentNullException.ThrowIfNull(item);
41+
42+
// NetTopologySuite Point: Y = latitude, X = longitude
43+
destination[0] = item.Y;
44+
destination[1] = item.X;
5045
}
5146
}

src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,27 @@
66
using Microsoft.Extensions.Logging;
77
using PolylineAlgorithm.Internal;
88
using PolylineAlgorithm.Internal.Diagnostics;
9+
using System.Buffers;
910
using System.Runtime.CompilerServices;
1011

1112
namespace PolylineAlgorithm.Abstraction;
1213

1314
/// <summary>
14-
/// Provides a base implementation for decoding encoded polyline strings into sequences of geographic coordinates.
15+
/// Provides a base implementation for decoding encoded polyline strings into sequences of items.
1516
/// </summary>
1617
/// <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.
18+
/// Derive from this class to implement a decoder for a specific polyline type. Override
19+
/// <see cref="ValuesPerItem"/>, <see cref="GetReadOnlyMemory"/>, and <see cref="CreateItem"/> to provide
20+
/// type-specific behavior.
21+
/// <para>
22+
/// The polyline format encodes each item as a fixed-length run of <see cref="ValuesPerItem"/> delta-compressed
23+
/// values. All items in a single polyline must have the same number of values. For example, a 2D GPS decoder
24+
/// sets <see cref="ValuesPerItem"/> to 2 (latitude, longitude), while a 3D GPS decoder sets it to 3
25+
/// (latitude, longitude, altitude).
26+
/// </para>
1927
/// </remarks>
2028
/// <typeparam name="TPolyline">The type that represents the encoded polyline input.</typeparam>
21-
/// <typeparam name="TCoordinate">The type that represents a decoded geographic coordinate.</typeparam>
29+
/// <typeparam name="TCoordinate">The type that represents a decoded item.</typeparam>
2230
public abstract class AbstractPolylineDecoder<TPolyline, TCoordinate> : IPolylineDecoder<TPolyline, TCoordinate> {
2331
private readonly ILogger<AbstractPolylineDecoder<TPolyline, TCoordinate>> _logger;
2432

@@ -53,6 +61,16 @@ protected AbstractPolylineDecoder(PolylineEncodingOptions options) {
5361
/// </summary>
5462
public PolylineEncodingOptions Options { get; }
5563

64+
/// <summary>
65+
/// Gets the number of encoded values that make up a single decoded item.
66+
/// </summary>
67+
/// <remarks>
68+
/// Override this property to specify the arity of each item. For example, return <c>2</c> for
69+
/// latitude/longitude pairs, <c>3</c> for latitude/longitude/altitude triples, or any other count
70+
/// that matches the encoding scheme used to produce the polyline.
71+
/// </remarks>
72+
protected abstract int ValuesPerItem { get; }
73+
5674
/// <summary>
5775
/// Decodes an encoded <typeparamref name="TPolyline"/> into a sequence of <typeparamref name="TCoordinate"/> instances,
5876
/// with support for cancellation.
@@ -64,7 +82,7 @@ protected AbstractPolylineDecoder(PolylineEncodingOptions options) {
6482
/// A <see cref="CancellationToken"/> that can be used to cancel the decoding operation.
6583
/// </param>
6684
/// <returns>
67-
/// An <see cref="IEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded latitude and longitude pairs.
85+
/// An <see cref="IEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded items.
6886
/// </returns>
6987
/// <exception cref="ArgumentNullException">
7088
/// Thrown when <paramref name="polyline"/> is <see langword="null"/>.
@@ -90,30 +108,45 @@ public IEnumerable<TCoordinate> Decode(TPolyline polyline, CancellationToken can
90108
ValidateSequence(sequence, _logger);
91109
ValidateFormat(sequence, _logger);
92110

111+
int valuesPerItem = ValuesPerItem;
93112
int position = 0;
94-
int encodedLatitude = 0;
95-
int encodedLongitude = 0;
113+
114+
int[]? runningRent = ArrayPool<int>.Shared.Rent(valuesPerItem);
115+
// Zero-initialize so delta decoding starts from 0 for all dimensions.
116+
for (int j = 0; j < valuesPerItem; j++) {
117+
runningRent[j] = 0;
118+
}
96119

97120
try {
98121
while (position < sequence.Length) {
99122
cancellationToken.ThrowIfCancellationRequested();
100123

101-
if (!PolylineEncoding.TryReadValue(ref encodedLatitude, sequence, ref position)
102-
|| !PolylineEncoding.TryReadValue(ref encodedLongitude, sequence, ref position)) {
124+
bool allRead = true;
125+
for (int j = 0; j < valuesPerItem; j++) {
126+
if (!PolylineEncoding.TryReadValue(ref runningRent[j], sequence, ref position)) {
127+
allRead = false;
128+
break;
129+
}
130+
}
131+
132+
if (!allRead) {
103133
_logger?.LogOperationFailedDebug(OperationName);
104134
_logger?.LogInvalidPolylineWarning(position);
105135

106136
ExceptionGuard.ThrowInvalidPolylineFormat(position);
107137
}
108138

109-
double decodedLatitude = PolylineEncoding.Denormalize(encodedLatitude, Options.Precision);
110-
double decodedLongitude = PolylineEncoding.Denormalize(encodedLongitude, Options.Precision);
139+
double[] decoded = new double[valuesPerItem];
140+
for (int j = 0; j < valuesPerItem; j++) {
141+
decoded[j] = PolylineEncoding.Denormalize(runningRent[j], Options.Precision);
142+
}
111143

112-
_logger?.LogDecodedCoordinateDebug(decodedLatitude, decodedLongitude, position);
144+
_logger?.LogDecodedValuesDebug(valuesPerItem, position);
113145

114-
yield return CreateCoordinate(decodedLatitude, decodedLongitude);
146+
yield return CreateItem(decoded.AsMemory());
115147
}
116148
} finally {
149+
ArrayPool<int>.Shared.Return(runningRent!);
117150
_logger?.LogOperationFinishedDebug(OperationName);
118151
}
119152
}
@@ -188,17 +221,19 @@ protected virtual void ValidateFormat(ReadOnlyMemory<char> sequence, ILogger? lo
188221
protected abstract ReadOnlyMemory<char> GetReadOnlyMemory(in TPolyline polyline);
189222

190223
/// <summary>
191-
/// Creates a <typeparamref name="TCoordinate"/> instance from the specified latitude and longitude values.
224+
/// Creates a <typeparamref name="TCoordinate"/> instance from the specified decoded values.
192225
/// </summary>
193-
/// <param name="latitude">
194-
/// The latitude component of the coordinate, in degrees.
195-
/// </param>
196-
/// <param name="longitude">
197-
/// The longitude component of the coordinate, in degrees.
226+
/// <param name="values">
227+
/// A <see cref="ReadOnlyMemory{T}"/> of <see cref="double"/> containing exactly <see cref="ValuesPerItem"/>
228+
/// decoded values for this item, in the same order they were encoded.
198229
/// </param>
199230
/// <returns>
200-
/// A <typeparamref name="TCoordinate"/> instance representing the specified geographic coordinate.
231+
/// A <typeparamref name="TCoordinate"/> instance representing the decoded item.
201232
/// </returns>
233+
/// <remarks>
234+
/// Implementations should read all required values from <paramref name="values"/> and construct the
235+
/// <typeparamref name="TCoordinate"/> immediately. The memory is valid only for the duration of this call.
236+
/// </remarks>
202237
[MethodImpl(MethodImplOptions.AggressiveInlining)]
203-
protected abstract TCoordinate CreateCoordinate(double latitude, double longitude);
238+
protected abstract TCoordinate CreateItem(ReadOnlyMemory<double> values);
204239
}

0 commit comments

Comments
 (0)