Skip to content

Commit 37db229

Browse files
Copilotpetesramek
andauthored
refactor: replace fixed lat/lon pair contract with N-value encoding abstraction
Agent-Logs-Url: https://github.com/petesramek/polyline-algorithm-csharp/sessions/3af24e5a-f856-4f0c-9ce5-b4d12c2364ab Co-authored-by: petesramek <2333452+petesramek@users.noreply.github.com>
1 parent a7b3318 commit 37db229

16 files changed

Lines changed: 308 additions & 195 deletions

File tree

benchmarks/PolylineAlgorithm.Benchmarks/PolylineDecoderBenchmark.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,32 +93,41 @@ 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 (double Latitude, double Longitude) CreateItem(ReadOnlyMemory<double> values) {
97+
ReadOnlySpan<double> span = values.Span;
98+
return (span[0], span[1]);
9899
}
99100

100101
protected override ReadOnlyMemory<char> GetReadOnlyMemory(in string polyline) {
101102
return polyline?.AsMemory() ?? Memory<char>.Empty;
102103
}
104+
105+
protected override int ValuesPerItem => 2;
103106
}
104107

105108
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);
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) {
111115
return polyline?.AsMemory() ?? Memory<char>.Empty;
112116
}
117+
118+
protected override int ValuesPerItem => 2;
113119
}
114120

115121
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);
122+
protected override (double Latitude, double Longitude) CreateItem(ReadOnlyMemory<double> values) {
123+
ReadOnlySpan<double> span = values.Span;
124+
return (span[0], span[1]);
118125
}
119126

120127
protected override ReadOnlyMemory<char> GetReadOnlyMemory(in ReadOnlyMemory<char> polyline) {
121128
return polyline;
122129
}
130+
131+
protected override int ValuesPerItem => 2;
123132
}
124133
}

benchmarks/PolylineAlgorithm.Benchmarks/PolylineEncoderBenchmark.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ public void PolylineEncoder_Encode_List() {
8686

8787
private sealed class StringPolylineEncoder : AbstractPolylineEncoder<(double Latitude, double Longitude), string> {
8888
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;
89+
protected override int ValuesPerItem => 2;
90+
protected override void GetValues((double Latitude, double Longitude) item, Span<double> values) {
91+
values[0] = item.Latitude;
92+
values[1] = item.Longitude;
93+
}
9194
}
9295
}

samples/PolylineAlgorithm.NetTopologySuite.Sample/NetTopologyPolylineDecoder.cs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,6 @@ namespace PolylineAlgorithm.NetTopologySuite.Sample;
1313
/// Polyline decoder using NetTopologySuite.
1414
/// </summary>
1515
internal sealed class NetTopologyPolylineDecoder : AbstractPolylineDecoder<string, Point> {
16-
/// <summary>
17-
/// Creates a NetTopologySuite point from latitude and longitude.
18-
/// </summary>
19-
/// <param name="latitude">Latitude value.</param>
20-
/// <param name="longitude">Longitude value.</param>
21-
/// <returns>Point instance.</returns>
22-
protected override Point CreateCoordinate(double latitude, double longitude) {
23-
// NetTopologySuite Point: x = longitude, y = latitude
24-
return new Point(longitude, latitude);
25-
}
26-
2716
/// <summary>
2817
/// Converts polyline string to read-only memory.
2918
/// </summary>
@@ -32,4 +21,21 @@ protected override Point CreateCoordinate(double latitude, double longitude) {
3221
protected override ReadOnlyMemory<char> GetReadOnlyMemory(in string polyline) {
3322
return polyline.AsMemory();
3423
}
24+
25+
/// <summary>
26+
/// Gets the number of values per point (latitude + longitude = 2).
27+
/// </summary>
28+
protected override int ValuesPerItem => 2;
29+
30+
/// <summary>
31+
/// Creates a NetTopologySuite Point from decoded values.
32+
/// </summary>
33+
/// <param name="values">Decoded values: values[0] = latitude, values[1] = longitude.</param>
34+
/// <returns>Point instance.</returns>
35+
protected override Point CreateItem(ReadOnlyMemory<double> values) {
36+
ReadOnlySpan<double> span = values.Span;
37+
38+
// NetTopologySuite Point: x = longitude (values[1]), y = latitude (values[0])
39+
return new Point(span[1], span[0]);
40+
}
3541
}

samples/PolylineAlgorithm.NetTopologySuite.Sample/NetTopologyPolylineEncoder.cs

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ 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.
@@ -26,26 +27,20 @@ protected override string CreatePolyline(ReadOnlyMemory<char> polyline) {
2627
}
2728

2829
/// <summary>
29-
/// Gets latitude from point.
30+
/// Gets the number of values per point (latitude + longitude = 2).
3031
/// </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-
}
32+
protected override int ValuesPerItem => 2;
3933

4034
/// <summary>
41-
/// Gets longitude from point.
35+
/// Extracts latitude and longitude from a NetTopologySuite Point into the provided span.
4236
/// </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">The point to extract values from.</param>
38+
/// <param name="values">The span to receive the values: values[0] = latitude (Y), values[1] = longitude (X).</param>
39+
protected override void GetValues(Point item, Span<double> values) {
40+
ArgumentNullException.ThrowIfNull(item);
41+
42+
// NetTopologySuite Point: Y = latitude, X = longitude
43+
values[0] = item.Y;
44+
values[1] = item.X;
5045
}
5146
}

src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
namespace PolylineAlgorithm.Abstraction;
1212

1313
/// <summary>
14-
/// Provides a base implementation for decoding encoded polyline strings into sequences of geographic coordinates.
14+
/// Provides a base implementation for decoding encoded polyline strings into sequences of items.
1515
/// </summary>
1616
/// <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.
17+
/// Derive from this class to implement a decoder for a specific polyline type. Override <see cref="GetReadOnlyMemory"/>,
18+
/// <see cref="ValuesPerItem"/>, and <see cref="CreateItem"/> to provide type-specific behavior.
1919
/// </remarks>
2020
/// <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>
21+
/// <typeparam name="TCoordinate">The type that represents a decoded item.</typeparam>
2222
public abstract class AbstractPolylineDecoder<TPolyline, TCoordinate> : IPolylineDecoder<TPolyline, TCoordinate> {
2323
private readonly ILogger<AbstractPolylineDecoder<TPolyline, TCoordinate>> _logger;
2424

@@ -64,7 +64,7 @@ protected AbstractPolylineDecoder(PolylineEncodingOptions options) {
6464
/// A <see cref="CancellationToken"/> that can be used to cancel the decoding operation.
6565
/// </param>
6666
/// <returns>
67-
/// An <see cref="IEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded latitude and longitude pairs.
67+
/// An <see cref="IEnumerable{T}"/> of <typeparamref name="TCoordinate"/> representing the decoded items.
6868
/// </returns>
6969
/// <exception cref="ArgumentNullException">
7070
/// Thrown when <paramref name="polyline"/> is <see langword="null"/>.
@@ -90,28 +90,37 @@ public IEnumerable<TCoordinate> Decode(TPolyline polyline, CancellationToken can
9090
ValidateSequence(sequence, _logger);
9191
ValidateFormat(sequence, _logger);
9292

93+
int valuesPerItem = ValuesPerItem;
94+
int[] accumulated = new int[valuesPerItem];
95+
double[] decodedValues = new double[valuesPerItem];
9396
int position = 0;
94-
int encodedLatitude = 0;
95-
int encodedLongitude = 0;
9697

9798
try {
9899
while (position < sequence.Length) {
99100
cancellationToken.ThrowIfCancellationRequested();
100101

101-
if (!PolylineEncoding.TryReadValue(ref encodedLatitude, sequence, ref position)
102-
|| !PolylineEncoding.TryReadValue(ref encodedLongitude, sequence, ref position)) {
102+
bool allRead = true;
103+
for (int v = 0; v < valuesPerItem; v++) {
104+
if (!PolylineEncoding.TryReadValue(ref accumulated[v], sequence, ref position)) {
105+
allRead = false;
106+
break;
107+
}
108+
}
109+
110+
if (!allRead) {
103111
_logger?.LogOperationFailedDebug(OperationName);
104112
_logger?.LogInvalidPolylineWarning(position);
105113

106114
ExceptionGuard.ThrowInvalidPolylineFormat(position);
107115
}
108116

109-
double decodedLatitude = PolylineEncoding.Denormalize(encodedLatitude, Options.Precision);
110-
double decodedLongitude = PolylineEncoding.Denormalize(encodedLongitude, Options.Precision);
117+
for (int v = 0; v < valuesPerItem; v++) {
118+
decodedValues[v] = PolylineEncoding.Denormalize(accumulated[v], Options.Precision);
119+
}
111120

112-
_logger?.LogDecodedCoordinateDebug(decodedLatitude, decodedLongitude, position);
121+
_logger?.LogDecodedItemDebug(valuesPerItem, position);
113122

114-
yield return CreateCoordinate(decodedLatitude, decodedLongitude);
123+
yield return CreateItem(decodedValues.AsMemory());
115124
}
116125
} finally {
117126
_logger?.LogOperationFinishedDebug(OperationName);
@@ -188,17 +197,24 @@ protected virtual void ValidateFormat(ReadOnlyMemory<char> sequence, ILogger? lo
188197
protected abstract ReadOnlyMemory<char> GetReadOnlyMemory(in TPolyline polyline);
189198

190199
/// <summary>
191-
/// Creates a <typeparamref name="TCoordinate"/> instance from the specified latitude and longitude values.
200+
/// Gets the number of values decoded per item from the polyline.
192201
/// </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.
202+
/// <remarks>
203+
/// Must be greater than zero. Each item in the decoded output is constructed from this many consecutive
204+
/// delta-decoded values. For a standard geographic coordinate pair (latitude + longitude), return <c>2</c>.
205+
/// </remarks>
206+
protected abstract int ValuesPerItem { get; }
207+
208+
/// <summary>
209+
/// Creates a <typeparamref name="TCoordinate"/> instance from the specified decoded values.
210+
/// </summary>
211+
/// <param name="values">
212+
/// A <see cref="ReadOnlyMemory{T}"/> of <see cref="double"/> containing the decoded values for this item.
213+
/// The length equals <see cref="ValuesPerItem"/>. Implementations should copy values out rather than store the memory.
198214
/// </param>
199215
/// <returns>
200-
/// A <typeparamref name="TCoordinate"/> instance representing the specified geographic coordinate.
216+
/// A <typeparamref name="TCoordinate"/> instance representing the decoded item.
201217
/// </returns>
202218
[MethodImpl(MethodImplOptions.AggressiveInlining)]
203-
protected abstract TCoordinate CreateCoordinate(double latitude, double longitude);
219+
protected abstract TCoordinate CreateItem(ReadOnlyMemory<double> values);
204220
}

src/PolylineAlgorithm/Abstraction/AbstractPolylineEncoder.cs

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ namespace PolylineAlgorithm.Abstraction;
1616
using System.Threading;
1717

1818
/// <summary>
19-
/// Provides a base implementation for encoding sequences of geographic coordinates into encoded polyline strings.
19+
/// Provides a base implementation for encoding sequences of items into encoded polyline strings.
2020
/// </summary>
2121
/// <remarks>
22-
/// Derive from this class to implement an encoder for a specific coordinate and polyline type. Override
23-
/// <see cref="GetLatitude"/>, <see cref="GetLongitude"/>, and <see cref="CreatePolyline"/> to provide type-specific behavior.
22+
/// Derive from this class to implement an encoder for a specific item and polyline type. Override
23+
/// <see cref="ValuesPerItem"/>, <see cref="GetValues"/>, and <see cref="CreatePolyline"/> to provide type-specific behavior.
2424
/// </remarks>
25-
/// <typeparam name="TCoordinate">The type that represents a geographic coordinate to encode.</typeparam>
25+
/// <typeparam name="TCoordinate">The type that represents an item to encode.</typeparam>
2626
/// <typeparam name="TPolyline">The type that represents the encoded polyline output.</typeparam>
2727
public abstract class AbstractPolylineEncoder<TCoordinate, TPolyline> : IPolylineEncoder<TCoordinate, TPolyline> {
2828
private readonly ILogger<AbstractPolylineEncoder<TCoordinate, TPolyline>> _logger;
@@ -88,41 +88,52 @@ public TPolyline Encode(ReadOnlySpan<TCoordinate> coordinates, CancellationToken
8888

8989
ValidateEmptyCoordinates(ref coordinates, _logger);
9090

91-
CoordinateDelta delta = new();
91+
int valuesPerItem = ValuesPerItem;
92+
93+
CoordinateDelta delta = new(valuesPerItem);
9294

9395
int position = 0;
9496
int consumed = 0;
95-
int length = GetMaxBufferLength(coordinates.Length);
97+
int length = GetMaxBufferLength(coordinates.Length, valuesPerItem);
9698

9799
char[]? temp = length <= Options.StackAllocLimit
98100
? null
99101
: ArrayPool<char>.Shared.Rent(length);
100102

101103
Span<char> buffer = temp is null ? stackalloc char[length] : temp.AsSpan(0, length);
104+
Span<double> doubleValues = stackalloc double[valuesPerItem];
105+
Span<int> intValues = stackalloc int[valuesPerItem];
102106

103107
string encodedResult;
104108

105109
try {
106110
for (var i = 0; i < coordinates.Length; i++) {
107111
cancellationToken.ThrowIfCancellationRequested();
108112

109-
delta
110-
.Next(
111-
PolylineEncoding.Normalize(GetLatitude(coordinates[i]), Options.Precision),
112-
PolylineEncoding.Normalize(GetLongitude(coordinates[i]), Options.Precision)
113-
);
113+
GetValues(coordinates[i], doubleValues);
114+
115+
for (int v = 0; v < valuesPerItem; v++) {
116+
intValues[v] = PolylineEncoding.Normalize(doubleValues[v], Options.Precision);
117+
}
118+
119+
delta.Next(intValues);
114120

115-
if (!PolylineEncoding.TryWriteValue(delta.Latitude, buffer, ref position)
116-
|| !PolylineEncoding.TryWriteValue(delta.Longitude, buffer, ref position)
117-
) {
121+
bool writeSucceeded = true;
122+
for (int v = 0; v < valuesPerItem; v++) {
123+
if (!PolylineEncoding.TryWriteValue(delta.Deltas[v], buffer, ref position)) {
124+
writeSucceeded = false;
125+
break;
126+
}
127+
}
128+
129+
if (!writeSucceeded) {
118130
// This shouldn't happen, but if it does, log the error and throw an exception.
119131
_logger
120132
.LogOperationFailedDebug(OperationName);
121133
_logger
122134
.LogCannotWriteValueToBufferWarning(position, consumed);
123135

124136
ExceptionGuard.ThrowCouldNotWriteEncodedValueToBuffer();
125-
126137
}
127138

128139
consumed++;
@@ -141,10 +152,11 @@ public TPolyline Encode(ReadOnlySpan<TCoordinate> coordinates, CancellationToken
141152
return CreatePolyline(encodedResult.AsMemory());
142153

143154
[MethodImpl(MethodImplOptions.AggressiveInlining)]
144-
static int GetMaxBufferLength(int count) {
155+
static int GetMaxBufferLength(int count, int valuesPerItem) {
145156
Debug.Assert(count > 0, "Count must be greater than zero.");
157+
Debug.Assert(valuesPerItem > 0, "ValuesPerItem must be greater than zero.");
146158

147-
int requestedBufferLength = count * 2 * Defaults.Polyline.Block.Length.Max;
159+
int requestedBufferLength = count * valuesPerItem * Defaults.Polyline.Block.Length.Max;
148160

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

@@ -175,23 +187,22 @@ static void ValidateEmptyCoordinates(ref ReadOnlySpan<TCoordinate> coordinates,
175187
protected abstract TPolyline CreatePolyline(ReadOnlyMemory<char> polyline);
176188

177189
/// <summary>
178-
/// Extracts the longitude value from the specified coordinate.
190+
/// Gets the number of values extracted from each <typeparamref name="TCoordinate"/> item during encoding.
179191
/// </summary>
180-
/// <param name="current">The coordinate from which to extract the longitude.</param>
181-
/// <returns>
182-
/// The longitude value as a <see cref="double"/>.
183-
/// </returns>
184-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
185-
protected abstract double GetLongitude(TCoordinate current);
192+
/// <remarks>
193+
/// Must be greater than zero. Each value is written as a separate delta-encoded block in the output polyline.
194+
/// For a standard geographic coordinate pair (latitude + longitude), return <c>2</c>.
195+
/// </remarks>
196+
protected abstract int ValuesPerItem { get; }
186197

187198
/// <summary>
188-
/// Extracts the latitude value from the specified coordinate.
199+
/// Extracts the encoded values from the specified item into the provided span.
189200
/// </summary>
190-
/// <param name="current">The coordinate from which to extract the latitude.</param>
191-
/// <returns>
192-
/// The latitude value as a <see cref="double"/>.
193-
/// </returns>
201+
/// <param name="item">The item from which to extract values.</param>
202+
/// <param name="values">
203+
/// A span that receives the extracted values. Its length equals <see cref="ValuesPerItem"/>.
204+
/// </param>
194205
[MethodImpl(MethodImplOptions.AggressiveInlining)]
195-
protected abstract double GetLatitude(TCoordinate current);
206+
protected abstract void GetValues(TCoordinate item, Span<double> values);
196207
}
197208

0 commit comments

Comments
 (0)