From 69df3e854c6ac9db9df8a238a21e343525d66408 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 4 Apr 2026 09:43:12 +0200
Subject: [PATCH 01/13] Release: v1.0 = core rewrite, CI/CD, docs, and test
suite overhaul (#166)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Overview
Promotes `preview/1.0` to `release/1.0` in preparation for the 1.0
release. This batch brings in a full rewrite of the library's core,
docs, CI/CD pipeline, and test suite accumulated since the last preview
snapshot.
## What's included
### 🏗️ CI/CD & Workflows
- **New workflows**: `bump-version`, `promote-branch` with source-branch
enforcement, auto merge-to-main on release
- **Branch protection**: new `lock`/`unlock` composite actions;
`preview/**` and `release/**` branches are now automatically locked
after creation
- **Fix**: removed invalid `administration` permission from
`release.yml` that caused workflow parse failures (#158)
- **Fix**: corrected pull-request-finding logic in promote workflow
- **Docs publishing**: URL-aware version switcher via `docs-versioning`
template; `versions.json` manifest; reworked `generate-docs` job (#157)
### 📚 Documentation
- New `/docs` developer guides: local development, testing conventions,
benchmarks, composite actions catalogue, CI workflow reference, branch
strategy, versioning pipeline, API doc generation, extensibility guide
(#155)
- Updated API reference YAMLs for version 1.0
- Fixed grammar, TOC typo, and outdated API references in docs
- Updated `CONTRIBUTING.md` with links to all new docs articles
- Flushed `PublicAPI.Unshipped.txt`
### 🔧 Source (library)
- Refactored internal structure: `CoordinateDelta`,
`CoordinateVariance`, `Pow10`, `Defaults`, `ExceptionGuard`, logging
helpers
- Updated `PolylineEncoder`, `PolylineDecoder`, `PolylineEncoding`,
`PolylineEncodingOptions`, `PolylineEncodingOptionsBuilder`
- Public API cleanup; `.editorconfig` updated (primary constructors
disabled)
### ✅ Tests
- Reorganized test files into proper namespace folders (`Abstraction/`,
`Extensions/`, `Internal/`)
- Added new test classes for `CoordinateDelta`, `CoordinateVariance`,
`Pow10`, `ExceptionGuard`, `LogDebugExtensions`, `LogWarningExtensions`,
`InvalidPolylineException`, `PolylineEncodingOptionsBuilder`, and
encoder/decoder extensions
- Removed stale/duplicate test files
### 🛠️ Build & Config
- Cleaned up `PolylineAlgorithm.slnx`, `Directory.Build.props`,
benchmarks config
- Updated `.gitignore` to exclude dynamically generated
`api-reference/_docs/`
- Misc cleanup (removed junk files, updated `AGENTS.md`)
---
.editorconfig | 276 ++
.gitattributes | 53 +-
.github/ISSUE_TEMPLATE/bug_report.md | 38 +
.github/ISSUE_TEMPLATE/feature_request.md | 20 +
.../documentation/docfx-build/action.yml | 45 +
.../documentation/docfx-metadata/action.yml | 65 +
.github/actions/git/push-changes/action.yml | 99 +
.../github/branch-protection/lock/action.yml | 40 +
.../branch-protection/unlock/action.yml | 21 +
.../actions/github/create-release/action.yml | 40 +
.../github/write-file-to-summary/action.yml | 25 +
.../actions/nuget/publish-package/action.yml | 61 +
.github/actions/source/compile/action.yml | 74 +
.github/actions/source/format/action.yml | 73 +
.../actions/testing/code-coverage/action.yml | 73 +
.../actions/testing/test-report/action.yml | 60 +
.github/actions/testing/test/action.yml | 84 +
.../versioning/extract-version/action.yml | 51 +
.../versioning/format-version/action.yml | 66 +
.github/dependabot.yml | 14 +
.github/workflows/build.yml | 258 ++
.github/workflows/bump-version.yml | 156 +
.github/workflows/codeql.yml | 97 -
.github/workflows/dotnet.yml | 28 -
.github/workflows/promote-branch.yml | 227 ++
.github/workflows/publish-documentation.yml | 174 +
.github/workflows/pull-request.yml | 258 ++
.github/workflows/release.yml | 368 +++
.github/workflows/static.yml | 44 -
.gitignore | 13 +-
AGENTS.md | 93 +
CONTRIBUTING.md | 50 +
Directory.Build.props | 25 +
DropoutCoder.PolylineAlgorithm.sln | 61 -
LICENSE | 2 +-
PolylineAlgorithm.slnx | 20 +
README.md | 215 +-
....Abstraction.AbstractPolylineDecoder-2.yml | 233 ++
....Abstraction.AbstractPolylineEncoder-2.yml | 219 ++
...gorithm.Abstraction.IPolylineDecoder-2.yml | 90 +
...gorithm.Abstraction.IPolylineEncoder-2.yml | 142 +
.../1.0/PolylineAlgorithm.Abstraction.yml | 34 +
...m.Extensions.PolylineDecoderExtensions.yml | 195 ++
...m.Extensions.PolylineEncoderExtensions.yml | 139 +
.../1.0/PolylineAlgorithm.Extensions.yml | 19 +
...hm.Internal.Diagnostics.ExceptionGuard.yml | 313 ++
...PolylineAlgorithm.Internal.Diagnostics.yml | 15 +
...lineAlgorithm.InvalidPolylineException.yml | 102 +
.../PolylineAlgorithm.PolylineEncoding.yml | 529 +++
...ylineAlgorithm.PolylineEncodingOptions.yml | 127 +
...gorithm.PolylineEncodingOptionsBuilder.yml | 141 +
api-reference/1.0/PolylineAlgorithm.yml | 38 +
api-reference/1.0/toc.yml | 40 +
api-reference/api-reference.json | 43 +
api-reference/assembly-metadata.json | 17 +
.../docs-versioning/layout/_master.tmpl | 163 +
.../public/version-switcher.js | 86 +
api-reference/favicon.ico | Bin 0 -> 249170 bytes
api-reference/guide/advanced-scenarios.md | 168 +
api-reference/guide/configuration.md | 75 +
api-reference/guide/faq.md | 63 +
api-reference/guide/getting-started.md | 76 +
api-reference/guide/introduction.md | 33 +
api-reference/guide/sample.md | 115 +
api-reference/guide/toc.yml | 12 +
api-reference/index.md | 18 +
.../media/polyline-algorithm-50x46.png | Bin 0 -> 5385 bytes
api-reference/media/polyline-algorithm.png | Bin 0 -> 488006 bytes
api-reference/toc.yml | 3 +
api-reference/versions.json | 4 +
...tCoder.PolylineAlgorithm.Benchmarks.csproj | 18 -
.../PolylineEncodingBenchmark.cs | 40 -
.../Program.cs | 13 -
.../Constants.cs | 82 -
.../DecodePerformanceBenchmark.cs | 200 --
...Algorithm.Implementation.Benchmarks.csproj | 19 -
.../EncodePerformanceBenchmark.cs | 222 --
.../Program.cs | 15 -
.../PolylineAlgorithm.Benchmarks.csproj | 26 +
.../PolylineDecoderBenchmark.cs | 124 +
.../PolylineEncoderBenchmark.cs | 92 +
.../PolylineEncodingBenchmark.cs | 38 +
.../PolylineAlgorithm.Benchmarks/Program.cs | 23 +
.../Properties/CodeCoverage.cs | 8 +
.../Properties/GlobalSuppressions.cs | 15 +
code-coverage-settings.xml | 26 +
docs/.gitignore | 9 -
docs/README.md | 23 +
docs/api-documentation.md | 136 +
docs/api/.gitignore | 4 -
docs/api/.manifest | 23 -
docs/api/index.md | 4 -
docs/benchmarks.md | 132 +
docs/branch-strategy.md | 113 +
docs/composite-actions.md | 307 ++
docs/docfx.json | 58 -
docs/extensibility.md | 128 +
docs/favicon.ico | Bin 99678 -> 0 bytes
docs/fonts/glyphicons-halflings-regular.eot | Bin 20127 -> 0 bytes
docs/fonts/glyphicons-halflings-regular.svg | 288 --
docs/fonts/glyphicons-halflings-regular.ttf | Bin 45404 -> 0 bytes
docs/fonts/glyphicons-halflings-regular.woff | Bin 23424 -> 0 bytes
docs/fonts/glyphicons-halflings-regular.woff2 | Bin 18028 -> 0 bytes
docs/local-development.md | 74 +
docs/logo.svg | 25 -
docs/manifest.json | 1 -
docs/search-stopwords.json | 121 -
docs/styles/docfx.css | 882 -----
docs/styles/docfx.js | 700 ----
docs/styles/docfx.vendor.css | 1466 ---------
docs/styles/docfx.vendor.js | 45 -
docs/styles/lunr.js | 2924 -----------------
docs/styles/lunr.min.js | 7 -
docs/styles/main.css | 0
docs/styles/main.js | 1 -
docs/styles/search-worker.js | 45 -
docs/testing.md | 135 +
docs/versioning.md | 99 +
docs/workflows.md | 178 +
global.json | 5 +
nuget/DropoutCoder.PolylineAlgorithm.nuspec | 19 -
.../NetTopologyPolylineDecoder.cs | 35 +
.../NetTopologyPolylineEncoder.cs | 55 +
...neAlgorithm.NetTopologySuite.Sample.csproj | 19 +
.../Properties/CodeCoverage.cs | 8 +
src/Constants.cs | 82 -
src/DropoutCoder.PolylineAlgorithm.csproj | 29 -
src/Encoding/IPolylineEncoding.cs | 38 -
src/Encoding/PolylineEncoding.cs | 38 -
src/Encoding/PolylineEncodingBase.cs | 72 -
src/ExceptionMessageResource.Designer.cs | 99 -
src/PolylineAlgorithm.cs | 216 --
.../Abstraction/AbstractPolylineDecoder.cs | 204 ++
.../Abstraction/AbstractPolylineEncoder.cs | 197 ++
.../Abstraction/IPolylineDecoder.cs | 49 +
.../Abstraction/IPolylineEncoder.cs | 77 +
.../Extensions/PolylineDecoderExtensions.cs | 97 +
.../Extensions/PolylineEncoderExtensions.cs | 84 +
.../Internal/CoordinateDelta.cs | 72 +
src/PolylineAlgorithm/Internal/Defaults.cs | 118 +
.../Internal/Diagnostics/ExceptionGuard.cs | 325 ++
.../Diagnostics/LogDebugExtensions.cs | 57 +
.../Diagnostics/LogWarningExtensions.cs | 101 +
src/PolylineAlgorithm/Internal/Pow10.cs | 40 +
.../InvalidPolylineException.cs | 45 +
.../PolylineAlgorithm.csproj | 73 +
src/PolylineAlgorithm/PolylineEncoding.cs | 461 +++
.../PolylineEncodingOptions.cs | 83 +
.../PolylineEncodingOptionsBuilder.cs | 102 +
.../ExceptionMessageResource.Designer.cs | 171 +
.../Properties}/ExceptionMessageResource.resx | 40 +-
.../Properties/GlobalSuppressions.cs | 1 +
src/PolylineAlgorithm/PublicAPI.Shipped.txt | 1 +
src/PolylineAlgorithm/PublicAPI.Unshipped.txt | 1 +
src/PolylineAlgorithm/README.md | 105 +
src/Validation/CoordinateValidator.cs | 47 -
tests/Defaults.cs | 77 -
...ropoutCoder.PolylineAlgorithm.Tests.csproj | 23 -
tests/Encoding/PolylineEncodingBaseTest.cs | 196 --
tests/Encoding/PolylineEncodingTest.cs | 55 -
.../AbstractPolylineDecoderTests.cs | 144 +
.../AbstractPolylineEncoderTests.cs | 152 +
.../PolylineDecoderExtensionsTests.cs | 172 +
.../PolylineEncoderExtensionsTests.cs | 123 +
.../Internal/CoordinateDeltaTests.cs | 136 +
.../Diagnostics/ExceptionGuardTests.cs | 609 ++++
.../Diagnostics/LogDebugExtensionsTests.cs | 222 ++
.../Diagnostics/LogWarningExtensionsTests.cs | 116 +
.../Internal/Pow10Tests.cs | 49 +
.../InvalidPolylineExceptionTests.cs | 44 +
.../PolylineAlgorithm.Tests.csproj | 34 +
.../PolylineEncodingOptionsBuilderTests.cs | 425 +++
.../PolylineEncodingTests.cs | 681 ++++
.../Properties/Category.cs | 16 +
.../Properties/CodeCoverage.cs | 8 +
.../Properties/GlobalSuppressions.cs | 10 +
.../Properties/GlobalUsings.cs | 6 +
.../Properties/MSTestSettings.cs | 9 +
tests/PolylineAlgorithmTest.cs | 177 -
tests/Properties/AssemblyInfo.cs | 11 -
tests/Validation/CoordinateValidatorTest.cs | 136 -
.../PolylineAlgorithm.Utility.csproj | 33 +
.../Properties/CodeCoverage.cs | 8 +
.../RandomValueProvider.cs | 120 +
.../StaticValueProvider.cs | 80 +
185 files changed, 13782 insertions(+), 8864 deletions(-)
create mode 100644 .editorconfig
create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md
create mode 100644 .github/actions/documentation/docfx-build/action.yml
create mode 100644 .github/actions/documentation/docfx-metadata/action.yml
create mode 100644 .github/actions/git/push-changes/action.yml
create mode 100644 .github/actions/github/branch-protection/lock/action.yml
create mode 100644 .github/actions/github/branch-protection/unlock/action.yml
create mode 100644 .github/actions/github/create-release/action.yml
create mode 100644 .github/actions/github/write-file-to-summary/action.yml
create mode 100644 .github/actions/nuget/publish-package/action.yml
create mode 100644 .github/actions/source/compile/action.yml
create mode 100644 .github/actions/source/format/action.yml
create mode 100644 .github/actions/testing/code-coverage/action.yml
create mode 100644 .github/actions/testing/test-report/action.yml
create mode 100644 .github/actions/testing/test/action.yml
create mode 100644 .github/actions/versioning/extract-version/action.yml
create mode 100644 .github/actions/versioning/format-version/action.yml
create mode 100644 .github/dependabot.yml
create mode 100644 .github/workflows/build.yml
create mode 100644 .github/workflows/bump-version.yml
delete mode 100644 .github/workflows/codeql.yml
delete mode 100644 .github/workflows/dotnet.yml
create mode 100644 .github/workflows/promote-branch.yml
create mode 100644 .github/workflows/publish-documentation.yml
create mode 100644 .github/workflows/pull-request.yml
create mode 100644 .github/workflows/release.yml
delete mode 100644 .github/workflows/static.yml
create mode 100644 AGENTS.md
create mode 100644 CONTRIBUTING.md
create mode 100644 Directory.Build.props
delete mode 100644 DropoutCoder.PolylineAlgorithm.sln
create mode 100644 PolylineAlgorithm.slnx
create mode 100644 api-reference/1.0/PolylineAlgorithm.Abstraction.AbstractPolylineDecoder-2.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.Abstraction.AbstractPolylineEncoder-2.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.Abstraction.IPolylineDecoder-2.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.Abstraction.IPolylineEncoder-2.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.Abstraction.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.Extensions.PolylineDecoderExtensions.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.Extensions.PolylineEncoderExtensions.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.Extensions.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.Internal.Diagnostics.ExceptionGuard.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.Internal.Diagnostics.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.InvalidPolylineException.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.PolylineEncoding.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.PolylineEncodingOptions.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.PolylineEncodingOptionsBuilder.yml
create mode 100644 api-reference/1.0/PolylineAlgorithm.yml
create mode 100644 api-reference/1.0/toc.yml
create mode 100644 api-reference/api-reference.json
create mode 100644 api-reference/assembly-metadata.json
create mode 100644 api-reference/docs-versioning/layout/_master.tmpl
create mode 100644 api-reference/docs-versioning/public/version-switcher.js
create mode 100644 api-reference/favicon.ico
create mode 100644 api-reference/guide/advanced-scenarios.md
create mode 100644 api-reference/guide/configuration.md
create mode 100644 api-reference/guide/faq.md
create mode 100644 api-reference/guide/getting-started.md
create mode 100644 api-reference/guide/introduction.md
create mode 100644 api-reference/guide/sample.md
create mode 100644 api-reference/guide/toc.yml
create mode 100644 api-reference/index.md
create mode 100644 api-reference/media/polyline-algorithm-50x46.png
create mode 100644 api-reference/media/polyline-algorithm.png
create mode 100644 api-reference/toc.yml
create mode 100644 api-reference/versions.json
delete mode 100644 benchmarks/DropoutCoder.PolylineAlgorithm.Benchmarks/DropoutCoder.PolylineAlgorithm.Benchmarks.csproj
delete mode 100644 benchmarks/DropoutCoder.PolylineAlgorithm.Benchmarks/PolylineEncodingBenchmark.cs
delete mode 100644 benchmarks/DropoutCoder.PolylineAlgorithm.Benchmarks/Program.cs
delete mode 100644 benchmarks/DropoutCoder.PolylineAlgorithm.Implementation.Benchmarks/Constants.cs
delete mode 100644 benchmarks/DropoutCoder.PolylineAlgorithm.Implementation.Benchmarks/DecodePerformanceBenchmark.cs
delete mode 100644 benchmarks/DropoutCoder.PolylineAlgorithm.Implementation.Benchmarks/DropoutCoder.PolylineAlgorithm.Implementation.Benchmarks.csproj
delete mode 100644 benchmarks/DropoutCoder.PolylineAlgorithm.Implementation.Benchmarks/EncodePerformanceBenchmark.cs
delete mode 100644 benchmarks/DropoutCoder.PolylineAlgorithm.Implementation.Benchmarks/Program.cs
create mode 100644 benchmarks/PolylineAlgorithm.Benchmarks/PolylineAlgorithm.Benchmarks.csproj
create mode 100644 benchmarks/PolylineAlgorithm.Benchmarks/PolylineDecoderBenchmark.cs
create mode 100644 benchmarks/PolylineAlgorithm.Benchmarks/PolylineEncoderBenchmark.cs
create mode 100644 benchmarks/PolylineAlgorithm.Benchmarks/PolylineEncodingBenchmark.cs
create mode 100644 benchmarks/PolylineAlgorithm.Benchmarks/Program.cs
create mode 100644 benchmarks/PolylineAlgorithm.Benchmarks/Properties/CodeCoverage.cs
create mode 100644 benchmarks/PolylineAlgorithm.Benchmarks/Properties/GlobalSuppressions.cs
create mode 100644 code-coverage-settings.xml
delete mode 100644 docs/.gitignore
create mode 100644 docs/README.md
create mode 100644 docs/api-documentation.md
delete mode 100644 docs/api/.gitignore
delete mode 100644 docs/api/.manifest
delete mode 100644 docs/api/index.md
create mode 100644 docs/benchmarks.md
create mode 100644 docs/branch-strategy.md
create mode 100644 docs/composite-actions.md
delete mode 100644 docs/docfx.json
create mode 100644 docs/extensibility.md
delete mode 100644 docs/favicon.ico
delete mode 100644 docs/fonts/glyphicons-halflings-regular.eot
delete mode 100644 docs/fonts/glyphicons-halflings-regular.svg
delete mode 100644 docs/fonts/glyphicons-halflings-regular.ttf
delete mode 100644 docs/fonts/glyphicons-halflings-regular.woff
delete mode 100644 docs/fonts/glyphicons-halflings-regular.woff2
create mode 100644 docs/local-development.md
delete mode 100644 docs/logo.svg
delete mode 100644 docs/manifest.json
delete mode 100644 docs/search-stopwords.json
delete mode 100644 docs/styles/docfx.css
delete mode 100644 docs/styles/docfx.js
delete mode 100644 docs/styles/docfx.vendor.css
delete mode 100644 docs/styles/docfx.vendor.js
delete mode 100644 docs/styles/lunr.js
delete mode 100644 docs/styles/lunr.min.js
delete mode 100644 docs/styles/main.css
delete mode 100644 docs/styles/main.js
delete mode 100644 docs/styles/search-worker.js
create mode 100644 docs/testing.md
create mode 100644 docs/versioning.md
create mode 100644 docs/workflows.md
create mode 100644 global.json
delete mode 100644 nuget/DropoutCoder.PolylineAlgorithm.nuspec
create mode 100644 samples/PolylineAlgorithm.NetTopologySuite.Sample/NetTopologyPolylineDecoder.cs
create mode 100644 samples/PolylineAlgorithm.NetTopologySuite.Sample/NetTopologyPolylineEncoder.cs
create mode 100644 samples/PolylineAlgorithm.NetTopologySuite.Sample/PolylineAlgorithm.NetTopologySuite.Sample.csproj
create mode 100644 samples/PolylineAlgorithm.NetTopologySuite.Sample/Properties/CodeCoverage.cs
delete mode 100644 src/Constants.cs
delete mode 100644 src/DropoutCoder.PolylineAlgorithm.csproj
delete mode 100644 src/Encoding/IPolylineEncoding.cs
delete mode 100644 src/Encoding/PolylineEncoding.cs
delete mode 100644 src/Encoding/PolylineEncodingBase.cs
delete mode 100644 src/ExceptionMessageResource.Designer.cs
delete mode 100644 src/PolylineAlgorithm.cs
create mode 100644 src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs
create mode 100644 src/PolylineAlgorithm/Abstraction/AbstractPolylineEncoder.cs
create mode 100644 src/PolylineAlgorithm/Abstraction/IPolylineDecoder.cs
create mode 100644 src/PolylineAlgorithm/Abstraction/IPolylineEncoder.cs
create mode 100644 src/PolylineAlgorithm/Extensions/PolylineDecoderExtensions.cs
create mode 100644 src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs
create mode 100644 src/PolylineAlgorithm/Internal/CoordinateDelta.cs
create mode 100644 src/PolylineAlgorithm/Internal/Defaults.cs
create mode 100644 src/PolylineAlgorithm/Internal/Diagnostics/ExceptionGuard.cs
create mode 100644 src/PolylineAlgorithm/Internal/Diagnostics/LogDebugExtensions.cs
create mode 100644 src/PolylineAlgorithm/Internal/Diagnostics/LogWarningExtensions.cs
create mode 100644 src/PolylineAlgorithm/Internal/Pow10.cs
create mode 100644 src/PolylineAlgorithm/InvalidPolylineException.cs
create mode 100644 src/PolylineAlgorithm/PolylineAlgorithm.csproj
create mode 100644 src/PolylineAlgorithm/PolylineEncoding.cs
create mode 100644 src/PolylineAlgorithm/PolylineEncodingOptions.cs
create mode 100644 src/PolylineAlgorithm/PolylineEncodingOptionsBuilder.cs
create mode 100644 src/PolylineAlgorithm/Properties/ExceptionMessageResource.Designer.cs
rename src/{ => PolylineAlgorithm/Properties}/ExceptionMessageResource.resx (76%)
create mode 100644 src/PolylineAlgorithm/Properties/GlobalSuppressions.cs
create mode 100644 src/PolylineAlgorithm/PublicAPI.Shipped.txt
create mode 100644 src/PolylineAlgorithm/PublicAPI.Unshipped.txt
create mode 100644 src/PolylineAlgorithm/README.md
delete mode 100644 src/Validation/CoordinateValidator.cs
delete mode 100644 tests/Defaults.cs
delete mode 100644 tests/DropoutCoder.PolylineAlgorithm.Tests.csproj
delete mode 100644 tests/Encoding/PolylineEncodingBaseTest.cs
delete mode 100644 tests/Encoding/PolylineEncodingTest.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineDecoderTests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Abstraction/AbstractPolylineEncoderTests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Extensions/PolylineDecoderExtensionsTests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Extensions/PolylineEncoderExtensionsTests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Internal/CoordinateDeltaTests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Internal/Diagnostics/ExceptionGuardTests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Internal/Diagnostics/LogDebugExtensionsTests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Internal/Diagnostics/LogWarningExtensionsTests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Internal/Pow10Tests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/InvalidPolylineExceptionTests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/PolylineAlgorithm.Tests.csproj
create mode 100644 tests/PolylineAlgorithm.Tests/PolylineEncodingOptionsBuilderTests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/PolylineEncodingTests.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Properties/Category.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Properties/CodeCoverage.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Properties/GlobalSuppressions.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Properties/GlobalUsings.cs
create mode 100644 tests/PolylineAlgorithm.Tests/Properties/MSTestSettings.cs
delete mode 100644 tests/PolylineAlgorithmTest.cs
delete mode 100644 tests/Properties/AssemblyInfo.cs
delete mode 100644 tests/Validation/CoordinateValidatorTest.cs
create mode 100644 utilities/PolylineAlgorithm.Utility/PolylineAlgorithm.Utility.csproj
create mode 100644 utilities/PolylineAlgorithm.Utility/Properties/CodeCoverage.cs
create mode 100644 utilities/PolylineAlgorithm.Utility/RandomValueProvider.cs
create mode 100644 utilities/PolylineAlgorithm.Utility/StaticValueProvider.cs
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..75d9084f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,276 @@
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+root = true
+
+# All files
+[*]
+end_of_line = lf
+insert_final_newline = true
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = false
+
+#### .NET Code Actions ####
+
+# Type members
+dotnet_hide_advanced_members = false
+dotnet_member_insertion_location = with_other_members_of_the_same_kind
+dotnet_property_generation_behavior = prefer_auto_properties
+
+# Symbol search
+dotnet_search_reference_assemblies = true
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = false
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false
+dotnet_style_qualification_for_field = false
+dotnet_style_qualification_for_method = false
+dotnet_style_qualification_for_property = false
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true
+dotnet_style_predefined_type_for_member_access = true
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_operators = always_for_clarity
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members
+
+# Expression-level preferences
+dotnet_prefer_system_hash_code = true
+dotnet_style_coalesce_expression = true
+dotnet_style_collection_initializer = true
+dotnet_style_explicit_tuple_names = true
+dotnet_style_namespace_match_folder = true
+dotnet_style_null_propagation = true
+dotnet_style_object_initializer = true
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true
+dotnet_style_prefer_collection_expression = when_types_loosely_match
+dotnet_style_prefer_compound_assignment = true
+dotnet_style_prefer_conditional_expression_over_assignment = true
+dotnet_style_prefer_conditional_expression_over_return = true
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
+dotnet_style_prefer_inferred_anonymous_type_member_names = true
+dotnet_style_prefer_inferred_tuple_names = true
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true
+dotnet_style_prefer_simplified_boolean_expressions = true
+dotnet_style_prefer_simplified_interpolation = true
+
+# Field preferences
+dotnet_style_readonly_field = true
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = non_public
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+# New line preferences
+dotnet_style_allow_multiple_blank_lines_experimental = false
+dotnet_style_allow_statement_immediately_after_block_experimental = false
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = false
+csharp_style_var_for_built_in_types = false
+csharp_style_var_when_type_is_apparent = false
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true
+csharp_style_expression_bodied_constructors = false
+csharp_style_expression_bodied_indexers = true
+csharp_style_expression_bodied_lambdas = when_on_single_line
+csharp_style_expression_bodied_local_functions = when_on_single_line
+csharp_style_expression_bodied_methods = false
+csharp_style_expression_bodied_operators = false
+csharp_style_expression_bodied_properties = true
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true
+csharp_style_pattern_matching_over_is_with_cast_check = true
+csharp_style_prefer_extended_property_pattern = true
+csharp_style_prefer_not_pattern = true
+csharp_style_prefer_pattern_matching = true
+csharp_style_prefer_switch_expression = true
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true
+
+# Modifier preferences
+csharp_prefer_static_anonymous_function = true
+csharp_prefer_static_local_function = true
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
+csharp_style_prefer_readonly_struct = true
+csharp_style_prefer_readonly_struct_member = true
+
+# Code-block preferences
+csharp_prefer_braces = true
+csharp_prefer_simple_using_statement = false
+csharp_prefer_system_threading_lock = true
+csharp_style_namespace_declarations = file_scoped
+csharp_style_prefer_method_group_conversion = false
+csharp_style_prefer_primary_constructors = false
+csharp_style_prefer_top_level_statements = false
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true
+csharp_style_deconstructed_variable_declaration = true
+csharp_style_implicit_object_creation_when_type_is_apparent = true
+csharp_style_inlined_variable_declaration = true
+csharp_style_prefer_implicitly_typed_lambda_expression = true
+csharp_style_prefer_index_operator = true
+csharp_style_prefer_local_over_anonymous_function = false
+csharp_style_prefer_null_check_over_type_check = true
+csharp_style_prefer_range_operator = true
+csharp_style_prefer_tuple_swap = true
+csharp_style_prefer_unbound_generic_type_in_nameof = true
+csharp_style_prefer_utf8_string_literals = true
+csharp_style_throw_expression = true
+csharp_style_unused_value_assignment_preference = discard_variable
+csharp_style_unused_value_expression_statement_preference = discard_variable
+
+# 'using' directive preferences
+csharp_using_directive_placement = inside_namespace
+
+# New line preferences
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
+csharp_style_allow_embedded_statements_on_same_line_experimental = false
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = false
+csharp_new_line_before_else = false
+csharp_new_line_before_finally = false
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = none
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = false
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.private_or_internal_const_field_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.private_or_internal_const_field_should_be_pascal_case.symbols = private_or_internal_const_field
+dotnet_naming_rule.private_or_internal_const_field_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.private_or_internal_field_should_be_underscore_camel_case.severity = suggestion
+dotnet_naming_rule.private_or_internal_field_should_be_underscore_camel_case.symbols = private_or_internal_field
+dotnet_naming_rule.private_or_internal_field_should_be_underscore_camel_case.style = underscore_camel_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
+dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
+dotnet_naming_symbols.private_or_internal_field.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, method, event
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+dotnet_naming_symbols.private_or_internal_const_field.applicable_kinds = field
+dotnet_naming_symbols.private_or_internal_const_field.applicable_accessibilities = internal, private, protected_internal, private_protected
+dotnet_naming_symbols.private_or_internal_const_field.required_modifiers = const
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.underscore_camel_case.required_prefix = _
+dotnet_naming_style.underscore_camel_case.required_suffix =
+dotnet_naming_style.underscore_camel_case.word_separator =
+dotnet_naming_style.underscore_camel_case.capitalization = camel_case
+
+# Public API analyzer
+
+dotnet_public_api_analyzer.require_api_files = true
diff --git a/.gitattributes b/.gitattributes
index 5fa1ed0e..e4cab669 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -10,7 +10,7 @@
# default for csharp files.
# Note: This is only used by command line
###############################################################################
-#*.cs diff=csharp
+*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
@@ -22,27 +22,28 @@
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
-#*.sln merge=binary
-#*.csproj merge=binary
-#*.vbproj merge=binary
-#*.vcxproj merge=binary
-#*.vcproj merge=binary
-#*.dbproj merge=binary
-#*.fsproj merge=binary
-#*.lsproj merge=binary
-#*.wixproj merge=binary
-#*.modelproj merge=binary
-#*.sqlproj merge=binary
-#*.wwaproj merge=binary
+*.sln merge=binary
+*.csproj merge=binary
+*.vbproj merge=binary
+*.vcxproj merge=binary
+*.vcproj merge=binary
+*.dbproj merge=binary
+*.fsproj merge=binary
+*.lsproj merge=binary
+*.wixproj merge=binary
+*.modelproj merge=binary
+*.sqlproj merge=binary
+*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
-#*.jpg binary
-#*.png binary
-#*.gif binary
+*.jpg binary
+*.png binary
+*.gif binary
+*.ico binary
###############################################################################
# diff behavior for common document formats
@@ -51,16 +52,16 @@
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
-#*.doc diff=astextplain
-#*.DOC diff=astextplain
-#*.docx diff=astextplain
-#*.DOCX diff=astextplain
-#*.dot diff=astextplain
-#*.DOT diff=astextplain
-#*.pdf diff=astextplain
-#*.PDF diff=astextplain
-#*.rtf diff=astextplain
-#*.RTF diff=astextplain
+*.doc diff=astextplain
+*.DOC diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot diff=astextplain
+*.DOT diff=astextplain
+*.pdf diff=astextplain
+*.PDF diff=astextplain
+*.rtf diff=astextplain
+*.RTF diff=astextplain
###############################################################################
# exclude files except those with cs file extension from repository language detection
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..dd84ea78
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..bbcbbe7d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/actions/documentation/docfx-build/action.yml b/.github/actions/documentation/docfx-build/action.yml
new file mode 100644
index 00000000..a24e6e75
--- /dev/null
+++ b/.github/actions/documentation/docfx-build/action.yml
@@ -0,0 +1,45 @@
+name: 'Generate docs with docfx'
+author: 'Pete Sramek'
+description: 'Generate documentation using docfx'
+inputs:
+# Required
+ artifact-name:
+ description: 'Name of the artifact to upload after generating documentation'
+ required: true
+ docfx-json-manifest:
+ description: 'Path to the docfx JSON manifest file'
+ required: true
+ output-directory:
+ description: 'Target directory for generated documentation'
+ required: true
+# Optional
+ dotnet_sdk_version:
+ description: '.NET SDK version. Default: 10.x'
+ required: false
+ default: '10.x'
+
+runs:
+ using: composite
+ steps:
+ - name: Dotnet Setup
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ inputs.dotnet_sdk_version }}
+ - name: 'testing variables'
+ shell: bash
+ run: |
+ echo "artifact-name: ${{ inputs.artifact-name }}"
+ echo "docfx-json-manifest: ${{ inputs.docfx-json-manifest }}"
+ echo "output-directory: ${{ inputs.output-directory }}"
+ echo "dotnet-sdk-version: ${{ inputs.dotnet_sdk_version }}"
+ - name: 'Update docfx tool'
+ run: dotnet tool update -g docfx
+ shell: bash
+ - name: 'Generate documentation'
+ run: docfx build ${{ inputs.docfx-json-manifest }}
+ shell: bash
+ - name: Upload artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: ${{ inputs.artifact-name }}
+ path: ${{ inputs.output-directory }}
diff --git a/.github/actions/documentation/docfx-metadata/action.yml b/.github/actions/documentation/docfx-metadata/action.yml
new file mode 100644
index 00000000..bbcd6383
--- /dev/null
+++ b/.github/actions/documentation/docfx-metadata/action.yml
@@ -0,0 +1,65 @@
+name: 'Generate metadata with docfx'
+author: 'Pete Sramek'
+description: 'Generate metadata using docfx'
+
+inputs:
+# Required
+ artifact-name:
+ description: 'Name of the artifact to upload after generating metadata'
+ required: true
+ docfx-json-manifest:
+ description: 'Path to the docfx JSON manifest file'
+ required: true
+ temporary-directory:
+ description: 'Temporary directory for docfx metadata generation'
+ required: true
+ output-directory:
+ description: 'Target directory for generated documentation'
+ required: true
+# Optional
+ dotnet_sdk_version:
+ description: '.NET SDK version. Default: 10.x'
+ required: false
+ default: '10.x'
+
+runs:
+ using: composite
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ - name: Dotnet Setup
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+ - name: 'Update docfx tool'
+ run: dotnet tool update -g docfx
+ shell: bash
+ - name: 'Delete temporary folder'
+ shell: bash
+ run: |
+ if [ -d "${{ inputs.temporary-directory }}" ]; then
+ rm -rf "${{ inputs.temporary-directory }}"
+ fi
+ - name: 'Delete temporary folder'
+ shell: bash
+ run: |
+ if [ -d "$${{ inputs.output-directory }}" ]; then
+ rm -rf "${{ inputs.output-directory }}"
+ fi
+ - name: 'Generate assembly metadata'
+ shell: bash
+ run: docfx metadata ${{ inputs.docfx-json-manifest }}
+ - name: 'List directory'
+ shell: bash
+ run: |
+ ls
+ - name: 'Copy metadata to output directory'
+ shell: bash
+ run: |
+ mkdir -p ${{ inputs.output-directory }}
+ cp -r ${{ inputs.temporary-directory }}/* ${{ inputs.output-directory }}
+ - name: 'Upload artifact'
+ uses: actions/upload-artifact@v7
+ with:
+ name: ${{ inputs.artifact-name }}
+ path: ${{ inputs.output-directory }}
diff --git a/.github/actions/git/push-changes/action.yml b/.github/actions/git/push-changes/action.yml
new file mode 100644
index 00000000..31ddd3f2
--- /dev/null
+++ b/.github/actions/git/push-changes/action.yml
@@ -0,0 +1,99 @@
+name: 'Push Changes'
+author: 'Pete Sramek'
+description: 'Push changes to a specified branch in the repository.'
+inputs:
+ # Required
+ commit-message:
+ description: 'The commit message to use when pushing changes.'
+ required: true
+# Optional
+ dotnet_sdk_version:
+ description: '.NET SDK version. Default: 10.x'
+ required: false
+ default: '10.x'
+ artifact-name:
+ description: 'Name of the artifact to download before pushing changes. Default: '''''
+ required: false
+ default: ''
+ working-directory:
+ description: 'The working directory where the changes will be pushed from. Default ''.'''
+ required: false
+ default: '.'
+ target-branch:
+ description: 'The branch to push changes to.. Default: '''''
+ required: false
+ default: ''
+
+runs:
+ using: "composite"
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: Dotnet Setup
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: Download a single artifact
+ if: ${{ inputs.artifact-name != '' }}
+ uses: actions/download-artifact@v8
+ with:
+ name: ${{ inputs.artifact-name }}
+ path: ${{ inputs.working-directory }}
+
+ - name: Stage changes in ${{ inputs.working-directory }}
+ shell: bash
+ run: |
+ git add .
+ working-directory: ${{ inputs.working-directory }}
+
+ - if: runner.os == 'Windows'
+ shell: bash
+ run: |
+ git config --global core.autocrlf false
+ git config --global core.eol lf
+
+ - if: runner.os != 'Windows'
+ shell: bash
+ run: |
+ git config --global core.autocrlf true
+ git config --global core.eol lf
+
+ - name: Create or switch to ${{ inputs.target-branch }}
+ if: ${{ inputs.target-branch != '' }}
+ shell: bash
+ run: |
+ git fetch origin
+ git stash push --keep-index
+ if git show-ref --verify --quiet refs/heads/${{ inputs.target-branch }}; then
+ echo "Branch ${{ inputs.target-branch }} already exists, switching to it."
+ git checkout ${{ inputs.target-branch }} --force
+ git pull origin ${{ inputs.target-branch }}
+ else
+ echo "Branch ${{ inputs.target-branch }} does not exist, creating it."
+ git checkout -b ${{ inputs.target-branch }} origin/${{ inputs.target-branch }} --force || git checkout -b ${{ inputs.target-branch }} --force
+ git pull origin ${{ inputs.target-branch }}
+ fi
+ git stash apply
+
+ - name: Validate changes
+ id: validate
+ shell: bash
+ run: |
+ set +e
+ git diff --quiet --exit-code --cached
+ echo has-changes="$?" >> $GITHUB_OUTPUT
+ set -e
+ working-directory: ${{ inputs.working-directory }}
+
+ - name: Push changes to ${{ github.head_ref || github.ref }}
+ if: ${{ fromJSON(steps.validate.outputs.has-changes) == '1' }}
+ shell: bash
+ run: |
+ git config user.name "$(git log -n 1 --pretty=format:%an)"
+ git config user.email "$(git log -n 1 --pretty=format:%ae)"
+ git commit -m '${{ inputs.commit-message }}'
+ git pull --rebase origin ${{ github.head_ref || github.ref }}
+ git push
+ working-directory: ${{ inputs.working-directory }}
diff --git a/.github/actions/github/branch-protection/lock/action.yml b/.github/actions/github/branch-protection/lock/action.yml
new file mode 100644
index 00000000..d8f345a1
--- /dev/null
+++ b/.github/actions/github/branch-protection/lock/action.yml
@@ -0,0 +1,40 @@
+name: 'Lock branch'
+author: 'Pete Sramek'
+description: 'Apply branch protection to prevent direct pushes. Requires PRs with at least one approval.'
+inputs:
+ branch:
+ description: 'Branch name to lock.'
+ required: true
+ token:
+ description: 'GitHub token with administration:write (repo admin) permission. Use a PAT; GITHUB_TOKEN cannot call the branch protection API.'
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: 'Lock branch ${{ inputs.branch }}'
+ shell: bash
+ env:
+ GH_TOKEN: ${{ inputs.token }}
+ run: |
+ if ! gh api --method PUT /repos/${{ github.repository }}/branches/${{ inputs.branch }}/protection \
+ --input - << 'EOF'
+ {
+ "required_status_checks": null,
+ "enforce_admins": false,
+ "required_pull_request_reviews": {
+ "dismiss_stale_reviews": true,
+ "require_code_owner_reviews": false,
+ "required_approving_review_count": 1
+ },
+ "restrictions": null,
+ "allow_force_pushes": false,
+ "allow_deletions": false,
+ "lock_branch": false
+ }
+ EOF
+ then
+ echo "::error::Failed to apply branch protection to '${{ inputs.branch }}'. Ensure the token has 'administration: write' permission and the branch exists."
+ exit 1
+ fi
+ echo "🔒 Branch '${{ inputs.branch }}' is now protected." >> $GITHUB_STEP_SUMMARY
diff --git a/.github/actions/github/branch-protection/unlock/action.yml b/.github/actions/github/branch-protection/unlock/action.yml
new file mode 100644
index 00000000..9c41d395
--- /dev/null
+++ b/.github/actions/github/branch-protection/unlock/action.yml
@@ -0,0 +1,21 @@
+name: 'Unlock branch'
+author: 'Pete Sramek'
+description: 'Remove branch protection to allow a workflow to push directly. Always re-lock after the operation.'
+inputs:
+ branch:
+ description: 'Branch name to unlock.'
+ required: true
+ token:
+ description: 'GitHub token with administration:write (repo admin) permission. Use a PAT; GITHUB_TOKEN cannot call the branch protection API.'
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: 'Unlock branch ${{ inputs.branch }}'
+ shell: bash
+ env:
+ GH_TOKEN: ${{ inputs.token }}
+ run: |
+ gh api --method DELETE /repos/${{ github.repository }}/branches/${{ inputs.branch }}/protection || true
+ echo "🔓 Branch protection removed from '${{ inputs.branch }}'." >> $GITHUB_STEP_SUMMARY
diff --git a/.github/actions/github/create-release/action.yml b/.github/actions/github/create-release/action.yml
new file mode 100644
index 00000000..0e0015ce
--- /dev/null
+++ b/.github/actions/github/create-release/action.yml
@@ -0,0 +1,40 @@
+name: 'Create GitHub release'
+author: 'Pete Sramek'
+description: 'Create GitHub release.'
+inputs:
+ release-version:
+ description: 'Version to use for the release.'
+ required: true
+ is-preview:
+ description: 'Is this a preview release?'
+ required: true
+ notes-start-tag:
+ description: 'Tag to start generating release notes from. Default: '''''
+ required: false
+ default: ''
+
+runs:
+ using: composite
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ - run: |
+ echo "release-version=${{ inputs.release-version }}"
+ echo "is-preview=${{ inputs.is-preview }}"
+ echo "preview-argument=${{ inputs.is-preview == 'true' && '--prerelease' || '' }}"
+ echo "notes-start-tag=${{ inputs.notes-start-tag }}"
+ echo "notes-start-tag-argument="${{ inputs.notes-start-tag != '' && '--notes-start-tag $(inputs.notes-start-tag)' || '' }}"
+ shell: bash
+ - name: 'Create git tag ${{ env.release-version }}'
+ shell: bash
+ run: |
+ git tag -a ${{ env.release-version }} -m "${{ env.release-version }}"
+ - name: 'Create GitHub release PolylineAlgorithm ${{ env.release-version }}'
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ release-version: ${{ inputs.release-version }}
+ preview-argument: "${{ inputs.is-preview == 'true' && '--prerelease' || '' }}"
+ notes-start-tag-argument: "${{ inputs.notes-start-tag != '' && '--notes-start-tag $(inputs.notes-start-tag)' || '' }}"
+ run: |
+ gh release create ${{ env.release-version }} --generate-notes --discussion-category "General" ${{ env.preview-argument }} ${{ env.notes-start-tag-argument }}
diff --git a/.github/actions/github/write-file-to-summary/action.yml b/.github/actions/github/write-file-to-summary/action.yml
new file mode 100644
index 00000000..c185ce4b
--- /dev/null
+++ b/.github/actions/github/write-file-to-summary/action.yml
@@ -0,0 +1,25 @@
+name: 'Write file to step summary'
+author: 'Pete Sramek'
+description: 'Writes file contents to step summary.'
+inputs:
+# Required
+ file-glob-pattern:
+ description: 'Search pattern for files to write.'
+ required: true
+
+# Optional
+ working-directory:
+ description: 'Working directory to search for file.'
+ required: false
+ default: ${{ github.workspace }}
+
+runs:
+ using: composite
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: Writing ${{ inputs.file }} to step summary
+ shell: bash
+ run: cat ${{ inputs.file }} >> $GITHUB_STEP_SUMMARY
+ working-directory: ${{ inputs.working-directory }}
\ No newline at end of file
diff --git a/.github/actions/nuget/publish-package/action.yml b/.github/actions/nuget/publish-package/action.yml
new file mode 100644
index 00000000..eec82f65
--- /dev/null
+++ b/.github/actions/nuget/publish-package/action.yml
@@ -0,0 +1,61 @@
+name: 'Publish packages to NuGet feed'
+author: 'Pete Sramek'
+description: 'Publishes packages in working directory to public NuGet feed'
+inputs:
+# Required
+ package-artifact-name:
+ description: 'Name of the artifact to download and publish'
+ required: true
+ nuget-feed-url:
+ description: 'NuGet feed URL'
+ required: true
+ nuget-feed-api-key:
+ description: 'NuGet feed API key'
+ required: true
+ nuget-feed-server:
+ description: 'NuGet feed server. Allowed values: ''NuGet'', ''AzureArtifacts'''
+ required: true
+# Optional
+ dotnet-sdk-version:
+ description: '.NET SDK version. Default: 10.x'
+ required: false
+ default: '10.x'
+ working-directory:
+ description: 'Working directory to search for file.'
+ required: false
+ default: ${{ github.workspace }}
+
+runs:
+ using: composite
+
+ steps:
+
+ - if: ${{ inputs.nuget-feed-server != 'NuGet' && inputs.nuget-feed-server != 'AzureArtifacts' }}
+ name: 'Validate NuGet feed server type'
+ shell: bash
+ run: |
+ echo "Invalid NuGet feed server type. Allowed values are ''NuGet'' or ''AzureArtifacts''."
+ exit 1
+
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: Download package artifact
+ uses: actions/download-artifact@v8
+ with:
+ name: ${{ inputs.package-artifact-name }}
+
+ - if: ${{ inputs.nuget-feed-server == 'NuGet' }}
+ name: 'Publish package to NuGet'
+ shell: bash
+ run: |
+ dotnet nuget push **/*.nupkg --source ${{ inputs.nuget-feed-url }} --api-key ${{ inputs.nuget-feed-api-key }}
+ working-directory: ${{ env.working-directory }}
+
+ - if: ${{ inputs.nuget-feed-server == 'AzureArtifacts' }}
+ name: 'Publish package to Azure Artifacts'
+ shell: bash
+ run: |
+ dotnet nuget add source ${{ inputs.nuget-feed-url }} --name nuget-feed --username username --password ${{ inputs.nuget-feed-api-key }} --store-password-in-clear-text
+ dotnet nuget push **/*.nupkg --source nuget-feed --api-key ${{ inputs.nuget-feed-api-key }} --skip-duplicate
+ working-directory: ${{ env.working-directory }}
\ No newline at end of file
diff --git a/.github/actions/source/compile/action.yml b/.github/actions/source/compile/action.yml
new file mode 100644
index 00000000..0af331ee
--- /dev/null
+++ b/.github/actions/source/compile/action.yml
@@ -0,0 +1,74 @@
+name: 'Compile source code'
+author: 'Pete Sramek'
+description: 'Compiles source code, uploads build artifacts.'
+inputs:
+# Required
+ assembly-version:
+ description: 'Assembly version.'
+ required: true
+ assembly-informational-version:
+ description: 'Assembly informational version.'
+ required: true
+ file-version:
+ description: 'Assembly file version.'
+ required: true
+ treat-warnins-as-error:
+ description: 'Treat warnings as errors.'
+ required: true
+ project-path:
+ description: 'Search pattern for source code.'
+ required: true
+# Optional
+ dotnet_sdk_version:
+ description: '.NET SDK version. Default: ''10.x'''
+ required: false
+ default: '10.x'
+ build-configuration:
+ description: 'Build configuration. Default: ''Release'''
+ required: false
+ default: 'Release'
+ build-platform:
+ description: 'Build platform. Deafult: ''Any CPU'''
+ required: false
+ default: 'Any CPU'
+ upload-build-artifacts:
+ description: 'Upload build artifacts, Default: ''true'''
+ required: false
+ default: 'true'
+ build-artifacts-glob-pattern:
+ description: 'Search pattern for build artifacts. Default: **/bin/*, **/obj/*'
+ required: false
+ default: |
+ **/bin/*
+ **/obj/*
+ build-artifacts-name:
+ description: 'Upload build artifacts, Default: ''build'''
+ required: false
+ default: 'build'
+
+runs:
+ using: "composite"
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Setup .NET ${{ inputs.dotnet_sdk_version }}'
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ inputs.dotnet_sdk_version }}
+
+ - if: ${{ inputs.treat-warnings-as-error == 'true' }}
+ name: 'Validate warnings with .NET CLI'
+ shell: bash
+ run: dotnet format analyzers --severity warn --verify-no-changes
+
+ - name: 'Build with .NET CLI'
+ shell: bash
+ run: dotnet build ${{ inputs.search-glob-pattern }} --configuration ${{ inputs.build-configuration }} /p:Platform="${{ inputs.build-platform }}" /p:Version=${{ inputs.assembly-version }} /p:AssemblyInformationalVersion=${{ inputs.assembly-informational-version }} /p:FileVersion=${{ inputs.file-version }}
+
+ - name: 'Upload build artifacts'
+ if: ${{ inputs.upload-build-artifacts == 'true' }}
+ uses: actions/upload-artifact@v7
+ with:
+ name: ${{ inputs.build-artifacts-name }}
+ path: ${{ inputs.build-artifacts-glob-pattern }}
diff --git a/.github/actions/source/format/action.yml b/.github/actions/source/format/action.yml
new file mode 100644
index 00000000..de2536c2
--- /dev/null
+++ b/.github/actions/source/format/action.yml
@@ -0,0 +1,73 @@
+name: 'Format source code'
+author: 'Pete Sramek'
+description: 'Formats source code using dotnet format tool. Pushes changes to the current branch.'
+inputs:
+# Required
+ project-path:
+ description: 'Path to the project or solution file.'
+ required: true
+# Optional
+ dotnet_sdk_version:
+ description: '.NET SDK version. Default: ''10.x'''
+ required: false
+ default: '10.x'
+ format-whitespace:
+ description: 'Format whitespace. Default: ''true'''
+ required: false
+ default: 'true'
+ format-style:
+ description: 'Format style. Default: ''true'''
+ required: false
+ default: 'true'
+ format-analyzers:
+ description: 'Format analyzers. Default: ''false'''
+ required: false
+ default: 'false'
+ format-analyzers-diagnostics-parameter:
+ description: 'Format analyzers diagnostics parameter. Default: '''''
+ required: false
+ default: ''
+
+runs:
+ using: "composite"
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Setup .NET ${{ inputs.dotnet_sdk_version }}'
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ inputs.dotnet_sdk_version }}
+
+ - name: Format whitespace
+ if: ${{ inputs.format-whitespace == 'true' }}
+ shell: bash
+ run: |
+ dotnet format whitespace
+ working-directory: ${{ github.workspace }}
+
+ - name: Format style
+ if: ${{ inputs.format-style == 'true' }}
+ shell: bash
+ run: |
+ dotnet format style
+ working-directory: ${{ github.workspace }}
+
+ - name: 'Set target branch'
+ if: ${{ inputs.format-analyzers == 'true' && inputs.format-analyzers-diagnostics-parameter != '' }}
+ id: set-diagnostics-parameter
+ shell: bash
+ run: |
+ echo "format-analyzers-diagnostics-parameter=--diagnostics ${{ inputs.format-analyzers-diagnostics-parameter }}" >> $GITHUB_OUTPUT
+
+ - name: Format analyzers
+ if: ${{ inputs.format-analyzers == 'true' }}
+ shell: bash
+ run: |
+ dotnet format analyzers ${{ env.format-analyzers-diagnostics-parameter }}
+ working-directory: ${{ github.workspace }}
+
+ - name: 'Push changes'
+ uses: './.github/actions/git/push-changes'
+ with:
+ commit-message: 'Formatted csharp files'
diff --git a/.github/actions/testing/code-coverage/action.yml b/.github/actions/testing/code-coverage/action.yml
new file mode 100644
index 00000000..8751a7b2
--- /dev/null
+++ b/.github/actions/testing/code-coverage/action.yml
@@ -0,0 +1,73 @@
+name: 'Test with .NET CLI'
+author: 'Pete Sramek'
+description: 'Run tests, collects code coverage, logs test results, uploads test artifacts'
+inputs:
+# Required
+ test-result-folder:
+ description: 'Folder where test results are stored'
+ required: true
+# Optional
+ dotnet_sdk_version:
+ description: '.NET SDK version. Default: 10.x'
+ required: false
+ default: '10.x'
+
+outputs:
+ code-coverage-report-file:
+ description: 'Path to the generated code coverage report'
+ value: ${{ steps.variables.outputs.code-coverage-report-file }}
+ code-coverage-merge-file:
+ description: 'Path to the merged code coverage file'
+ value: ${{ steps.variables.outputs.code-coverage-merge-file }}
+
+runs:
+ using: composite
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Setup .NET ${{ inputs.dotnet_sdk_version }}'
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ inputs.dotnet_sdk_version }}
+
+ - if: ${{ endsWith(inputs.test-result-folder, '/') }}
+ name: 'Set TEST_RESULT_FOLDER environment variable'
+ shell: bash
+ run: echo "TEST_RESULT_FOLDER=${{ inputs.test-result-folder }}" >> $GITHUB_ENV
+
+ - if: ${{ !endsWith(inputs.test-result-folder, '/') }}
+ name: 'Set TEST_RESULT_FOLDER environment variable'
+ shell: bash
+ run: echo "TEST_RESULT_FOLDER=${{ inputs.test-result-folder }}/" >> $GITHUB_ENV
+
+ - name: 'Set CODE_COVERAGE_REPORT_FOLDER environment variable'
+ shell: bash
+ run: echo "CODE_COVERAGE_REPORT_FOLDER=${{ env.TEST_RESULT_FOLDER }}coverage-report/" >> $GITHUB_ENV
+
+ - name: 'Set CODE_COVERAGE_MERGED_FILE environment variable'
+ shell: bash
+ run: echo "CODE_COVERAGE_MERGED_FILE=${{ env.CODE_COVERAGE_REPORT_FOLDER }}code-coverage.cobertura.xml" >> $GITHUB_ENV
+
+ - name: 'Set code-coverage-report output'
+ id: variables
+ shell: bash
+ run: |
+ echo "code-coverage-report-file=${{ env.CODE_COVERAGE_REPORT_FOLDER }}Summary.md" >> $GITHUB_OUTPUT
+ echo "code-coverage-merge-file=${{ env.CODE_COVERAGE_MERGED_FILE }}" >> $GITHUB_OUTPUT
+
+ - name: 'Update dotnet-coverage tool'
+ shell: bash
+ run: dotnet tool update --global dotnet-coverage
+
+ - name: 'Merge code coverage reports'
+ shell: bash
+ run: dotnet-coverage merge --output ${{ env.CODE_COVERAGE_MERGED_FILE }} --output-format cobertura "${{ env.TEST_RESULT_FOLDER }}**/*.cobertura.xml"
+
+ - name: 'Update dotnet-reportgenerator-globaltool tool'
+ shell: bash
+ run: dotnet tool update --global dotnet-reportgenerator-globaltool
+
+ - name: 'Generate code coverage report'
+ shell: bash
+ run: reportgenerator -reports:${{ env.CODE_COVERAGE_MERGED_FILE }} -targetdir:${{ env.CODE_COVERAGE_REPORT_FOLDER }} -reporttypes:"MarkdownSummary"
diff --git a/.github/actions/testing/test-report/action.yml b/.github/actions/testing/test-report/action.yml
new file mode 100644
index 00000000..40e66385
--- /dev/null
+++ b/.github/actions/testing/test-report/action.yml
@@ -0,0 +1,60 @@
+name: 'Generate test report with Liquid Test Reports CLI'
+author: 'Pete Sramek'
+description: 'Run tests, collects code coverage, logs test results, uploads test artifacts'
+inputs:
+# Required
+ test-result-folder:
+ description: 'Folder where test results are stored'
+ required: true
+# Optional
+ dotnet_sdk_version:
+ description: '.NET SDK version. Default: 10.x'
+ required: false
+ default: '10.x'
+ test-report-filename:
+ description: 'Filename of the test report. Default: test-report.md'
+ required: false
+ default: 'test-report.md'
+
+outputs:
+ test-report-file:
+ description: 'Path to the generated test report'
+ value: ${{ steps.variables.outputs.test-report-file }}
+
+runs:
+ using: composite
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Setup .NET ${{ inputs.dotnet_sdk_version }}'
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ inputs.dotnet_sdk_version }}
+
+ - name: Install LiquidTestReports.Cli
+ shell: bash
+ run: dotnet tool install --global LiquidTestReports.Cli --prerelease
+
+ - if: ${{ endsWith(inputs.test-result-folder, '/') }}
+ name: 'Set TEST_RESULT_FOLDER environment variable'
+ shell: bash
+ run: echo "TEST_RESULT_FOLDER=${{ inputs.test-result-folder }}" >> $GITHUB_ENV
+
+ - if: ${{ !endsWith(inputs.test-result-folder, '/') }}
+ name: 'Set TEST_RESULT_FOLDER environment variable'
+ shell: bash
+ run: echo "TEST_RESULT_FOLDER=${{ inputs.test-result-folder }}/" >> $GITHUB_ENV
+
+ - name: 'Set TEST_REPORT_FILE environment variable'
+ shell: bash
+ run: echo "TEST_REPORT_FILE=${{ env.TEST_RESULT_FOLDER }}${{ inputs.test-report-filename }}" >> $GITHUB_ENV
+
+ - name: 'Set test-report-file output'
+ id: variables
+ shell: bash
+ run: echo "test-report-file=${{ env.TEST_REPORT_FILE }}" >> $GITHUB_OUTPUT
+
+ - name: Generate test report
+ shell: bash
+ run: liquid --inputs "Folder=${{ env.TEST_RESULT_FOLDER }};File=**/*.trx;Format=Trx" --output-file ${{ env.TEST_REPORT_FILE }}
diff --git a/.github/actions/testing/test/action.yml b/.github/actions/testing/test/action.yml
new file mode 100644
index 00000000..b4aff310
--- /dev/null
+++ b/.github/actions/testing/test/action.yml
@@ -0,0 +1,84 @@
+name: 'Test with .NET CLI'
+author: 'Pete Sramek'
+description: 'Run tests, collects code coverage, logs test results, uploads test artifacts'
+inputs:
+# Required
+ project-path:
+ description: 'Search pattern for test projects.'
+ required: true
+# Optional
+ dotnet_sdk_version:
+ description: '.NET SDK version. Default: 10.x'
+ required: false
+ default: '10.x'
+ build-configuration:
+ description: 'Build configuration. Default: Release'
+ required: false
+ default: 'Release'
+ use-trf-logger:
+ description: 'Use TRX logger. Default: true'
+ required: false
+ default: 'true'
+ collect-code-coverage:
+ description: 'Collect code coverage. Default: true'
+ required: false
+ default: 'true'
+ code-coverage-output-format:
+ description: 'Code coverage output format. Default: cobertura'
+ required: false
+ default: 'cobertura'
+ code-coverage-settings-file:
+ description: 'Code coverage settings file. Default: ""'
+ required: false
+ default: ''
+ upload-test-artifacts:
+ description: 'Upload test artifacts, Default: true'
+ required: false
+ default: 'true'
+ test-results-directory:
+ description: 'Search pattern for test artifacts. Default: "test-results"'
+ required: false
+ default: 'test-results'
+ test-artifacts-name:
+ description: 'Test artifacts name, Default: "test-results"'
+ required: false
+ default: 'test-results'
+
+runs:
+ using: composite
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Setup .NET ${{ inputs.dotnet_sdk_version }}'
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ inputs.dotnet_sdk_version }}
+
+ - if: ${{ inputs.use-trf-logger == 'true' }}
+ name: 'Set TRX_LOGGER_ARGS environment variable'
+ shell: bash
+ run: echo "TRX_LOGGER_ARGS=--report-trx" >> $GITHUB_ENV
+
+ - if: ${{ inputs.collect-code-coverage == 'true' }}
+ name: 'Set CODE_COVERAGE_ARGS environment variable'
+ shell: bash
+ run: |
+ echo "CODE_COVERAGE_ARGS=--coverage --coverage-output-format ${{ inputs.code-coverage-output-format}}" >> $GITHUB_ENV
+
+ - if: ${{ inputs.collect-code-coverage == 'true' && inputs.code-coverage-settings-file != ''}}
+ name: 'Set CODE_COVERAGE_ARGS environment variable'
+ shell: bash
+ run: |
+ echo "CODE_COVERAGE_ARGS=${{ env.CODE_COVERAGE_ARGS }} --coverage-settings ${{ inputs.code-coverage-settings-file }}" >> $GITHUB_ENV
+
+ - name: 'Test with .NET CLI'
+ shell: bash
+ run: dotnet test --project ${{ inputs.project-path }} --configuration ${{ inputs.build-configuration }} ${{ env.CODE_COVERAGE_ARGS }} ${{ env.TRX_LOGGER_ARGS }} --results-directory ${{ inputs.test-results-directory }}
+
+ - name: 'Upload test results'
+ if: ${{ inputs.upload-test-artifacts == 'true' }}
+ uses: actions/upload-artifact@v7
+ with:
+ name: '${{ inputs.test-artifacts-name }}'
+ path: '${{ inputs.test-results-directory }}*'
diff --git a/.github/actions/versioning/extract-version/action.yml b/.github/actions/versioning/extract-version/action.yml
new file mode 100644
index 00000000..ba604761
--- /dev/null
+++ b/.github/actions/versioning/extract-version/action.yml
@@ -0,0 +1,51 @@
+name: 'Extract version'
+author: 'Pete Sramek'
+description: 'Determines versions for the build.'
+inputs:
+# Required
+ branch-name:
+ description: 'Branch name.'
+ required: true
+# Optional
+ default-version:
+ description: 'Default version to use if no match is found. Default: ''0.0'''
+ required: false
+ default: '0.0'
+ version-format:
+ description: 'Version format. Default: ''(\d+).(\d+)'''
+ required: false
+ default: '(\d+).(\d+)'
+ dotnet_sdk_version:
+ description: '.NET SDK version. Default: ''10.x'''
+ required: false
+ default: '10.x'
+outputs:
+ version:
+ description: 'Version extracted from the branch name.'
+ value: ${{ steps.resolve-version.outputs.version }}
+
+runs:
+ using: "composite"
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Setup .NET ${{ inputs.dotnet_sdk_version }}'
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ inputs.dotnet_sdk_version }}
+
+ - name: 'Extract version from branch name'
+ id: regex-match
+ uses: actions-ecosystem/action-regex-match@v2
+ with:
+ text: ${{ inputs.branch-name }}
+ regex: ${{ inputs.version-format }}
+ flags: 'g'
+
+ - name: 'Resolve version'
+ id: resolve-version
+ shell: bash
+ run: |
+ VERSION="${{ steps.regex-match.outputs.match }}"
+ echo "version=${VERSION:-${{ inputs.default-version }}}" >> $GITHUB_OUTPUT
diff --git a/.github/actions/versioning/format-version/action.yml b/.github/actions/versioning/format-version/action.yml
new file mode 100644
index 00000000..70289ee0
--- /dev/null
+++ b/.github/actions/versioning/format-version/action.yml
@@ -0,0 +1,66 @@
+name: 'Format version'
+author: 'Pete Sramek'
+description: 'Formats versions.'
+inputs:
+# Required
+ version:
+ description: 'Version to use.'
+ required: true
+ patch:
+ description: 'Patch version to append to the formatted version.'
+ required: true
+ build-number:
+ description: 'Build number to append to the formatted version.'
+ required: true
+ sha:
+ description: 'Commit SHA to append to the formatted version.'
+ required: true
+ pre-release-tag:
+ description: 'Pre-release tag to append to the formatted version.'
+ required: true
+# Optional
+ dotnet_sdk_version:
+ description: '.NET SDK version. Default: ''10.x'''
+ required: false
+ default: '10.x'
+
+outputs:
+ friendly-version:
+ description: 'Formatted friendly version.'
+ value: ${{ steps.format-version.outputs.friendly-version }}
+ assembly-version:
+ description: 'Formatted assembly version.'
+ value: ${{ steps.format-version.outputs.assembly-version }}
+ assembly-informational-version:
+ description: 'Formatted assembly informational version.'
+ value: ${{ steps.format-version.outputs.assembly-informational-version }}
+ file-version:
+ description: 'Formatted file version.'
+ value: ${{ steps.format-version.outputs.file-version }}
+ release-version:
+ description: 'Formatted release version.'
+ value: ${{ steps.format-version.outputs.release-version }}
+
+runs:
+ using: "composite"
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Setup .NET ${{ inputs.dotnet_sdk_version }}'
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ inputs.dotnet_sdk_version }}
+ - name: 'Format version'
+ shell: bash
+ id: format-version
+ run: |
+ echo "friendly-version=${{ inputs.version }}" >> $GITHUB_OUTPUT
+ echo "assembly-version=${{ inputs.version }}.${{ inputs.patch }}.${{ inputs.build-number }}" >> $GITHUB_OUTPUT
+ echo "assembly-informational-version=${{ inputs.version }}.${{ inputs.patch }}+${{ inputs.sha }}" >> $GITHUB_OUTPUT
+ echo "file-version=${{ inputs.version }}.${{ inputs.patch }}.${{ inputs.build-number }}" >> $GITHUB_OUTPUT
+ if [ -n "${{ inputs.pre-release-tag }}" ]; then
+ echo "release-version=${{ inputs.version }}.${{ inputs.patch }}-${{ inputs.pre-release-tag }}.${{ inputs.build-number }}" >> $GITHUB_OUTPUT
+ else
+ echo "release-version=${{ inputs.version }}.${{ inputs.patch }}.${{ inputs.build-number }}" >> $GITHUB_OUTPUT
+ fi
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..02a54502
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+version: 2
+updates:
+ - package-ecosystem: "dotnet-sdk"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ - package-ecosystem: "nuget"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..047d4afd
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,258 @@
+name: Build
+
+on:
+ workflow_dispatch:
+ push:
+ branches-ignore:
+ - 'preview/**'
+ - 'release/**'
+ paths:
+ - 'src/**'
+
+permissions:
+ actions: read
+ pages: write
+ id-token: write
+ contents: write
+
+concurrency:
+ group: build-${{ github.head_ref || github.ref }}
+ cancel-in-progress: false
+
+env:
+ dotnet-sdk-version: '10.x'
+ build-configuration: 'Release'
+ build-platform: 'Any CPU'
+ git-version: '6.0.x'
+ test-result-directory: 'test-results'
+ nuget-packages-directory: 'nuget-packages'
+
+jobs:
+ workflow-variables:
+ name: 'Set workflow variables'
+ runs-on: ubuntu-latest
+
+ outputs:
+ is-release: ${{ startsWith(github.ref_name, 'release') }}
+ is-preview: ${{ startsWith(github.ref_name, 'preview') }}
+
+ steps:
+ - name: 'Set workflow variables'
+ id: github
+ run: |
+ echo "is-release:${{ startsWith(github.ref_name, 'release') }}"
+ echo "is-preview:${{ startsWith(github.ref_name, 'preview') }}"
+
+ versioning:
+ name: 'Extract version'
+ runs-on: ubuntu-latest
+ outputs:
+ friendly-version: ${{ steps.format-version.outputs.friendly-version }}
+ assembly-version: ${{ steps.format-version.outputs.assembly-version }}
+ assembly-informational-version: ${{ steps.format-version.outputs.assembly-informational-version }}
+ file-version: ${{ steps.format-version.outputs.file-version }}
+ release-version: ${{ steps.format-version.outputs.release-version }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ - name: 'Setup .NET ${{ env.dotnet-sdk-version }}'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+ - name: 'Extract version from branch name'
+ id: extract-version
+ uses: './.github/actions/versioning/extract-version'
+ with:
+ branch-name: ${{ github.ref_name }}
+ - name: 'Create build number'
+ shell: bash
+ id: create-build-number
+ run: |
+ git fetch --unshallow --filter=tree:0
+ build_number=$(git rev-list --count origin/${{ github.ref_name }} ^origin/main)
+ echo "build-number=$build_number" >> $GITHUB_OUTPUT
+ - name: 'Create pre-release tag'
+ shell: bash
+ id: create-pre-release-tag
+ env:
+ build-number: ${{ steps.create-build-number.outputs.build-number }}
+ run: |
+ if [[ '${{ needs.workflow-variables.outputs.is-release }}' == 'true' ]]; then
+ echo "pre-release-tag=" >> $GITHUB_OUTPUT
+ elif [[ '${{ needs.workflow-variables.outputs.is-preview }}' == 'true' ]]; then
+ pre_release_tag='preview'
+ echo "pre-release-tag=$pre_release_tag" >> $GITHUB_OUTPUT
+ else
+ pre_release_tag=$(echo ${{ github.ref_name }} | tr '/' '-' | tr '.' '-'| tr '_' '-')
+ echo "pre-release-tag=$pre_release_tag" >> $GITHUB_OUTPUT
+ fi
+ - name: 'Format version'
+ id: format-version
+ uses: ./.github/actions/versioning/format-version
+ with:
+ version: ${{ steps.extract-version.outputs.version }}
+ patch: ${{ github.run_number }}
+ build-number: ${{ steps.create-build-number.outputs.build-number }}
+ sha: ${{ github.sha }}
+ pre-release-tag: ${{ steps.create-pre-release-tag.outputs.pre-release-tag }}
+
+ format:
+ name: 'Format source code'
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Format source code'
+ uses: ./.github/actions/source/format
+ with:
+ project-path: '**/PolylineAlgorithm.csproj'
+
+ build:
+ name: 'Compile source code'
+ needs: [workflow-variables, versioning, format]
+ runs-on: ubuntu-latest
+
+ env:
+ assembly-version: ${{ needs.versioning.outputs.assembly-version }}
+ assembly-informational-version: ${{ needs.versioning.outputs.assembly-informational-version }}
+ file-version: ${{ needs.versioning.outputs.file-version }}
+
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Compile source code'
+ uses: ./.github/actions/source/compile
+ with:
+ project-path: '**/PolylineAlgorithm.csproj'
+ assembly-version: ${{ env.assembly-version }}
+ assembly-informational-version: ${{ env.assembly-informational-version }}
+ file-version: ${{ env.file-version }}
+ treat-warnins-as-error: ${{ needs.workflow-variables.outputs.is-release }}
+
+ test:
+ name: 'Run tests'
+ needs: [build]
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Setup .NET'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: 'Run tests'
+ uses: ./.github/actions/testing/test
+ with:
+ project-path: './tests/PolylineAlgorithm.Tests/PolylineAlgorithm.Tests.csproj'
+ test-results-directory: '${{ runner.temp }}/${{ env.test-result-directory }}/'
+ code-coverage-settings-file: '${{ github.workspace}}/code-coverage-settings.xml'
+
+ - name: 'Generate test report'
+ uses: ./.github/actions/testing/test-report
+ id: test-report
+ with:
+ test-result-folder: '${{ runner.temp }}/${{ env.test-result-directory }}/'
+
+ - name: Write test report summary
+ run: cat ${{ steps.test-report.outputs.test-report-file }} >> $GITHUB_STEP_SUMMARY
+
+ - name: 'Generate code coverage'
+ uses: ./.github/actions/testing/code-coverage
+ id: code-coverage-report
+ with:
+ test-result-folder: '${{ runner.temp }}/${{ env.test-result-directory }}/'
+
+ - name: Write code coverage report summary
+ run: cat ${{ steps.code-coverage-report.outputs.code-coverage-report-file }} >> $GITHUB_STEP_SUMMARY
+
+ pack:
+ name: 'Package binaries'
+ needs: [versioning, build]
+ runs-on: ubuntu-latest
+ env:
+ assembly-version: ${{ needs.versioning.outputs.assembly-version }}
+ assembly-informational-version: ${{ needs.versioning.outputs.assembly-informational-version }}
+ file-version: ${{ needs.versioning.outputs.file-version }}
+ release-version: ${{ needs.versioning.outputs.release-version }}
+ package-artifact-name: package
+ outputs:
+ package-artifact-name: ${{ env.package-artifact-name }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: Download Build
+ uses: actions/download-artifact@v8
+ with:
+ name: build
+
+ - name: Pack with .NET
+ run: |
+ dotnet pack ${{ vars.SRC_DEFAULT_GLOB_PATTERN }} --configuration ${{ env.build-configuration }} /p:Platform="${{ env.build-platform }}" /p:PackageVersion=${{ env.release-version }} /p:Version=${{ env.assembly-version }} /p:AssemblyInformationalVersion=${{ env.assembly-informational-version }} /p:FileVersion=${{ env.file-version }} --output ${{ runner.temp }}/${{ env.nuget-packages-directory }}
+
+ - name: Upload Package
+ uses: actions/upload-artifact@v7
+ with:
+ name: ${{ env.package-artifact-name }}
+ path: |
+ ${{ runner.temp }}/${{ env.nuget-packages-directory }}/**/*.nupkg
+ ${{ runner.temp }}/${{ env.nuget-packages-directory }}/**/*.snupkg
+
+ publish-package:
+ name: 'Publish development package'
+ needs: [pack]
+ env:
+ package-artifact-name: ${{ needs.pack.outputs.package-artifact-name }}
+ runs-on: ubuntu-latest
+ environment: 'Development'
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: 'Publish package to Azure Artifact feed'
+ uses: ./.github/actions/nuget/publish-package
+ with:
+ package-artifact-name: ${{ env.package-artifact-name }}
+ nuget-feed-url: ${{ vars.NUGET_PACKAGE_FEED_URL }}
+ nuget-feed-api-key: ${{ secrets.NUGET_PACKAGE_FEED_API_KEY }}
+ nuget-feed-server: 'AzureArtifacts'
+ working-directory: ${{ runner.temp }}/${{ env.nuget-packages-directory }}
+ dotnet-sdk-version: ${{ env.dotnet-sdk-version }}'
+
+ generate-assembly-metadata:
+ name: 'Generate assembly metadata'
+ needs: [versioning, build]
+ env:
+ friendly-version: ${{ needs.versioning.outputs.friendly-version }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ - name: 'Generate assembly metadata'
+ uses: ./.github/actions/documentation/docfx-metadata
+ with:
+ artifact-name: 'assembly-metadata'
+ docfx-json-manifest: './api-reference/assembly-metadata.json'
+ temporary-directory: './api-reference/temp'
+ output-directory: './api-reference/${{ env.friendly-version }}'
+ - name: 'Push assembly metadata artifact'
+ uses: ./.github/actions/git/push-changes
+ with:
+ artifact-name: 'assembly-metadata'
+ commit-message: 'Updated docs for version ${{ env.friendly-version }}'
+ working-directory: './api-reference/${{ env.friendly-version }}'
diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml
new file mode 100644
index 00000000..6f79680c
--- /dev/null
+++ b/.github/workflows/bump-version.yml
@@ -0,0 +1,156 @@
+name: 'Bump version'
+
+on:
+ workflow_dispatch:
+ inputs:
+ bump-type:
+ type: choice
+ options:
+ - 'minor'
+ - 'major'
+ description: 'Version bump type. Use ''minor'' to add features (keeps API files), ''major'' for breaking changes (resets API files).'
+ required: true
+
+permissions:
+ actions: read
+ contents: write
+ pull-requests: write
+
+concurrency:
+ group: bump-version
+ cancel-in-progress: false
+
+env:
+ dotnet-sdk-version: '10.x'
+
+jobs:
+ detect-version:
+ name: 'Detect current version and calculate next'
+ runs-on: ubuntu-latest
+ outputs:
+ current-version: ${{ steps.detect.outputs.current-version }}
+ next-version: ${{ steps.calculate.outputs.next-version }}
+ target-branch: ${{ steps.calculate.outputs.target-branch }}
+ steps:
+ - name: 'Checkout main'
+ uses: actions/checkout@v6
+ with:
+ ref: main
+ fetch-depth: 0
+
+ - name: 'Detect highest release branch'
+ id: detect
+ run: |
+ git fetch origin
+ latest=$(git ls-remote --heads origin 'release/*' | grep -oP 'release/\K\d+\.\d+' | sort -V | tail -1)
+ if [[ -z "$latest" ]]; then
+ latest="0.0"
+ fi
+ echo "Detected current version: $latest"
+ echo "current-version=$latest" >> $GITHUB_OUTPUT
+
+ - name: 'Calculate next version'
+ id: calculate
+ run: |
+ current="${{ steps.detect.outputs.current-version }}"
+ major="${current%%.*}"
+ minor="${current##*.}"
+ if [[ "${{ inputs.bump-type }}" == "major" ]]; then
+ next_major=$((major + 1))
+ next_version="${next_major}.0"
+ else
+ next_minor=$((minor + 1))
+ next_version="${major}.${next_minor}"
+ fi
+ echo "Next version: $next_version"
+ echo "next-version=$next_version" >> $GITHUB_OUTPUT
+ echo "target-branch=develop/${next_version}" >> $GITHUB_OUTPUT
+
+ validate:
+ name: 'Validate bump'
+ needs: [detect-version]
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Checkout main'
+ uses: actions/checkout@v6
+ with:
+ ref: main
+ fetch-depth: 1
+
+ - name: 'Validate workflow is running from main'
+ if: ${{ github.ref_name != 'main' }}
+ run: |
+ echo "This workflow must be run from the 'main' branch. Current branch: '${{ github.ref_name }}'."
+ exit 1
+
+ - name: 'Validate target branch does not exist'
+ run: |
+ set +e
+ git ls-remote --exit-code --heads origin "${{ needs.detect-version.outputs.target-branch }}"
+ if [[ $? -eq 0 ]]; then
+ echo "Target branch '${{ needs.detect-version.outputs.target-branch }}' already exists."
+ exit 1
+ fi
+ set -e
+
+ create-branch:
+ name: 'Create ${{ needs.detect-version.outputs.target-branch }}'
+ needs: [detect-version, validate]
+ runs-on: ubuntu-latest
+ env:
+ next-version: ${{ needs.detect-version.outputs.next-version }}
+ target-branch: ${{ needs.detect-version.outputs.target-branch }}
+ steps:
+ - name: 'Checkout main'
+ uses: actions/checkout@v6
+ with:
+ ref: main
+ fetch-depth: 0
+
+ - name: 'Setup .NET ${{ env.dotnet-sdk-version }}'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: 'Configure git'
+ run: |
+ git config user.name "$(git log -n 1 --pretty=format:%an)"
+ git config user.email "$(git log -n 1 --pretty=format:%ae)"
+
+ - name: 'Create develop branch from main'
+ run: |
+ git checkout -b ${{ env.target-branch }}
+
+ - name: 'Reset PublicAPI files for major bump'
+ if: ${{ inputs.bump-type == 'major' }}
+ run: |
+ find . \( -name "PublicAPI.Shipped.txt" -o -name "PublicAPI.Unshipped.txt" \) | while read file; do
+ printf '\xef\xbb\xbf#nullable enable\n' > "$file"
+ echo "Reset: $file"
+ done
+
+ - name: 'Commit API file reset'
+ if: ${{ inputs.bump-type == 'major' }}
+ run: |
+ git add .
+ git diff --staged --quiet || git commit -m "Reset PublicAPI files for major version ${{ env.next-version }}"
+
+ - name: 'Push develop branch'
+ run: |
+ git push --set-upstream origin ${{ env.target-branch }}
+
+ - name: 'Write summary'
+ run: |
+ echo "## ✅ Branch created: \`${{ env.target-branch }}\`" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| | |" >> $GITHUB_STEP_SUMMARY
+ echo "|---|---|" >> $GITHUB_STEP_SUMMARY
+ echo "| **Bump type** | ${{ inputs.bump-type }} |" >> $GITHUB_STEP_SUMMARY
+ echo "| **Previous version** | ${{ needs.detect-version.outputs.current-version }} |" >> $GITHUB_STEP_SUMMARY
+ echo "| **New version** | ${{ env.next-version }} |" >> $GITHUB_STEP_SUMMARY
+ echo "| **Target branch** | \`${{ env.target-branch }}\` |" >> $GITHUB_STEP_SUMMARY
+ if [[ "${{ inputs.bump-type }}" == "major" ]]; then
+ echo "| **API files** | Reset (major bump) |" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "| **API files** | Preserved (minor bump) |" >> $GITHUB_STEP_SUMMARY
+ fi
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
deleted file mode 100644
index 1b11d4ec..00000000
--- a/.github/workflows/codeql.yml
+++ /dev/null
@@ -1,97 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL Advanced"
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
- schedule:
- - cron: '29 17 * * 3'
-
-jobs:
- analyze:
- name: Analyze (${{ matrix.language }})
- # Runner size impacts CodeQL analysis time. To learn more, please see:
- # - https://gh.io/recommended-hardware-resources-for-running-codeql
- # - https://gh.io/supported-runners-and-hardware-resources
- # - https://gh.io/using-larger-runners (GitHub.com only)
- # Consider using larger runners or machines with greater resources for possible analysis time improvements.
- runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
- permissions:
- # required for all workflows
- security-events: write
-
- # required to fetch internal or private CodeQL packs
- packages: read
-
- # only required for workflows in private repositories
- actions: read
- contents: read
-
- strategy:
- fail-fast: false
- matrix:
- include:
- - language: csharp
- build-mode: none
- # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
- # Use `c-cpp` to analyze code written in C, C++ or both
- # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
- # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
- # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
- # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
- # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
- # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v3
- with:
- dotnet-version: 9.0.x
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v3
- with:
- languages: ${{ matrix.language }}
- build-mode: ${{ matrix.build-mode }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
-
- # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
- # queries: security-extended,security-and-quality
-
- # If the analyze step fails for one of the languages you are analyzing with
- # "We were unable to automatically build your code", modify the matrix above
- # to set the build mode to "manual" for that language. Then modify this step
- # to build your code.
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- - if: matrix.build-mode == 'manual'
- shell: bash
- run: |
- echo 'If you are using a "manual" build mode for one or more of the' \
- 'languages you are analyzing, replace this with the commands to build' \
- 'your code, for example:'
- echo ' make bootstrap'
- echo ' make release'
- exit 1
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
- with:
- category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
deleted file mode 100644
index 0bfc8aa7..00000000
--- a/.github/workflows/dotnet.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-# This workflow will build a .NET project
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
-
-name: .NET
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - name: Setup .NET
- uses: actions/setup-dotnet@v3
- with:
- dotnet-version: 8.0.x
- - name: Restore dependencies
- run: dotnet restore
- - name: Build
- run: dotnet build --no-restore
- - name: Test
- run: dotnet test --no-build --verbosity normal
diff --git a/.github/workflows/promote-branch.yml b/.github/workflows/promote-branch.yml
new file mode 100644
index 00000000..8e566ec3
--- /dev/null
+++ b/.github/workflows/promote-branch.yml
@@ -0,0 +1,227 @@
+name: 'Promote branch'
+
+on:
+ workflow_dispatch:
+ inputs:
+ promotion-type:
+ type: choice
+ options:
+ - 'preview'
+ - 'release'
+ description: 'Promotion type.'
+ required: true
+ base-branch:
+ type: string
+ description: 'Base branch to create target from.'
+ default: 'main'
+ required: true
+
+permissions:
+ actions: read
+ id-token: write
+ contents: write
+ pull-requests: write
+
+concurrency:
+ group: 'promote-branch-${{ inputs.promotion-type }}-${{github.ref_name }}'
+ cancel-in-progress: false
+
+env:
+ dotnet-sdk-version: '10.x'
+ is-development-branch: ${{ startsWith(github.ref_name, 'develop') }}
+ is-maintenance-branch: ${{ startsWith(github.ref_name, 'support') }}
+ is-preview-branch: ${{ startsWith(github.ref_name, 'preview') }}
+
+jobs:
+ versioning:
+ name: 'Extract version'
+ runs-on: ubuntu-latest
+ outputs:
+ friendly-version: ${{ steps.extract-version.outputs.version }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ - name: 'Setup .NET ${{ env.dotnet-sdk-version }}'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+ - name: 'Extract version from branch name'
+ id: extract-version
+ uses: './.github/actions/versioning/extract-version'
+ with:
+ branch-name: ${{ github.ref_name }}
+ workflow-variables:
+ name: 'Set workflow variables'
+ needs: [versioning]
+ runs-on: ubuntu-latest
+ outputs:
+ base-branch: ${{ steps.set-base-branch.outputs.base-branch }}
+ target-branch: ${{ steps.set-target-branch.outputs.target-branch }}
+ target-branch-exists: ${{ steps.check-target-branch-exists.outputs.target-branch-exists }}
+ pull-request-exists: ${{ steps.check-pull-request-exists.outputs.pull-request-exists }}
+ steps:
+ - name: 'Set target branch'
+ id: set-target-branch
+ run: |
+ if [[ "${{ inputs.promotion-type }}" == "preview" ]]; then
+ target_branch="preview/${{ needs.versioning.outputs.friendly-version }}"
+ elif [[ "${{ inputs.promotion-type }}" == "release" ]]; then
+ target_branch="release/${{ needs.versioning.outputs.friendly-version }}"
+ fi
+
+ echo "Setting target branch $target_branch."
+
+ echo "target-branch=$target_branch" >> $GITHUB_OUTPUT
+ - name: 'Set base branch'
+ id: set-base-branch
+ run: |
+ base_branch=${{ inputs.base-branch }}
+
+ echo "Setting base branch $base_branch."
+
+ echo "base-branch=$base_branch" >> $GITHUB_OUTPUT
+ - name: 'Check if base branch exists'
+ id: check-target-branch-exists
+ env:
+ target-branch: ${{ steps.set-target-branch.outputs.target-branch }}
+ run: |
+ set +e
+
+ git ls-remote --exit-code --heads origin ${{ env.target-branch }}
+
+ if [[ $? -eq 0 ]]; then
+ echo "Target branch ${{ env.target-branch }} does exist."
+ target_branch_exists="true"
+ else
+ echo "Target branch ${{ env.target-branch }} does not exist."
+ target_branch_exists="false"
+ fi
+
+ echo "target-branch-exists=$target_branch_exists" >> $GITHUB_OUTPUT
+
+ set -e
+ - name: 'Check if base branch exists'
+ id: check-base-branch-exists
+ env:
+ base-branch: ${{ steps.set-base-branch.outputs.base-branch }}
+ run: |
+ set +e
+
+ git ls-remote --exit-code --heads origin ${{ env.base-branch }}
+
+ if [[ $? -eq 0 ]]; then
+ echo "Base branch ${{ env.base-branch }} does exist."
+ base_branch_exists="true"
+ else
+ echo "Base branch ${{ env.base-branch }} does not exist."
+ base_branch_exists="false"
+ fi
+
+ echo "base-branch-exists=$base_branch_exists" >> $GITHUB_OUTPUT
+
+ set -e
+ - name: 'Check if pull request exists exists'
+ id: check-pull-request-exists
+ env:
+ GH_TOKEN: ${{ github.token }}
+ GH_REPO: ${{ github.repository_owner }}/${{ github.event.repository.name }}
+ current-branch: ${{ github.ref_name }}
+ target-branch: ${{ steps.set-target-branch.outputs.target-branch }}
+ run: |
+ pull_request_count=$(gh pr list --head ${{ env.current-branch }} --base ${{ env.target-branch }} --state open --limit 1 --json id --jq '. | length')
+
+ if [[ $pull_request_count -eq 0 ]]; then
+ echo "Pull request does not exist."
+ pull_request_exists="false"
+ else
+ echo "Pull request does exist."
+ pull_request_exists="true"
+ fi
+
+ echo "pull-request-exists=$pull_request_exists" >> $GITHUB_OUTPUT
+
+ validate-promotion:
+ name: 'Validate promotion'
+ needs: [ versioning, workflow-variables ]
+ runs-on: ubuntu-latest
+ env:
+ promotion-type: ${{ inputs.promotion-type }}
+ base-branch: ${{ needs.workflow-variables.outputs.base-branch }}
+ current-branch: ${{ github.ref_name }}
+ target-branch: ${{ needs.workflow-variables.outputs.target-branch }}
+ pull-request-exists: ${{ needs.workflow-variables.outputs.pull-request-exists }}
+ outputs:
+ target-branch: ${{ env.target-branch }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ - name: 'Check promotion type'
+ if: ${{ (env.promotion-type != 'release') && (env.promotion-type != 'preview') }}
+ run: |
+ echo "Invalid promotion type ${{ inputs.promotion-type }}"
+ exit 1
+ - name: 'Validate source branch for preview promotion'
+ if: ${{ env.promotion-type == 'preview' && (env.is-development-branch == 'false') && (env.is-maintenance-branch == 'false') }}
+ run: |
+ echo "Preview promotion requires a 'develop/**' or 'support/**' source branch. Current branch: '${{ github.ref_name }}'"
+ exit 1
+ - name: 'Validate source branch for release promotion'
+ if: ${{ env.promotion-type == 'release' && env.is-preview-branch == 'false' }}
+ run: |
+ echo "Release promotion requires a 'preview/**' source branch. Current branch: '${{ github.ref_name }}'"
+ exit 1
+ - name: 'Validate default and current branch'
+ if: ${{ env.base-branch == env.current-branch }}
+ run: |
+ echo "Default and current branch cannot be the same."
+ echo "Default branch is '${{ env.base-branch }}'"
+ echo "Current branch is '${{ env.current-branch }}'"
+ exit 1
+
+ - name: 'Validate target and current branch'
+ if: ${{ env.target-branch == env.current-branch }}
+ run: |
+ echo "Default and target branch cannot be the same."
+ echo "Default branch is '${{ env.target-branch }}'"
+ echo "Current branch is '${{ env.current-branch }}'"
+ exit 1
+
+ - name: 'Validate pull request'
+ if: ${{ env.pull-request-exists == 'true' }}
+ run: |
+ echo "Pull request exists."
+ exit 1
+
+ promote-branch:
+ name: 'Promote branch ${{ github.ref_name }} to ${{ needs.workflow-variables.outputs.target-branch }}'
+ needs: [ workflow-variables, validate-promotion ]
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ - name: 'Setup .NET ${{ env.dotnet-sdk-version }}'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+ - name: 'Create target branch'
+ if: ${{ needs.workflow-variables.outputs.target-branch-exists == 'false' }}
+ env:
+ base-branch: ${{ needs.workflow-variables.outputs.base-branch }}
+ target-branch: ${{ needs.workflow-variables.outputs.target-branch }}
+ run: |
+ git fetch origin
+ git push origin origin/${{ env.base-branch }}:refs/heads/${{ env.target-branch }}
+ - name: 'Lock target branch'
+ if: ${{ needs.workflow-variables.outputs.target-branch-exists == 'false' }}
+ uses: './.github/actions/github/branch-protection/lock'
+ with:
+ branch: ${{ needs.workflow-variables.outputs.target-branch }}
+ token: ${{ secrets.GH_ADMIN_TOKEN }}
+ - name: 'Create PR: "Promote ${{ env.current-branch }} to ${{ env.target-branch }}"'
+ if: ${{ needs.workflow-variables.outputs.pull-request-exists == 'false' }}
+ env:
+ GH_TOKEN: ${{ github.token }}
+ current-branch: ${{ github.ref_name }}
+ target-branch: ${{ needs.workflow-variables.outputs.target-branch }}
+ run: |
+ gh pr create --title "Promote ${{ env.current-branch }} to ${{ env.target-branch }}" --fill --base ${{ env.target-branch }} --head ${{ env.current-branch }}
diff --git a/.github/workflows/publish-documentation.yml b/.github/workflows/publish-documentation.yml
new file mode 100644
index 00000000..2a579943
--- /dev/null
+++ b/.github/workflows/publish-documentation.yml
@@ -0,0 +1,174 @@
+name: 'Publish documentation'
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - 'release/**'
+ paths:
+ - 'src/**'
+ - 'api-reference/**'
+
+permissions:
+ actions: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: publish-docs-${{ github.head_ref || github.ref }}
+ cancel-in-progress: true
+
+env:
+ dotnet-sdk-version: '10.x'
+
+jobs:
+ validate-branch:
+ name: 'Validate branch'
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Ensure documentation is published from a release branch'
+ if: ${{ !startsWith(github.ref_name, 'release/') }}
+ run: |
+ echo "Documentation should only be published from 'release/**' branches."
+ echo "Current branch: '${{ github.ref_name }}'"
+ exit 1
+
+ workflow-variables:
+ name: 'Workflow variables'
+ needs: [validate-branch]
+ runs-on: ubuntu-latest
+
+ outputs:
+ github-run-number: ${{ github.run_number }}
+
+ steps:
+ - name: 'Workflow variables'
+ id: github
+ run: |
+ echo "github-run-number:${{ github.run_number }}"
+
+ versioning:
+ name: 'Extract version'
+ needs: [workflow-variables]
+ runs-on: ubuntu-latest
+ outputs:
+ friendly-version: ${{ steps.versioning.outputs.version }}
+
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0 # Ensure the full git history is available for versioning
+ - name: 'Setup .NET ${{ env.dotnet-sdk-version }}'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+ - name: 'Extract version from branch name'
+ id: versioning
+ uses: './.github/actions/versioning/extract-version'
+ with:
+ branch-name: ${{ github.ref_name }}
+
+ generate-docs:
+ name: 'Generate documentation'
+ needs: [workflow-variables, versioning]
+ runs-on: ubuntu-latest
+ env:
+ friendly-version: ${{ needs.versioning.outputs.friendly-version }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Setup .NET ${{ env.dotnet-sdk-version }}'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: 'Install docfx'
+ shell: bash
+ run: dotnet tool update -g docfx
+
+ - name: 'Regenerate API metadata for v${{ env.friendly-version }}'
+ shell: bash
+ run: |
+ rm -rf api-reference/${{ env.friendly-version }}
+ cd api-reference
+ docfx metadata assembly-metadata.json
+ mv temp ${{ env.friendly-version }}
+
+ - name: 'Discover all versions'
+ id: discover-versions
+ shell: bash
+ run: |
+ versions=$(ls api-reference/ | grep -E '^\d+\.\d+$' | sort -V | paste -sd ',' -)
+ latest=$(ls api-reference/ | grep -E '^\d+\.\d+$' | sort -V | tail -1)
+ echo "versions=$versions" >> $GITHUB_OUTPUT
+ echo "latest=$latest" >> $GITHUB_OUTPUT
+
+ - name: 'Update versions.json'
+ shell: bash
+ run: |
+ versions_array=$(echo "${{ steps.discover-versions.outputs.versions }}" | tr ',' '\n' | jq -R . | jq -s .)
+ jq --arg latest "${{ steps.discover-versions.outputs.latest }}" \
+ --argjson versions "$versions_array" \
+ '.latest = $latest | .versions = $versions' \
+ api-reference/versions.json > /tmp/versions.json
+ mv /tmp/versions.json api-reference/versions.json
+
+ - name: 'Update api-reference.json with version groups'
+ shell: bash
+ run: |
+ cp api-reference/api-reference.json /tmp/api-reference.json
+ for ver in $(echo "${{ steps.discover-versions.outputs.versions }}" | tr ',' ' '); do
+ jq --arg ver "$ver" --arg group "v${ver}" \
+ '.build.content += [{"dest": "", "files": ["*.yml"], "group": $group, "src": $ver, "rootTocPath": "~/toc.html"}] |
+ .build.groups = (.build.groups // {}) |
+ .build.groups[$group] = {"dest": $ver}' \
+ /tmp/api-reference.json > /tmp/api-reference-new.json
+ mv /tmp/api-reference-new.json /tmp/api-reference.json
+ done
+ mv /tmp/api-reference.json api-reference/api-reference.json
+
+ - name: 'Update toc.yml with Reference dropdown'
+ shell: bash
+ run: |
+ latest="${{ steps.discover-versions.outputs.latest }}"
+ {
+ echo "### YamlMime:TableOfContent"
+ echo "- name: Guide"
+ echo " href: guide/"
+ echo "- name: Reference"
+ echo " dropdown: true"
+ echo " items:"
+ for ver in $(echo "${{ steps.discover-versions.outputs.versions }}" | tr ',' '\n' | sort -Vr); do
+ if [ "$ver" = "$latest" ]; then
+ echo " - name: v${ver} (latest)"
+ else
+ echo " - name: v${ver}"
+ fi
+ echo " href: ${ver}/PolylineAlgorithm.html"
+ done
+ } > api-reference/toc.yml
+
+ - name: 'Build documentation'
+ shell: bash
+ run: docfx build api-reference/api-reference.json
+
+ - name: 'Upload artifact'
+ uses: actions/upload-pages-artifact@v4
+ with:
+ name: github-pages
+ path: './api-reference/_docs'
+
+ publish-docs:
+ name: 'Publish documentation'
+ needs: [workflow-variables, versioning, generate-docs]
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+
+ - name: 'Deploy to GitHub Pages'
+ id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
new file mode 100644
index 00000000..8ea9622c
--- /dev/null
+++ b/.github/workflows/pull-request.yml
@@ -0,0 +1,258 @@
+name: 'Pull Request'
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ - reopened
+ branches:
+ - 'preview/**'
+ - 'release/**'
+
+permissions:
+ actions: read
+ pages: write
+ id-token: write
+ contents: write
+
+concurrency:
+ group: pull-request-${{ github.head_ref || github.ref }}
+ cancel-in-progress: true
+
+env:
+ dotnet-sdk-version: '10.x'
+ build-configuration: 'Release'
+ build-platform: 'Any CPU'
+ git-version: '6.0.x'
+ test-result-directory: 'test-results'
+ nuget-packages-directory: 'nuget-packages'
+
+jobs:
+ workflow-variables:
+ name: 'Set workflow variables'
+ runs-on: ubuntu-latest
+
+ outputs:
+ is-release: ${{ startsWith(github.base_ref, 'release') }}
+ is-preview: ${{ startsWith(github.base_ref , 'preview') }}
+
+ steps:
+ - name: 'Set workflow variables'
+ run: |
+ echo "is-release:${{ startsWith(github.base_ref, 'release') }}"
+ echo "is-preview:${{ startsWith(github.base_ref, 'preview') }}"
+
+ versioning:
+ name: 'Extract version from branch'
+ runs-on: ubuntu-latest
+ needs: [workflow-variables]
+ outputs:
+ friendly-version: ${{ steps.format-version.outputs.friendly-version }}
+ assembly-version: ${{ steps.format-version.outputs.assembly-version }}
+ assembly-informational-version: ${{ steps.format-version.outputs.assembly-informational-version }}
+ file-version: ${{ steps.format-version.outputs.file-version }}
+ release-version: ${{ steps.format-version.outputs.release-version }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ - name: 'Setup .NET ${{ env.dotnet-sdk-version }}'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+ - name: 'Extract version from branch name'
+ id: extract-version
+ uses: './.github/actions/versioning/extract-version'
+ with:
+ branch-name: ${{ github.base_ref }}
+ - name: 'Create build number'
+ shell: bash
+ id: create-build-number
+ run: |
+ git fetch --unshallow --filter=tree:0
+ build_number=$(git rev-list --count origin/${{ github.base_ref }} ^origin/main)
+ echo "build-number=$build_number" >> $GITHUB_OUTPUT
+ - name: 'Create pre-release tag'
+ shell: bash
+ id: create-pre-release-tag
+ env:
+ build-number: ${{ steps.create-build-number.outputs.build-number }}
+ run: |
+ if [[ '${{ needs.workflow-variables.outputs.is-release }}' == 'true' ]]; then
+ echo "pre-release-tag=" >> $GITHUB_OUTPUT
+ elif [[ '${{ needs.workflow-variables.outputs.is-preview }}' == 'true' ]]; then
+ pre_release_tag='preview'
+ echo "pre-release-tag=$pre_release_tag" >> $GITHUB_OUTPUT
+ else
+ pre_release_tag=$(echo ${{ github.base_ref }} | tr '/' '-' | tr '.' '-'| tr '_' '-')
+ echo "pre-release-tag=$pre_release_tag" >> $GITHUB_OUTPUT
+ fi
+ - name: 'Format version'
+ id: format-version
+ uses: ./.github/actions/versioning/format-version
+ with:
+ version: ${{ steps.extract-version.outputs.version }}
+ patch: ${{ github.run_number }}
+ build-number: ${{ steps.create-build-number.outputs.build-number }}
+ sha: ${{ github.sha }}
+ pre-release-tag: ${{ steps.create-pre-release-tag.outputs.pre-release-tag }}
+
+ build:
+ name: 'Compile source code'
+ needs: [workflow-variables, versioning]
+ runs-on: ubuntu-latest
+
+ env:
+ assembly-version: ${{ needs.versioning.outputs.assembly-version }}
+ assembly-informational-version: ${{ needs.versioning.outputs.assembly-informational-version }}
+ file-version: ${{ needs.versioning.outputs.file-version }}
+
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Compile source code'
+ uses: ./.github/actions/source/compile
+ with:
+ project-path: '**/PolylineAlgorithm.csproj'
+ assembly-version: ${{ env.assembly-version }}
+ assembly-informational-version: ${{ env.assembly-informational-version }}
+ file-version: ${{ env.file-version }}
+ treat-warnins-as-error: ${{ needs.workflow-variables.outputs.is-release }}
+
+ test:
+ name: 'Run tests'
+ needs: [build]
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Setup .NET'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: 'Run tests'
+ uses: ./.github/actions/testing/test
+ with:
+ project-path: './tests/PolylineAlgorithm.Tests/PolylineAlgorithm.Tests.csproj'
+ test-results-directory: '${{ runner.temp }}/${{ env.test-result-directory }}/'
+ code-coverage-settings-file: '${{ github.workspace}}/code-coverage-settings.xml'
+
+ - name: 'Generate test report'
+ uses: ./.github/actions/testing/test-report
+ id: test-report
+ with:
+ test-result-folder: '${{ runner.temp }}/${{ env.test-result-directory }}/'
+
+ - name: Write test report summary
+ run: cat ${{ steps.test-report.outputs.test-report-file }} >> $GITHUB_STEP_SUMMARY
+
+ - name: 'Generate code coverage'
+ uses: ./.github/actions/testing/code-coverage
+ id: code-coverage-report
+ with:
+ test-result-folder: '${{ runner.temp }}/${{ env.test-result-directory }}/'
+
+ - name: Write code coverage report summary
+ run: cat ${{ steps.code-coverage-report.outputs.code-coverage-report-file }} >> $GITHUB_STEP_SUMMARY
+
+ pack:
+ name: 'Package binaries'
+ needs: [versioning, build]
+ runs-on: ubuntu-latest
+ env:
+ assembly-version: ${{ needs.versioning.outputs.assembly-version }}
+ assembly-informational-version: ${{ needs.versioning.outputs.assembly-informational-version }}
+ file-version: ${{ needs.versioning.outputs.file-version }}
+ release-version: ${{ needs.versioning.outputs.release-version }}
+ package-artifact-name: package
+ outputs:
+ package-artifact-name: ${{ env.package-artifact-name }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: Download Build
+ uses: actions/download-artifact@v8
+ with:
+ name: build
+
+ - name: Pack with .NET
+ run: |
+ dotnet pack ${{ vars.SRC_DEFAULT_GLOB_PATTERN }} --configuration ${{ env.build-configuration }} /p:Platform="${{ env.build-platform }}" /p:PackageVersion=${{ env.release-version }} /p:Version=${{ env.assembly-version }} /p:AssemblyInformationalVersion=${{ env.assembly-informational-version }} /p:FileVersion=${{ env.file-version }} --output ${{ runner.temp }}/${{ env.nuget-packages-directory }}
+
+ - name: Upload Package
+ uses: actions/upload-artifact@v7
+ with:
+ name: ${{ env.package-artifact-name }}
+ path: |
+ ${{ runner.temp }}/${{ env.nuget-packages-directory }}/**/*.nupkg
+ ${{ runner.temp }}/${{ env.nuget-packages-directory }}/**/*.snupkg
+
+ publish-development-package:
+ name: 'Publish development package'
+ needs: [pack]
+ env:
+ package-artifact-name: ${{ needs.pack.outputs.package-artifact-name }}
+ runs-on: ubuntu-latest
+ environment: 'Development'
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: 'Publish package to Azure Artifact feed'
+ uses: ./.github/actions/nuget/publish-package
+ with:
+ package-artifact-name: ${{ env.package-artifact-name }}
+ nuget-feed-url: ${{ vars.NUGET_PACKAGE_FEED_URL }}
+ nuget-feed-api-key: ${{ secrets.NUGET_PACKAGE_FEED_API_KEY }}
+ nuget-feed-server: 'AzureArtifacts'
+ working-directory: ${{ runner.temp }}/${{ env.nuget-packages-directory }}
+ dotnet-sdk-version: ${{ env.dotnet-sdk-version }}'
+
+ benchmark:
+ if: ${{ github.env.is_release || vars.BENCHMARKDOTNET_RUN_OVERRIDE == 'true' }}
+ name: Benchmark with .NET CLI on ${{ matrix.os }}
+ needs: [build]
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ - name: Install .NET SDK
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: |
+ 8.x
+ 10.x
+ - name: Download Build
+ uses: actions/download-artifact@v8
+ with:
+ name: build
+ - name: Benchmark
+ working-directory: ${{ vars.BENCHMARKDOTNET_WORKING_DIRECTORY }}
+ run: dotnet run --configuration ${{ env.build-configuration }} /p:Platform=${{ env.build-platform }} --framework ${{ vars.DEFAULT_BUILD_FRAMEWORK }} --runtimes ${{ vars.BENCHMARKDOTNET_RUNTIMES }} --filter ${{ vars.BENCHMARKDOTNET_FILTER }} --artifacts ${{ runner.temp }}/benchmarks/ --exporters GitHub --memory --iterationTime 100 --join
+ - name: Upload Benchmark Results
+ uses: actions/upload-artifact@v7
+ with:
+ name: benchmark-${{ matrix.os }}
+ path: |
+ ${{ runner.temp }}/benchmarks/**/*-report-github.md
+ - name: Write Benchmark Summary
+ shell: bash
+ run: cat **/*-report-github.md > $GITHUB_STEP_SUMMARY
+ working-directory: ${{ runner.temp }}/benchmarks/
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..b8113bf4
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,368 @@
+name: 'Release'
+
+on:
+ push:
+ branches:
+ - 'preview/**'
+ - 'release/**'
+ paths:
+ - 'src/**'
+
+permissions:
+ actions: read
+ pages: write
+ id-token: write
+ contents: write
+
+concurrency:
+ group: release-${{ github.head_ref || github.ref }}
+ cancel-in-progress: false
+
+env:
+ dotnet-sdk-version: '10.x'
+ build-configuration: 'Release'
+ build-platform: 'Any CPU'
+ git-version: '6.0.x'
+ test-result-directory: 'test-results'
+ nuget-packages-directory: 'nuget-packages'
+
+jobs:
+ workflow-variables:
+ name: 'Set workflow variables'
+ runs-on: ubuntu-latest
+
+ outputs:
+ is-release: ${{ startsWith(github.ref_name, 'release') }}
+ is-preview: ${{ startsWith(github.ref_name, 'preview') }}
+
+ steps:
+ - name: 'Set workflow variables'
+ id: github
+ run: |
+ echo "is-release:${{ startsWith(github.ref_name, 'release') }}"
+ echo "is-preview:${{ startsWith(github.ref_name, 'preview') }}"
+
+ validate-release:
+ name: 'Validate release'
+ needs: [workflow-variables]
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Validate release branch'
+ if: ${{ needs.workflow-variables.outputs.is-release != 'true' && needs.workflow-variables.outputs.is-preview != 'true' }}
+ run: |
+ echo "This workflow can only be run on 'release/**' or 'preview/**' branches."
+ exit 1
+
+ versioning:
+ name: 'Extract version from branch'
+ runs-on: ubuntu-latest
+ needs: [workflow-variables, validate-release]
+ outputs:
+ friendly-version: ${{ steps.format-version.outputs.friendly-version }}
+ assembly-version: ${{ steps.format-version.outputs.assembly-version }}
+ assembly-informational-version: ${{ steps.format-version.outputs.assembly-informational-version }}
+ file-version: ${{ steps.format-version.outputs.file-version }}
+ release-version: ${{ steps.format-version.outputs.release-version }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ - name: 'Setup .NET ${{ env.dotnet-sdk-version }}'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+ - name: 'Extract version from branch name'
+ id: extract-version
+ uses: './.github/actions/versioning/extract-version'
+ with:
+ branch-name: ${{ github.ref_name }}
+ - name: 'Create build number'
+ shell: bash
+ id: create-build-number
+ run: |
+ git fetch --unshallow --filter=tree:0
+ build_number=$(git rev-list --count origin/${{ github.ref_name }} ^origin/main)
+ echo "build-number=$build_number" >> $GITHUB_OUTPUT
+ - name: 'Create pre-release tag'
+ shell: bash
+ id: create-pre-release-tag
+ env:
+ build-number: ${{ steps.create-build-number.outputs.build-number }}
+ run: |
+ if [[ '${{ needs.workflow-variables.outputs.is-release }}' == 'true' ]]; then
+ echo "pre-release-tag=" >> $GITHUB_OUTPUT
+ elif [[ '${{ needs.workflow-variables.outputs.is-preview }}' == 'true' ]]; then
+ pre_release_tag='preview'
+ echo "pre-release-tag=$pre_release_tag" >> $GITHUB_OUTPUT
+ else
+ pre_release_tag=$(echo ${{ github.ref_name }} | tr '/' '-' | tr '.' '-'| tr '_' '-')
+ echo "pre-release-tag=$pre_release_tag" >> $GITHUB_OUTPUT
+ fi
+ - name: 'Format version'
+ id: format-version
+ uses: ./.github/actions/versioning/format-version
+ with:
+ version: ${{ steps.extract-version.outputs.version }}
+ patch: ${{ github.run_number }}
+ build-number: ${{ steps.create-build-number.outputs.build-number }}
+ sha: ${{ github.sha }}
+ pre-release-tag: ${{ steps.create-pre-release-tag.outputs.pre-release-tag }}
+
+ build:
+ name: 'Compile source code'
+ needs: [workflow-variables, versioning, validate-release]
+ runs-on: ubuntu-latest
+
+ env:
+ assembly-version: ${{ needs.versioning.outputs.assembly-version }}
+ assembly-informational-version: ${{ needs.versioning.outputs.assembly-informational-version }}
+ file-version: ${{ needs.versioning.outputs.file-version }}
+
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Compile source code'
+ uses: ./.github/actions/source/compile
+ with:
+ project-path: '**/PolylineAlgorithm.csproj'
+ assembly-version: ${{ env.assembly-version }}
+ assembly-informational-version: ${{ env.assembly-informational-version }}
+ file-version: ${{ env.file-version }}
+ treat-warnins-as-error: ${{ needs.workflow-variables.outputs.is-release }}
+
+ test:
+ name: 'Run tests'
+ needs: [build, validate-release]
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Setup .NET'
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: 'Run tests'
+ uses: ./.github/actions/testing/test
+ with:
+ project-path: './tests/PolylineAlgorithm.Tests/PolylineAlgorithm.Tests.csproj'
+ test-results-directory: '${{ runner.temp }}/${{ env.test-result-directory }}/'
+ code-coverage-settings-file: '${{ github.workspace}}/code-coverage-settings.xml'
+
+ - name: 'Generate test report'
+ uses: ./.github/actions/testing/test-report
+ id: test-report
+ with:
+ test-result-folder: '${{ runner.temp }}/${{ env.test-result-directory }}/'
+
+ - name: Write test report summary
+ run: cat ${{ steps.test-report.outputs.test-report-file }} >> $GITHUB_STEP_SUMMARY
+
+ - name: 'Generate code coverage'
+ uses: ./.github/actions/testing/code-coverage
+ id: code-coverage-report
+ with:
+ test-result-folder: '${{ runner.temp }}/${{ env.test-result-directory }}/'
+
+ - name: Write code coverage report summary
+ run: cat ${{ steps.code-coverage-report.outputs.code-coverage-report-file }} >> $GITHUB_STEP_SUMMARY
+
+ pack:
+ name: 'Package binaries'
+ needs: [versioning, build, test, validate-release]
+ runs-on: ubuntu-latest
+ env:
+ assembly-version: ${{ needs.versioning.outputs.assembly-version }}
+ assembly-informational-version: ${{ needs.versioning.outputs.assembly-informational-version }}
+ file-version: ${{ needs.versioning.outputs.file-version }}
+ release-version: ${{ needs.versioning.outputs.release-version }}
+ package-artifact-name: package
+ outputs:
+ package-artifact-name: ${{ env.package-artifact-name }}
+ steps:
+ - name: 'Checkout ${{ github.base_ref }}'
+ uses: actions/checkout@v6
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: Download Build
+ uses: actions/download-artifact@v8
+ with:
+ name: build
+
+ - name: Pack with .NET
+ run: |
+ dotnet pack ${{ vars.SRC_DEFAULT_GLOB_PATTERN }} --configuration ${{ env.build-configuration }} /p:Platform="${{ env.build-platform }}" /p:PackageVersion=${{ env.release-version }} /p:Version=${{ env.assembly-version }} /p:AssemblyInformationalVersion=${{ env.assembly-informational-version }} /p:FileVersion=${{ env.file-version }} --output ${{ runner.temp }}/${{ env.nuget-packages-directory }}
+
+ - name: Upload Package
+ uses: actions/upload-artifact@v7
+ with:
+ name: ${{ env.package-artifact-name }}
+ path: |
+ ${{ runner.temp }}/${{ env.nuget-packages-directory }}/**/*.nupkg
+ ${{ runner.temp }}/${{ env.nuget-packages-directory }}/**/*.snupkg
+
+ publish-package:
+ name: 'Publish package'
+ needs: [pack, validate-release]
+ env:
+ package-artifact-name: ${{ needs.pack.outputs.package-artifact-name }}
+ runs-on: ubuntu-latest
+ environment: 'NuGet'
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ env.dotnet-sdk-version }}
+
+ - name: 'Publish package to Azure Artifact feed'
+ uses: ./.github/actions/nuget/publish-package
+ with:
+ package-artifact-name: ${{ env.package-artifact-name }}
+ nuget-feed-url: ${{ vars.NUGET_PACKAGE_FEED_URL }}
+ nuget-feed-api-key: ${{ secrets.NUGET_PACKAGE_FEED_API_KEY }}
+ nuget-feed-server: 'NuGet'
+ working-directory: ${{ runner.temp }}/${{ env.nuget-packages-directory }}
+ dotnet-sdk-version: ${{ env.dotnet-sdk-version }}'
+
+ release:
+ name: 'Create release'
+ needs: [workflow-variables, publish-package, validate-release, versioning]
+ runs-on: ubuntu-latest
+ env:
+ release-version: ${{ needs.versioning.outputs.release-version }}
+ is-preview: ${{ needs.workflow-variables.outputs.is-preview }}
+ notes-start-tag: ${{ needs.workflow-variables.outputs.notes-start-tag }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+
+ - name: 'Determine notes start tag'
+ id: determine-notes-start-tag
+ run: |
+ notes-start-tag=$(git describe --abbrev=0 --tags)
+ echo "notes-start-tag=$notes-start-tag" >> $GITHUB_OUTPUT
+ shell: bash
+
+ - name: 'Create GitHub Release'
+ uses: ./.github/actions/github/create-release
+ with:
+ release-version: ${{ env.release-version }}
+ is-preview: ${{ env.is-preview }}
+ notes-start-tag: ${{ steps.determine-notes-start-tag.outputs.notes-start-tag }}
+
+ merge-to-main:
+ name: 'Merge ${{ github.ref_name }} into main'
+ needs: [workflow-variables, release, versioning]
+ if: ${{ needs.workflow-variables.outputs.is-release == 'true' }}
+ runs-on: ubuntu-latest
+ env:
+ GH_TOKEN: ${{ github.token }}
+ current-branch: ${{ github.ref_name }}
+ current-version: ${{ needs.versioning.outputs.friendly-version }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: 'Detect if current branch is the latest release'
+ id: detect-latest
+ run: |
+ git fetch origin
+ latest_version=$(git ls-remote --heads origin 'release/*' | grep -oP 'release/\K\d+\.\d+' | sort -V | tail -1)
+ current_version=$(echo "${{ env.current-version }}" | grep -oP '^\d+\.\d+')
+ echo "Latest release branch version: $latest_version"
+ echo "Current version (normalized): $current_version"
+ if [[ "$latest_version" == "$current_version" ]]; then
+ echo "is-latest=true" >> $GITHUB_OUTPUT
+ else
+ echo "is-latest=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: 'Check if PR to main already exists'
+ id: check-pr
+ if: ${{ steps.detect-latest.outputs.is-latest == 'true' }}
+ run: |
+ pr_count=$(gh pr list --head "${{ env.current-branch }}" --base main --state open --limit 1 --json id --jq '. | length')
+ if [[ $pr_count -gt 0 ]]; then
+ echo "pr-exists=true" >> $GITHUB_OUTPUT
+ else
+ echo "pr-exists=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: 'Create PR: Merge ${{ env.current-branch }} into main'
+ if: ${{ steps.detect-latest.outputs.is-latest == 'true' && steps.check-pr.outputs.pr-exists == 'false' }}
+ run: |
+ gh pr create \
+ --title "Merge ${{ env.current-branch }} into main" \
+ --fill \
+ --base main \
+ --head "${{ env.current-branch }}"
+
+ - name: 'Write merge summary'
+ run: |
+ if [[ "${{ steps.detect-latest.outputs.is-latest }}" == "true" ]]; then
+ echo "✅ PR created to merge **${{ env.current-branch }}** into **main**." >> $GITHUB_STEP_SUMMARY
+ else
+ echo "⏭️ Skipped merge to main: **${{ env.current-branch }}** is not the highest release branch." >> $GITHUB_STEP_SUMMARY
+ fi
+
+ create-support-branch:
+ name: 'Create support branch for ${{ github.ref_name }}'
+ needs: [workflow-variables, release, versioning]
+ if: ${{ needs.workflow-variables.outputs.is-release == 'true' }}
+ runs-on: ubuntu-latest
+ env:
+ current-version: ${{ needs.versioning.outputs.friendly-version }}
+ steps:
+ - name: 'Checkout ${{ github.head_ref || github.ref }}'
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: 'Resolve support branch name'
+ id: resolve-support-branch
+ run: |
+ major_minor=$(echo "${{ env.current-version }}" | grep -oP '^\d+\.\d+')
+ echo "support-branch=support/$major_minor" >> $GITHUB_OUTPUT
+
+ - name: 'Check if support branch already exists'
+ id: check-support-branch
+ run: |
+ git fetch origin
+ if git ls-remote --exit-code --heads origin "${{ steps.resolve-support-branch.outputs.support-branch }}" > /dev/null 2>&1; then
+ echo "support-branch-exists=true" >> $GITHUB_OUTPUT
+ else
+ echo "support-branch-exists=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: 'Create support branch'
+ if: ${{ steps.check-support-branch.outputs.support-branch-exists == 'false' }}
+ run: |
+ git config user.name "$(git log -n 1 --pretty=format:%an)"
+ git config user.email "$(git log -n 1 --pretty=format:%ae)"
+ git checkout -b "${{ steps.resolve-support-branch.outputs.support-branch }}"
+ git push --set-upstream origin "${{ steps.resolve-support-branch.outputs.support-branch }}"
+
+ - name: 'Lock support branch'
+ if: ${{ steps.check-support-branch.outputs.support-branch-exists == 'false' }}
+ uses: './.github/actions/github/branch-protection/lock'
+ with:
+ branch: ${{ steps.resolve-support-branch.outputs.support-branch }}
+ token: ${{ secrets.GH_ADMIN_TOKEN }}
+
+ - name: 'Write support branch summary'
+ run: |
+ if [[ "${{ steps.check-support-branch.outputs.support-branch-exists }}" == "false" ]]; then
+ echo "✅ Created and locked support branch **${{ steps.resolve-support-branch.outputs.support-branch }}**." >> $GITHUB_STEP_SUMMARY
+ else
+ echo "⏭️ Support branch **${{ steps.resolve-support-branch.outputs.support-branch }}** already exists." >> $GITHUB_STEP_SUMMARY
+ fi
diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
deleted file mode 100644
index 1526b959..00000000
--- a/.github/workflows/static.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-# Your GitHub workflow file under .github/workflows/
-# Trigger the action on push to main
-on:
- push:
- branches:
- - main
-
-# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
-permissions:
- actions: read
- pages: write
- id-token: write
-
-# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
-# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
-concurrency:
- group: "pages"
- cancel-in-progress: false
-
-jobs:
- publish-docs:
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v3
- - name: Dotnet Setup
- uses: actions/setup-dotnet@v3
- with:
- dotnet-version: 8.x
-
- - run: dotnet tool update -g docfx
- - run: docfx ./docs/docfx.json
-
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v3
- with:
- # Upload entire repository
- path: './docs/_site'
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index 3c4efe20..2118d126 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-## Ignore Visual Studio temporary files, build results, and
+## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
@@ -258,4 +258,13 @@ paket-files/
# Python Tools for Visual Studio (PTVS)
__pycache__/
-*.pyc
\ No newline at end of file
+*.pyc
+
+# BenchmarkDotNet artifacts
+**/BenchmarkDotNet.Artifacts/
+
+# GitHub Copilot Testing folder
+/.codetesting
+
+# DocFX build output
+api-reference/_docs/
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..cb8bb32a
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,93 @@
+# Polyline Algorithm Agents Instructions
+
+## Purpose
+
+Instructions for automated agents (bots, CI, and code review tools) and contributors interacting with the Polyline Algorithm library.
+
+---
+
+## General Guidelines
+
+- All contributions and automation **must adhere to code style** (`.editorconfig`, `dotnet format`).
+- **Unit tests** are required for new features and bug fixes (`tests/` directory).
+- **Benchmarks** must be updated for performance-impacting changes (`benchmarks/` directory).
+
+---
+
+## Pull Requests
+
+Agents and contributors should:
+
+- **Attach benchmark results** for encoding/decoding performance changes
+- Document **public API changes** in XML comments and verify updates at [API Reference](https://petesramek.github.io/polyline-algorithm-csharp/)
+- Run format and static analysis tools before submitting (`dotnet format`, analyzers)
+- Update **README.md** and `/samples` for public API changes
+
+---
+
+## Error Handling and Logging
+
+- Throw **descriptive exceptions** for invalid input/edge cases
+- Use internal logging helpers for operational status (`LogInfoExtensions`, `LogWarningExtensions`)
+
+---
+
+## Encoding/Decoding Agents
+
+- Use abstraction interfaces (`IPolylineEncoder`, `IPolylineDecoder` if available)
+- Prefer extension methods for collections and arrays
+- Validate latitude/longitude ranges
+
+---
+
+## Issue and PR Templates
+
+Agents should reference standardized templates from `.github`. Contributors must use them for new issues or PRs.
+
+---
+
+## Extensibility
+
+- Add encoding schemes or coordinate types in **separate classes/files**
+- Register via factory pattern if supporting multiple algorithms
+- Do not mix logic between different polyline versions
+
+---
+
+## Future-proofing
+
+- Support for precision or custom coordinate fields: update `PolylineEncodingOptions` with clear doc comments
+
+---
+
+## Documentation
+
+- Keep XML doc comments up-to-date in source files
+- API reference is auto-generated and hosted at
+ [https://petesramek.github.io/polyline-algorithm-csharp/](https://petesramek.github.io/polyline-algorithm-csharp/)
+- After public API changes, verify docs render correctly on the website
+- Add usage samples in XML comments and `/samples` directory
+
+---
+
+## Agent File Format (for `.github/agents`)
+
+Each agent instruction file should specify:
+
+```
+# AGENT INSTRUCTIONS
+
+- Purpose and scope
+- Required tools/commands
+- Coding and testing requirements
+- Logging/error handling expectations
+- Documentation or samples to update
+```
+
+---
+
+## Contact & Questions
+
+Questions or clarifications: open a GitHub issue and tag `@petesramek`.
+
+---
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..a2492d27
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,50 @@
+# Contributing to PolylineAlgorithm
+
+Thank you for your interest in improving this library!
+
+## Developer Documentation
+
+In-depth developer guides are in the [`/docs`](./docs/README.md) folder:
+
+- [Local Development](./docs/local-development.md) — build, test, and format commands
+- [Testing](./docs/testing.md) — how to write unit tests
+- [Benchmarks](./docs/benchmarks.md) — how to write and run benchmarks
+- [Composite Actions](./docs/composite-actions.md) — reusable CI actions catalogue
+- [Workflows](./docs/workflows.md) — CI/CD pipeline overview
+- [Branch Strategy](./docs/branch-strategy.md) — branch lifecycle and environments
+- [Versioning](./docs/versioning.md) — branch naming and the version pipeline
+- [API Documentation](./docs/api-documentation.md) — DocFX and the API reference site
+
+## Guidelines
+
+- **Follow code style:** Use `.editorconfig` and run `dotnet format`.
+- **Add unit tests:** Place all tests in `/tests`, following naming conventions.
+- **Benchmark updates:** Add or update `/benchmarks` for major changes.
+
+## Issue and PR Templates
+
+Please use the provided templates in `.github` for all new issues or pull requests.
+
+## API Documentation
+
+API reference is auto-generated from XML comments and published at
+👉 [API Reference](https://petesramek.github.io/polyline-algorithm-csharp/)
+
+- All public classes, interfaces, and methods require XML doc comments.
+- After merging, verify that documentation renders correctly.
+- Add usage samples where applicable.
+
+## Submitting a Change
+
+1. Fork the repo and create a new branch.
+2. Implement your changes, tests, and update doc comments.
+3. Run `dotnet format`, and all tests/benchmarks.
+4. Submit a pull request, using the provided template.
+
+## Contact
+
+For help or questions, open an issue and tag `@petesramek`.
+
+## License
+
+MIT License © Pete Sramek
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 00000000..96ebb124
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,25 @@
+
+
+ This method reverses the normalization performed by
+
+ The calculation is performed inside a checked block to ensure that any arithmetic overflow is detected
+
+ and an
+
+ For example, with a precision of 5:
+
+
+ options is null.
+- h2: Properties
+- api3: Options
+ id: PolylineAlgorithm_Abstraction_AbstractPolylineDecoder_2_Options
+ src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs#L54
+ metadata:
+ uid: PolylineAlgorithm.Abstraction.AbstractPolylineDecoder`2.Options
+ commentId: P:PolylineAlgorithm.Abstraction.AbstractPolylineDecoder`2.Options
+- markdown: Gets the encoding options used by this polyline decoder.
+- code: public PolylineEncodingOptions Options { get; }
+- h4: Property Value
+- parameters:
+ - type:
+ - text: PolylineEncodingOptions
+ url: PolylineAlgorithm.PolylineEncodingOptions.html
+- h2: Methods
+- api3: CreateCoordinate(double, double)
+ id: PolylineAlgorithm_Abstraction_AbstractPolylineDecoder_2_CreateCoordinate_System_Double_System_Double_
+ src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs#L202
+ metadata:
+ uid: PolylineAlgorithm.Abstraction.AbstractPolylineDecoder`2.CreateCoordinate(System.Double,System.Double)
+ commentId: M:PolylineAlgorithm.Abstraction.AbstractPolylineDecoder`2.CreateCoordinate(System.Double,System.Double)
+- markdown: Creates a TCoordinate instance from the specified latitude and longitude values.
+- code: protected abstract TCoordinate CreateCoordinate(double latitude, double longitude)
+- h4: Parameters
+- parameters:
+ - name: latitude
+ type:
+ - text: double
+ url: https://learn.microsoft.com/dotnet/api/system.double
+ description: The latitude component of the coordinate, in degrees.
+ - name: longitude
+ type:
+ - text: double
+ url: https://learn.microsoft.com/dotnet/api/system.double
+ description: The longitude component of the coordinate, in degrees.
+- h4: Returns
+- parameters:
+ - type:
+ - TCoordinate
+ description: A TCoordinate instance representing the specified geographic coordinate.
+- api3: Decode(TPolyline, CancellationToken)
+ id: PolylineAlgorithm_Abstraction_AbstractPolylineDecoder_2_Decode__0_System_Threading_CancellationToken_
+ src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs#L81
+ metadata:
+ uid: PolylineAlgorithm.Abstraction.AbstractPolylineDecoder`2.Decode(`0,System.Threading.CancellationToken)
+ commentId: M:PolylineAlgorithm.Abstraction.AbstractPolylineDecoder`2.Decode(`0,System.Threading.CancellationToken)
+- markdown: >-
+ Decodes an encoded TPolyline into a sequence of TCoordinate instances,
+
+ with support for cancellation.
+- code: public IEnumerableTPolyline instance containing the encoded polyline string to decode.
+ - name: cancellationToken
+ type:
+ - text: CancellationToken
+ url: https://learn.microsoft.com/dotnet/api/system.threading.cancellationtoken
+ description: A TCoordinate representing the decoded latitude and longitude pairs.
+- h4: Exceptions
+- parameters:
+ - type:
+ - text: ArgumentNullException
+ url: https://learn.microsoft.com/dotnet/api/system.argumentnullexception
+ description: Thrown when polyline is null.
+ - type:
+ - text: ArgumentException
+ url: https://learn.microsoft.com/dotnet/api/system.argumentexception
+ description: Thrown when polyline is empty.
+ - type:
+ - text: InvalidPolylineException
+ url: PolylineAlgorithm.InvalidPolylineException.html
+ description: Thrown when the polyline format is invalid or malformed at a specific position.
+ - type:
+ - text: OperationCanceledException
+ url: https://learn.microsoft.com/dotnet/api/system.operationcanceledexception
+ description: Thrown when cancellationToken is canceled during decoding.
+- api3: GetReadOnlyMemory(in TPolyline)
+ id: PolylineAlgorithm_Abstraction_AbstractPolylineDecoder_2_GetReadOnlyMemory__0__
+ src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/Abstraction/AbstractPolylineDecoder.cs#L187
+ metadata:
+ uid: PolylineAlgorithm.Abstraction.AbstractPolylineDecoder`2.GetReadOnlyMemory(`0@)
+ commentId: M:PolylineAlgorithm.Abstraction.AbstractPolylineDecoder`2.GetReadOnlyMemory(`0@)
+- markdown: Extracts the underlying read-only memory region of characters from the specified polyline instance.
+- code: protected abstract ReadOnlyMemoryTPolyline instance from which to extract the character sequence.
+- h4: Returns
+- parameters:
+ - type:
+ - text: ReadOnlyMemory
+ url: https://learn.microsoft.com/dotnet/api/system.readonlymemory-1
+ - <
+ - text: char
+ url: https://learn.microsoft.com/dotnet/api/system.char
+ - '>'
+ description: A options is null
+- h2: Properties
+- api3: Options
+ id: PolylineAlgorithm_Abstraction_AbstractPolylineEncoder_2_Options
+ src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/Abstraction/AbstractPolylineEncoder.cs#L57
+ metadata:
+ uid: PolylineAlgorithm.Abstraction.AbstractPolylineEncoder`2.Options
+ commentId: P:PolylineAlgorithm.Abstraction.AbstractPolylineEncoder`2.Options
+- markdown: Gets the encoding options used by this polyline encoder.
+- code: public PolylineEncodingOptions Options { get; }
+- h4: Property Value
+- parameters:
+ - type:
+ - text: PolylineEncodingOptions
+ url: PolylineAlgorithm.PolylineEncodingOptions.html
+- h2: Methods
+- api3: CreatePolyline(ReadOnlyMemoryTPolyline representing the encoded polyline.
+- api3: Encode(ReadOnlySpanTCoordinate instances into an encoded TPolyline string.
+- code: >-
+ [SuppressMessage("Design", "MA0051:Method is too long", Justification = "Method contains local methods. Actual method only 55 lines.")]
+
+ public TPolyline Encode(ReadOnlySpanTCoordinate objects to encode.
+ - name: cancellationToken
+ type:
+ - text: CancellationToken
+ url: https://learn.microsoft.com/dotnet/api/system.threading.cancellationtoken
+ description: A TPolyline representing the encoded coordinates.
+- h4: Exceptions
+- parameters:
+ - type:
+ - text: ArgumentNullException
+ url: https://learn.microsoft.com/dotnet/api/system.argumentnullexception
+ description: Thrown when coordinates is null.
+ - type:
+ - text: ArgumentException
+ url: https://learn.microsoft.com/dotnet/api/system.argumentexception
+ description: Thrown when coordinates is an empty enumeration.
+ - type:
+ - text: InvalidOperationException
+ url: https://learn.microsoft.com/dotnet/api/system.invalidoperationexception
+ description: Thrown when the internal encoding buffer cannot accommodate the encoded value.
+- api3: GetLatitude(TCoordinate)
+ id: PolylineAlgorithm_Abstraction_AbstractPolylineEncoder_2_GetLatitude__0_
+ src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/Abstraction/AbstractPolylineEncoder.cs#L194
+ metadata:
+ uid: PolylineAlgorithm.Abstraction.AbstractPolylineEncoder`2.GetLatitude(`0)
+ commentId: M:PolylineAlgorithm.Abstraction.AbstractPolylineEncoder`2.GetLatitude(`0)
+- markdown: Extracts the latitude value from the specified coordinate.
+- code: protected abstract double GetLatitude(TCoordinate current)
+- h4: Parameters
+- parameters:
+ - name: current
+ type:
+ - TCoordinate
+ description: The coordinate from which to extract the latitude.
+- h4: Returns
+- parameters:
+ - type:
+ - text: double
+ url: https://learn.microsoft.com/dotnet/api/system.double
+ description: The latitude value as a LatLng type or a ValueTuple<double,double>).
+- h2: Methods
+- api3: Decode(TPolyline, CancellationToken)
+ id: PolylineAlgorithm_Abstraction_IPolylineDecoder_2_Decode__0_System_Threading_CancellationToken_
+ src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/Abstraction/IPolylineDecoder.cs#L48
+ metadata:
+ uid: PolylineAlgorithm.Abstraction.IPolylineDecoder`2.Decode(`0,System.Threading.CancellationToken)
+ commentId: M:PolylineAlgorithm.Abstraction.IPolylineDecoder`2.Decode(`0,System.Threading.CancellationToken)
+- markdown: >-
+ Decodes the specified encoded polyline into an ordered sequence of geographic coordinates.
+
+ The sequence preserves the original vertex order encoded by the polyline.
+- code: IEnumerableTPolyline instance containing the encoded polyline to decode.
+
+ Implementations SHOULD validate the input and may throw TValue representing the decoded
+
+ latitude/longitude pairs (or equivalent coordinates) in the same order they were encoded.
+- h4: Remarks
+- markdown: >-
+ Implementations commonly follow the Google Encoded Polyline Algorithm Format, but this interface
+
+ does not mandate a specific encoding. Consumers should rely on a concrete decoder's documentation
+
+ to understand the exact encoding supported.
+- h4: Exceptions
+- parameters:
+ - type:
+ - text: OperationCanceledException
+ url: https://learn.microsoft.com/dotnet/api/system.operationcanceledexception
+ description: Thrown when the provided cancellationToken requests cancellation.
+languageId: csharp
+metadata:
+ description: Defines a contract for decoding an encoded polyline into a sequence of geographic coordinates.
diff --git a/api-reference/1.0/PolylineAlgorithm.Abstraction.IPolylineEncoder-2.yml b/api-reference/1.0/PolylineAlgorithm.Abstraction.IPolylineEncoder-2.yml
new file mode 100644
index 00000000..979fe774
--- /dev/null
+++ b/api-reference/1.0/PolylineAlgorithm.Abstraction.IPolylineEncoder-2.yml
@@ -0,0 +1,142 @@
+### YamlMime:ApiPage
+title: Interface IPolylineEncoderTValue type and produce an encoded
+
+ representation of those coordinates as TPolyline.
+- code: public interface IPolylineEncoderLatitude and Longitude values). Implementations must document the expected shape,
+
+ units (typically decimal degrees), and any required fields for TValue.
+
+ Common shapes:
+
+ - A struct or class with two double properties named Latitude and Longitude.
+
+ - A tuple-like type (for example ValueTuple<double,double>) where the encoder documents
+ which element represents latitude and longitude.
+ - name: TPolyline
+ description: >-
+ The encoded polyline representation returned by the encoder (for example string,
+
+ ReadOnlyMemory<char>, or a custom wrapper type). Concrete implementations should document
+
+ the chosen representation and any memory / ownership expectations.
+- h4: Extension Methods
+- list:
+ - text: PolylineEncoderExtensions.Encodecoordinates is preserved in the encoded result.
+- code: TPolyline Encode(ReadOnlySpanTValue instances to encode into a polyline.
+
+ The span may be empty; implementations should return an appropriate empty encoded representation
+
+ (for example an empty string or an empty memory slice) rather than null.
+ - name: cancellationToken
+ type:
+ - text: CancellationToken
+ url: https://learn.microsoft.com/dotnet/api/system.threading.cancellationtoken
+ description: >-
+ A TPolyline containing the encoded polyline that represents the input coordinates.
+
+ The exact format and any delimiting/terminating characters are implementation-specific and must be
+
+ documented by concrete encoder types.
+- h4: Examples
+- markdown: >-
+
+- h4: Remarks
+- markdown: >-
+ - Implementations should validate input as appropriate and document any preconditions (for example
+ if coordinates must be within [-90,90] latitude and [-180,180] longitude).
+ - For large input sequences, implementations may provide streaming or incremental encoders; those
+ variants can still implement this interface by materializing the final encoded result.
+- h4: Exceptions
+- parameters:
+ - type:
+ - text: OperationCanceledException
+ url: https://learn.microsoft.com/dotnet/api/system.operationcanceledexception
+ description: Thrown if the operation is canceled via // Example pseudocode for typical usage with a string-based encoder:
+
+ var coords = new[] {
+ new Coordinate { Latitude = 47.6219, Longitude = -122.3503 },
+ new Coordinate { Latitude = 47.6220, Longitude = -122.3504 }
+ };
+
+ IPolylineEncoder<Coordinate,string> encoder = new GoogleEncodedPolylineEncoder();
+
+ string encoded = encoder.Encode(coords, CancellationToken.None);cancellationToken.
+languageId: csharp
+metadata:
+ description: >-
+ Contract for encoding a sequence of geographic coordinates into an encoded polyline representation.
+
+ Implementations interpret the generic TValue type and produce an encoded
+
+ representation of those coordinates as TPolyline.
diff --git a/api-reference/1.0/PolylineAlgorithm.Abstraction.yml b/api-reference/1.0/PolylineAlgorithm.Abstraction.yml
new file mode 100644
index 00000000..e9de48f7
--- /dev/null
+++ b/api-reference/1.0/PolylineAlgorithm.Abstraction.yml
@@ -0,0 +1,34 @@
+### YamlMime:ApiPage
+title: Namespace PolylineAlgorithm.Abstraction
+body:
+- api1: Namespace PolylineAlgorithm.Abstraction
+ id: PolylineAlgorithm_Abstraction
+ metadata:
+ uid: PolylineAlgorithm.Abstraction
+ commentId: N:PolylineAlgorithm.Abstraction
+- h3: Classes
+- parameters:
+ - type:
+ text: AbstractPolylineDecoderTValue type and produce an encoded
+
+ representation of those coordinates as TPolyline.
+languageId: csharp
diff --git a/api-reference/1.0/PolylineAlgorithm.Extensions.PolylineDecoderExtensions.yml b/api-reference/1.0/PolylineAlgorithm.Extensions.PolylineDecoderExtensions.yml
new file mode 100644
index 00000000..4b97f584
--- /dev/null
+++ b/api-reference/1.0/PolylineAlgorithm.Extensions.PolylineDecoderExtensions.yml
@@ -0,0 +1,195 @@
+### YamlMime:ApiPage
+title: Class PolylineDecoderExtensions
+body:
+- api1: Class PolylineDecoderExtensions
+ id: PolylineAlgorithm_Extensions_PolylineDecoderExtensions
+ src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/Extensions/PolylineDecoderExtensions.cs#L16
+ metadata:
+ uid: PolylineAlgorithm.Extensions.PolylineDecoderExtensions
+ commentId: T:PolylineAlgorithm.Extensions.PolylineDecoderExtensions
+- facts:
+ - name: Namespace
+ value:
+ text: PolylineAlgorithm.Extensions
+ url: PolylineAlgorithm.Extensions.html
+ - name: Assembly
+ value: PolylineAlgorithm.dll
+- markdown: Provides extension methods for the TValue containing the decoded coordinate pairs.
+- h4: Type Parameters
+- parameters:
+ - name: TValue
+ description: The coordinate type returned by the decoder.
+- h4: Exceptions
+- parameters:
+ - type:
+ - text: ArgumentNullException
+ url: https://learn.microsoft.com/dotnet/api/system.argumentnullexception
+ description: Thrown when decoder or polyline is null.
+- api3: DecodeTValue containing the decoded coordinate pairs.
+- h4: Type Parameters
+- parameters:
+ - name: TValue
+ description: The coordinate type returned by the decoder.
+- h4: Exceptions
+- parameters:
+ - type:
+ - text: ArgumentNullException
+ url: https://learn.microsoft.com/dotnet/api/system.argumentnullexception
+ description: Thrown when decoder is null.
+- api3: DecodeTValue containing the decoded coordinate pairs.
+- h4: Type Parameters
+- parameters:
+ - name: TValue
+ description: The coordinate type returned by the decoder.
+- h4: Exceptions
+- parameters:
+ - type:
+ - text: ArgumentNullException
+ url: https://learn.microsoft.com/dotnet/api/system.argumentnullexception
+ description: Thrown when decoder or polyline is null.
+languageId: csharp
+metadata:
+ description: Provides extension methods for the interface to facilitate decoding encoded polylines.
diff --git a/api-reference/1.0/PolylineAlgorithm.Extensions.PolylineEncoderExtensions.yml b/api-reference/1.0/PolylineAlgorithm.Extensions.PolylineEncoderExtensions.yml
new file mode 100644
index 00000000..aeba23f9
--- /dev/null
+++ b/api-reference/1.0/PolylineAlgorithm.Extensions.PolylineEncoderExtensions.yml
@@ -0,0 +1,139 @@
+### YamlMime:ApiPage
+title: Class PolylineEncoderExtensions
+body:
+- api1: Class PolylineEncoderExtensions
+ id: PolylineAlgorithm_Extensions_PolylineEncoderExtensions
+ src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/Extensions/PolylineEncoderExtensions.cs#L19
+ metadata:
+ uid: PolylineAlgorithm.Extensions.PolylineEncoderExtensions
+ commentId: T:PolylineAlgorithm.Extensions.PolylineEncoderExtensions
+- facts:
+ - name: Namespace
+ value:
+ text: PolylineAlgorithm.Extensions
+ url: PolylineAlgorithm.Extensions.html
+ - name: Assembly
+ value: PolylineAlgorithm.dll
+- markdown: Provides extension methods for the TCoordinate instances into an encoded polyline.
+- code: >-
+ [SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "We need a list as we do need to marshal it as span.")]
+
+ [SuppressMessage("Design", "MA0016:Prefer using collection abstraction instead of implementation", Justification = "We need a list as we do need to marshal it as span.")]
+
+ public static TPolyline EncodeTCoordinate objects to encode.
+- h4: Returns
+- parameters:
+ - type:
+ - TPolyline
+ description: A TPolyline instance representing the encoded polyline for the provided coordinates.
+- h4: Type Parameters
+- parameters:
+ - name: TCoordinate
+ description: The type that represents a geographic coordinate to encode.
+ - name: TPolyline
+ description: The type that represents the encoded polyline output.
+- h4: Exceptions
+- parameters:
+ - type:
+ - text: ArgumentNullException
+ url: https://learn.microsoft.com/dotnet/api/system.argumentnullexception
+ description: Thrown when encoder or coordinates is null.
+- api3: EncodeTCoordinate instances into an encoded polyline.
+- code: public static TPolyline EncodeTCoordinate objects to encode.
+- h4: Returns
+- parameters:
+ - type:
+ - TPolyline
+ description: A TPolyline instance representing the encoded polyline for the provided coordinates.
+- h4: Type Parameters
+- parameters:
+ - name: TCoordinate
+ description: The type that represents a geographic coordinate to encode.
+ - name: TPolyline
+ description: The type that represents the encoded polyline output.
+- h4: Exceptions
+- parameters:
+ - type:
+ - text: ArgumentNullException
+ url: https://learn.microsoft.com/dotnet/api/system.argumentnullexception
+ description: Thrown when encoder or coordinates is null.
+languageId: csharp
+metadata:
+ description: Provides extension methods for the interface to facilitate encoding geographic coordinates into polylines.
diff --git a/api-reference/1.0/PolylineAlgorithm.Extensions.yml b/api-reference/1.0/PolylineAlgorithm.Extensions.yml
new file mode 100644
index 00000000..c39da0ca
--- /dev/null
+++ b/api-reference/1.0/PolylineAlgorithm.Extensions.yml
@@ -0,0 +1,19 @@
+### YamlMime:ApiPage
+title: Namespace PolylineAlgorithm.Extensions
+body:
+- api1: Namespace PolylineAlgorithm.Extensions
+ id: PolylineAlgorithm_Extensions
+ metadata:
+ uid: PolylineAlgorithm.Extensions
+ commentId: N:PolylineAlgorithm.Extensions
+- h3: Classes
+- parameters:
+ - type:
+ text: PolylineDecoderExtensions
+ url: PolylineAlgorithm.Extensions.PolylineDecoderExtensions.html
+ description: Provides extension methods for the precision is 0,
+
+ the value is returned as a double without division.
+
+
+
+
+
+ If the input value is 0, the method returns 0.0 immediately.
+
+
+
+ This method determines how many characters will be needed to represent an integer delta value when encoded
+
+ using the polyline encoding algorithm. It performs the same zigzag encoding transformation as
+ + The calculation process: + + +
+ + This method is useful for pre-allocating buffers of the correct size before encoding polyline data, helping to avoid + + buffer overflow checks during the actual encoding process. + +
+ ++ + The method uses a long internally to prevent overflow during the left-shift operation on large negative values. + +
+- h4: See Also +- list: + - - text: PolylineEncoding + url: PolylineAlgorithm.PolylineEncoding.html + - . + - text: TryWriteValue + url: PolylineAlgorithm.PolylineEncoding.html#PolylineAlgorithm_PolylineEncoding_TryWriteValue_System_Int32_System_Span_System_Char__System_Int32__ + - ( + - text: int + url: https://learn.microsoft.com/dotnet/api/system.int32 + - ',' + - " " + - text: Span + url: https://learn.microsoft.com/dotnet/api/system.span-1 + - < + - text: char + url: https://learn.microsoft.com/dotnet/api/system.char + - '>' + - ',' + - " " + - ref + - " " + - text: int + url: https://learn.microsoft.com/dotnet/api/system.int32 + - ) +- api3: Normalize(double, uint) + id: PolylineAlgorithm_PolylineEncoding_Normalize_System_Double_System_UInt32_ + src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/PolylineEncoding.cs#L61 + metadata: + uid: PolylineAlgorithm.PolylineEncoding.Normalize(System.Double,System.UInt32) + commentId: M:PolylineAlgorithm.PolylineEncoding.Normalize(System.Double,System.UInt32) +- markdown: Normalizes a geographic coordinate value to an integer representation based on the specified precision. +- code: public static int Normalize(double value, uint precision = 5) +- h4: Parameters +- parameters: + - name: value + type: + - text: double + url: https://learn.microsoft.com/dotnet/api/system.double + description: The numeric value to normalize. Must be a finite number (not NaN or infinity). + - name: precision + type: + - text: uint + url: https://learn.microsoft.com/dotnet/api/system.uint32 + description: >- + The number of decimal places of precision to preserve in the normalized value. + + The value is multiplied by 10^precision before rounding.
+
+ Default is 5, which is standard for polyline encoding.
+ optional: true
+- h4: Returns
+- parameters:
+ - type:
+ - text: int
+ url: https://learn.microsoft.com/dotnet/api/system.int32
+ description: An integer representing the normalized value. Returns 0 if the input value is 0.0.
+- h4: Remarks
+- markdown: >-
+ + + This method converts a floating-point coordinate value into a normalized integer by multiplying it by 10 raised + + to the power of the specified precision, then truncating the result to an integer. + +
+ ++ + For example, with the default precision of 5: + + +
+ + The method validates that the input value is finite (not NaN or infinity) before performing normalization. + + If the precision is 0, the value is rounded without multiplication. + +
+- h4: Exceptions +- parameters: + - type: + - text: ArgumentOutOfRangeException + url: https://learn.microsoft.com/dotnet/api/system.argumentoutofrangeexception + description: Thrown whenvalue is not a finite number (NaN or infinity).
+ - type:
+ - text: OverflowException
+ url: https://learn.microsoft.com/dotnet/api/system.overflowexception
+ description: Thrown when the normalized result exceeds the range of a 32-bit signed integer during the conversion from double to int.
+- api3: TryReadValue(ref int, ReadOnlyMemory
+
+ This method decodes a value from a polyline-encoded character buffer, starting at the given position. It reads
+
+ characters sequentially, applying the polyline decoding algorithm, and updates the delta with
+
+ the decoded value. The position is advanced as characters are processed.
+
+
+ + The decoding process continues until a character with a value less than the algorithm's space constant is encountered, + + which signals the end of the encoded value. If the buffer is exhausted before a complete value is read, the method returns false. + +
+ +
+
+ The decoded value is added to delta using zigzag decoding, which handles both positive and negative values.
+
+
+ + This method encodes an integer delta value into a polyline-encoded format and writes it to the provided character buffer, + + starting at the given position. It applies zigzag encoding followed by the polyline encoding algorithm to represent + + both positive and negative values efficiently. + +
+ ++ + The encoding process first converts the value using zigzag encoding (left shift by 1, with bitwise inversion for negative values), + + then writes it as a sequence of characters. Each character encodes 5 bits of data, with continuation bits indicating whether + + more characters follow. The position is advanced as characters are written. + +
+ +
+
+ Before writing, the method validates that sufficient space is available in the buffer by calling
+
+ This method is the inverse of
+
+ Iterates through the polyline, counting the length of each block (a sequence of characters representing an encoded value).
+
+ Throws an
+ + Uses SIMD vectorization for efficient validation of large spans. Falls back to scalar checks for any block where an invalid character is detected. + +
+ +
+
+ The valid range is from '?' (63) to '_' (95), inclusive. If an invalid character is found, an
+ + This method performs two levels of validation on the provided polyline segment: + +
+ +
+
+ If an invalid character or block structure is detected, an
+ + This class allows you to configure various aspects of polyline encoding, including: + +
+ ++ + All properties have internal setters and should be configured through a builder or factory pattern. + +
+- h2: Properties +- api3: LoggerFactory + id: PolylineAlgorithm_PolylineEncodingOptions_LoggerFactory + src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/PolylineEncodingOptions.cs#L41 + metadata: + uid: PolylineAlgorithm.PolylineEncodingOptions.LoggerFactory + commentId: P:PolylineAlgorithm.PolylineEncodingOptions.LoggerFactory +- markdown: Gets the logger factory used for diagnostic logging during encoding operations. +- code: public ILoggerFactory LoggerFactory { get; } +- h4: Property Value +- parameters: + - type: + - text: ILoggerFactory + url: https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.iloggerfactory +- h4: Remarks +- markdown: >- + The default logger factory is+ + The precision determines the number of decimal places to which each coordinate value (latitude or longitude) + + is multiplied and truncated (not rounded) before encoding. For example, a precision of 5 means each coordinate is multiplied by 10^5 + + and truncated to an integer before encoding. + +
+ ++ + This setting does not directly correspond to a physical distance or accuracy in meters, but rather controls + + the granularity of the encoded values. + +
+- api3: StackAllocLimit + id: PolylineAlgorithm_PolylineEncodingOptions_StackAllocLimit + src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/PolylineEncodingOptions.cs#L73 + metadata: + uid: PolylineAlgorithm.PolylineEncodingOptions.StackAllocLimit + commentId: P:PolylineAlgorithm.PolylineEncodingOptions.StackAllocLimit +- markdown: Gets the maximum buffer size (in characters) that can be allocated on the stack for encoding operations. +- code: public int StackAllocLimit { get; } +- h4: Property Value +- parameters: + - type: + - text: int + url: https://learn.microsoft.com/dotnet/api/system.int32 +- h4: Remarks +- markdown: >- + When the required buffer size for encoding exceeds this limit, memory will be allocated on the heap instead of the stack. + + This setting specifically applies to stack allocation of character arrays (stackalloc char[]) used during polyline encoding,
+
+ balancing performance and stack safety.
+languageId: csharp
+metadata:
+ description: Provides configuration options for polyline encoding operations.
diff --git a/api-reference/1.0/PolylineAlgorithm.PolylineEncodingOptionsBuilder.yml b/api-reference/1.0/PolylineAlgorithm.PolylineEncodingOptionsBuilder.yml
new file mode 100644
index 00000000..92e6d942
--- /dev/null
+++ b/api-reference/1.0/PolylineAlgorithm.PolylineEncodingOptionsBuilder.yml
@@ -0,0 +1,141 @@
+### YamlMime:ApiPage
+title: Class PolylineEncodingOptionsBuilder
+body:
+- api1: Class PolylineEncodingOptionsBuilder
+ id: PolylineAlgorithm_PolylineEncodingOptionsBuilder
+ src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/PolylineEncodingOptionsBuilder.cs#L15
+ metadata:
+ uid: PolylineAlgorithm.PolylineEncodingOptionsBuilder
+ commentId: T:PolylineAlgorithm.PolylineEncodingOptionsBuilder
+- facts:
+ - name: Namespace
+ value:
+ text: PolylineAlgorithm
+ url: PolylineAlgorithm.html
+ - name: Assembly
+ value: PolylineAlgorithm.dll
+- markdown: Provides a builder for configuring options for polyline encoding operations.
+- code: public sealed class PolylineEncodingOptionsBuilder
+- h4: Inheritance
+- inheritance:
+ - text: object
+ url: https://learn.microsoft.com/dotnet/api/system.object
+ - text: PolylineEncodingOptionsBuilder
+ url: PolylineAlgorithm.PolylineEncodingOptionsBuilder.html
+- h4: Inherited Members
+- list:
+ - text: object.Equals(object)
+ url: https://learn.microsoft.com/dotnet/api/system.object.equals#system-object-equals(system-object)
+ - text: object.Equals(object, object)
+ url: https://learn.microsoft.com/dotnet/api/system.object.equals#system-object-equals(system-object-system-object)
+ - text: object.GetHashCode()
+ url: https://learn.microsoft.com/dotnet/api/system.object.gethashcode
+ - text: object.GetType()
+ url: https://learn.microsoft.com/dotnet/api/system.object.gettype
+ - text: object.ReferenceEquals(object, object)
+ url: https://learn.microsoft.com/dotnet/api/system.object.referenceequals
+ - text: object.ToString()
+ url: https://learn.microsoft.com/dotnet/api/system.object.tostring
+- h2: Methods
+- api3: Build()
+ id: PolylineAlgorithm_PolylineEncodingOptionsBuilder_Build
+ src: https://github.com/petesramek/polyline-algorithm-csharp/blob/develop/1.0/src/PolylineAlgorithm/PolylineEncodingOptionsBuilder.cs#L38
+ metadata:
+ uid: PolylineAlgorithm.PolylineEncodingOptionsBuilder.Build
+ commentId: M:PolylineAlgorithm.PolylineEncodingOptionsBuilder.Build
+- markdown: Builds a new stackAllocLimit is less than 1.
+languageId: csharp
+metadata:
+ description: Provides a builder for configuring options for polyline encoding operations.
diff --git a/api-reference/1.0/PolylineAlgorithm.yml b/api-reference/1.0/PolylineAlgorithm.yml
new file mode 100644
index 00000000..b60dc3c0
--- /dev/null
+++ b/api-reference/1.0/PolylineAlgorithm.yml
@@ -0,0 +1,38 @@
+### YamlMime:ApiPage
+title: Namespace PolylineAlgorithm
+body:
+- api1: Namespace PolylineAlgorithm
+ id: PolylineAlgorithm
+ metadata:
+ uid: PolylineAlgorithm
+ commentId: N:PolylineAlgorithm
+- h3: Namespaces
+- parameters:
+ - type:
+ text: PolylineAlgorithm.Abstraction
+ url: PolylineAlgorithm.Abstraction.html
+ - type:
+ text: PolylineAlgorithm.Extensions
+ url: PolylineAlgorithm.Extensions.html
+- h3: Classes
+- parameters:
+ - type:
+ text: InvalidPolylineException
+ url: PolylineAlgorithm.InvalidPolylineException.html
+ description: Exception thrown when a polyline is determined to be malformed or invalid during processing.
+ - type:
+ text: PolylineEncoding
+ url: PolylineAlgorithm.PolylineEncoding.html
+ description: >-
+ Provides methods for encoding and decoding polyline data, as well as utilities for normalizing and de-normalizing
+
+ geographic coordinate values.
+ - type:
+ text: PolylineEncodingOptions
+ url: PolylineAlgorithm.PolylineEncodingOptions.html
+ description: Provides configuration options for polyline encoding operations.
+ - type:
+ text: PolylineEncodingOptionsBuilder
+ url: PolylineAlgorithm.PolylineEncodingOptionsBuilder.html
+ description: Provides a builder for configuring options for polyline encoding operations.
+languageId: csharp
diff --git a/api-reference/1.0/toc.yml b/api-reference/1.0/toc.yml
new file mode 100644
index 00000000..290b7c63
--- /dev/null
+++ b/api-reference/1.0/toc.yml
@@ -0,0 +1,40 @@
+### YamlMime:TableOfContent
+- name: PolylineAlgorithm
+ href: PolylineAlgorithm.yml
+ items:
+ - name: Classes
+ - name: InvalidPolylineException
+ href: PolylineAlgorithm.InvalidPolylineException.yml
+ - name: PolylineEncoding
+ href: PolylineAlgorithm.PolylineEncoding.yml
+ - name: PolylineEncodingOptions
+ href: PolylineAlgorithm.PolylineEncodingOptions.yml
+ - name: PolylineEncodingOptionsBuilder
+ href: PolylineAlgorithm.PolylineEncodingOptionsBuilder.yml
+- name: PolylineAlgorithm.Abstraction
+ href: PolylineAlgorithm.Abstraction.yml
+ items:
+ - name: Classes
+ - name: AbstractPolylineDecoderpI^)bB_>Kt^dh`}&oD!}J^W_$>}g!dz40$=!(qzcZ0L z$YCho{hyvmQtsnsWWk3gn3D#~beWSD2JFe-4?2<`VSe5q`?fVU^~ROb=3{W!k* zaHhB->OVgGaB>}96g)H7H2G4+6&owzAw7~=zEFnG`k~a#c~}
SYywJFFtHbpvB|7*X}e5#=t za5wW9U=$<{IC(OmREs5XKohVV&2uS?e@SRPaDZB(lcEkw(mbf%yBZ2A?eicm>4>`~ z-2-u9NyE#KEBR>HWjYjcn!dudL1&Yn $b+LL~YDTLNrK1gvO);rCIBZzGgcM`%H_#&Kl0XeUo zq0mO8^LwD+JO=E*-HHpg@Vwb34wF~x&TR2UCre=HIg(qkNyhZtFQpefD^&~sF4H%E zEH^#*mAvxB5&8SBLvsH^56SS6L&U}Vs0?682P6Xm1!a=azDiPne^&c-lGU+U@@HHp z`L!2`B@i>#)u)L;w^wp<3&fL>Ay!ua>cDk?cMsR;LN4&I);e4Wj!tZan| aHx1H{Ou*$+Q;TM|}T5>GxaVZe8)nYTj5|E7>p_lx3P!@ztn zUJ51bX1`p4``iR2yoY)E1LIM@-Sh*yIq5w1RWYwutbEg7C-b+*;⪼`@8$3ARlf? z6GlOVWC}-cUvWR6v^3ym4{IpNJ_!;=LTP(>%|2v ?XeciW<$>=ni2 &YuU?q$^rx-WvCUZr&FFvTeQhL98Aakf;xLusdK74FUQc@>K#?VuxpmGh={azV) z{*y9k?iQ(D{)KdH-Y-}DY`5J1`hIz9?-BXzz)|`6!;j10p(6zoqp MOJj-+3?t;w4#$%*Ha &yAi8~U4*>T9?sKv^DWThGHC=(a zvXN-|-mu5?M8sp-lIk^YXY7l3%txS44C_~{*`M9!MWApv_x4R&fCK{8I?(+vqSFx- zo^)twi3(DE!()iZFh7>u3F7StP%b=<3Qn*FECk+8J(|mUlyQLD`hxP4H$ll`Am&Gi z-b+l038%m|CUcK@4D|+T21qUaYXijr@xR}fjt_D*MIe3}TlNf>d6m~;xf}le1h@4L z<4XpWwZ?7g?HWU!U@D+j@Vl8ByTffbeJb_D?>iZ7?qtAErp@94Ele4xaaui+np-PF zYL-iJ=Up=7tS6=H+IOUN?RGi!rk&D$>t3n8`JkMC*FJgZy@T@Jp@XvZz+rjx$v?~3 zvE#&RO~&!YH9HqNp`t&4C2;3WmRR#D$vlSs%uWbE_h!kOexsChED&c}3D@Q{v_Gpi zU4}MXD#K=Ie6UOgH_wr*$!AIm@q;s%FE&R|%v`UXC8oo`&;XbZmK|eeXPYnrI=F8% zB4eNzNr>2yK_#;JK*(kN3 `$Ha6FGj#Xh@u2P8TX&@?W0N8R~?q*g}k2{ zM}7Zq>MPM|N@xkVO^-2p8Em2WfY*{fvWV5HMI#t~Thg&ukYzWvUECU|f71dJ9er@< zd&Dsv2{^60Q=Cp>MAtKLg?qeK(|Wrp;Q~{_fCiAK7Ek3vO^Oe^^o+7^ Wh)}vF|?B0;&n1fQETe$EXh3 z0iSk=6&~gGQLsaCUYO_gwHAw0+^OSa%qe$E`JBH=?V6oZx#5UZZW5`!?x-|uJ|wN1 z56Fdge<{CyZ@+wmzu$T2usr_UUu1IGRNe!AfyGrArUX+c2=L*Yfj`&a%9$vcIR7~O zH%L|&F+lfql0N+wDLiGf#3r30HfNCZg03wEMfs&tG99AOaXp0LddcnBDEVzGCBNnx z$(k@*yoFQ69Lg7qD k%G1Rw*c 6_Pn8tX3a)6OSlnR(1ec*Z&~S+R*MI=qqqdGf zfPyB953z|{lMj2?XCGDWR9nEn7MQowsPrcB@MHLv@7v9ZbCVMXbYKfcu|9n%rIWu& zX?$Qjozu5ZRK||+_eUis3~X_llP_~Qldto*lJD`kO-~|`zXh%p@B2euM{n5Y6dwDE zsgCa(zngWTz)Bx|*hFpA(9tKYpRl(+aoJ2O`I}jcZ|V#DC$ >miWNGmR94eV{z#VCy{x~}#q&0C>bkQR N=iHn8(YOb7{ny}9~YluOLY$Qe@4NbMpwy+ zC)aE065q29A9@ez%%?TAsVNV*+7X86Fs@Ut^`8g#`j7Y4cfa5t3U0!$dNPJd>d;2X zEjwF=)X$OOQ_q)xuLL}}jgp-NZ%_s`EtcsUzmUd_`1(z~(zxlMG-CtWZp02;w@a?P zXOH~x!(RCc+#fmED=)qLl1!b-IwbQR;LC~4N%mz(Ud5R*q4Oef_(T6G{uTSF(ErTt z4U!GMGkR{6f|)l+UirDC0@DcQu^}4BXO_sYt_70cy-9LAH%MCZ5^+wrM6w!JN(Ocy zwQU{T0DD4+5NW+iB9&)Ja9EvqqQk&^lu*WtJ cLtvp*CAdO$hx(}D zgc}N>b*V0>kdv+F#te9s#yXS8keT+QhkS~wco8Z6*T|wb+f9j=V|tr0!AcKf3^mER zR0`|=YA99nFmm|4GQyjgE=%IIP>t^qiapGD1vkHqu^*blI%@c$>!|mw+f>sZxOLo` znl6>?jA8s6de5bEt1imyJy<`3(eWeAlKFGTN?!8{Np1PAq&D3sIW-GN-i?86dwC0w z*sOTk31>?C>bIqC!y#$exL@iv;_=rWm1$Ul_RTwG(fyyx>)ZFrj>CuLvx6efzW9pN zmY0jm%(Pii;qj`DP`cz)oh~J_*UOOBE5sRydw;!bH{L%Nq{ceeO1dJsbF*Ym-ynHC z>m;*BMSz=N0Bev4r;17S8=^fM>?}IARf>rM3cA)yN!uJTMJps{%H;?X8x83&Mg}3h zZ8PZK#N#@$3d<$4d9I{ZoiD*LEfUEeBKANkVu6>a0w$!91x&@relW!zwKB&nfQKpe z7~eC+%8L29CY!nbBV?X-0w>y;Eq|np`G+oR(o&Z246+DQSkhdEvNzoV-vOyS{Qv$^ zv}-0u-zSU6Zz8p>?L?#(BB(46+D&&LVEi71{^c;yU)XKm9dbDiGl<@g4d9!CH&^H? z^p)Kh)=c++4&5r;uim2_mSm^+%IU${2D8Ybgv5bmRK{pnLbl#96M*(+eT)LTHm*zW z@0<8K=KZaYq16$w*p2(rwlSwaoe1!1$*R1D?_&~G`Il@_!Nl2;H+`K%5SI#@7fTKq zrQo=3@gX`n!xWlPq++o-jeQD)5vWu&(s_wP-J|${4LMvyz#gFHg$<~L%}6m65^Z0% z9E36KV7%dX*nUsB`VZVz%Q94!b4*DIttdz}w60e3`1MFOzOUiu9p UIE{xL?6fiP?M_7V zV5m^ii>FBOsAjG{!tW*G|3F kmoAn!{2L_pjM-P|84m#fAg2;OD#L)vbr+;E}^};Bc=z{rpQZrMyZU7TzD^ ztN!8&q)R4 !AXWKLf%#Z6a<&mZM_;o>@jg%hO&)JM8EN|ccWdb2=ujOWso2Xt+g zf*G47xAqb-d(+spT2g2eODGG=b-Cnrsg+nQ(W R{q z`ew<1C!}jkp}eANtrWK|<~=W$(AaLuj3yG}WU|ZQDj0hob3{pmAswpIvx6GDS_Bdc zesqJN=0eFYdO?ylI~dIf9{b^-$G#g*@DYaldCHaUbK8>F+AYZ!f)H&?8U{R48AjRR ze*qOmy2g9@8ie`p6ll!H4w+7~BhA!_tB7TPN(}NR_Ql^KZp+7v-4VC-FnfhWhK-oj zVT>@lRUNlm_eh_U;87d0Qh3xxEVXtncD@CP*W&Pt&7X~fER*yRr%G({V#%w%P9lS6 zh}G=owW _M8$!bHvftcVUeBJ>eW&X{Ols`WAb9uS!7O-q zlbb|Xc&G%5Iwfn;RZ`rzPNvMdO>#ywVh_L$Yi~y9OB+!o<7Tdq?DiWZr{e~Rjcp@F z_8af* xh{j&U5yXD30M`YWPBeLu85qbXk z7p1Cv3e9|YeE7b}0%ll~Lim2}jP;T>9WLIp(U^0GFUFj?v8rZCEh?2kB>F<#H%V&O z&63uBi-cM?^SDW39qX|N>m+CTCMlV@R)T}dSsO3f4ET4uB{;NFidoA@r>^NbDZy@J z^BCnZ3;U6YElFqH`-a+p40Oee?(5MQunCkIWp=EVqK-u}xc*{^6j#6kyhhr?&<4Pi zW|boBdo5wMGA|lC`c!k$co3VbgR)abT!Ye>lX#Jgk_ipgvQ5_(yZ#6S^EQ%e^YH*P zu?JIN2csFIh#5vD@<_g2zb~bUvDut-I%eZaxaoE9xqEnvr|6k_gK~vWwPYcTv`Zx; zTb$63OVa*IpH^8bq=7=5oP14Q`14$nzLxNYEu)f$)$J3DFGp;tr4X=FBs6lqWR$ZF zntzCd@)HR++(SP0Udfw$r8vw!IPpKipKH=k^JP*1X9N0g>M)i#_m4yBALMNZ(9Nm3 zLh_qeO6CaQ3_LYs6wDYQMV$-Te%DD3#4;1tkj@rKYr9UoE!Rs*3+|(J1-~~}aw^Z2 z$jDYm@dPT+b0rCrXd<29uq80Z0}Pj-xns?QsauFkBM@q1F4QdkXTy-;l_(tn-p7aj z8wl$#?nBL(F}Us^FpUq*x
|q&t{u6}UtAR^8ZVL&H?izbqp%k^vmmxhHq*x&_?J@{?`Z39uzY#8y zNoPMOwW|*>j!5I`LsHLUHPCMc`YV6APu|?#D|?R~lrQCwJpRIqg!*Mrh6JuLQ7nYg zmO!=?RGdKx&pJuP?PttbDN#&7X5(D31+#HYE~7jrm8{*MuG=I${Z1Kt!NW3n!QUkF z^j}M;WCP6WGg6wjLb=%4MzK7X=nsm(rbT!@Sw eW!=x{Rq`Ifwd>ID>9uB$urJg^c@hj-qGn0^D7~0p zFVcpO;eoh2*eh0xl3Zw4Xpp4j*Gt-%3niywxnwnbPom8aNVxU)k~ZUc$v$g~ 0gz|vomCYrPi;zY+?)`NO>R`Kn v+Q+jyU08n;7C$ICs3C)>s>y>`7u8=ap41|vR^N?a>hxsF zBphTr`TUwy2c>1rerZ{ESSr`{O6yI#WYh0=%3Gh~@X6`#J# d>IVsih^22eeR5<5bw2I=REO5a u^CAOghLXF+T) s}1+=6@6AP-!s4~?D6}1G~v&85nk#-OT zX&+W&pFV=x_;4m4APhJ9tMvCy+#^cb)avU{FYm|eE5g+irSjS^*Q%!`x$Of9{1{x1 z-5wN&Cr`ZTr4k-8TVkaPB&QBJrTK2Q`LD5>zn9|E|01I=eMia`d@dEsNan2Qm5JAU zDBh-D;UOQ8+$jsiW`e9??f&_ixW1R2)^FDPQ{K|Tbts`e4u4fF?FRfWCW*XG3Tm$u zdnngnIWEY2#*`k(>cR8i@%!MP4Kd86zB~sUWKAcD+;tOP0rJU+u^+P8Zw2jm$JSL4 z=y{Sg UGiaM#r2z|u=QtB(EW%M&wf!xUihJuE+QchUh4_a z+t&9=_lm8OJ!-bN@EB3>qQPxwc$p0CULr%gRvS`?FLylqOtIvXBwMzOs#_x6D?gOB zHM^yC{XuEpuus}I?UCs>eI*s`qt5uAgfj+1su3vgt|@udGVY9rrE1j<>=i6yC5eUg zM_?U0r10$jkd)TjNCsRF{=q(@sPkHJ#|9yCxWR&lL}04qPdSZZfMt@_u>upmTCy2g z9oPeyLk3KsZ>YRzj4l#{(|H~1uZ0Ggyl)ovKqbe*nG3`>coNq};REZcHBrIAQ>3VU zu4GhSEoqa_GJ-{4#vt+ev SDIQ~uFEc-VSe?~z4HU7bm-4FY(yhzfU!GH5Y@nlg1fNk_p^x+*c zjkP7vB}p6w_v%lwSaZYev$quQSqMcrIuMJprr*HpZ D&O8&b3Okl}lpmmig; zRY#>(1;*98CI7s~B-F-x;n$=1@f@x_XWDE;8ulbYfEoMlN*gYN+pof}FOhU2xg2bE zW(P??Y+NQnU*Cvf4~zl+GrPe*QD6@HAR9`U)ur`@*I^T`$L7xwXL &K8kpz$wMV2*$C_TLUU^t*RvcmU zN~I3$@5IYLkwDeMlG*TUDJY%K-%24ddh8yQ7kxeO|NlDte|;VJFB1RJ@=GMrv{v%k zmPuf6HS2~y#opS|ON<&)UF#@~B{6J3rm>%heDU*{gaFyBeJ&%<7!;KCU>B4RU^j=H z{j^M+` XE#xL`|86|$G *|kMu8E0hSiN<2uWUP zj|?M?Hi)P{ui_jEB0>g7(lkdnW2iLD`6JldC9NwCNGp+lHN3z1hP`s*<9p=&T@>{k zJ|sI19hJxa^1Mtcn*i6Rhz{FjVxBVwjg>KAaMQwVnm?&%#jfwMbg^yN=U?W(O( zy=t#iEqX_SIa7(4Nk!sc!^N%AyzE2v IEc~siigZ0?5D)8U1_JCBZ+adWE{)xt* zTMcd>B}y-9TP6OS$wrZ)6AV~vA<3QuFX~xJF%Z5V{AYJ81Jf(9^WdL2AU@*XXJ}l2 zsF<#?0L2C|_C-v205+fiUyw)9L2C86WQo%#epFs<6*KBmUga55h=3Wy7x)n;!^9>T zq!shq7D#r@#S$GqT>|-&NO)v1H(nGPEkH#mWPa%?z$R$%Bldv3;A1an>|pE@Y(;=Q zU=$zl_cZmS1xhq18gULqUT_NPv+WVo37Rg_2Z_J1kt{bnt7f6(HQ=wBek55pEizN?XAJhO+JQX79_XI<9=7wZ?8}2vyAJ h*(LXI_-W#J&LDS zwLJkYpy_6do1{`$9BISwFVpFjrKY~^hcf(>U-EuW%Fy#(m$GX RAl_ zNg~LRAYtN~+Ide&-KyPEvkV)8@1JqwPPy%={qpv%UfFp>i~5er(=Wb6K_9$9_cp#t z3ux=>D&@id_>0U~ewReSe_$p)zx#G6IrW!PxA=8wr#Pr}-F|EnTxIFIaQ-rWSCdb4 z;SB0#FK(445(|wh!2i;tQqLo_RXVT*HLDJx2ketc7r)55-3q558A!sSfW)OG95S>L zRYDz+BJ9F7;CwZhjN?DM6D1hVpn8CgzVFldKF%kUQ>a?VUO*X)G4hC2qA10g#4j1+ zyWjv`l793wbFYqQu?*^1EHRRiVPb`JhVq8YHtawvc7)h7w}rC{=uOBgn=X+-Q%Nru zVb?=oh{9^Nsfu&Zerm^nKagiA)F~ukDKvaQw8ItX(pha`s3q<_G+Y9?_2M6Kp`=b( z#1^B@8ETi;g=?Dq8yR}`voiY1H)ZPLt Ry8J)Cbr;NMy6B&N- zD^hgYql6OoQh$1@WL1%P7 (3q|L)+#_A#^vNdmDtA8_}~|Tm@4JJ2{AtzCJ7A#)$UCgb!P#V&y*R*t}bA ze`3FU3gzE_WH0bPBrm-7I=TG{5)LFF*taTmq@{S|7r(fdn!ls+r}uWqS+_ncMd$xe ziY|N yV@_^ay+4?2bYxkH*TYxO|Cak1XdA;w{8 z;or3@$T_Z}zL0AgddZ)mw6{n;?zd2n2^h(>5Vq+1BqMG|Ml^AcgaDZTbm)H!37`-5 z3jO~I{$HB_g)}lDvPCZ8%b=c>;w^#Ffk*8ZF(b(a3QJ{h&tlBODg*u+5*9J?k&j{> zH Jf4 4hb~eC%)!KF*U!D?8);aZPe+~IQMbsxegP#>421N#Es*o z8ld!z%fU4!w4Twp96Nw1owatil+S)ZtSJ>7vviSkF4`uw3q)!bi$NKy*Bq28=6}K^ zABn$F@qZuqU;ORIzsgt8|FmhBN_rz DY%@=MSV};oC@HiaGY74npD9wR@%V!oNx}wy}2MJJ?skCumbS z*sE093SP@s?w1M3sU;V^iQMuS65Y?GuyU!GNYcf7(9JTq>>` BH#qY|r zYj;TV^1U+c!Z&1O%fr}!ozl$TY{I@YvM$y9oqFtf!>ahG!$s<-U5<1Eb!%XK>(owN z`Zx)m(e&pdBjWlUDS3qC7gHn4J%gJp_UhoCO)_LARd!@cT=d}tsVj1+EJe*k-TcSo zj9WjIpFX=s-uvQ^?B2gu_8s0YFK>BMY8xAH`$}2k`&PqNExer8P$LgN^Nj2~cvRl~ zV!zz=myhMj``(w%8+VX8fRYfJv@xb@Dq+dH{GCboIxvNV9vOTZ)MckMa<6qup{Prs zSWu8k zH674S5)hi-H~MUVKA&!ce{uJJoC**QM4*USgoJrr-<23NCaZG}^sEqDihm}IGh=U} zW9y}8<`P02tuMG9pF&N8QU+wBbgebgo`o4mA6m=MM*R^XXQA3%Xh&h)0`9pHDo^MR z_O;H$8yO<0C5@6j;VjNhz`xf)LmO_BXv5FI;6v0lJu5@bCxpND@5nv-r249N#S Y(}!?4LT_yZDmMHM?b4^N(ck)TJE5^cT3m zK0*N9L--GTO9MV6j{gtA|NZ#Vxc|42`TAD-fAH->e#t1m44;e5Cj>}uBgCaG)SErr zkk4Ik01JCgkCM?NWmNjlAUSNLi%!7576+uk4T?_tHGX%mG| oL@eud0fuua;S-{y7u@LvZQgjM+8Mnc(octRbclJINVg3n~~8NU#Vw*bzkDKeW3 z!ss?E`-s=UFQ~8S-nd)F&iIWa`_GZV72lWHNMG$x`4-l(fwie*{h)n@Jy)Avr|ZT# z*MgC@RR;_^RKMU|$wc `~AD*i@gWsuUp=ehSoMCwWnunsALu5k@GVQ{5$dG zLxvBRJMXzmKH2+)d~jf&{0-ajgFo$-vu@cg&FjB{ryRh~vlkb>N1YWRGTgz#`5|dT z&fxlY$MFyUtYH5(6ZW^O-B>EpzG{!u&HIQJ<2}fO@YK0)gIyB$*s%N_)_Ht0>*^;w z_L1K3OUkgRQ21-{^+ jT)7M ?eC-w|2}nLuS{W2lwmik*6hLdGJovL3j9+I z`>A97ZkfGqFSY=zv#-mQTQc7*@U8kq*bU}?0{ma`A32794N)~_`PTA(1OMqrpO;BC z_|IrsC;ldt_0~#J6Jc)p5ax=wirP)>{|T0q6Dlq;{Qp05fEaQ>m~d{?h0h2kMCwVS zHEhCvU4KL-oc_2Ba5PD%aF%pl`&aA%{+T)L1i$TIzKxixmC?*-0{e{&wI4)=(lP%< zaiwV5D#55U&IQDEq}8TrtOjo=JNqf(l{(2Be5Q0Repfn(H5z$d?Q#pguYtd%Jfj95 zsUwd4xDBYs|97q4E+y0E!uR2p>`@ajTWm}#zJ589CNemCS}W `)V?CGUWHqJS#g79FX0N*Wcn=x~3x&A~(Z<^+W}grL~C6kgM_k5tW3n z1<}k@xoYLL^2YY}< *$Z3l{l<_UM(fNOhjGOTv(#AB@F8ET?XZ%oNv({2qw1IHu zT5*txQ-#xyD>bF015H%Ac5Yk4J7AxV8h9f7|JnT?uMdI_YVA-CA!7DSzSk^J&&^Uq z*>beBOKcRBXo+p(%w`(sI@Bq-WJ9?8==)rU#`IZ&icUZ6bQAQ_qga&=Rgjja+%J zX)wuJ{zfYnpb7L>E&ew^zqb7xFa7IFL{Q^Hk3x3E<@9#o5n9(tumu4ZFJ8b>=GC8% zP?TX*b!-2E2{AJ;tPzy}fml63`v#H%dfW_xk1{|QTiM( ?!cvOd4#VF?dgDcy5lmM;2hY7uNJcn=lwTUpCyEPB00!XUp6g{EHrt=|(V zH{lhgo^h8ry_v97?dT)vP&|RoxW%BIf*ZDE-b@bEL)6Z!UnbdgK!ZpiA8U|5lcFKo zqAXrc(Nd9_LZ(?~&X)JT+$%4<`HpmVwo+=QNla}A(@2%q@Mt8bh#0i+LOZXu7)MJ> z1AeEpozf`}Q55vfzJs!5*8%y>I|pUX13RVX`p>a!&r2xEp?`?qF1%lC*cs9TD{Wm* z9}#oiGUs*52I<`Q4yMqel7fUhiIud=tOYMir|#$ZdnhQlOR`Q`%{{GWVAZD1q;fri zm`D G~tBqh^P15Ker6Jt(GpffIqfB6`NMLAxka1EQ`{CH*%Z_ij_$S`>XQk_x zwm Urqy^>SErUM0yt@E^wPr?(;Swr(U++#m%#D=6GJm2%!F-WM+ib4{i~J`L|U z6z`~!e+U~K#@%ajAfw|4Qn>_X&irds+Ker(2anB)W7dUj0@hWTQ}3_X|3^{2PoUS| zS24bw->qBxinv1~P<=SPf_+*tWj-Ez2l!@BvldmTw)LxaN*n4;J4v?&I_j%=z6smh zzIG=Gxr1EKeyM?>5kRt@`w{s`0`hzZ?~^&K3ruOEnUgJ2La4)+%aHC>FjQrxoQ!b_ zNp0A^Eu&c?B{O6QtTk)q&5}9uhce<6RLaq{B-1EpLy-4}g0gh=DrsnIffZ0bs%oVU z@cz$3i&f&K&y=FW0Q6%$v_*2z*kN+x&weWJ?%XZ!?A|AT`1r6~_vB8w^0p@>l0D3L zPfZ-=3_VRwTfI}7P+3~J_nr-VWc-;Ah_9eiyctuZr0gnbpYyV`lcZ{5Z`RKLf}}lV z>DaU^7`G;Cv~u5>J6TK=HyKI~>^x=MsS>WDXiMEcc3tsbK-Whec+F&;qpd4R>{5`# z$WWCK+n{Vw3xYCn;~6|gnXgoEor)dEBEm01(Th rSxzIUUkO;IPOt3Lz{Gx{qxE>~%yY%)Sro_bG7dlNs24@<{9t#XaAv zlBa4g`$_FW75=Aw @TTd@TN%{j(;a;g&3WrdYNcSV7 z78{`NtBO_)HbIpERhp}K?<(v->BXr3b@xh;BEbyBzXklWulfpHwOC9GU3q-jY5Qi2 z|BO?AjIMt^K2n(|do9I--wCD}1*d78AQNoCzH3Q;%LWu!qB)8{icX=-dCClu^Vo0H zVh2jDjl^8mqz) q7aiU9Nb?ZZCl^EF+1wW-qTHy2E~V5+SD!0WTVrTj*Vk j{-lXInn$TnyC4~PixO-ueFJqv#;;nX5YVVzy5dwODm9WJ3=f{K5Q zC{Dz_MuL4q@vnA3@gIQ?Xg{WdbqS@1=a~%a#w`l6UqTmOp qldQ~(sNp+l&0p#-Nc65r&dk}`RTgp9G6 z?-xs?jN(CxID$wlL5N(aZLNfvyHx&0>I{4dk-ip;<`~L=7L6*l;fgqe6Y(D-k=%!W zqs@RKfsC#nkxc!}z<-D0AFgI`pe9%YD3_@+@w5jF%o{}y++#J^tky?{;!^Rc_}BZ_ zj87HcSMfOJlD8zU@q3a#Zl;mo)iwb2)9T-}Kq!=5PC>_W1`p8Wek&?K6?R}63I1B; z6KZ4Bwj3K(>{rGAqzgaz*5cn{8rVXn^f6A~==Bjn>ZxBToCf3Pq^gV&6(nC5z+2Wz zS~Fo7F{Pd;V-$UZ?-Dp&`nV2B BFiuIu;7WKQ=8XbTytoEtmkrl?mt`4s;K2LM}rHXvTC7`F??=@|S>)xa=5 zox}Ex_7p90^|Lmu?KSeqGf&C;`wq*CZ@(?0M-G;dw#_&PN^EqO^sM}f7?-)Et*9Ly z+y-CjfOB;)=c=kSp>{P812tkx$6o#_cKSAo3$(*m6Zgu|Dbj9VLO~3D8t{MZ-qkL~ z)ZxvNRz@HAcna9Ya)992YZ>~uL;@3*N{|siF87XEczk$9@$XSf#Xo+jgr+Q$bnHPk zrwM7{nA(~QR8#}saQg`SAWZxiJwZwA!@fQme1O8jjIJMG!-&hTLn-3aq|(!W`ZfOT zX~Y8$!U5IK!}-YKDDJCx)N$Mf;Hx#etJxkBNm9P(0QUWHPDZ|-vl=FXf3m!)&}m^2 zBdp!3WWHj K`}US?wtT9@E6*YA~9{AR@!4;gK15ekZ&aKPB4S{XD8j?X^K zWUpp}`OFzN5!c>|68K%>obO5bmCs8XzdH{7VG4hD_VO3`8)K 9*VCPk(+|3JY_L z41(Kzl8mdKA(!0n8F38!o{V(|9KVzKZd=Q#LYy?vflcVZZ?&>_>xp?v&iezsTg24F z@ln-lu-T~%>m{Xpfn=1Pi9M%-AN!*n@AhzsL?@gt-f?rq4ekTu=SgV7wURP{!sm(c zkuqVP1jk>?o2!*trq}|A7(9#4~)cI7d+UHj*)Vp}0yHNf7Ns_fU-aj3W7^QAim@ zD~J+P^koqK0smZ|Y6IyMke4rg2mgN%{2!9Zr~U-hKJNcL!~vye{*o*waUIw*I61aZ z?cs?-pVvwcRerTXXTqf)N_PES*w6E+5K3W9m~(Ki%_uhfznwJ+c_^@Q2V`i)#n{tr za47Xv=m8Xxsn74jexLu3;~yJ)&O73(`I-0{eq!L?L7}zA7RNZh@u;ytipTmg*^qxh z^^Z3HXv?g||M(>CQ%@>W{^RteNOW8$nc=09(L#I&{$s7g0!Z+wx*q6(TEG< Qs9E*qW7>&k+x6e|4CP`RG8G@J%|H8otr*g5 zd>yYf@W133{*x)j<21^_Qx{10+Ap!mhlSof 5G07cuiOib!0foNA3+P3v{b`KRwDuFE^mf`bIk`Y_mlcyC?Tf&F++V5kpGiYQ zj5gbhjyovuevZ?*_ZYE8IdMn(rq5*cy&uWk<+pI!0og!e06$eee( nN+F(;50hVY7ZNTaW!5wsvPurDy(wh%BlEW)oEjGWHbSZ2R^SOb?O}P z6;^;PGV8 yhTCe` jKfiCGP&2E3 zo|Ikj3LGC@4*y?IA#4*v^>)p)*O8&t=hO}y$3N?%{NO|zpx9UKR&7Nb|6B0?6U1{I z{|mwYS4QPvh2~|j?Z$hu_nXlRYQba!xtsDUUXY?zs+ovmC!BT{`uSGYkeC6Tpi=)P zv0Q`hnGIxwiSM-#-Z&k|Z=nczCijZ}QM;!+fC{zDjvvb8E1$s+kz=@yy#>FXvGEJJ z_4gmk>z^K!H{RJQ<0g!!dH{Ul{}umwVy^mk#lD;77S&sFGr}?`KT`^`qmmx<7 ;WbNC~7yl_8x1Bz)gsu=W+)e17z-{7g8*+SAP5XKDp}tFGvGu58!U( zO=uHlvBj8X=s)sD#kE_JGVVm0Bcy9tEn)1YpWZazq~%cj<&r{SY|i8}#o?o1mfC9L z!$9cc4P?-t<)a3d1ON0h#vEh`_0z`VLwo2*ZD69jT$hW3kKFW;dc%VxkXc55RGXxc zp^S{aRQzM;J0w)FjRg|OVv#CpiwQA}J_1aD78LazTx!q+sDi>j(!5zl(*MwQ1MpY( zZ@@nldBx9MM3Mi0am@WwIAUxg!hIeqp+n_Bz7h&qi%e9b=?}KwgiGF+thzgle%=II z)PTJ9DRMxqA#X=g)8t2Y{z%%|kc72hk%OEZzI>U`b1Ufs3ai*+)x4*Mvnnn2nVTxM ztkVCHXTBlc>U+dn_hZBTJ1H{wui#G#=NpB;A^(&9Gvt5nSLOeZhtm)|6!&QwfP+k3 z9B67MMU&k?KbV?n@^IM{1ZArUXjOBqGB4R<`X+o)3Q>`{bbBXL{1O{`4)XdZMsGv| z@NWR}O@vI%B;K{)MhkJYxLS*<6?RRGX5g U-2r1y?dl-GvN*Np?l+A zx%JVV^5L%ivgg1ddFH9-W%MxQdFa2M`|kt3@gSeG2o188n-P-Yyr>lCr%6dcniS d|HLkt zeG8ocEA|?R&PEJv*YbnXwd{b5yX1A8{_T`^u9V0WM7xQI6?CRJCN2|y=~Bro Bp6=;sh*8eOG`gi25{A(M*!{BwRStv7s$ z^FJV^Gk!!l8pkuyRp218Htn1Tjig@#j#&v%J7TDodrl* w)iqmWI6_d({12pb z@qW6}9+M)19h-kJJIRNk<$N1 EOi`=-B zVu6K{JCss6tiOxBF_5I(u Q6TaYAWSE zWpn>b7Y z_Xz4Gi|UzKT7rxEaDIUwtL47PrvpLU}f z9c IFXtF~(3*#TtFRcj6};sKwS(F|_MF zH6tijE}A2+ZQVw(K(D;;MXx;Y`a!w!$Dd G^Tmr` z@52L!2%<8kULoNTbb3WcL43n_bGj}ah!Z3-NKP5TrFK3WWCS9kiA+7x?LQlOh>Q+R zW@sY8I0TgYHjXT($j0H%5pT{k35_}f0pTj~Pe2$Li*)jI**<8J U#@z1A|U|a02u#}(s3(7xPe+b;L?gu2D T6z;u~+ R(|qO8ym9bj1%c%Et)M&W7CiL?CY62t~%O+r+r zvt8#eF8nI?kB@H!{sZ9On-Z0wWzAxX eY z7A@Fb?J{lOP^|MligkWwJi Nu_?U2o|wXs^qfv( zlD1i0^5oxMkxxkyzO(b7{Aud}S@zo<*u>9_^4Ad;JTE~FcFUHCour)x`)O0a{^av% zZQ|2`5y12qbM34(&flVlNk6W_r6MJ3tLxi0bT4~I8hJR%Q6KdnVLevSi`~-h{1hXB zi5=j3eXRyFda6&VJAvp$yR|7J_hg7ScbY^-o-LsfaDg$4#WZn&n5He@EIB<@K!sO5 z@k9kmjxhxD)hqulryypQb^kDqf7%G_?ub;J|4V74JhY9(K;>nB;taM4oY9siVGc4L zXf364b%IRn{yux?6B#`FQ9kdCzvnM9+J^LaQsp_?n&hIeZ0N8d@(SgepK}27r-zQp z`#e7S(koIrb*!ZM=|cCpC7n-U?!59T#KprhiE_+Jl1nx0xpL(jFx`S#&xyO@mlCLa zKr+F ~yiR|E3}CjBOP&$*%x)rJ8)Qq*AEWa2XxZ2!SL7 zGa5 UN3gLu$J{*NkkxU9p zZ d_YnY4VBfP4MKGVl#X{S2SB(W?Ikw?6d+48xc6Z$0c>f2OUsMzV~1?mMzmA5XMX zalQ*@VNd8uUfSs#L}1qw-_&;56u-e-@fX!dWZVVf9=}+U$1E4yq-BzdikE}$&n0P? z3H~F9uH&!%3uX7aq^#rnw9} bW6_Y zD=3C~7{15JT-UzO*@WMhU_m!+!4Y_w+9 #J6Cq>KP;c`KO$dpGTXKT zy|V3)$it66DPu>CKp=CG7)0 O{!G9J> z51Tvs|H6Nq3;YYt{XBN^59?&`f5S$zK2|R=D84?wi3H9NT0-5_f)k6HNQ9+w^3Ra= zd0>Bq1gFfWj{9oKop_Ph(??UsjnJ*}A$!8jVdq)^r$@U6@tt833PBVB7oGcNRF=Hb zc_i$15tAwRCP4@0+YnMUX{$xP+8(SYr&PhA8Ytp!p+HAl0~P ?ySL-tajI=3R369UseYUfLtO_wM8Lt^@MNm$pcCeXCKlqaXXJzTWWriXFuX z{v|6KFnoS-ek%BnNMTMyG9sM12iGS7B$@nNabH{zJ8lQm7R1shvSwfQ*?>N58U7a= zqZ}i|9#$I=gfA!;h!uuq`KGn$%xM-kziK>K;BN{MBtT`TX0&F>M7HTY-(AC+Q`f z#5h^lQ#bZovB~e#p01zMaQtJPcy9w6$8c{a7O-K@td>Ly+BmfYA=8O%($}l~(4qe} zID^gu*WSl UQ#)=$i41{%&f`P}C@20`{5Nr8Tk}RO28Tng+DFmH$1-c($DD*l za(Bf(8Ghzdk_#R!E?QYgUc1?wdP1S@b1xW(W<=#bo_JDrDCYMZl&@&r*l|#8z){({ z|EN6p@E>K+U<$9mh7*M>I;vSZmb_{xg4NnmIG6o5|0`6oha^xzk_G(dPF@6`Ci%xb zAH)2Kk1umR@i(jf{U`Vj;QyTzex-48RR-t&7ByeR$zLTTMM-{QA01vkyrOg_N5fwu zA&N;eOD~qxyc&ECex7}2p=id($v2Zbmhh3)#08VOBtRjxJCu!Y34kTeOr^j&wQ#!3 zSotyB9A6FoXQB@%{*|kDkeF6JV5rt;DBY{SlosN*7Hn%fez%e0AD<>_j^V$!d@*(b z{1bC2{^J~9l|W52e9i5T+XA%%$MH|m54=EaLmdAHXo>wJ1^pBVOutPEYcCRyUlW@s z+8!q{O&MCk3v%I)K9d)>?2&D>^L>VIc=>Nzq@lVY&WqX0J``|2d(t?43QVeR$xQVf z=ln(a5j4M4d_FC96kI6w(-8i1GqDNB@UOU6E}&yjVU`qRMR}d7|7u&}T7cq5i;OiZ zpyyVnfL}fNN)v}3_Od_dl+({VMP7XUZ}JhEz$-fs$=!eZm{Wo7lj7QW63%PjG!2sH za1>Q;J(^vB!}*NZh0}G2`&IdO^K(9;P0R_rluu4b4e+VMMN(N;DIKStA{ShJh0IyD zST0|1t#q9}TdG@|WPIs3$<5D_a2R1;FGnvKGoW40TGOPTgsz`fuJlTK9>UNiPT|LF za1yyEotoup368x4Gqr>y0XfNuOC+s<(38AN_ROD1_tJNP|INTYh-l*UANUd7q 5S#GC3nWD6U;cCs z!mT`Ak~t(vKc_=K^4XDAKrQIG90E#rFcPtkkVgAftZaE_xR?VHFO{-OACjKicS $(EHnq-ferQp~8i>?LWu z>}jL6#}zwGrgD-;m-ccje3g#;G33+iPzh@PrY*!Q>^ADVDhU78q-dy6Ye)#Zp686b zp2`TvMjeR=H33HbA#^~)1U&_$V#!uM8RIS~U|(f*-Ogv^tmkm8;f6a65eB*GS2=Cr z9rDDx9Nx3*Al>}? P+5?pxPg9AF8}6vH34~ %qf(e~^RC z_Q*RszLZzreMcU9_F1|8uDj);tFMv?Ws{AX1;uz8^hB@It Z`PhV zjw%u|nv~O|Q}v==4wyPD;}&n{z{5u*I^t5+Csk6JH^b#YRaL_DVlso&Rs8h6`*HWZ zgms5WsT`J%Ih^^E0|$9z96TbQ@U!iQkI3!!K za>5EcXBY} zcCPgo5-Pu6VpR`HKDCkhxj*r6Noh}97CB}E{-yZuqh=}Ge;~wxXcI1y5XCduoY<1z zLkKpy4OxhE0Md?wxr)&7n^$%^^)!Xh0+P_UI ^9FzRS=lO&qJztN}iIq4a&N`Afx39b?9-MapBLXWv}vN@l=n^E9=h$NfSBz5FD z(zs-sbn<@n3l#rjB>=wk)8>k%E}*6@j{j)}{#62m$|JuR*jN1P^C#k;`j N^H!s-H3S{cS!ptV{c?F_>j$#p#v3IV zi5TbU1y#g{P-wuegYS)V2ypmrb20%*R8BwpOc4L5e7NHajw?jyIHH2MfOe5>h+ o)HHLn#Kht9kw}TRW{{63Z%PVicBdga@5z6^S#`#>DwBT>(Sv`6rsD2)#BGKrK zB2dy}fYqy85Cwe0duR}zC*HBk#f~W8oy^CSlf)YjYje`?Z{tiRi!)6sE_{F>T&r *Vj-K9_BJ zKY0DEpnWS=?Ni3b`wxNqBl4+Q2IgTKDc+lZbcbZ-;sHqtI5_Hk5ND0F6L>VPIY5Wg zU&UYkE3Ck8_{6uB{||HT9VJznwSBMeopBUNopVlfA|p9x=!T~0ZgLhA=9~aYN)iKR z)KSMc=72emX&iM_6ay$CG@-l6Ss2{k@48RjO~L5%yx;oPQvbNusj9B7I(5!{?Yysz z^B<3tg|-k0>k;=yLcgo{OUk5@`&=ru=M!+D&z X-J~wPsca4%=Ty6$%}V{ z(9hphmZCr(R$Ixe)z+NKv$#}d @+%1R=~dqh!<-CaUK_u@GB%3Sil}C0`7vmUxhiT82uy# z5dm@r6b$rEL242u8r%y{l^!kv^jOw*5Nv0!32TUe*raG1G_1t(3JM5tbDm%zK7hRF zDCCDEVF0T@NqMlrgG%g$S6;I%d-m8aMiR;YtwRwi4z>v&c$0kLW7bCEpd0rdvO1Jz zt>7bZ0A#k27nw5$Ch*YKPl}JRc5U0h_)XjogIf+lJWy2bow3m59OZW?ehjN4K67e8 zxy4gBd|c-x_Lr_SrS)EDVpncTuA-5|1n}>B`ePV@P Z3&x68)0?&>o zzl~w~Ao;&>#EcUVGm7 nx4+GP}zgPX?GC=BH_}{!&v9bXFTQLPbgX*w7`7}362gjv1&cFvBePmZJ zT4V(+nq$0kEhS39d?F@DGRBtVfYgnS5vZ7k$*`PuIM?EPQ>)O6hQd~ph{;0O;nYxy z^_qM?F`vCQc*ZUpICHyoy|m7X&iEDzA=srW@3tI_$c9ZdCQ+uwU-(QKo79}awNY%g zci%qtef37_H^9E+7&2@Vzk36}Lo%Qi{MYT@Z?*dlARmwid-mJE)*Z6NPj0iRSG;6- z)HKD?WZmo3yKMBrHP-pm4=u6eX-gdZB7<+1;?Rh$?nvw=r21tF{N|1S;rRFGq4-{7 z_+H|>oCUE3ITv!SNvQKo2pb9}(Vzk2?+L{9-8^63N^eYZ0lXNMm(vSQPXKY5OyHk6 z<{`rO)x=wXGL~2wq1ei+H#%bK;~0BZ*n0+ST>BlP;; 86&9Yr57f-5~jIHkCT2_W1~p*jb5 $j^f(ZXBKYHQfC7c)5y;78M zk+$@ g|yDPL7YWd+vXbb0W^iIV;5P;E&q(@UH{@TX?={?_pcB<7Zp3i4w~fciW`f zezGaI{Krn6_YqtSeeL+Dd{o@|)_wGS49>XUGE1Jo`d@;HlI`}0uDL|9jv+6AzW?8+ z^pA6>SO1LNW`Zr&Kk!e-v&zDcPlmZQk3LQHz81mz5eoz8Um7vOFEWX~513mkF2`4< z7?=``TzDNc@FRnMnBH@Ka3vAyjZzPO`P>Z_Us!I(6K`ruq4y;EfmGr%jJ~uA)Nds* zno;8Ap~OeN_(bHxC`>tMeJBM~+0QTIzokO|;9pG3^8MA%slI*}_Wu?9597Tm4Ge}f z9JFqie*?G7GB?jmlQOf$K4Sx+$`aFVCBu3q+`7v;Rm^rwU`de9M-y{sN Xqwp`2LNXvPJK3cM9sWrfY9EE~B`*h@Y?2A!_nGjI9AM0)! yV z!GXMm6!3S$(xp~MI Ehw$-&;?51VQthjY6OaV$Rkxg<*;=z49GJtV@ZfV#<6ZT$O zZi(eI>rX5Swqi`L7)EbqcRSx=`#uP}?<07x4>8*CUMn07Qz1P_O`@_fS;3sVAg)D_ z%H(so&MH?OF>;)(+q~6kDVf>~zJuQ;6F@OPng3G%$TQ4>H9O5d+kDt=e{G+gbLVb5 z`8HD5Hy*ahx1;Yae1_h)c6QwHDR5}@BF%lJH6L=1wV=E?TB$#-xiGK2pkqh|{O0(t z@B0ajK@7$pVhYjknb>8TC1FQrQhJ$zDO> -zg-G{C=r-p^xs!qv%%sDN7 IszMDv{L`GzJ^bS*gs{4iv70$|0ChM8GJRR zSD5mO>z9-38BT9o#Z|S~81#3^!$+owvAJBHKjm4aaJZ-+dBD6K*6b7*B*i~LnR))? zCyCo`uyHEpC*q) nDY{;&s UOe|+x?7k|zz#@FiXaFW@5+IdCkXVu-XAO%!^a-b|D<6@6g6Z1B2fYjHgos~ zD@n)|-#-{G1AMuj_L*kW%e_{tUD$UE>^mMZ%9}2|>`JTMxfdNL+{^S8?umHG6yD1U zKk6}S6R&G+q_m*^v7X}sY$A3p*Wso+@5a3#9V&CFp-rDX%XTP|fOx5qiGc{H(c^tG z1>Ug%WkXPvP;Thb0)I`(O~fovDn({ML}PiMT>hv8PMtP=;2#U&s%Sg1VUo}7$vmUZ zGx0VjL-gMUaRPxOreX@gt9Vwl>rLX)bGtNHxLLDYd-<(*Z7V;g#+~+mY=irGSpHN_ zRTK0J;Lr0thi&B+vqwJMWz+83iP}GC({9~EB{Ez2daBrN*<; k;I znRn88)}i}_44S8+oA2}Rceh01IGULszqR@o?O%juel k76So0rcpo(pM1x-XmC60o$tinEyQQhsqyJ %7J?SWuKzq!a2*D9of>cHl_gft<>O6@e zf>BXfX=^st68S P7;6J54`E{i@7W-;0N8KbjhD}HvqVAYxhf4d zVM?#v%#5PJ16<{!RCqytGv=IqPBa0`<5JX;JWzG5A_O%Di5ikp`(o`{JLRltL Ru}TYq^rB=(uoSJ)QrDDb z#>A7hUr1773tqje?BVx2mxl9m;*Q_C9ucU(zplZCT|e84U+lND@7+TGA*9N;{A}ZH z{*R3% puB7ftyzXr#_ z`WbZn2hB%Z3apW6GJnZkPG&a30@yrZxnkBreia$O@pu71TjSjPOb^HLPeHc0opZ1h zw6e!B3QY0;i4OwVS_gesd@m24PVFiR<`u|O#{XFOt0i#Q+#qXLf<@rkGl6K(C?vKe zf`oIvW6lTKANV&U@Ys?9_L0{plJK*&D|;9b+lAajGmA>^Z-Y*M1(pU4+vdCQEJjeA zU5X{pAFrSU_*W@PFu%rfPkqC(&iDXpW2cS1fia)JU&X^CY2Z5L#%;FrtsVBk#=~~O z^to&q0#Yo;;^Cpsxda;b1XtLrhMtyW>jgy*a}VodkAi>rYkq4Sebkm=y8~4%1q78> z&|~lI>vM9_5}mlwxf~7oz(a7aVF3hH0vr)zvVsc>^6ek*ePG*xW1XPSs$=`h0u}_E zdIlDooEHGZ t|&T( z8viByi-o11ynW_Xj3Q?{jN<@s+6QkyEY1~p48!JrN8w+9e n3KdsGF!`2^`(6=k`cUnW&=9^b!; zM`w*Ad)b>>k_9^I{eesomIE_Y1+u}t!rI4cP{rJl_eefSr3J5fx#0I$`1h;O-FqR? zJf1%DD%(M{ZYwz4A{pS+vLb&0{;N^z>!sGgy<|Ww$6DmVH|sasYwv$xciex!&A;JB zo4t6kEx2(h)jp5dhhKbYYc_1O@e{{ku2Vz^=F*S@&px}H`3EErNe;-IP>{WbRm ls?ez#WeJGlnn+|7j&-gTY) z!u$^M{0*%fljoZs*72K>XOc%=_3Jt3 rBx)Ja&NM&LRWWj-- z0UKW1^WMRG-s__LGP_4o2QUV+T7|vR{ceFjHTw=FVOx3?-u5gg9dH}U7f?NkM?Z`; z?wTEL4uh~ b|`><`tJ$>}=P-PIw z6N8@{gRH#-=4Fcn_Y_@9q~A64QofU(h`c`r-+EuTz7KyaS)nDH!OH>vxw=0`7P4B; zL*Fl~YlUMx{ITC>dQH*5fMGl+@-Hqfw2xP;#2v>3XWwt5Zl{i;YG0fV1PALe`PZ?8 zRYe9qS^164S+K~u_v}Sdl4NlV*Mx*(L?boLaV>aW#ODqjT0%4=k-s4?IlsXJ`r7&p zo2_m)1=9}uvH?Q&26|TE07V1qs43ctj9F7n0=cX_(Bt4Z0y)?SS)#Cj8~G}m06DA% zsDLpkh!D!n5dI_G_e%CCjKF -nk(gkZl`?_zC|W_w`54??+@_ z^_~N^@Y?y7h)dEC+LQLfXWOJ(*AdyGt_ZzVfxa3(eTB89Q#B&98%+g7ambFPQkAHz zQ6w3PI6@H4W(mo9-P_Q8TG6!O@xQVD7wdmRXg^M*@rV87>`Pp!#B9sx0zC^9M%h)@ zG0$cZLm6Od4PpPk@GnLtMt@2sh!tK8l3BbrgW8!aMbZ{AhfdLbbgxEfjs $q_X>L=;hzpz$=(hGx-_IZ+Dt?8223YLlhItS>+1<70J~M zxx#*PnH;c#hC%ABkS`k9J06>3EK$1(e2gmO%m}i5ReVP!k-#c43S+LJQV;?OF*!Dh z>t(%3@>hWU0oeZKe8+eyiI+UL)jr$KtUfARx3fQ l8aq~ zTOHB;Ixw&mIUw7=9{=dY4Ng~+&7+a<-iIHyPF;xPV{b ;!2&MajC0S78YzyQNuibRZZQzaSXu=$^xcFlx zjI+^`CgT5T^k=BC$m{=*4d8#P @MvB2i_!$q&-iW5c zJ&FRA#x&9;6%Viis^lXYB=WcVh=0ATN8bPD?!SxvH4c&fYnv$=N|cJ|Usl)C=(j|b zk6_#nglr0mi-cKb|52v@NU48bBTDGuuQ81xbIig8NFQ^r^=F))`gX S&4eb^+C S}$azG{5e0C)KVxa`7=bxo;#_{Ic|{mWCSdB1;XFoOx!O7nyp|;ANhI(|Vv>yL zHTeNZviDhU%!>-xd*(mAjpUBZ!2{g-1NQRU??JHLiOIs4mu!2nTZ!yA(Ius{y_KYw z>Tc}nc3Krt0!r^*Mv~Njm}td3|24Slb#2{nRsLTh05RiwR6|GKSnzAG_pf_M=(&SG z-=kz-YHE_bLJ|LV;Jz6ZS3}}UrtnefA1G`h0={wQE?Y!VKuQ`F!@x?6NE!p1fCx+= zk)B9VxQ_J;$Oj2Kk>*P6Hne{I`r30Zyo~$5&%wVMGeA(=%&Jks0LaV4kQ?&xXJ5El zA(bV|yzkVhgZ+EuO4~?7&ZEnT(vBTv85x`lBCIiu2~8+dZi@L4!M@OhaIGlRt;`Z7 zYjZO*?6j#T+goqHV_T`&k-M+jJ{P8d+d~jnB+`XEBnCJSEx9Arh>D>@hf{nYlS!r@ zBMVUhCq!mgyKX02`}QZ(IFwIxPe~z|*hu`H5K%~Se30dly>3 Xj`%Mz3zEC37EiBbdL_j$ZcqbmKDZ91yCWAw6zSBxLI5ne z0Fn)40vstGvLcsIVFu%VJ6`;u8^x!-{8C)|QgG-RGUNiN<_{A A_<8R|m~Q==vn=k&aY_pc{v3rRjEz})3~Z3XeG zZ3lm{ZA7Bq{rFSs*#n*dr|tv%s&`%3)4q1 M#K4ayy_3|OR^q3X*qMA|MOV)$MDY$f2`hMttfEPkFmpR zH`O__UoN+BzL`*$OZ;PwE5ug~2IQ7M@Yutanw|u{aJ$+6%A9EmeBhGk_ag_x7r0d$ zK`gSu{T8m5u3LUiw#~ kM!$^UM_#qn83$s9%fRp_L!hKd~46Y5!yDYO9>W#gWGcI8|K8=x!F)U1dZ z(yf1`W#zP|DFtFsYFVR?XIL2F#HNHM8$_g9Qu7Jc`Q&G;%hcB_ui{B-HvCx{|E{Lz z74;v6f9->i4EP<~|6u4B`=uWL;{AwDZf1GCnR4Id3XAG}H3L7EP|iqd1-QnPN z0{Zu4NL44!T4Up;e`Ay8ueVbm(;rW$Wx{Pg+nGz(TWT&fpon@rtD>IMDJy2qk34%s zh{}XTAu=kos; n^27#<(;Djr5-=0Yo|+(&aN}Y&8)$z7TmpMuiyXQ>&STp@DD{NWx7!AKX&2- zd-s!nyBfi2%IB*Q12P4+lL2`C Ybg$$80YmulWh zRBi*N$*T3$_UTt&+pBN=!(MsgEqm{uAKACxueD8N5w;_~)Ui~@v4;Oq$+9b3mYU}~ zB?4sS>-Sdg1^&<^=(Sq17Y{!9q@^MD QEat zgwil`khLFsKZ)#hM7EeUamttUDnG-lgQu(j3X?_+Q{27s_an>{{6oNByz)(As8#DQ z+LC)*0`0=3^jcnNDT77#r6`}weU=jgMEe!-ck935uNhLB;+ze?Zt1n#T|0gmk-Re0 zy=eTL8-Us$rf3`@LDW1$fPX1-`V_UBNO8a<$Ua9y-!k^jy>MCYwgoR7w%cCVVg;@G zfPH!pp{gkAaRsyPcA)M9{Hy0zvjs%|r)Wcu)1R=Uv`!Y^fCv?ZA2ICvxU^O_@bp`4 z>}{KD0GXR|>U*a@R%0JmA7tA0emit{r>$DM%1TF!vj*^ri?&Z$CgIHAJAafoSFKJC z)EvUS@7nQnp4sW}Wh1M84gbQ1TYR?5PH|NKtunDv4M}*ua?N5%UaMr^6&hd;tLQ+{ z X!$6=Gxfo_ zlXJYUn$J|xg6f<^`SHOgpIO^BZDDIYj_BS0V~tZ=+aP+;httYZb;C{@bnfHKgrp#( zpf6Fs1y*(Cx9mM;g0PQ=P{h*yl+~6z^aWJtbF>7_r*D*biodXj*h@tJep}iPmHx9o zaR0^mA4H_p_p80qkm8B>=4JHYo@)s`rd#8#S3_Si-*PI&`H$+aVjk#-Ma+z14oDdO zeR#mBe_Dp}7}J_D=`Kw3YR1gc|GSWus0BpocwCC>uaJOh^3?Y$S3q^bWkm4G$f8!< zKuzVHTW#i3wRZ1^d+eoe4%%Da{A9O3^qOU6=it^c>#|WaGyi5l`$ww~|36rtg?y=` zPzZ)X8*;(FES|ZTiOdE{AQIc4X_Vzb(K2S%D>mxx9oF|2vV6B5unV84vwvap?O@+; z!`)w9z0O8VngX3nH0~uPJNiy$tXy+p%3m*CD`8($Uc$cW_MPs>25<#H)bux}{$I7f z!iV&~{#XB}rO)z@b=2R4d*>d=MPR9?pjGoiText6wQJLcDdfs}p-aJ|>i4}|@}K9! z2fz5)y3go;xh#r;efaOszz^mxnA!}EoDAS`kkJq7_EwYO>)yLJeI7*7Ip0M6X6bv} z{y1EIWkMC@(^0=sZjff#NiOOfjy;c{&QPC2_<-M$mK0|bPMu=!toYP6?c8e%ue*Vw zAG#Vj*J!Tyv{NQq4W@n_MR!}I^M!k17L3+_)h(FowH#foK6tO@{5NovDPM U> yy8n6M|8f! ~bn z$CE`li(31|&+o9OSL~%x{V=ZL&-RZGzqX4nnN6=uK7IJ`Yp_2WHB7L!C5yQB`=DH; zSP=Y6&WxsLaO%SC*0%cv*k`fW3eZ9~PGZK-X;k*CvGQev2f+T&+Yi`zk8h=Y>nGcF zV83mLy6>wEbvF9cvuSaPX66leB-5;ZA7RF0)ni!tIX6AAzDZxYUk&@-{ci#OTT%mD zn49AK1L5D>d_MH!pX(@EJh%8 R>yahlb^*P%AKM%XXvRgq613>}OAU&36y zY>|8cpR%dDck2>ra|<%?s_|78UfKO>u%Hh2ZB+8S{Knf=b%Wd81w-5#l@iqxWOD z55m1ZQx!?_f%I6*U#p|P8~a7LMg}!+mPMZfG hjwss#_R$V?eyOz#Zl|!ye-{GEf2h&=xoz^dp_hXOJ``-Sq=JVF> z-0!lRt0+DkJ8q0|c_ZL#KjApUQC2tWI^_WyLZDycB8SZ-$xm@gDXOa!l{FA`rjm7y z1M16jvD$VxV?Cn}fq(gO%zqmH8>Mk+0x|C|OX&9h=C!{L{-KfIN46l_liRE0Xe6sC zu37}qz?4>HmIlLLJYpb*R)&PZvl!Jvbdb@n=@bTJj8H@%Tm(=IIFgQGYJgKFEVeEe ze8}jn8cR6o5^Fx_CRjQ#*Kx}=a#aj}Lm8zm=B~4s?XhJq|77p1{*SHOg=@EezkT-g zcQ#}8^(5XpQfmuE3~*@7`V)|D64%B`&VJr?5BKLjsAgCrZ6}fq7 ujnt^(hY2s^Ec z%`{fWsb&YCKz$3oDz+ G=Ms0&?9XbO4Sl}NX_E7jk5m1>#;pb5Q z-MW6bGg2>UMDE8;9AmwDbqz!!;w3d+W>P4`K_+U>ylaRA9>Be)0FRp6jk`(Ek%)B* zxQdoXkxCb> Bj&$aOvHMqcB&>sI@KO2=GTbGc#BQ%YU!OR z3hqr 3*0111Qct^{yRu=c~KylONc3+~du-Vym}ovD+TJmoa_ap&O&s7M8!56WRAC zHn8JQgr%J^tbI?vA2xukF7&4Y;Q O+#?opW8- z>=2G;J-prC{_cS7Bm*Q?*R|Vs+NIO4q`i)4E} 3%u^) zJjoDLJN%>ad0dBCum#*}DQlpNp01IWFDwjP1wtD9R$ar5l5L^oqWk)M%m}^j#W&x9 z?SwuB&bcA-|9IqV6DF0kEIHG%aeov0Ek>VTZ}Fw~Q0V@Kb-HMc4Q6C)$vh^ITv^NG z?G(E5T&-o`ztgl;bp1bP>9ijg^jbhjgSJ|CACBxL?J2UxzkT*EjP+9{quYb*>$t=O zYu2haJyR{|tr6pI11K5QhsFFKrobc?4YkCs=ULOPm%$o+xux` nix81 Y$+wJBz_uBh}-DH05_z$vv?+#o3 z_8V3?Zk)xE$!V$#F8d~uLiI+3U!#)RTDy|jQ~`WJ$exJ{e4auE!?C>v;y#qpG%$49 zJG`$on2jJy*c9DgK|%I+R?Z0Qikp72Q}5qq%fH!gJLompedr)_diGJcy3mp!r3$bU z?AfF7qt}%23fz6q%jNN}o*&QDp>Tk=0ra}l`|NMmUxg0!xIgwNRuNaA#vZx$y2Z9- z*G^i|573TER&F=@mwhYtjh&qL{SQ2VE{(#yK7#+F&o{vPv9GD6UiiQ8qKhcNKa8EF zaJTSJQ4YFay*z8d!VhZQhw*3XX{WfN881`9-HUXtS997MHMK~#j;0qlVk6uhF`W1? zfc`gND98Tcd__Z`^HQ241^4Nki_fw1FFl`$N6A!J5K2G>#BrTFb?NBZfmc^=#?(h9 zktUFfu0rHZgy?Ho3J0oS*`)*=mV #5QN2l`H28*) NRWO1;l zcRcxh8$9QL^_WG`I<2K`r>tOp+ViyjFQ+J-2@8>_*p#|o3M26wL*Wwk6!`yb!+(u_ z0srwa5wQDY6RJ4Ha{HWXan1XZ4v2BGT<&&L7@IX^rBOzENZ7_%T#vIE1A74zqpomF zA^BCXvsW?3eFT%6Ml6IdYysnfZ?is^e9fFKLVQ&3lrJK)cgr?gynK&+xaNTEfDvln zp*=3__U7lG+ZmUhV;PLUYjPsGSFSECzD?2n(Xlz!ve%h5;Jmjf^rzLIqTvDf4ufwX zoCw9Mo8ChzVbsiR*1Y31LUv6po&6ON4;|!r4}yDD0UWgR?ys>|zTWMy|MSm#ar^h# z!kd>-P)H>Sdf)GJ9goIN;MOY4UmxPf)z@R+uB-hndss9d9{>8AbfM(yui?M`d$c!r zuW&JX%t(9ty?04pZKWp_rWx?52AK`mDX&qnGrFqE6_SOY@Bj3;{yU`qVf0=^As%Yg zx%;wxmHC&>Trcc{i?w{;2IxOVLxCF!_2;plt Nf^q0LQS;Z%s&uvT0|ZZa2b5*tskI4MloEYd-c0g~X*pO6~r?KVqw> z!jX-yb6khak{k%^7YdZYx^TTNw3NM +M3Y1+=5MnOQ{Q1m;vE*-YY`PPH&|LJRdFTPxRK!{=dYw- zb)R*+0^NUMo#hXI$8!2UYt08fWQ83FqcJlu6}kuQhd&bjb#GO<7EjT6lQ_cFg#6M5 z&$og~Mt*mzVjOA#J|HP` @dE?fQGTD$g|>#SA5N!I8% zKATFes3w0#-@`N=k!HE=#@fJ>U$D^>2Ww1dZ~Uu1r1u9y%~6Ukp$Q*icOOL>us5t@ zk)$uO@J%&xJHOim8+Ti^4W(J@ya#sJt6%J<$?ZS(6Sl^IgL~|r2OqNJ%siUL0{tGo z&yQe6K3YS^#^rF0zptZozsJ7zxt44I`N8V_m0c_bFTE~ZA5`uIkG<{>{|@a7*$P3E z8k3W9GHvXHarQ6dVjcef+S)pscJ8^BP3S(BRG_2o;ogOxv)9kz$MC<)9@kWWi!Zy7 zvEd)uKfhdQD}P)Ewl> )crGp3B(pzq|kH7o|p8&dHFf4yyJ+{$0Y?lE4$Ohy?(7P`kk8EQp<)`D? z?fdMVPrk54G%NM#Lxm~#E2@#Gg{bOde@9Z$6G7al+ql0%-@7ZAG)E&zC9HFp0 h-ynkaoFRis67xK50R}k}KlrW=y3;HohF`tQnRGf){Eft^X zkA#2rUw!(|*=tZB<#wCGq{4-a`K7OaG;*K}JHL59C|6Xyq?9z*Nwm97(U}>YEUn{M zsKQTYg5%jtl)1!G$>(RF|Ko= l|Y45_m)^+qfRzVX$pR0%$%sE7=K-GOT=)xE%diUYj z{G;gmKkbe>JL~4pEvZF`MUs|ILC3_U S>w=T+Y_}6RF_a6Ic36V7aCA+_=`d7L! zAF3XO#@%P(udDIaWB=HrQuFf9U93U=Fsmssz>6=xgtCN#w)~}+2*oE7az}sidJM5C z*Yt9X4Djb4e)RIdG4J8iB@w5~!vD@MYGy6lwz3ZB-ERGRTS+AoEGA8|sb`*L=be9^ zwI=nbB7gt?I{MF5{8KVeOLmd>e>CBsFqilU)JKv5N3Mb5mO&YT(i&R+b5+UMkeE|9 zQjO2Q{EFQR182t$C$Z1eY~8?8`M)G&K{9c{w#CJE^5n@-l-+F0U#HaP(-rn5O?+!9 z^;rkW@H%WH*-2WfX+V7c)6ebM*WR#euD{VnPnc-MMFmctCpA5em?o(mY<;Co8 T)31tP!Hgi4t}1v+Qm_k}LKl|OvcIMGIK^Ta84yXS zUDCiAmOf;*^*L*qJ^4|ceN!hJlUUm! a I{{6NSm-?v}U$%ngt#J1QeM~Y6#HCWh zx3?gx=c)0L&dgVc--r1Heeb?`lV6#CnFV@}H~$5VG|b!oJNPTa7Ki{>DTruM4_ohE z-Rz@}KelP7o{CAsx$+)`C1ZiHhvNTs^e|JeqB8fchnQ@yGdVC#98(~lrx1k9@>o?J zIg^F=yX|@es{XqK{}%b#z(3QaLQ)ujBR5A3hbak|Ub&E;5*qL#K;j|jyFc<-swi+` zfZvpe$k)j5R&7qQN1l3?i3(rZd5FH;JO%@D9V8l4I6pD4ixNbPh#6@Rq88*@*RCCH zP)UC)8$QH_l$TQUTVf@pgRDo7ZjRa~DLIjp7OI6k7Om 2rz2Wp-m&4%160To%)y_zE=WNLgJEeI5Tu@Q>r3H;QHGSJzXh_`Al^MqEdF zZ!uLqRQ{>|Zvs_- xw4Y`vm;+bd z^bBDbs<6Ob4ZUM8zxywnblRzKdMDWN$HVOo4|t;n3~iFx#do(anP%f>{lkXOsj 5ZS1|ppqMqIWDyK zKBA}fKR?^9pAXw^RfHWPRCMToz4?z *xj|ltC^RtkT8C(YqF;Z%iPzOBtsY$p(*`!EG z2`CMcpOfrN1oiA|xk@GEk?=1wCY@`OjB6=3JcVf**DhOTKWyJ(FTVe_jXQ0MW$`^? z>lFQ0d^G8q*b|U7N|)-INgk4}<^IQVzLB_&5rh(TR27k9XXN^7uu-(smb>4$`SH!H zxbqYncJ3=yN!q9qH*y$w7f*$T!m4jocppT1d9Yq5 zE}dp=PhMuNPFrp*&-{!kt6-R4>HMIwZ#eR}68@ke*V5!e8gKg3b@nlhI_gKi^S=8b zZfEG2_5t_5JNn>(#~-!7Kl`*jw)`1; Y^ax2`BLI0TjC60`sGA{_ Rrcm6Bc>M0HvTazN?+VU(8)qmiX- z9y$n{v~1=&>pb#r48N-)Jf1^}kCYkrG#Ohat`WQk$*mYZKEYacJRLbiwUfK==uB)G zEy)1(+6qcNeuv)wMlSuYbMp9)V(4#nH}F4D_-D9x2>(juXOFxESKt=M^x0zkA}g%C z)Dqeb0`n;}KA|s>$&u{u<4GGvWHh(JE<^2sXI{0Xx8H7^JGUpS+t5xpfvUX*RMs)8 zMfOl?PQEqoKi-N773Pe(m1^R i*71_>Y%rD7;_VtrW&aQccb5}}83x0uC>EzK z+iidUbg%tDRoD-kw_)>@W80}*K{g4$2OX43WodE(E_ysNI4+uTIdSf>^nvz7a4(MF zDF{&oa#^}juDr1Cb-zD)8KBUA5$ zav-aY==9*613iD#V_^gQ3tO&ZhtClHBM7Z@?A*cDZrDHyMAdUjD`<$teje{7thS3m zp|!S>>eojfe;oTdng0*|SNPX=IQ(;;o00l&LCVkLU%xw_L8ck0Tyt=r%r$pMn2fV% zgmf(DgbRx(kx-~4I5*iA$S3(F>_hnjt^pGKM`Bk-<9kh@0{DyXzoXMf>B*mL)rMMI z{@R;%;bm7?5_Y6yz6Q=VM$g8^q=G-6#trUm_;pQ?EzbKjp-D`wC*pp_ VKx zOv~ X}Ol&W0)0 {!KF5wCEO3W5#4{&e!J$`o%VT+x gvH=bar;xYtV_AGrxIAI zR@E_BfxZvl6GyNZ$PV2brR{}pVO}LYO6h6O`q%y6gny53$pP(gEj 9>@hV z;slYsukStfJOUf!2)_Mue~$inaFnc)ZVxRm>8kQ&W5~B>IU7^h3gN#gsg-GGon_le z_id$_R$=mL(tg!+{srZ8RMk^{CEmW5=%Pzbp{os@EZGr`fBAg{xyZ+m{y+8_q$k6D zqj&&IL(9clo<&JOu}c@oUr2Y?ROjdPAlF=efUxiIuk+;^$zIk#-m+ms=*Ias<%&O1 zu0hi< Kff;_eYip5(*k>+&p7%Tlb*##h=a5tk}?ccA Kk%H~*2$a* zIOrPFiF(|LiPj{w14DAIw1G1|upXy IB~xma^(45o^j@2>yNq2G=#xq`t* zzYG4^fA!4&Zh8gfz8?Q6r1LY-|LW#RWu*}VC>VdOCBuD|*z-K5V=f`hzr>2^0%$V| z7Vw^9nA4VHCt%k%By8IVKe92n*R+KQY^@WqjhgcJyiSAcq><-X@#MLdIBt=}5?{z* zm~;CxUqSxXy4Z^*1`fTJVL+4(NbgrbtvrOsLvJ{2r`^5PURViLt4h|kZX-oDkr)ng z2U{{0yE2aNmYx!Gyr`qKRH~}wo^w7PN5}3D+5L*``7R#qQ yW=;4ph29OuqI_I`5csPp#kala8?CkCHg%gKQ- z8E~XCPaQeP8+2Yeaj>VCKTp~ (pIEd*551u4eLXp+&{~8646j)oO?p7h{i78t(l45dJ+$imqLP z^Ebdex+*3D|AhZbCFT&_G{aH~r&_Dw&oexF4}6d{R# Bi=)hh};CqN2;=e{ TYA|5NDrb@(4k8i11CG+6%gnC+KUKGTwWpKGnE z;03HACRn93;Zkck_F8MnfUvm29` c1JP=Up8 zg)m$I5mGq$5$k#7N>n`5dkhH}4)hh;FDKhF99O@B%xgKcT!McU6@S;gwAH@fd5Fs0 zgZAnhZ(3$%x>HdK=_)*|Y_V>66wf1%S$a(-<}bltp>etVGW(_S74DO|SEx_W*WbJd z@L9h9$^7@Q(9xR%f<{Y*VT-w2)$ShpeybmWcA&cc_vaxM&+iI1{~h!t_GDTk3O)R% zPMK&Mw$xEgr%FpIe8`}0paO0q;aN4niOt*5dy@UX;Npv1h}2i^c=umsyDFL$*7v_l z$73Jsc_wCm63spFn3iJbmzn7srM-yFp+Z9DnokADJ(COVmt1>QI_U3h+O@VvpMM^6 z<&gd3vo9Gep6ks2DAajMY91zbGGPV2%eg8?)W6#ggaTs~{^z~o=}{&-1y!Z2Vr0`l zThq8y%WQY4wVTMmX{vYoo%tpsA4ggWGY8X}mp~KO4B9_9V@Oem`=>#pBumK~U|ng1 ztC;hVRb0Q#O4zG{{!l`Mt*3r((KP=i40+6QI!;G5CGtCtsDEenaNmAU^PhcpME!G3 zqA;B@I!&UgX}+aa!S_QYZw_2uIaKqC|05HQ4_DnkVxFZ8I0tT%nRNMx|3}UKORd#} z>#gHBDuO$ef%!yh5S3`jg`KTc$y960>-nS*(#I?$t9ZSokNm5(o%SB4)pmG4gn38+ zbi~~kFGvMh)=CC{Rbd7={FCxKg$|+DRvm!9g~iaQv(LQ{&Zj_~>)Ix9JykKHZWXz$ zM=iI9Fe~fT!qMipQf|Vy%zRb#XbJBg|9Y$?6G85P_OB0X9c%I*i-*8Xz&)lBFda@X z$)9LvrUd8kzvKK@ T{0q)(0>fwT52i|5C7?YtLvg_SSej(&A~pz5mA5Uwc~iNOhTT=98Yx% zpP$g0!ShS4+nH+!2a } z^`|0DX#=IH1{1QD`cEB#=|ALA@G!$w%g7}4=KmiB|GM7N50cGM+@q-E982$1X$4iY z(9vKUB9A=W{agloWgrLA!Mn_Y)L}E}{-M%`!C)D5`-mSb4-=qh++xB4S2ON-n&l2X z2i(Jd#?Y{QXhbDXlBg!goAS8zyqpjp)jR`9ttgzT;eQ&)H(Yw3u-|a5<#57&Rq%I> zVgOg^^#|eR+DFCuL8|r-z{|CQG+%SVs==O-+wTV3=za?a?!E_Rq&KBCc*^U3fAsHD z+DfTwb>{h_f2`+zsZvt9TWP^&q_jNt!!ch>+Pk=bw4pnq8wr2s;QmT)YG1oO9y&kQ z5vKFi>&PHC7)<(*-{aq>V~{>P`uu~^1zzK=?x*Q(JY_43`8ooqHszyx_wQ?Oz4snP zMC$RU5soD;49D?<_rIA8ike|m0K5}^97X<;DZo=PvS&9ARH{y?fh+D;ag)TFqJ1ez z6t=Qt@oPG9AtOrtQ8UW&FsV|BdL+kF84yolBcdW`98jTfP%NyXNmU<2;=wtlUVS&u zRSe~doaD9f+vFu=XQkQbQ5ASI6j!6p^>=x9@_@7jB>q(7tfRb_fs8Qz^4m3b_x%sS z97^Vz_liAS*#@yVt2jrEE)9uDM`aCWmgO_n_55nQfS;^n_9kmrNyAT@an^C*nKt60 zr(yW7u}P@ F>C6#qch;+LDAc-M{|X|h a-d(o5Cs)P0Y}g*IO?{&_T-oFbI_;1jS8Mw^A=;Cs+vN3G$aBze{V{MAM4>C zdS5uK^jPne`1`fxn{{ mQtvhfpiuKKdL_Xx_zFQ#mnkS3 z5S@YQn+5H3iXV~*@cJbY(2UCxE&{wL$RqffuZhRaQruWn^$oaQ0lcCXFs5p{u`Ypr zzF&g8zg_jWR1iPrR{Fo`y-v#lXVvuP=4RUquf1ip^p&pLzS~Zxrm+c-cbA-NrY#h$ zURMH$k^J7jG>x~+j#I7Q1@BXMRtu!5D~DcU-1Xb- nNKmPg0=`@P`zfR578X$jF->ayhH z-7$dg5t!>91iwAFAL0M+AL~8g@YiD%1!=HPp;TSS^ZhUQqxp+ShNYtxHB`Zid;htv z#-F{Qptpk1QCOlPt9MJu6CR6Nt`S8nC(`rW5a!`Vc&|DJi+WXpo&g(YfPY!aIYdMH z4d`cA&7I4PtxxEY-B03-I(WQ&^}Bie>nQ4*V5w*BpnKnw&pwAbhx$bxlvJ+F3aKsd zuf3a|7)kIG%bg|s3+t(P14*cM1^IQ9Owj8d_mTx#3jP;hF{w8&FDt=4mJD#c!{A>s zK%-6L5tYKflLb15&^`8hRp+7W= wyqK8d>_o)-uyby*vv04(li!MJrvMErRLN*D%8qBRYGVB$ ziUdcI92kA^_tt9A5{t|~*#=zx6_Kj{IQ&QVd)`t@o+Y6^!x}V9`@`VhQ-AS$UGYyK zi;#g7UPVU4U=m>olgv+zBe=}HXc`&ghJvNG>|sR%r&`g78AJpWAy{mQ!>_Z1$|aUq zCDQ<=@DZ2_k^zhh&A@X=fda7L)F&C@vj+7}@?<_H715PJ6i;YgU)8K#HvFpn&I53k zzpVeTRmS6T2CO{(=}*wP9iV6Jpxu1WgWOx@sL50$TBQ3Hz88ei!W?;*>e0!n54H=p z9?Qq7`Y?GME}PwB|34o4Ix2WC^VdiDyd2Q;V!e*VpGnb=5@kxj>pZn*^*vJiKG{X$ zmjE(4TL}|-9vf4{5KqxUaf?Fh-LI#WR}HswF1*ODf(v8DoVj+z%vpBHmDBC~OD=V~ zeFOzNu27fH& O2692s6-8N6kQJ%O z3Pn;JZK?_WsOIcRKYxCT)%f3mbzt?aH>zp*8+@3X0QZL-Q6BnJ*q z1V|wuk*QMnYev!^&}YmGmY7;bzkGM=ee$DLiCiefHE4C(N{i|DB7JnrEvNfc)VGtA zl)Q1*G1v#8J@=dIe^33zxzzXmC_)k!mq^h (WltqjP@5J_!4~I! zB2m4lyv~-{Yog_qV&MI;KYwS
>S->ENKVw*`;Jopwj=9`Z=3j2}+<=9f^|9zLOtfP4U;6D2SE{wtD z!?8~p1f~9$(Dl^O?+NLA6{ZXST0YSx+rwjit zTYAY0mJ9wHHcrK!3Vwsm*XsiI;ct$A@BVWR_4t>48X1>D!O}F6mh&iDILngS^sy6D z3(0y!h3r(qFI?Nkgh?7jC0KN3OUvw1VMS% q9{%M9I<{r}(iA4wt5y&q~6z1a8<+e0tD zVX5feD0IJUI9*5KS4Zs&;aW3rrT4Xje-+$#{CXLn_xb<*|AK%0zubR)hmzb*--G#Z z{h#N0UbSySnK!OkP@F3)N#Brw2J?-PUpbuLh4kXS_1*`zaf|pqgmrKW*4_ARo+~o% zIBM9((&K)K_v(``zdk!{}fi9alYc82l2KdnXUQ2JRAFLz(49A7T{~y6VK7jk| zL-z1fPcWZ0mHt;}7ArF5*oDL3ufS&pYCb2Ox-N-;F!&3yGWWCin^W7kJU$vI=u4d^ z0&_1ub<}0lN+P(bDd=gm4~M%AdYnT2pNF>-MIkZZU;Cw_27X+0$t6}p8?#!3^>Y>R z)T~m~JJn+9y#D@smeWiuvVp0i_RlyH0@^~+)TwBWXj*oH-Tl}Dj&kJV4TtQnZ|}A< zmTe|cT}NBsetLrF2cRB(DAWLz)TO9Kt%Ui1CD2T?KK(0;>HCUhl)Old;A~nO|Jd-C zn2kW?CwD)?;)l}j19eY}aq}#{;!0kdXiaiD+X+$0(4k2s5M?F0s# IoGZ5ekuEyk+z;eIIs7Kl X$Bdd&MA8*eBw=tYOb ziQJ1o-4g-`nE }bxFLVgbR4*EZlP=C={v@GVj%DhITa?VPiDY+21{ecRWxO8=u z?3Gm2LK4n%<~_e#u22DCJuRtw$pNqWg>5b2{8;?!xn7rS&`Og#K+i>ysMYBADS`?{ zp&WmIb&vHpc#e!h?5XfWN9_fohn+fhvM;_~<%rECHC5M84g6hwgDxj)Gttm1X3xU& z;wKUTNFbjd3;d%f57tbX7@`5vbj?m`Z_mE<566VIs%F2v@Wla}@hAkbOLnlucU##F zlnAnw#{&B**eC`uNMa!5;LiH*`?@6$ddaeYf1{=<{ns`PG?p% G26_L?(airDf14}&%P%?0Qd-cdADcqtiCkzyk~KsM z6g`cgb6O)2lMDM;|5I;Z&d N_oM`EAjw5NE7)3k9D4;z8aajq$e|f0J zNATAmfN_Y`5=B>;19Hc!baSK0wjKW2-umbxM0PVYr{JXRscoR!9xQ2Fk;S*bgA_FL zfno~kN~|V{FdHR?pdQELi6>9C7hiwPTDNKCtTJ~U1bZNx%<}M;C#n17Ed)Il{Ixv= z`+`Dh0QjyVARx~*o9`E~eGL04o_&>sDiUL6oFaQzEK)a#iSaNE&agF`Y8XsGzMVZP zBI|IiEKA*4>xh6OQ97~TQL`f$^LH^`RAZfv8w(SV#Bsyp9DCi`jnwy$BbD~oOf8Y* z2NnaO1^7E_Lh5kOUi;f)k6K~#A}og^o^|*h_4^}izq;!ob&){po$7l6%SR>8z_x&P zDH4P73M3jN2Bi80f5AK43On|3GWSK|KvCNQm i(+sK=<2;f7F0x zk0sMq<;H!=G@#w~_B-#Rycvt%i96Uq!26FO8BmQE@FUk?1LH-%T=S!K?$QCqF=_#1 zm9doVaHA3VEO`-)@Tl6rLbm+vH(?*!Z(nWw$^N lDSFSBb9}w4xO; z$ZliJ`<`Px8L>HZUJW&Tl< XaGqoC%9 zbL4)j7Da4#+OMiz(f+)8{(P8scRBA}t&3aOtKpWpkID(>TF2sFGC-#6dj7TzujqpF zVPTUu1pZsKEVeIUQxc1(SaR!G@Guu7PquI^H3( )~&u|&w%78=o&!P3X9{*e$k9l23j{|qj*+&oVK<_xIyM?& zN?? jLfr>7*WjX;6wsP{H*YMKW*{kS~KEQ00>%P`&2K z9gxuB^#A9OeFRBW^Oh|s^;1Gx9!LOvmAOhCNHs`Bs54KU_xZp-O#vsioJ5-fml41) zB{R)VJ?Ctu)>q>gAFvNTUtwK)_p(U5g9v#LA-D_v3G53m5 ciph z|K_@ObZvnD4L4jD1QbYy=H+DA2cN9~_J|E7{Mi4F*qgxD7J?`1w$vi}9 I8Ip7Wb@ZB(ix)t(dTf|m6!FUSY|6%h#c{&gMwH4KM)_-lHs zzp1{$!(S%A(eP(37$%SFb|TTQ4wFpAd%pDg8!qv&h2QO`i#zjQ_~%+n7Kj*79*|}R zt*foG@(~*AiTT85_&O2E19`V9BTYnYs1z#$k$>sj+4dzQgf%2f*C1kFt3G5`FW+So zZ`_Xe3b8yvtN~Z7x9owBllXtyQieTd**&MjLJ;6z*EfWFfBZwJe+8r>W0NhT%Ot1% zQ|SE9V1joJ*v}n*D=q+YJjN}vl(Baq50+Uop686a({d&}Y9+J&Wo1{qN*DFjme!&a zvh;Myqa*Mp=^UXmN;Ab`n24N{)yCQnI^PCe@Ek<|o1BUts4nZd5UbH`IfBSQ8NtH7 zlunLWveRDp6p}Ck{#$?AXV)xV>|6~6(PHI6qwaini`0Qszc1NWk~xnelVk$WNfdNA z^^M7X`q`)3v#-A5NF+BB8QO$7SF`s3#RSxaJ$Rpu7+v8y&79}X-j0_HV1@5lF9*^o z;msqtUr O5Q2*)G>J=bHw -k_R827sx=7@+n!P3b zdz^c )z$AhB6YqY?Ek|4blCSYKu5ht`z63XGJs+csrYP?`s)5YHtDZu zfCBk4|J}Vf=9&uc;n[to1-73Mg`6?I$%X20AN7BW955I_!~`nkO4TyOzXeVNvb zr4v5E*j+L%vkRag7Ik!<7Qqv7hClx%@nkBLm4kJ; 5hNB+NK}7I3TMpV&pX{@{{H<9;$!qO_9ww&HG*{-Tm4&u|t zh}b_Y{4>W<(GUDH$I1ktmIuf;BNC8@Nsu$@4s!f=S@#Rx;Wxv@O8{>O70{K7))LEq z!dmw})1n!bc6^gqMxMc|+88cJA_Wm@wob^m?ABwf=eYZ9 #te)0q~ zR?lND#{*mJhi$6$IcVRkTW8&S_N1!=OB(o#oJOKwC9YoeOEpME$od!jGekl`Bq$a2 zr15#h&GYS(m7hEAF~NQl0rE{d36cRLS;RZwBE0Rdw?QC*sA6k76YYP1e=3At%}{lP za9&IOXJ7$qI|}wet{=~oeWN)ZF{lsmc)s-d>zJjee5aO!oV&bojn{MP->GU}j}vy} zJ=(9b-fKc*t7 T%f%ofgzfFU69MJ{T6#@Et|8_T{GOO*~f4y(_K5(y{an|Wf zE^q6m);5*Z%$|1D(O^cNO0>Ka!8x>!QBPh?Ia9&cF*q^6?-H4YOeTG=L_jWNKgt7Y zDfs7?yZ{&EN341g;8p$+_y?we;4cqL@NZcV%u-R)r3gbD^kw~H_Qjy~V;MiDI;r9o zMfM?PkEWO_h#HQ6U1N9scwL=My4HG4<9@^szLEN<^RBp%nn3O!*GAnV!l=A85hx@o z!oa;v=66a=MYE?`bz*Gc!w=cU?R)u7cvAQ8vsYLDWLG`86`sH^tj{@bTC)+a&@@l< zug`2$SDM=kzi@5;5cv1rztn#+Gd)sK|CvPpvhe(q$1bxB@Lxy=K>o0~)c!Epd(_>? zhG%T>)ig_k{lR$bI#wbB#@>9uCf~9fa-@G*ud#Ic758&QXn$h-UL)i{tO!zw9>z9> z3@VR~z_NMP|FXB}xZ1=hP-=s&-fe$ 2CO$<6?>afc+Hqn?_#6fo-=? z10w2Bm!88vGN6FHmz;n~!K_UQ;Xetrc*ePB+T+hJhe%?rZ3h1}G>~fk#Rs2%VOP$Z zZG|mpB d52Q z@O{d|smxvhczON7bJRM&H3;$(sK+#@a}Bx&?Yo+-_VKsh*}I>ww6{L_++O?e6QWV? z5VT)z_pt}(FId3$$75<~-!@=;RtxLW?IdePgfb5ICxR$hBug_*B-@1jAR(dJLn1aZ zZ|ilFWU6EVYTe;mN8A_c B@fi(tXTk?sg3+})mPTGeQR`#S_1SP%5x(t)vV7_f dT=$bL{l{h-zu;fieB7JV!mxVILAGEX%7&1vJbID)|NWwW zUKYE#KMA Ljk&XhWtwZ!P ^P-a8-f?ppG(2wT`%~ zdaTb@LMljrgMT`Ol^ECgDZ(qADt)SiqV!{aUar0S;YZk|io#3hFk)MF0Q$&{vcruO zkG=L{HE!i5BAwsWY_i8+c-AhUE}&${U~AdBh2`bvz}Jf#OvFy4HzGAsy`P(#!+vz% z&pAl`$bHp01ba<*MqavgM9&90JP1|9(=n@rzsM0>C=9&Ek_A)8UOY9Si7B0I(gjai z|6w!i#3a1sNCMi8-4YZMF~?-?L`SO87xj%8PzhEpxmTAcAy+EC19|ic?)z=0%c)D8 z#Q!9Pg8#1Ziikoki37o3X27wgiQ`8E{t<|S=9KAu^5xf#(tQgeKqC@rF$0t!5yUs( z?Yn#_w_BH8-v5s*jS$#~_q3T+hX`13!}W+ll|0Kc5~qUH|B=h)8wzy{yh_X{#97&h zN_+jEAGs*YI`XWF3cR#>7cBxStTj}?dEI9b5le9DLBB({#2@+nD u8;vhASo>iG+?AXS~OZKxRkFRdf!_u5LaX;r|h14g~ UBG8^4xDNr}-$0Yyiw D 2Y%L@An1CG@es$-y71yeS^fC0AT&+qkaV@ZwYzDGjd@2Mq^SaJ81}y_r7o z+P(V_4GOA4AEdk~e-9>v1zk^Hkq>F#QEcO?D#g>l`met}A=vugrE8;ekfo>zna!a} zy!+vYZ8P0P+mPfxBEjF;xZmbK{FU{sm_hJ|>3L!I#y 8#lOzIu4gTq)rTlN9 z_ivFEPh5)P|EqPo@MF@R`&{#rgFY?#8oXHsDgWr34%nER_uKfTz@28Pk+<#PIRpMM z3MSoj*v8$#^QEh;_q6+=S{O~tF~h~8RC$$3`?gqI<4KQq>(R}+oWuaH&}Gvl^Os*o zul$u>_VCv+-KB|9yu3eL%V;YGxqv2CRZ(d#Qt{HMeS2;(Vgf~-2rJ)zEycB*=m=VZ zJ^Zs^&r|`t2HdE4T?VPDWCVYg;#H7B@aG(=DBgeLt+#D`?N*dO#y^g`itRUGfVzP- zazJ^$g_PX$uf1+<+O|;vp6dwaDAH&x`Jp