Skip to content

Commit 3ad54ff

Browse files
authored
Merge branch 'develop/1.0' into bugfix/incorrect-normalization
2 parents 7748529 + 730c70b commit 3ad54ff

20 files changed

Lines changed: 560 additions & 69 deletions

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,6 @@ dotnet_naming_style.underscore_camel_case.capitalization = camel_case
274274
# Public API analyzer
275275

276276
dotnet_public_api_analyzer.require_api_files = true
277+
278+
# RS0017: Symbol removed from public API - error severity (IDE guidance; build enforcement via WarningsAsErrors in .csproj)
279+
dotnet_diagnostic.RS0017.severity = error

.github/actions/source/compile/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ inputs:
1212
file-version:
1313
description: 'Assembly file version.'
1414
required: true
15-
treat-warnins-as-error:
15+
treat-warnings-as-error:
1616
description: 'Treat warnings as errors.'
1717
required: true
1818
project-path:

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ jobs:
130130
assembly-version: ${{ env.assembly-version }}
131131
assembly-informational-version: ${{ env.assembly-informational-version }}
132132
file-version: ${{ env.file-version }}
133-
treat-warnins-as-error: ${{ needs.workflow-variables.outputs.is-release }}
133+
treat-warnings-as-error: ${{ needs.workflow-variables.outputs.is-release }}
134134

135135
test:
136136
name: 'Run tests'

.github/workflows/pull-request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ jobs:
120120
assembly-version: ${{ env.assembly-version }}
121121
assembly-informational-version: ${{ env.assembly-informational-version }}
122122
file-version: ${{ env.file-version }}
123-
treat-warnins-as-error: ${{ needs.workflow-variables.outputs.is-release }}
123+
treat-warnings-as-error: ${{ needs.workflow-variables.outputs.is-release }}
124124

125125
test:
126126
name: 'Run tests'

.github/workflows/release.yml

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ jobs:
129129
assembly-version: ${{ env.assembly-version }}
130130
assembly-informational-version: ${{ env.assembly-informational-version }}
131131
file-version: ${{ env.file-version }}
132-
treat-warnins-as-error: ${{ needs.workflow-variables.outputs.is-release }}
132+
treat-warnings-as-error: ${{ needs.workflow-variables.outputs.is-release }}
133133

134134
test:
135135
name: 'Run tests'
@@ -169,6 +169,89 @@ jobs:
169169
- name: Write code coverage report summary
170170
run: cat ${{ steps.code-coverage-report.outputs.code-coverage-report-file }} >> $GITHUB_STEP_SUMMARY
171171

172+
update-api-unshipped:
173+
name: 'Update PublicAPI.Unshipped.txt'
174+
needs: [workflow-variables, validate-release]
175+
# Run even when build fails — the build may fail solely because a dropped preview API is still
176+
# listed in Unshipped.txt (RS0017). This job fixes exactly that, so it must not be gated on build.
177+
if: ${{ needs.workflow-variables.outputs.is-preview == 'true' && needs.validate-release.result == 'success' }}
178+
runs-on: ubuntu-latest
179+
steps:
180+
- name: 'Checkout ${{ github.ref }}'
181+
uses: actions/checkout@v6
182+
183+
- name: 'Setup .NET'
184+
uses: actions/setup-dotnet@v5
185+
with:
186+
dotnet-version: ${{ env.dotnet-sdk-version }}
187+
188+
- name: 'Snapshot PublicAPI.Shipped.txt'
189+
shell: bash
190+
run: cp src/PolylineAlgorithm/PublicAPI.Shipped.txt src/PolylineAlgorithm/PublicAPI.Shipped.txt.bak
191+
192+
- name: 'Sync PublicAPI.Unshipped.txt (add new + remove dropped preview APIs)'
193+
shell: bash
194+
run: dotnet format analyzers src/PolylineAlgorithm/PolylineAlgorithm.csproj --diagnostics RS0016 RS0017
195+
196+
- name: 'Guard against accidental shipped API removal'
197+
shell: bash
198+
run: |
199+
SHIPPED="src/PolylineAlgorithm/PublicAPI.Shipped.txt"
200+
if ! diff -q "$SHIPPED" "$SHIPPED.bak" > /dev/null 2>&1; then
201+
echo "::error::Breaking change detected — a shipped API was removed from PublicAPI.Shipped.txt."
202+
echo "::error::This requires a major version bump. Reverting the unintended change."
203+
cp "$SHIPPED.bak" "$SHIPPED"
204+
exit 1
205+
fi
206+
rm -f "$SHIPPED.bak"
207+
208+
- name: 'Configure git identity'
209+
uses: './.github/actions/git/configure-identity'
210+
211+
- name: 'Commit and push updated API files'
212+
shell: bash
213+
run: |
214+
git add src/PolylineAlgorithm/PublicAPI.Unshipped.txt
215+
git diff --staged --quiet || (
216+
git commit -m "Sync PublicAPI.Unshipped.txt (add new, remove dropped preview APIs)" &&
217+
git pull --rebase origin ${{ github.ref_name }} &&
218+
git push
219+
)
220+
221+
promote-api-files:
222+
name: 'Promote PublicAPI files (Unshipped -> Shipped)'
223+
needs: [workflow-variables, test, validate-release]
224+
if: ${{ needs.workflow-variables.outputs.is-release == 'true' }}
225+
runs-on: ubuntu-latest
226+
steps:
227+
- name: 'Checkout ${{ github.ref }}'
228+
uses: actions/checkout@v6
229+
230+
- name: 'Promote Unshipped.txt into Shipped.txt'
231+
shell: bash
232+
run: |
233+
UNSHIPPED="src/PolylineAlgorithm/PublicAPI.Unshipped.txt"
234+
SHIPPED="src/PolylineAlgorithm/PublicAPI.Shipped.txt"
235+
236+
# Append every non-blank, non-header line from Unshipped into Shipped
237+
tail -n +2 "$UNSHIPPED" | grep -v '^[[:space:]]*$' >> "$SHIPPED" || true
238+
239+
# Reset Unshipped to just the nullable-enable header (with BOM to match convention)
240+
printf '\xef\xbb\xbf#nullable enable\n' > "$UNSHIPPED"
241+
242+
- name: 'Configure git identity'
243+
uses: './.github/actions/git/configure-identity'
244+
245+
- name: 'Commit and push promoted API files'
246+
shell: bash
247+
run: |
248+
git add src/PolylineAlgorithm/PublicAPI.Shipped.txt src/PolylineAlgorithm/PublicAPI.Unshipped.txt
249+
git diff --staged --quiet || (
250+
git commit -m "Promote PublicAPI.Unshipped.txt into PublicAPI.Shipped.txt for release" &&
251+
git pull --rebase origin ${{ github.ref_name }} &&
252+
git push
253+
)
254+
172255
pack:
173256
name: 'Package binaries'
174257
needs: [versioning, build, test, validate-release]
@@ -219,8 +302,14 @@ jobs:
219302

220303
publish-package:
221304
name: 'Publish package'
222-
needs: [pack, validate-release, publish-documentation]
223-
if: ${{ always() && needs.pack.result == 'success' && needs.validate-release.result == 'success' && (needs.publish-documentation.result == 'success' || needs.publish-documentation.result == 'skipped') }}
305+
needs: [pack, validate-release, publish-documentation, update-api-unshipped, promote-api-files]
306+
if: |
307+
always() &&
308+
needs.pack.result == 'success' &&
309+
needs.validate-release.result == 'success' &&
310+
(needs.publish-documentation.result == 'success' || needs.publish-documentation.result == 'skipped') &&
311+
(needs.promote-api-files.result == 'success' || needs.promote-api-files.result == 'skipped') &&
312+
(needs.update-api-unshipped.result == 'success' || needs.update-api-unshipped.result == 'skipped')
224313
env:
225314
package-artifact-name: ${{ needs.pack.outputs.package-artifact-name }}
226315
runs-on: ubuntu-latest
@@ -277,6 +366,9 @@ jobs:
277366
needs: [workflow-variables, release, versioning]
278367
if: ${{ needs.workflow-variables.outputs.is-release == 'true' }}
279368
runs-on: ubuntu-latest
369+
permissions:
370+
pull-requests: write
371+
contents: read
280372
env:
281373
GH_TOKEN: ${{ github.token }}
282374
current-branch: ${{ github.ref_name }}
@@ -368,17 +460,10 @@ jobs:
368460
git checkout -b "${{ steps.resolve-support-branch.outputs.support-branch }}"
369461
git push --set-upstream origin "${{ steps.resolve-support-branch.outputs.support-branch }}"
370462
371-
- name: 'Lock support branch'
372-
if: ${{ steps.check-support-branch.outputs.support-branch-exists == 'false' }}
373-
uses: './.github/actions/github/branch-protection/lock'
374-
with:
375-
branch: ${{ steps.resolve-support-branch.outputs.support-branch }}
376-
token: ${{ secrets.GH_ADMIN_TOKEN }}
377-
378463
- name: 'Write support branch summary'
379464
run: |
380465
if [[ "${{ steps.check-support-branch.outputs.support-branch-exists }}" == "false" ]]; then
381-
echo "✅ Created and locked support branch **${{ steps.resolve-support-branch.outputs.support-branch }}**." >> $GITHUB_STEP_SUMMARY
466+
echo "✅ Created support branch **${{ steps.resolve-support-branch.outputs.support-branch }}**." >> $GITHUB_STEP_SUMMARY
382467
else
383468
echo "⏭️ Support branch **${{ steps.resolve-support-branch.outputs.support-branch }}** already exists." >> $GITHUB_STEP_SUMMARY
384469
fi

PolylineAlgorithm.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
</Folder>
88
<Folder Name="/samples/">
99
<Project Path="samples/PolylineAlgorithm.NetTopologySuite.Sample/PolylineAlgorithm.NetTopologySuite.Sample.csproj" Id="27a8fc47-0a3c-49c7-af5e-530514827785" />
10+
<Project Path="samples/PolylineAlgorithm.SensorData.Sample/PolylineAlgorithm.SensorData.Sample.csproj" />
1011
</Folder>
1112
<Folder Name="/src/">
1213
<Project Path="src/PolylineAlgorithm/PolylineAlgorithm.csproj" />

README.md

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# PolylineAlgorithm for .NET
22

3-
Lightweight .NET Standard 2.1 library implementing Google-compliant Encoded Polyline Algorithm with strong input validation, modern API patterns, and extensibility for custom coordinate types.
3+
[![NuGet](https://img.shields.io/nuget/v/PolylineAlgorithm)](https://www.nuget.org/packages/PolylineAlgorithm)
4+
[![Build](https://github.com/petesramek/polyline-algorithm-csharp/actions/workflows/build.yml/badge.svg)](https://github.com/petesramek/polyline-algorithm-csharp/actions/workflows/build.yml)
5+
[![License: MIT](https://img.shields.io/github/license/petesramek/polyline-algorithm-csharp)](./LICENSE)
6+
7+
Google's Encoded Polyline Algorithm compresses sequences of geographic coordinates into a compact ASCII string, widely used in mapping APIs. This library provides a fully compliant .NET implementation with extensible, type-safe encoding and decoding APIs.
48

59
## Table of Contents
610

@@ -17,15 +21,16 @@ Lightweight .NET Standard 2.1 library implementing Google-compliant Encoded Poly
1721
## Features
1822

1923
- Fully compliant Google Encoded Polyline Algorithm for .NET Standard 2.1+
20-
- Extensible encoding and decoding APIs for custom coordinate and polyline types (`IPolylineEncoder<TCoordinate, TPolyline>`, `IPolylineDecoder<TPolyline, TCoordinate>`, `AbstractPolylineEncoder<TCoordinate, TPolyline>`, `AbstractPolylineDecoder<TPolyline, TCoordinate>`)
21-
- Extension methods for encoding from `List<T>` and arrays (`PolylineEncoderExtensions`)
22-
- Robust input validation with descriptive exceptions for malformed/invalid data
24+
- Extensible APIs — implement your own encoder/decoder for any coordinate or polyline type
25+
- Robust input validation with descriptive exceptions for malformed or out-of-range data
2326
- Advanced configuration via `PolylineEncodingOptions` (precision, buffer size, logging)
24-
- Logging and diagnostic support for CI/CD and developer diagnostics via `Microsoft.Extensions.Logging`
25-
- Low-level utilities for normalization, validation, encoding and decoding via static `PolylineEncoding` class
27+
- Extension methods for encoding directly from `List<T>` and arrays
28+
- Logging and diagnostic support via `Microsoft.Extensions.Logging`
29+
- Low-level utilities for normalization, validation, and bit-level operations via static `PolylineEncoding` class
30+
- Thread-safe, stateless APIs
2631
- Thorough unit tests and benchmarks for correctness and performance
2732
- Auto-generated API documentation ([API Reference](https://petesramek.github.io/polyline-algorithm-csharp/))
28-
- Support for .NET Core, .NET 5+, Xamarin, Unity, Blazor, and other platforms supporting `netstandard2.1`
33+
- Supports .NET Core, .NET 5+, Xamarin, Unity, Blazor, and any platform targeting `netstandard2.1`
2934

3035
## Installation
3136

@@ -43,7 +48,19 @@ Install-Package PolylineAlgorithm
4348

4449
## Usage
4550

46-
The library provides abstract base classes to implement your own encoder and decoder for any coordinate and polyline type.
51+
The library provides abstract base classes to implement your own encoder and decoder for any coordinate and polyline type. Inherit from `AbstractPolylineEncoder` or `AbstractPolylineDecoder`, override the coordinate accessors, then call `Encode` or `Decode`.
52+
53+
### Quick Start
54+
55+
```csharp
56+
// 1. Implement a minimal encoder (see full example below)
57+
var encoder = new MyPolylineEncoder();
58+
string encoded = encoder.Encode(coordinates); // e.g. "yseiHoc_MwacOjnwM"
59+
60+
// 2. Implement a minimal decoder (see full example below)
61+
var decoder = new MyPolylineDecoder();
62+
IEnumerable<(double Latitude, double Longitude)> decoded = decoder.Decode(encoded);
63+
```
4764

4865
### Custom encoder and decoder
4966

@@ -56,23 +73,9 @@ using PolylineAlgorithm;
5673
using PolylineAlgorithm.Abstraction;
5774

5875
public sealed class MyPolylineEncoder : AbstractPolylineEncoder<(double Latitude, double Longitude), string> {
59-
public MyPolylineEncoder()
60-
: base() { }
61-
62-
public MyPolylineEncoder(PolylineEncodingOptions options)
63-
: base(options) { }
64-
65-
protected override double GetLatitude((double Latitude, double Longitude) coordinate) {
66-
return coordinate.Latitude;
67-
}
68-
69-
protected override double GetLongitude((double Latitude, double Longitude) coordinate) {
70-
return coordinate.Longitude;
71-
}
72-
73-
protected override string CreatePolyline(ReadOnlyMemory<char> polyline) {
74-
return polyline.ToString();
75-
}
76+
protected override double GetLatitude((double Latitude, double Longitude) coordinate) => coordinate.Latitude;
77+
protected override double GetLongitude((double Latitude, double Longitude) coordinate) => coordinate.Longitude;
78+
protected override string CreatePolyline(ReadOnlyMemory<char> polyline) => polyline.ToString();
7679
}
7780
```
7881

@@ -102,19 +105,8 @@ using PolylineAlgorithm;
102105
using PolylineAlgorithm.Abstraction;
103106

104107
public sealed class MyPolylineDecoder : AbstractPolylineDecoder<string, (double Latitude, double Longitude)> {
105-
public MyPolylineDecoder()
106-
: base() { }
107-
108-
public MyPolylineDecoder(PolylineEncodingOptions options)
109-
: base(options) { }
110-
111-
protected override (double Latitude, double Longitude) CreateCoordinate(double latitude, double longitude) {
112-
return (latitude, longitude);
113-
}
114-
115-
protected override ReadOnlyMemory<char> GetReadOnlyMemory(in string polyline) {
116-
return polyline.AsMemory();
117-
}
108+
protected override (double Latitude, double Longitude) CreateCoordinate(double latitude, double longitude) => (latitude, longitude);
109+
protected override ReadOnlyMemory<char> GetReadOnlyMemory(in string polyline) => polyline.AsMemory();
118110
}
119111
```
120112

@@ -136,8 +128,13 @@ Full API docs and guides (auto-generated from source) are available at [API Refe
136128

137129
## Benchmarks
138130

139-
- See `/benchmarks` in the repo for performance evaluation.
140-
- Contributors: Update benchmarks and document results for performance-impacting PRs.
131+
- See [`/benchmarks`](./benchmarks) in the repo for benchmark projects.
132+
- Run benchmarks with:
133+
```shell
134+
dotnet run --project benchmarks/PolylineAlgorithm.Benchmarks --configuration Release
135+
```
136+
- For guidance on writing and interpreting benchmarks, see [docs/benchmarks.md](./docs/benchmarks.md).
137+
- Contributors: update benchmarks and document results for performance-impacting PRs.
141138

142139
## FAQ
143140

@@ -173,13 +170,13 @@ A: Currently, only batch encode/decode is supported. For streaming scenarios, im
173170

174171
## Contributing
175172

176-
- Follow code style and PR instructions in [AGENTS.md](./AGENTS.md).
173+
- Follow code style and PR instructions in [CONTRIBUTING.md](./CONTRIBUTING.md).
177174
- Ensure all features are covered by tests and XML doc comments.
178175
- For questions or suggestions, open an issue and use the provided templates.
179176

180177
## Support
181178

182-
Have a question, bug, or feature request? [Open an issue!](https://github.com/petesramek/polyline-algorithm-csharp/issues)
179+
Have a question, bug, or feature request? [Open an issue](https://github.com/petesramek/polyline-algorithm-csharp/issues/new/choose) — bug report and feature request templates are available to guide you.
183180

184181
---
185182

samples/PolylineAlgorithm.NetTopologySuite.Sample/NetTopologyPolylineDecoder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace PolylineAlgorithm.NetTopologySuite.Sample;
1212
/// <summary>
1313
/// Polyline decoder using NetTopologySuite.
1414
/// </summary>
15-
public sealed class NetTopologyPolylineDecoder : AbstractPolylineDecoder<string, Point> {
15+
internal sealed class NetTopologyPolylineDecoder : AbstractPolylineDecoder<string, Point> {
1616
/// <summary>
1717
/// Creates a NetTopologySuite point from latitude and longitude.
1818
/// </summary>

samples/PolylineAlgorithm.NetTopologySuite.Sample/NetTopologyPolylineEncoder.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace PolylineAlgorithm.NetTopologySuite.Sample;
1111
/// <summary>
1212
/// Polyline encoder using NetTopologySuite's Point type.
1313
/// </summary>
14-
public sealed class NetTopologyPolylineEncoder : AbstractPolylineEncoder<Point, string> {
14+
internal sealed class NetTopologyPolylineEncoder : AbstractPolylineEncoder<Point, string> {
1515
/// <summary>
1616
/// Creates encoded polyline string from memory.
1717
/// </summary>
@@ -31,9 +31,7 @@ protected override string CreatePolyline(ReadOnlyMemory<char> polyline) {
3131
/// <param name="current">Point instance.</param>
3232
/// <returns>Latitude value.</returns>
3333
protected override double GetLatitude(Point current) {
34-
if (current is null) {
35-
throw new ArgumentNullException(nameof(current));
36-
}
34+
ArgumentNullException.ThrowIfNull(current);
3735

3836
// NetTopologySuite Point: Y = latitude
3937
return current.Y;
@@ -45,9 +43,7 @@ protected override double GetLatitude(Point current) {
4543
/// <param name="current">Point instance.</param>
4644
/// <returns>Longitude value.</returns>
4745
protected override double GetLongitude(Point current) {
48-
if (current is null) {
49-
throw new ArgumentNullException(nameof(current));
50-
}
46+
ArgumentNullException.ThrowIfNull(current);
5147

5248
// NetTopologySuite Point: X = longitude
5349
return current.X;

samples/PolylineAlgorithm.NetTopologySuite.Sample/PolylineAlgorithm.NetTopologySuite.Sample.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>netstandard2.1</TargetFramework>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
56
</PropertyGroup>
67

78
<PropertyGroup>

0 commit comments

Comments
 (0)