diff --git a/.github/agents/copilot-instructions.md b/.github/agents/copilot-instructions.md index b434432..b034f24 100644 --- a/.github/agents/copilot-instructions.md +++ b/.github/agents/copilot-instructions.md @@ -9,6 +9,8 @@ Auto-generated from all feature plans. Last updated: 2026-01-09 - File-based I/O (CSV / GeoJSON); no DB. R-tree (`rstar`) in-memory spatial index reused for coordinate resolution. (004-train-detections) - Rust 1.75 (tp-net crate) + C# 12 / .NET 8 (TpLib managed) + `csbindgen` (FFI stub generation), `serde_json` (FFI marshalling), `tp-lib-core` (core algorithms); C# side: `System.Text.Json` (deserialization), xUnit (testing) (005-dotnet-bindings) - N/A β€” stateless function calls only (005-dotnet-bindings) +- Rust 2021 workspace (`tp-core`, `tp-cli`, `tp-py`, `tp-net`) + Python bindings via `pyo3` + C# 12 / .NET 8 bindings + Existing workspace crates (`geo`, `geojson`, `chrono`, `serde`, `serde_json`, `clap`, `pyo3`, `csbindgen`) plus an HTTPS client for SPARQL access (`ureq ` blocking client with JSON response handling) (006-download-rinf-topology) +- N/A for persistent storage; per-run in-memory retrieval/validation only (006-download-rinf-topology) - Rust 1.75+ (edition 2021) (002-train-path-calculation) @@ -43,9 +45,9 @@ For Python source files (`.py`) changed under `tp-py/`: Rust 1.75+ (edition 2021): Follow standard conventions ## Recent Changes +- 006-download-rinf-topology: Added Rust 2021 workspace (`tp-core`, `tp-cli`, `tp-py`, `tp-net`) + Python bindings via `pyo3` + C# 12 / .NET 8 bindings + Existing workspace crates (`geo`, `geojson`, `chrono`, `serde`, `serde_json`, `clap`, `pyo3`, `csbindgen`) plus an HTTPS client for SPARQL access (`reqwest` blocking client with JSON response handling) - 005-dotnet-bindings: Added Rust 1.75 (tp-net crate) + C# 12 / .NET 8 (TpLib managed) + `csbindgen` (FFI stub generation), `serde_json` (FFI marshalling), `tp-lib-core` (core algorithms); C# side: `System.Text.Json` (deserialization), xUnit (testing) - 004-train-detections: Added Rust 1.91.1+ (workspace edition 2021) + `geo` 0.28, `rstar` 0.12, `geojson` 0.24, `csv` 1.x, `serde`/`serde_json`, `chrono` (DateTime), `petgraph`, `proj4rs` 0.1.9; webapp: `axum`, `tokio`, Leaflet (static) -- 003-path-review-webapp: Added Rust 2021 edition, latest stable (1.80+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3be886..f276ffa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,11 +82,14 @@ jobs: maturin develop - name: Install test dependencies - run: .venv/bin/pip install pytest + run: .venv/bin/pip install pytest ruff - name: Run Python tests run: .venv/bin/pytest tp-py/python/tests/ + - name: Run ruff + run: .venv/bin/ruff check tp-py/python + dotnet: name: .NET Tests runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index dcced7f..56f57df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,10 @@ tokio = { version = "1", features = ["full"] } rust-embed = { version = "8", features = ["debug-embed"] } open = "5" +# HTTP client (RINF SPARQL retrieval β€” feature 006) +ureq = { version = "2.10", features = ["json"] } +url = "2.5" + [profile.dev] debug = 2 opt-level = 0 diff --git a/README.md b/README.md index f2603c6..3173edd 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,11 @@ Train positioning library excels in post-processing the GNSS positions of your m - πŸ›€οΈ **Train Path Calculation**: Probabilistic path calculation through rail networks using topology - πŸ—ΊοΈ **Interactive Path Review**: Browser-based map webapp to visually review and edit calculated paths before projection - 🌍 **CRS Aware**: Explicit coordinate reference system handling (EPSG codes) -- ⏰ **Timezone Support**: RFC3339 timestamps with explicit timezone offsets; timezone-less ISO 8601 datetimes assumed UTC +- ⏰ **Timezone Support**: RFC3339 timestamps with explicit timezone offsets; naive (timezone-less) ISO 8601 datetimes are accepted on input and assumed to be in the host's **local** timezone. All emitted timestamps include an explicit timezone offset. - πŸ“Š **Multiple Formats**: CSV and GeoJSON input/output - πŸ§ͺ **Well Tested**: 460 comprehensive tests (all passing) - unit, integration, contract, CLI, and doctests - ⚑ **Production Ready**: Full CLI interface with validation and error handling +- 🌐 **Automatic RINF Retrieval**: When a topology file is omitted, the library can download a bounding-box subset of the [ERA RINF](https://data-interop.era.europa.eu/) network on demand ## Train Path Calculation @@ -55,6 +56,12 @@ tp-cli --gnss positions.csv --network network.geojson --output result.csv --revi # Launch standalone webapp to review/edit a path file tp-cli webapp --network network.geojson --train-path path.csv --output reviewed_path.csv + +# Fetch the RINF topology covering a GNSS file (inspection helper). +# Writes the retrieved netelements + netrelations to GeoJSON even when +# validation reports issues (e.g. coarse geometries), then exits with the +# corresponding non-zero status code. +tp-cli fetch-topology --gnss positions.geojson --output topology.geojson ``` ### Debug Output @@ -295,6 +302,28 @@ fn main() -> Result<(), Box> { } ``` +## Automatic RINF Topology Retrieval + +When you do not supply a `network` file, `tp-lib` can derive the bounding box from +your GNSS input (and optional path) and download a fresh subset of the ERA RINF +topology from a SPARQL endpoint. + +- **Default endpoint**: `https://graph.data.era.europa.eu/repositories/rinf-plus` +- **Default buffer**: 1000 m around the GNSS extent +- **Default timeout**: 60 seconds per HTTP request +- **Coarse-geometry warning threshold**: netelements longer than 250 m without a WKT geometry + +Outcome categories (mapped to typed errors / exit codes across all bindings): + +| Category | CLI exit code | .NET exception | Python exception | +|---|---|---|---| +| `invalid_gnss_input` | 4 | `TpLibInvalidGnssInputException` | `InvalidGnssInputError` | +| `rinf_missing_coverage` | 5 | `TpLibRinfMissingCoverageException` | `RinfMissingCoverageError` | +| `rinf_incomplete_topology` | 6 | `TpLibRinfIncompleteTopologyException` | `RinfIncompleteTopologyError` | +| `rinf_retrieval_failed` | 7 | `TpLibRinfRetrievalFailedException` | `RinfRetrievalFailedError` | + +See per-language READMEs (`tp-cli/`, `tp-py/`, `tp-net/`) for end-to-end examples. + ## Implementation Notes ### Performance @@ -313,7 +342,7 @@ latitude,longitude,timestamp,altitude,hdop 50.8503,4.3517,2025-12-09T14:30:00+01:00,100.0,2.0 ``` -- RFC3339 timestamps with timezone (e.g. `2025-12-09T14:30:00+01:00` or `2025-12-09T14:30:00Z`); timezone-less ISO 8601 datetimes (e.g. `2025-12-09T14:30:00`) are accepted and assumed UTC +- RFC3339 timestamps with timezone (e.g. `2025-12-09T14:30:00+01:00` or `2025-12-09T14:30:00Z`); naive ISO 8601 datetimes without timezone (e.g. `2025-12-09T14:30:00` or `2025-12-09 14:30:00`) are accepted on input and interpreted in the host's **local** timezone. All output timestamps are emitted in RFC3339 form with an explicit timezone offset. - CRS must be specified via `--crs` flag - Column names configurable with `--lat-col`, `--lon-col`, `--time-col` diff --git a/deny.toml b/deny.toml index 144a61c..283002b 100644 --- a/deny.toml +++ b/deny.toml @@ -100,12 +100,13 @@ allow = [ "ISC", "BSD-2-Clause", "BSD-3-Clause", - "BSL-1.0", # Boost Software License (permissive) - "CC0-1.0", # Creative Commons Zero (public domain) - "Unlicense", # Public domain - "Zlib", # zlib License (permissive) - "Unicode-DFS-2016", # Unicode License (for Unicode data) - "Unicode-3.0", # Unicode License v3 (for Unicode data, used by unicode-ident) + "BSL-1.0", # Boost Software License (permissive) + "CC0-1.0", # Creative Commons Zero (public domain) + "Unlicense", # Public domain + "Zlib", # zlib License (permissive) + "Unicode-DFS-2016", # Unicode License (for Unicode data) + "Unicode-3.0", # Unicode License v3 (for Unicode data, used by unicode-ident) + "CDLA-Permissive-2.0", # Community Data License Agreement Permissive 2.0 (permissive) ] # The confidence threshold for detecting a license from license text confidence-threshold = 0.8 diff --git a/specs/006-download-rinf-topology/checklists/requirements.md b/specs/006-download-rinf-topology/checklists/requirements.md new file mode 100644 index 0000000..79c4352 --- /dev/null +++ b/specs/006-download-rinf-topology/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: ERA RINF Network Download + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-05-13 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Validation passed on first review. +- External source behavior is specified in outcome terms only; technical query design is intentionally deferred to planning. \ No newline at end of file diff --git a/specs/006-download-rinf-topology/contracts/api.md b/specs/006-download-rinf-topology/contracts/api.md new file mode 100644 index 0000000..6eefd3b --- /dev/null +++ b/specs/006-download-rinf-topology/contracts/api.md @@ -0,0 +1,194 @@ +# API Contracts: ERA RINF Topology Retrieval + +**Phase**: Phase 1 β€” Design & Contracts +**Date**: 2026-05-13 +**Feature**: `006-download-rinf-topology` + +This document specifies the public contracts for automatic topology retrieval across the external SPARQL boundary and the repo's user-facing integration surfaces. + +--- + +## 1. External SPARQL Contract + +### Endpoint + +- `POST https://graph.data.era.europa.eu/repositories/rinf-plus` +- Request content type: `application/sparql-query` or standard form-encoded `query=` payload +- Response content type: `application/sparql-results+json` + +### Query A: Netelements by Search Polygon + +**Inputs**: +- `search_polygon_wkt: string` in WGS84 lon/lat order + +**Result columns**: +- `netelement` +- `netelement_wkt` + +**Contract requirements**: +- Returns every `era:LinearElement` whose geometry intersects the search polygon. +- Returned `netelement_wkt` must be parseable into a LineString. + +### Query B: Netrelations by Retrieved Elements + +**Inputs**: +- `seed element IRIs` from Query A +- current date for validity filtering + +**Result columns**: +- `netrelation` +- `netelementA` +- `netelementB` +- `isOnOriginOfElementA` +- `isOnOriginOfElementB` +- `navigability` + +**Contract requirements**: +- Only currently valid netrelations are returned. +- Each row must reference two netelements that can be mapped into the retrieved topology bundle. + +--- + +## 2. CLI Contract (`tp-cli`) + +Topology-dependent commands continue to support manual topology input and gain automatic retrieval when `--network` is omitted. + +### Default / `calculate-path` + +```text +tp-cli calculate-path --gnss [--network ] [--rinf-endpoint ] [--rinf-buffer-meters ] +``` + +**Behavior**: +- If `--network` is provided, CLI uses supplied topology and does not contact RINF. +- If `--network` is omitted, CLI derives a search polygon from GNSS, retrieves RINF topology, validates it, and then runs path calculation. + +**Failure outcomes**: +- Invalid GNSS input: non-zero exit, stderr explains GNSS input is empty/invalid. +- Missing coverage: non-zero exit, stderr explains no topology was available for the area. +- Incomplete topology: non-zero exit, stderr explains coarse geometry or missing netrelations. +- Endpoint failure: non-zero exit, stderr explains retrieval failed upstream. + +### `simple-projection` and other topology-dependent commands + +Same source-selection rule applies: omit `--network` to trigger automatic RINF retrieval. + +--- + +## 3. Python Binding Contract (`tp-py`) + +### Retrieval options class + +```python +class RinfRetrievalOptions: + endpoint_url: str = "https://graph.data.era.europa.eu/repositories/rinf-plus" + buffer_meters: float = 1000.0 +``` + +### Projection + +```python +project_gnss( + gnss_file: str, + gnss_crs: str, + network_file: str | None = None, + network_crs: str | None = None, + target_crs: str | None = None, + config: ProjectionConfig | None = None, + rinf_options: RinfRetrievalOptions | None = None, +) +``` + +### Path calculation + +```python +calculate_train_path( + gnss_file: str, + gnss_crs: str, + network_file: str | None = None, + network_crs: str | None = None, + config: PathConfig | None = None, + rinf_options: RinfRetrievalOptions | None = None, +) +``` + +### Detections preparation + +```python +prepare_detections( + gnss_file: str, + detections_file: str, + network_file: str | None = None, + rinf_options: RinfRetrievalOptions | None = None, +) +``` + +**Behavior**: +- `network_file is not None`: supplied topology is authoritative; no retrieval. +- `network_file is None`: bindings invoke Rust retrieval/validation logic using `rinf_options` (or defaults). +- Missing coverage and endpoint failures surface as typed Python exceptions + (`InvalidGnssInputError`, `RinfMissingCoverageError`, + `RinfIncompleteTopologyError`, `RinfRetrievalFailedError`). + +--- + +## 4. .NET Contract (`tp-net`) + +Because C# overload resolution cannot distinguish overloads that differ only +in nullable-reference annotations, the auto-retrieval entry points use the +`*Auto` suffix. + +### Projection + +```csharp +public static IReadOnlyList Projection.ProjectGnssAuto( + NetworkInput? network, + GnssInput gnss, + ProjectionConfig? config = null, + RinfRetrievalOptions? rinfOptions = null); +``` + +### Path calculation + +```csharp +public static PathResult PathCalculation.CalculateTrainPathAuto( + NetworkInput? network, + GnssInput gnss, + PathConfig? config = null, + PreparedDetections? detections = null, + RinfRetrievalOptions? rinfOptions = null); +``` + +### Retrieval options type + +```csharp +public sealed class RinfRetrievalOptions +{ + public string EndpointUrl { get; set; } = "https://graph.data.era.europa.eu/repositories/rinf-plus"; + public double BufferMeters { get; set; } = 1000.0; +} +``` + +**Behavior**: +- `network != null`: no RINF retrieval is attempted. +- `network == null`: the wrapper calls the Rust retrieval workflow before + invoking the existing algorithms. +- Failures surface as distinct typed exceptions: + `TpLibInvalidGnssInputException`, `TpLibRinfMissingCoverageException`, + `TpLibRinfIncompleteTopologyException`, `TpLibRinfRetrievalFailedException`. + +--- + +## 5. Shared Outcome Contract + +All surfaces must preserve the same semantic outcome categories: + +| Outcome | Meaning | +|---|---| +| `success` | Topology was supplied or retrieved and validated successfully | +| `invalid_input` | GNSS data was empty or unusable before any retrieval attempt | +| `missing_coverage` | No suitable topology could be retrieved for the search region | +| `incomplete_topology` | Retrieved topology failed validation, including coarse geometry or zero netrelations | +| `endpoint_failure` | The external SPARQL request failed or returned an unusable response | + +No interface is allowed to collapse these categories into a generic failure message. \ No newline at end of file diff --git a/specs/006-download-rinf-topology/data-model.md b/specs/006-download-rinf-topology/data-model.md new file mode 100644 index 0000000..fed64fe --- /dev/null +++ b/specs/006-download-rinf-topology/data-model.md @@ -0,0 +1,186 @@ +# Data Model: ERA RINF Network Download + +**Phase**: Phase 1 β€” Design & Data Model +**Date**: 2026-05-13 +**Feature**: `006-download-rinf-topology` + +--- + +## 1. RetrievalArea + +Represents the spatial search region sent to the RINF SPARQL endpoint. + +**Fields**: +- `min_longitude: f64` +- `max_longitude: f64` +- `min_latitude: f64` +- `max_latitude: f64` +- `expansion_meters: f64` default `1000.0` +- `polygon_wkt: String` closed WGS84 polygon +- `source_crs: String` expected `EPSG:4326` after normalization + +**Validation rules**: +- At least one usable GNSS coordinate is required to create the area. +- `min_* <= max_*` for both axes. +- `polygon_wkt` must be closed and contain 5 coordinate pairs for a rectangle. + +**Relationships**: +- Derived from `AutoTopologyRequest.gnss_positions`. +- Consumed by `RinfRetrievalRequest`. + +--- + +## 2. AutoTopologyRequest + +Represents a workflow invocation that may need automatic topology retrieval. + +**Fields**: +- `workflow_kind: enum { projection, path_calculation, detection_preparation, path_review }` +- `gnss_positions: Vec` +- `supplied_topology_present: bool` +- `rinf_endpoint_url: String` +- `retrieval_area: Option` +- `requested_at: DateTime` + +**Validation rules**: +- If `supplied_topology_present == true`, automatic retrieval is skipped. +- If `supplied_topology_present == false`, `gnss_positions` must contain at least one usable coordinate. + +**Relationships**: +- Produces either a `RetrievedTopology` or a terminal `RetrievalOutcome`. + +--- + +## 3. RinfNetelementRow + +Typed representation of one row returned by the netelement SPARQL query. + +**Fields**: +- `netelement_iri: String` +- `netelement_id: String` derived stable identifier used by tp-lib +- `wkt: String` +- `geometry_point_count: usize` +- `length_meters: f64` + +**Validation rules**: +- `wkt` must parse into a LineString. +- `geometry_point_count >= 2`. +- If `length_meters > 250.0`, then `geometry_point_count > 2`. + +**Relationships**: +- Maps to existing core `Netelement`. +- Referenced by `RinfNetrelationRow.element_a_id` and `.element_b_id`. + +--- + +## 4. RinfNetrelationRow + +Typed representation of one row returned by the netrelation SPARQL query. + +**Fields**: +- `netrelation_iri: String` +- `element_a_id: String` +- `element_b_id: String` +- `is_on_origin_of_element_a: bool` +- `is_on_origin_of_element_b: bool` +- `navigability: enum { both, AB, BA, none }` +- `valid_on_date: NaiveDate` + +**Validation rules**: +- Both endpoint element IDs must be non-empty. +- `navigability` must map to a supported tp-lib direction model. +- The referenced elements must exist in the retrieved netelement set after mapping. + +**Relationships**: +- Maps to existing core `NetRelation`. +- Belongs to `RetrievedTopology.netrelations`. + +--- + +## 5. RetrievedTopology + +Normalized topology bundle prepared for downstream workflows. + +**Fields**: +- `netelements: Vec` +- `netrelations: Vec` +- `retrieval_area: RetrievalArea` +- `endpoint_url: String` +- `retrieved_at: DateTime` +- `validation_report: TopologyValidationReport` + +**Validation rules**: +- `netelements.len() > 0` for a successful retrieval. +- `netrelations.len() > 0` for a topology-valid success. +- `validation_report.status == valid` before downstream use. + +**Relationships**: +- Output of `AutoTopologyRequest` when retrieval and validation succeed. +- Input to all existing topology-dependent workflow functions. + +--- + +## 6. TopologyValidationReport + +Explains whether the downloaded topology is usable. + +**Fields**: +- `status: enum { valid, missing_coverage, incomplete_topology, invalid_input, endpoint_failure }` +- `netelement_count: usize` +- `netrelation_count: usize` +- `coarse_geometry_ids: Vec` +- `uncovered_gnss_indices: Vec` +- `message: String` + +**Validation rules**: +- `status == valid` is compatible with a non-empty `coarse_geometry_ids` as + long as at least one netelement has a refined (>2-point) geometry. The list + is then informational and surfaced to the caller for diagnostics. +- `status == incomplete_topology` when *every* returned netelement is coarse + (i.e. `coarse_geometry_ids.len() == netelement_count`), or when + `netelement_count > 0 && netrelation_count == 0`. +- `status == invalid_input` when retrieval never starts because GNSS input is unusable. + +**Relationships**: +- Embedded in `RetrievedTopology`. +- Converted into binding/CLI-facing `RetrievalOutcome` diagnostics. + +--- + +## 7. RetrievalOutcome + +Caller-visible outcome for source selection and validation. + +**Fields**: +- `source_used: enum { supplied_topology, era_rinf }` +- `status: enum { success, invalid_input, missing_coverage, incomplete_topology, endpoint_failure }` +- `detail_message: String` +- `diagnostic_area_wkt: Option` +- `affected_gnss_indices: Vec` + +**Validation rules**: +- `source_used == supplied_topology` bypasses all RINF-specific failure states. +- `status == success` requires a non-empty topology bundle. + +**Relationships**: +- Returned or surfaced by CLI, Python, and .NET adapters. + +--- + +## State Transitions + +```text +AutoTopologyRequest + -> invalid_input + -> retrieval_area_built + -> queried_endpoint + -> parsed_rows + -> validation_failed(missing_coverage | incomplete_topology | endpoint_failure) + -> validated_topology + -> success +``` + +Rules: +- `supplied_topology_present == true` short-circuits directly to `success` with `source_used = supplied_topology`. +- Any invalid-input failure occurs before `queried_endpoint`. +- Any topology-dependent workflow consumes only the `validated_topology` state. \ No newline at end of file diff --git a/specs/006-download-rinf-topology/plan.md b/specs/006-download-rinf-topology/plan.md new file mode 100644 index 0000000..ee4ac16 --- /dev/null +++ b/specs/006-download-rinf-topology/plan.md @@ -0,0 +1,119 @@ +# Implementation Plan: ERA RINF Network Download + +**Branch**: `006-download-rinf-topology` | **Date**: 2026-05-13 | **Spec**: [spec.md](spec.md) +**Input**: Feature specification from `specs/006-download-rinf-topology/spec.md` + +## Summary + +Add an automatic topology source for all existing topology-dependent workflows when the caller does not supply a network file or network object. The implementation keeps the current pure topology algorithms unchanged and adds a new retrieval-and-validation layer in `tp-core` that: derives a GNSS search polygon from the dataset bounds, expands it by 1 km, issues two SPARQL `SELECT` queries against the ERA RINF endpoint, converts tabular rows into existing `Netelement` and `NetRelation` structures, validates microtopology quality, and returns explicit outcomes for invalid input, missing coverage, incomplete topology, or endpoint failure. The same behavior is then surfaced through the CLI, Python bindings, and .NET bindings. + +## Technical Context + +**Language/Version**: Rust 2021 workspace (`tp-core`, `tp-cli`, `tp-py`, `tp-net`) + Python bindings via `pyo3` + C# 12 / .NET 8 bindings +**Primary Dependencies**: Existing workspace crates (`geo`, `geojson`, `chrono`, `serde`, `serde_json`, `clap`, `pyo3`, `csbindgen`) plus an HTTPS client for SPARQL access (`reqwest` blocking client with JSON response handling) +**Storage**: N/A for persistent storage; per-run in-memory retrieval/validation only +**Testing**: `cargo test --workspace`, targeted `tp-core` integration tests for SPARQL query generation and topology validation, `pytest` for `tp-py`, `dotnet test` for `tp-net`, CLI smoke tests against a fixed polygon fixture +**Target Platform**: Cross-platform Rust library/CLI plus Python and .NET consumers with outbound HTTPS access to `https://graph.data.era.europa.eu/repositories/rinf-plus` +**Project Type**: Multi-crate Rust workspace with CLI and language bindings +**Performance Goals**: Bounding-box derivation and query generation under 10 ms for 10k GNSS points; response parsing and validation under 500 ms for a route-sized payload; topology-dependent workflows should add no more than one retrieval step per run when topology is absent +**Constraints**: Manual topology remains authoritative when provided; no partial workflow results on incomplete coverage; fail validation when any returned netelement longer than 250 m has WKT with 2 or fewer points; fail validation when netelements are returned but no netrelations are returned; preserve explicit CRS and timezone handling; default retrieval region is a single GNSS-derived polygon expanded by 1 km +**Scale/Scope**: One external SPARQL endpoint, one retrieval region per workflow invocation, all existing topology-dependent workflows in core/CLI/Python/.NET, route-sized datasets rather than nationwide bulk sync + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +### Pre-Phase 0 Gate + +| Principle | Check | Notes | +|---|---|---| +| I β€” Library-First | βœ… PASS | Retrieval lives in `tp-core` as a reusable library capability rather than CLI-only logic | +| II β€” CLI Interface Mandatory | βœ… PASS | Existing `tp-cli` commands gain the automatic retrieval path when `--network` is omitted | +| III β€” High Performance | βœ… PASS | One retrieval per run, route-sized search region, no persistent graph materialization | +| IV β€” TDD | βœ… GATE MUST PASS | Query builder, parser, validator, CLI behavior, Python/.NET wrappers all require failing tests first | +| V β€” Full Test Coverage | βœ… GATE MUST PASS | Unit, integration, contract, and binding tests required for retrieval outcomes and validation failures | +| VI β€” Timezone Awareness | βœ… PASS | GNSS timestamps remain timezone-aware; RINF validity filtering is date-based and explicit | +| VII β€” CRS Awareness | βœ… PASS | Retrieval area is derived from explicit GNSS coordinates and emitted as WGS84 polygon WKT for GeoSPARQL | +| VIII β€” Thorough Error Handling | βœ… PASS | Invalid input, missing coverage, incomplete topology, and endpoint failures are distinct typed outcomes | +| IX β€” Data Provenance and Audit Trail | βœ… PASS | Retrieval diagnostics include endpoint, polygon, counts, and validation result for downstream reporting | +| X β€” Integration Flexibility | βœ… PASS | Same source-selection behavior is exposed through core, CLI, Python, and .NET | +| XI β€” Modern Module Organization | βœ… PASS | New Rust modules use `foo.rs` + `foo/` layout, not `mod.rs` | + +### Post-Phase 1 Re-check + +| Principle | Check | Notes | +|---|---|---| +| I β€” Library-First | βœ… PASS | Design centers on `tp-core` retrieval services reused by all callers | +| II β€” CLI Interface Mandatory | βœ… PASS | Quickstart and contracts define CLI usage without a topology file | +| III β€” High Performance | βœ… PASS | Two tabular `SELECT` queries avoid RDF graph parsing overhead | +| IV β€” TDD | βœ… PASS (planned) | Contracts and quickstart define the failing test surfaces before implementation | +| V β€” Full Test Coverage | βœ… PASS (planned) | Validation matrix covers covered, uncovered, coarse-geometry, and zero-netrelation cases | +| VI β€” Timezone Awareness | βœ… PASS | No naive timestamps introduced | +| VII β€” CRS Awareness | βœ… PASS | Retrieval polygon and parsed geometry remain explicitly WGS84 | +| VIII β€” Thorough Error Handling | βœ… PASS | Data model includes outcome categories and validation reports | +| IX β€” Data Provenance and Audit Trail | βœ… PASS | Retrieval outcome model includes source and validation metadata | +| X β€” Integration Flexibility | βœ… PASS | API contract includes CLI, Python, and .NET surfaces | +| XI β€” Modern Module Organization | βœ… PASS | Proposed file layout conforms to constitution | + +## Project Structure + +### Documentation (this feature) + +```text +specs/006-download-rinf-topology/ +β”œβ”€β”€ plan.md +β”œβ”€β”€ research.md +β”œβ”€β”€ data-model.md +β”œβ”€β”€ quickstart.md +β”œβ”€β”€ contracts/ +β”‚ └── api.md +└── tasks.md +``` + +### Source Code (repository root) + +```text +tp-core/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ lib.rs +β”‚ β”œβ”€β”€ errors.rs +β”‚ β”œβ”€β”€ io.rs +β”‚ β”œβ”€β”€ io/ +β”‚ β”‚ β”œβ”€β”€ geojson.rs +β”‚ β”‚ └── rinf.rs # SPARQL query builder, endpoint client, row parsing +β”‚ β”œβ”€β”€ workflow.rs # High-level topology source selection for topology-dependent flows +β”‚ β”œβ”€β”€ models.rs +β”‚ └── models/ +β”‚ └── retrieval.rs # Retrieval outcome, validation report, options +└── tests/ + └── rinf_topology.rs # Integration/contract tests for query generation and validation + +tp-cli/ +└── src/ + └── main.rs # Optional network argument, RINF flags, diagnostics mapping + +tp-py/ +β”œβ”€β”€ src/ +β”‚ └── lib.rs # Optional network input / RINF-enabled overloads +└── python/ + └── tp_lib/ + └── __init__.py # Wrapper docs for auto-retrieval entry points + +tp-net/ +β”œβ”€β”€ src/ +β”‚ └── lib.rs # FFI entry points for auto-topology workflows +└── csharp/ + β”œβ”€β”€ TpLib.cs # Public API overloads / nullable network input + β”œβ”€β”€ Models.cs # Retrieval options and result enums + └── Tests/ + └── RinfTopologyTests.cs + +test-data/ +└── ... # Smoke-test GNSS fixture inside the known-good polygon added during implementation +``` + +**Structure Decision**: Extend the existing multi-crate workspace. Keep the low-level map-matching and parsing algorithms in `tp-core` unchanged, and add a reusable retrieval orchestration layer plus thin CLI/Python/.NET adapters on top. + +## Complexity Tracking + +No constitution violations currently require justification. diff --git a/specs/006-download-rinf-topology/quickstart.md b/specs/006-download-rinf-topology/quickstart.md new file mode 100644 index 0000000..128274c --- /dev/null +++ b/specs/006-download-rinf-topology/quickstart.md @@ -0,0 +1,126 @@ +# Quickstart: ERA RINF Network Download + +**Feature**: `006-download-rinf-topology` +**Audience**: developers validating automatic topology retrieval through CLI, Python, and .NET. + +This walkthrough exercises the covered-area success path, the missing-coverage path, and the validation-failure path for auto-retrieved topology. + +--- + +## Prerequisites + +- Workspace builds locally. +- Outbound HTTPS access to `https://graph.data.era.europa.eu/repositories/rinf-plus`. +- A GNSS fixture located inside the known-good smoke-test polygon from `research.md`. +- Optional negative fixtures: + - empty or invalid GNSS file + - GNSS file outside available RINF coverage + - mocked or recorded RINF response with coarse netelement geometry or zero netrelations + +--- + +## Example 1: CLI path calculation without `--network` + +```powershell +cargo run -p tp-cli -- calculate-path ` + --gnss test-data/rinf_smoke_gnss.geojson ` + --output target/tmp/rinf_path.geojson ` + --verbose +``` + +Expected behavior: +- CLI derives the retrieval polygon from GNSS. +- CLI downloads and validates RINF topology. +- CLI calculates the path and writes the output file. +- Stderr identifies that ERA RINF was used as the topology source. + +--- + +## Example 2: CLI invalid-input failure + +```powershell +cargo run -p tp-cli -- calculate-path ` + --gnss test-data/rinf_empty_gnss.geojson ` + --output target/tmp/should_not_exist.geojson +``` + +Expected behavior: +- Command fails before contacting the endpoint. +- Stderr reports invalid or empty GNSS input. + +--- + +## Example 3: Python binding with automatic retrieval + +```python +from tp_lib import calculate_train_path, RinfRetrievalOptions + +result = calculate_train_path( + gnss_positions=gnss_positions, + network=None, + rinf_options=RinfRetrievalOptions( + endpoint_url="https://graph.data.era.europa.eu/repositories/rinf-plus", + buffer_meters=1000.0, + ), +) + +print(len(result.projected_positions)) +``` + +Expected behavior: +- No network file is required. +- Retrieval/validation happens inside the Rust core path. +- Failure categories surface as distinct Python exceptions + (`InvalidGnssInputError`, `RinfMissingCoverageError`, + `RinfIncompleteTopologyError`, `RinfRetrievalFailedError`). + +--- + +## Example 4: .NET binding with automatic retrieval + +```csharp +using TpLib; + +var gnss = GnssInput.FromGeoJson(File.ReadAllText("test-data/rinf_smoke_gnss.geojson")); +var rinf = new RinfRetrievalOptions +{ + EndpointUrl = "https://graph.data.era.europa.eu/repositories/rinf-plus", + BufferMeters = 1000.0, +}; + +var result = PathCalculation.CalculateTrainPathAuto(network: null, gnss, rinfOptions: rinf); +Console.WriteLine(result.HasPath); +``` + +Expected behavior: +- Passing `null` for the network triggers ERA RINF retrieval. +- Missing coverage, incomplete topology, endpoint failure, and invalid + GNSS each surface as distinct typed exceptions + (`TpLibRinfMissingCoverageException`, + `TpLibRinfIncompleteTopologyException`, + `TpLibRinfRetrievalFailedException`, + `TpLibInvalidGnssInputException`). + +--- + +## Example 5: Endpoint smoke test using the known-good polygon + +Use the polygon from `research.md` as the acceptance fixture for the external integration test. The test should confirm: +- netelements are returned +- netrelations are returned +- no returned netelement longer than 250 m is represented by only two points + (validation only rejects the bundle when *every* netelement is coarse; + individual coarse segments are reported but do not fail the workflow) + +--- + +## Validation Checklist + +- [ ] Covered-area CLI run succeeds without `--network`. +- [ ] Missing-coverage CLI run fails with the `missing_coverage` outcome. +- [ ] Invalid/empty GNSS input fails before any retrieval request. +- [ ] A mocked coarse-geometry response fails validation. +- [ ] A mocked response with netelements but zero netrelations fails validation. +- [ ] Python bindings can run a topology-dependent workflow without a supplied topology file. +- [ ] .NET bindings can run a topology-dependent workflow without a supplied topology file. +- [ ] Manual topology still takes precedence when explicitly supplied. \ No newline at end of file diff --git a/specs/006-download-rinf-topology/research.md b/specs/006-download-rinf-topology/research.md new file mode 100644 index 0000000..67c3c96 --- /dev/null +++ b/specs/006-download-rinf-topology/research.md @@ -0,0 +1,214 @@ +# Research: ERA RINF Network Download + +**Phase**: Phase 0 β€” Outline & Research +**Date**: 2026-05-13 +**Feature**: `006-download-rinf-topology` + +--- + +## Decision 1: Retrieval Region Geometry + +**Decision**: Use a single GNSS-derived search polygon built from the dataset envelope and expanded by 1 km in every direction. The first implementation uses an axis-aligned bounding box polygon in WGS84 rather than a convex hull or buffered line corridor. + +**Rationale**: +- The specification explicitly accepts a simple min/max X/Y bounding box as sufficient. +- A single rectangular polygon is easy to generate deterministically from all supported GNSS inputs. +- It reduces implementation risk for the first release and keeps the SPARQL query builder simple and testable. +- Downloading more topology than strictly required is acceptable for this feature. + +**How the 1 km expansion works**: +- Parse GNSS positions into WGS84 longitude/latitude first. +- Compute `min_lon`, `max_lon`, `min_lat`, `max_lat` across usable points. +- Expand latitude bounds by `1000 / 111_320` degrees. +- Expand longitude bounds by `1000 / (111_320 * cos(latitude_of_bbox_center))` degrees. +- Emit the expanded rectangle as a closed WKT polygon in lon/lat order. + +**Alternatives considered**: +- **Convex hull + 1 km buffer**: More spatially selective, but adds geometry-buffering complexity for limited practical gain in the first release. +- **Polyline corridor around the GNSS path**: Most precise, but couples retrieval to path-shape heuristics before topology exists. +- **Multiple clusters / multiple polygons**: Rejected because the clarified spec explicitly allows one larger retrieval region. + +--- + +## Decision 2: SPARQL Access Pattern + +**Decision**: Query the RINF knowledge graph via two tabular `SELECT` queries against `https://graph.data.era.europa.eu/repositories/rinf-plus`, one for netelements and one for netrelations, and request JSON SPARQL results over HTTPS. + +**Rationale**: +- The user explicitly confirmed that splitting the construct query into two `SELECT` queries yields tabular results for both entity types. +- Tabular JSON is cheaper to parse and validate than an RDF `CONSTRUCT` graph. +- The repository already standardizes on typed Rust models; tabular rows map directly into those structs. +- Two queries are easier to test independently: geometry/query correctness for netelements and topology completeness/validity for netrelations. + +**Operational choice**: +- Use a synchronous HTTP client in the library path so CLI, Python, and .NET can reuse the same Rust retrieval logic without introducing async boundary changes. +- Send the SPARQL query to the endpoint with `Accept: application/sparql-results+json`. + +**Alternatives considered**: +- **Single `CONSTRUCT` query**: More faithful to RDF semantics, but requires graph parsing and more complex mapping code. +- **SPARQL client library with RDF model objects**: Heavier dependency surface than needed for two bounded query shapes. +- **Async-only client**: Would force Tokio or executor concerns into call sites that are currently synchronous. + +--- + +## Decision 3: Spatial Predicate and Query Shape + +**Decision**: Filter netelements with a polygon WKT and a GeoSPARQL intersection predicate so any netelement partly inside the retrieval area is eligible. The smoke-test polygon from the prompt becomes the fixed integration test region. + +**Rationale**: +- The clarified feature rule says any netelement partly inside the expanded search box must be downloaded. +- `sfContains` is too strict for boundary-crossing lines; the desired semantics are intersection-based. +- The prompt already supplies a polygon known to return valid netelements and netrelations, making it suitable as the feature's endpoint-backed smoke test. + +**Selected query shapes**: + +Netelements query: + +```sparql +PREFIX era: +PREFIX gsp: +PREFIX geof: + +SELECT ?netelement ?netelement_wkt +WHERE { + ?netelement a era:LinearElement ; + gsp:hasGeometry/gsp:asWKT ?netelement_wkt . + FILTER(geof:sfIntersects( + ?netelement_wkt, + "POLYGON((...closed search polygon...))"^^gsp:wktLiteral + )) +} +``` + +Netrelations query: + +```sparql +PREFIX era: +PREFIX xsd: +PREFIX time: + +SELECT ?netrelation ?netelementA ?netelementB ?isOnOriginOfElementA ?isOnOriginOfElementB ?navigability +WHERE { + VALUES ?seed_element { <...netelement IRIs returned by query 1...> } + { + BIND(?seed_element AS ?netelementA) + ?netrelation a era:NetRelation ; + era:elementA ?netelementA ; + era:elementB ?netelementB ; + era:isOnOriginOfElementA ?isOnOriginOfElementA ; + era:isOnOriginOfElementB ?isOnOriginOfElementB ; + era:navigability ?navigability ; + era:validity/time:hasBeginning/time:inXSDDate ?valid_from_date . + OPTIONAL { + ?netrelation era:validity/time:hasEnd/time:inXSDDate ?valid_to_date . + FILTER (xsd:date(now()) >= ?valid_to_date) + } + FILTER (xsd:date(now()) >= ?valid_from_date && !BOUND(?valid_to_date)) + } + UNION + { + BIND(?seed_element AS ?netelementB) + ?netrelation a era:NetRelation ; + era:elementA ?netelementA ; + era:elementB ?netelementB ; + era:isOnOriginOfElementA ?isOnOriginOfElementA ; + era:isOnOriginOfElementB ?isOnOriginOfElementB ; + era:navigability ?navigability ; + era:validity/time:hasBeginning/time:inXSDDate ?valid_from_date . + OPTIONAL { + ?netrelation era:validity/time:hasEnd/time:inXSDDate ?valid_to_date . + FILTER (xsd:date(now()) >= ?valid_to_date) + } + FILTER (xsd:date(now()) >= ?valid_from_date && !BOUND(?valid_to_date)) + } +} +``` + +**Smoke-test polygon**: + +```text +POLYGON((10.99113464355469 59.939604892689715, +11.035079956054688 59.521503830930165, +11.82060241699219 59.443399524042945, +12.90275573730469 60.3443471917804, +12.106246948242188 60.668873461039, +11.51847839355469 60.57185665386768, +10.99113464355469 59.939604892689715)) +``` + +**Alternatives considered**: +- **Continue using the provided `CONSTRUCT` query verbatim**: Good for smoke testing, but less convenient for typed row parsing. +- **Filter netrelations spatially rather than by retrieved seed netelements**: Harder to guarantee consistency with the loaded element set. + +--- + +## Decision 4: Topology Validation Rules + +**Decision**: Validate the retrieved topology before any downstream workflow uses it. Fail fast on coarse geometry and missing relations. + +**Validation rules**: +- If the GNSS dataset is empty or has no usable coordinates, fail before any HTTP request. +- If no netelements are returned, report missing coverage. +- If netelements are returned but no netrelations are returned, report incomplete topology. +- For every returned netelement longer than 250 m, fail validation if its WKT contains 2 or fewer coordinates. + +**Rationale**: +- The prompt identifies macro-topology migration as a real data-quality risk. +- The workflow should not continue on coarse or relationless topology because that produces misleading path results. +- These checks are deterministic and can be covered by unit tests and smoke tests. + +**Alternatives considered**: +- **Warn but continue**: Rejected because the feature specification explicitly requires fail-fast behavior for incomplete coverage. +- **Auto-fallback to topology-free logic**: Rejected because topology-dependent workflows would silently change semantics. + +--- + +## Decision 5: Integration Architecture + +**Decision**: Add automatic retrieval as a new high-level topology-source layer in `tp-core`, while leaving the low-level `project_gnss`, `calculate_train_path`, and `prepare_detections` algorithms unchanged. CLI and bindings call the new orchestration layer only when topology is absent. + +**Rationale**: +- Existing algorithms already assume a validated `Netelement`/`NetRelation` graph. +- Keeping retrieval separate from core matching logic reduces regression risk and preserves current tests. +- This architecture makes manual topology and auto-retrieved topology equivalent once converted into core models. + +**Integration consequences**: +- `tp-cli`: `--network` becomes optional for topology-dependent commands; omission triggers retrieval. +- `tp-py`: add API entry points or overloads that accept GNSS-only input and optional retrieval options. +- `tp-net`: add overloads or nullable-network entry points with matching retrieval options. + +**Alternatives considered**: +- **Inject HTTP retrieval directly into existing path/projection functions**: Too much branching inside algorithm code. +- **Retrieve topology separately in each binding**: Would fragment behavior and duplicate SPARQL logic across languages. + +--- + +## Decision 6: First-Release Scope for Caching and Endpoint Configuration + +**Decision**: Do not add persistent topology caching in the first release. Use the production endpoint by default, but make the endpoint URL overridable for tests and advanced callers. + +**Rationale**: +- The specification is about automatic retrieval and validation, not offline caching. +- Keeping retrieval stateless avoids cache invalidation questions during an active topology migration. +- A configurable endpoint is necessary for smoke tests, staging, and reproducible integration tests. + +**Alternatives considered**: +- **Disk cache keyed by bbox**: Useful later, but out of scope for the first feature slice. +- **Hard-coded endpoint with no override**: Simpler, but harder to test and debug. + +--- + +## Resolved NEEDS CLARIFICATION + +All planning-level unknowns for this feature are resolved: + +| Unknown | Resolution | +|---|---| +| Retrieval geometry | Single envelope polygon expanded by 1 km | +| Endpoint | `https://graph.data.era.europa.eu/repositories/rinf-plus` | +| Query format | Two JSON `SELECT` queries | +| Spatial predicate | Intersection semantics for partial overlap | +| Validation rules | Coarse-geometry and zero-netrelation failures are mandatory | +| Workflow scope | All existing topology-dependent workflows | +| Manual-topology precedence | Manual input remains authoritative | +| Empty GNSS behavior | Fail validation before retrieval | \ No newline at end of file diff --git a/specs/006-download-rinf-topology/spec.md b/specs/006-download-rinf-topology/spec.md new file mode 100644 index 0000000..1dc6c5a --- /dev/null +++ b/specs/006-download-rinf-topology/spec.md @@ -0,0 +1,124 @@ +# Feature Specification: ERA RINF Network Download + +**Feature Branch**: `006-download-rinf-topology` +**Created**: 2026-05-13 +**Status**: Draft +**Input**: User description: "now we need to implement a new feature to download the network topology (netelements and netrelations) from the ERA RINF knowledge graph https://rinf.data.era.europa.eu/ instead of relying on the user to supply the network topology. We will talk about the technical implementation in the plan step where we detail the SPARQL queries to be made to retrieve the data. The RINF data is not necesary available at the place where the GNSS positions are located. So we'll need to provide proper feedback to the user when that's not the case. This feature needs to become available also through the python and .net bindings." + +## Clarifications + +### Session 2026-05-13 + +- Q: When both supplied topology and ERA RINF retrieval are available, which source should take precedence? β†’ A: Use ERA RINF retrieval only when no topology is supplied. +- Q: What should happen when only part of the GNSS dataset falls inside sufficient ERA RINF coverage? β†’ A: Fail the workflow and report which GNSS records or area are uncovered. +- Q: Which workflows should support automatic ERA RINF retrieval in the first release? β†’ A: All existing topology-dependent workflows use automatic retrieval when no topology is supplied. +- Q: How should the retrieval area be defined for GNSS datasets, including ones that span a larger area? β†’ A: Use one bounding box around the GNSS dataset, expand it by 1 km, and download any netelement that lies partly inside that box. +- Q: What should happen when the GNSS dataset is empty or has no usable coordinates? β†’ A: Fail validation before retrieval and report invalid or empty GNSS input. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Retrieve Network Topology Automatically (Priority: P1) + +A user who wants to project GNSS positions onto the railway network submits GNSS data without separately providing a network topology file. The system determines the relevant geographic area, retrieves the matching netelements and netrelations from the ERA RINF dataset, and uses that retrieved topology for the rest of the workflow. + +**Why this priority**: This is the core value of the feature. It removes the current prerequisite that users must source and prepare network topology themselves. + +**Independent Test**: Can be fully tested by running a topology-dependent workflow with GNSS data in an area covered by ERA RINF and verifying that the workflow succeeds without any user-supplied topology file. + +**Acceptance Scenarios**: + +1. **Given** GNSS data located in an area covered by ERA RINF, **When** the user starts a topology-dependent workflow without providing a network file, **Then** the system retrieves the required netelements and netrelations automatically and continues the workflow successfully. +2. **Given** GNSS data that spans a continuous route segment, **When** the system retrieves ERA RINF topology for that area, **Then** the retrieved network contains the relationships needed to represent the traversable route for subsequent processing. + +--- + +### User Story 2 - Receive Clear Coverage Feedback (Priority: P2) + +A user submits GNSS data for a location where ERA RINF does not provide sufficient topology coverage. Instead of failing silently or producing an unclear downstream error, the system explains that topology for the relevant area could not be found or was insufficient for processing. + +**Why this priority**: Automatic retrieval is only trustworthy if the user can immediately distinguish between successful retrieval and missing or incomplete source coverage. + +**Independent Test**: Can be fully tested by running the same workflow with GNSS data in an uncovered or partially covered area and verifying that the user receives a clear, actionable message about the missing topology coverage. + +**Acceptance Scenarios**: + +1. **Given** GNSS data for an area with no matching ERA RINF topology, **When** the user starts a topology-dependent workflow, **Then** the system stops before downstream processing and reports that topology is unavailable for the submitted area. +2. **Given** GNSS data for an area with only partial ERA RINF coverage, **When** the retrieved topology is insufficient to support the requested workflow, **Then** the system reports that coverage is incomplete and identifies the affected input area or records. +3. **Given** a temporary retrieval failure from the external source, **When** automatic topology retrieval cannot complete, **Then** the system reports that retrieval failed and distinguishes this from a genuine lack of source coverage. + +--- + +### User Story 3 - Use Retrieval Through Language Bindings (Priority: P3) + +A Python or .NET developer integrates tp-lib into an application and expects the same automatic topology retrieval capability that is available through the core library and CLI workflows. They invoke the relevant API without separately supplying topology data and receive either a successful result or the same coverage feedback available in the main workflow. + +**Why this priority**: The new capability needs to be available across the supported integration surfaces, not only in the primary Rust-facing workflow. + +**Independent Test**: Can be fully tested by invoking the feature through the Python and .NET bindings with covered and uncovered GNSS datasets and verifying equivalent outcomes and feedback in both bindings. + +**Acceptance Scenarios**: + +1. **Given** a Python application using tp-lib bindings, **When** it runs a topology-dependent workflow without supplying topology input, **Then** it can request automatic ERA RINF retrieval and receive the resulting topology-backed workflow output. +2. **Given** a .NET application using tp-lib bindings, **When** it runs a topology-dependent workflow without supplying topology input, **Then** it can request automatic ERA RINF retrieval and receive the resulting topology-backed workflow output. +3. **Given** either binding is used in an area without sufficient ERA RINF coverage, **When** the workflow is executed, **Then** the binding surfaces a clear failure result consistent with the core library behavior. +4. **Given** any existing topology-dependent workflow is invoked without supplied topology, **When** the workflow requires railway network data, **Then** it uses automatic ERA RINF retrieval instead of requiring manual topology input. + +### Edge Cases + +- What happens when GNSS points are spread across multiple distant areas? The system still uses a single retrieval area defined as the GNSS dataset bounding box expanded by 1 km, and downloads any netelement that lies partly inside that box. +- How does the system behave when ERA RINF returns netelements for the area but omits the netrelations needed to connect them into a usable topology? +- What happens when only a subset of GNSS records falls inside available ERA RINF coverage? The workflow fails and identifies the uncovered records or area instead of returning partial processing results. +- How does the system respond when the submitted GNSS dataset is empty or contains no usable coordinates for identifying a retrieval area? The workflow fails validation before any ERA RINF request and reports invalid or empty GNSS input. +- How does the system distinguish between missing source coverage, invalid input coordinates, and temporary source unavailability? + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The system MUST allow users to run topology-dependent workflows without supplying a network topology file when ERA RINF data is intended to be used as the topology source. +- **FR-001A**: All existing topology-dependent workflows MUST support this automatic retrieval path when no topology is supplied by the caller. +- **FR-002**: The system MUST derive a single retrieval area from the submitted GNSS positions by taking the GNSS dataset bounding box and expanding it by 1 km before requesting the relevant ERA RINF netelements and netrelations. +- **FR-003**: The system MUST assemble the retrieved ERA RINF netelements and netrelations into the same logical network representation expected by downstream topology-dependent workflows. +- **FR-003A**: The system MUST include any ERA RINF netelement that lies at least partly inside the retrieval area. +- **FR-004**: The system MUST verify that the retrieved topology is sufficient for the requested workflow before continuing with downstream processing. +- **FR-005**: The system MUST stop the workflow with a clear user-facing message when no ERA RINF topology is available for the relevant GNSS area. +- **FR-006**: The system MUST stop the workflow with a clear user-facing message when ERA RINF topology is only partially available and that partial coverage is insufficient for the requested workflow. +- **FR-007**: The system MUST report retrieval failures from the ERA RINF source separately from missing data coverage so users can distinguish source availability problems from geographic coverage gaps. +- **FR-008**: The system MUST preserve the existing ability for workflows to operate on user-supplied topology where that capability already exists, and MUST use the supplied topology as the authoritative source whenever the caller provides it. +- **FR-009**: The Python bindings MUST expose the automatic ERA RINF topology retrieval capability for the same topology-dependent workflows supported by the core library. +- **FR-010**: The .NET bindings MUST expose the automatic ERA RINF topology retrieval capability for the same topology-dependent workflows supported by the core library. +- **FR-011**: The Python and .NET bindings MUST surface missing-coverage and retrieval-failure feedback with equivalent meaning to the core library behavior. +- **FR-012**: The system MUST record enough retrieval outcome detail for users or calling applications to identify whether processing used downloaded topology, failed because coverage was missing, or failed because retrieval could not be completed. +- **FR-013**: The system MUST attempt automatic ERA RINF retrieval only when a topology-dependent workflow is invoked without caller-supplied topology. +- **FR-014**: The system MUST fail the workflow rather than return partial results when any required portion of the submitted GNSS dataset lacks sufficient ERA RINF topology coverage. +- **FR-015**: When failing due to incomplete coverage, the system MUST report which GNSS records, route segment, or geographic area could not be covered well enough for the requested workflow. +- **FR-016**: The system MUST validate that the submitted GNSS dataset contains usable coordinates before making any ERA RINF retrieval request. +- **FR-017**: The system MUST fail with an invalid-input outcome, rather than a coverage-related outcome, when the GNSS dataset is empty or contains no usable coordinates for defining the retrieval area. + +### Key Entities *(include if feature involves data)* + +- **Retrieval Area**: A single bounding box derived from the submitted GNSS positions and expanded by 1 km in every direction to determine which ERA RINF topology data must be requested. +- **Retrieved Topology**: The set of netelements and netrelations obtained from ERA RINF for a retrieval area and prepared for downstream workflow use. +- **Coverage Assessment**: The outcome of checking whether the retrieved topology sufficiently covers the GNSS area and supports the requested workflow. +- **Retrieval Outcome**: The result returned to the user or caller indicating success, missing coverage, partial coverage, invalid input, or external retrieval failure. + +## Assumptions + +- ERA RINF is the authoritative external source for this feature and provides the topology data needed for the supported workflows when coverage exists. +- Existing workflows that accept user-supplied topology remain supported; this feature adds an automatic retrieval path rather than removing manual input, and manual topology input remains authoritative when provided. +- Retrieval scope is determined from the submitted GNSS positions rather than from a separate user-entered area selection, using one bounding box expanded by 1 km. +- If coverage is insufficient for reliable downstream processing, the workflow fails clearly and reports the uncovered portion rather than returning best-effort partial results. +- The first release targets all workflows that already depend on topology rather than a subset limited to projection or path calculation. +- Python and .NET bindings are expected to expose this feature at the same functional level as the core library, even if their API shapes differ. +- Invalid or empty GNSS input is treated as a caller input problem and is reported before any attempt to contact ERA RINF. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: In a representative covered-area test set, users can complete a topology-dependent workflow without providing a topology file in at least 95% of runs. +- **SC-002**: In a representative uncovered-area test set, 100% of failed runs report a user-visible outcome that explicitly identifies missing or insufficient topology coverage. +- **SC-003**: In a representative source-failure test set, 100% of failed runs report a user-visible outcome that distinguishes external retrieval failure from missing geographic coverage. +- **SC-004**: The Python and .NET bindings both support at least one end-to-end topology-dependent workflow using automatic ERA RINF retrieval with outcomes equivalent to the core library for covered and uncovered areas. +- **SC-005**: Users or calling applications can determine from every automatic-retrieval attempt whether downloaded topology was used successfully, coverage was insufficient, or retrieval failed. +- **SC-006**: Every existing topology-dependent workflow can be executed without caller-supplied topology in covered areas by using automatic ERA RINF retrieval. diff --git a/specs/006-download-rinf-topology/tasks.md b/specs/006-download-rinf-topology/tasks.md new file mode 100644 index 0000000..cf2fe79 --- /dev/null +++ b/specs/006-download-rinf-topology/tasks.md @@ -0,0 +1,205 @@ +# Tasks: ERA RINF Network Download + +**Feature**: `006-download-rinf-topology` +**Input**: `specs/006-download-rinf-topology/` +**Prerequisites**: plan.md βœ“, spec.md βœ“, research.md βœ“, data-model.md βœ“, contracts/api.md βœ“, quickstart.md βœ“ + +--- + +## Format: `[ID] [P?] [Story] Description with file path` + +- **[P]**: Can run in parallel (different files, no dependencies on incomplete tasks) +- **[Story]**: User story label β€” US1, US2, US3 (setup/foundational/polish phases carry no label) + +--- + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Prepare dependencies, fixtures, and workspace hooks required by the retrieval feature. + +- [X] T001 Add RINF retrieval dependencies and shared feature flags in Cargo.toml and tp-core/Cargo.toml +- [X] T002 [P] Add covered, uncovered, and invalid GNSS fixtures in test-data/rinf_smoke_gnss.geojson, test-data/rinf_uncovered_gnss.geojson, and test-data/rinf_empty_gnss.geojson +- [X] T003 [P] Add reusable RINF endpoint test fixtures and sample responses in tp-core/tests/fixtures/rinf_smoke_netelements.json and tp-core/tests/fixtures/rinf_smoke_netrelations.json +- [X] T004 [P] Register feature validation commands for Rust, Python, and .NET in .github/workflows/ci.yml + +**Checkpoint**: Dependencies, fixtures, and CI hooks exist so the retrieval implementation can be developed and validated consistently. + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Build the shared retrieval and outcome infrastructure that every user story depends on. + +**⚠️ CRITICAL**: No user story work should start until this phase is complete. + +- [X] T005 Create retrieval domain models and status enums in tp-core/src/models/retrieval.rs and tp-core/src/models.rs +- [X] T006 [P] Add RINF-specific error variants and diagnostic payload support in tp-core/src/errors.rs +- [X] T007 [P] Implement retrieval-area construction and source-selection request types in tp-core/src/workflow.rs +- [X] T008 Implement the SPARQL query builder, blocking endpoint client, and row parsing in tp-core/src/io/rinf.rs and tp-core/src/io.rs +- [X] T009 Expose retrieval modules and shared configuration plumbing in tp-core/src/lib.rs +- [X] T010 [P] Add shared CLI and binding option models for endpoint override and buffer distance in tp-cli/src/main.rs, tp-py/src/lib.rs, and tp-net/csharp/Models.cs + +**Checkpoint**: `tp-core` can build retrieval requests, parse endpoint responses, and represent typed outcomes before any workflow-specific integration begins. + +--- + +## Phase 3: User Story 1 - Retrieve Network Topology Automatically (Priority: P1) 🎯 MVP + +**Goal**: Users can run topology-dependent workflows without a supplied network file and the system downloads usable ERA RINF topology automatically. + +**Independent Test**: Run a topology-dependent workflow with covered GNSS input and no network file, and verify that the workflow succeeds using downloaded netelements and netrelations. + +### Tests for User Story 1 ⚠️ + +> **NOTE: Write these tests first, ensure they fail before implementation** + +- [X] T011 [P] [US1] Add failing covered-area retrieval and assembly tests in tp-core/tests/rinf_topology.rs +- [X] T012 [P] [US1] Add failing CLI smoke tests for topology-dependent commands without `--network` in tp-cli/tests/cli_integration_test.rs +- [X] T013 [P] [US1] Add failing contract assertions for query shape and valid relation mapping in tp-core/tests/contract.rs + +### Implementation for User Story 1 + +- [X] T014 [US1] Map RINF netelement and netrelation rows into existing topology structures in tp-core/src/io/rinf.rs +- [X] T015 [US1] Route topology-dependent core workflows through automatic source selection in tp-core/src/workflow.rs and tp-core/src/lib.rs +- [X] T016 [US1] Make manual topology optional and trigger auto-retrieval for CLI topology workflows in tp-cli/src/main.rs +- [X] T017 [US1] Add successful source-used diagnostics for auto-retrieval runs in tp-cli/src/main.rs +- [X] T018 [US1] Add covered-area smoke fixture usage to path-calculation and projection integration tests in tp-core/tests/integration/tests.rs and tp-cli/tests/cli_integration_test.rs + +**Checkpoint**: User Story 1 is complete when covered-area workflows succeed from the core library and CLI without any supplied topology file. + +--- + +## Phase 4: User Story 2 - Receive Clear Coverage Feedback (Priority: P2) + +**Goal**: Users receive distinct, actionable outcomes for invalid input, missing coverage, incomplete topology, and endpoint failures. + +**Independent Test**: Run the workflow with uncovered, partially covered, invalid, and endpoint-failure scenarios and verify that each produces a distinct failure outcome before downstream processing starts. + +### Tests for User Story 2 ⚠️ + +> **NOTE: Write these tests first, ensure they fail before implementation** + +- [X] T019 [P] [US2] Add failing missing-coverage, incomplete-topology, invalid-input, and endpoint-failure tests in tp-core/tests/rinf_topology.rs +- [X] T020 [P] [US2] Add failing CLI error-reporting tests for uncovered and invalid-input scenarios in tp-cli/tests/cli_integration_test.rs +- [X] T021 [P] [US2] Add failing coarse-geometry and zero-netrelation validation tests in tp-core/tests/unit.rs and tp-core/tests/contract.rs + +### Implementation for User Story 2 + +- [X] T022 [US2] Implement uncovered-area assessment and affected-GNSS diagnostics in tp-core/src/workflow.rs and tp-core/src/models/retrieval.rs +- [X] T023 [US2] Implement coarse-geometry and zero-netrelation validation failures in tp-core/src/io/rinf.rs and tp-core/src/errors.rs +- [X] T024 [US2] Propagate distinct retrieval outcome messages and exit handling in tp-cli/src/main.rs +- [X] T025 [US2] Add retrieval provenance and failure-detail reporting for callers in tp-core/src/lib.rs and tp-core/src/models/retrieval.rs + +**Checkpoint**: User Story 2 is complete when each failure mode is surfaced clearly and no downstream topology workflow runs on invalid or incomplete RINF data. + +--- + +## Phase 5: User Story 3 - Use Retrieval Through Language Bindings (Priority: P3) + +**Goal**: Python and .NET consumers can omit topology input and get the same retrieval behavior and failure semantics as the core library and CLI. + +**Independent Test**: Invoke covered and uncovered workflows through Python and .NET bindings with `network` omitted and verify successful retrieval or equivalent failure semantics. + +### Tests for User Story 3 ⚠️ + +> **NOTE: Write these tests first, ensure they fail before implementation** + +- [X] T026 [P] [US3] Add failing Python auto-retrieval success and failure tests in tp-py/python/tests/test_path_calculation.py +- [X] T027 [P] [US3] Add failing Python manual-topology precedence tests in tp-py/python/tests/test_projection.py +- [X] T028 [P] [US3] Add failing .NET auto-retrieval success and failure tests in tp-net/csharp/Tests/PathCalculationTests.cs +- [X] T029 [P] [US3] Add failing .NET nullable-network and precedence tests in tp-net/csharp/Tests/ProjectionTests.cs + +### Implementation for User Story 3 + +- [X] T030 [US3] Expose optional network and RINF retrieval arguments plus typed error mapping in tp-py/src/lib.rs +- [X] T031 [US3] Update Python package exports and developer-facing wrappers for auto-retrieval workflows in tp-py/python/tp_lib/__init__.py +- [X] T032 [US3] Add nullable-network retrieval support and RINF option plumbing to the Rust FFI surface in tp-net/src/lib.rs +- [X] T033 [US3] Expose `RinfRetrievalOptions` and nullable-network overloads in tp-net/csharp/Models.cs and tp-net/csharp/TpLib.cs +- [X] T034 [US3] Map core retrieval outcomes to distinct .NET exceptions and diagnostics in tp-net/csharp/Exceptions.cs and tp-net/csharp/TpLib.cs + +**Checkpoint**: User Story 3 is complete when Python and .NET callers can use automatic retrieval without a network file and observe the same outcome categories as Rust callers. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Finish documentation, align examples, and run the full validation matrix requested for the feature. + +- [X] T035 [P] Update feature overview and validation commands in README.md +- [X] T036 [P] Update CLI auto-retrieval usage in tp-cli/README.md +- [X] T037 [P] Update Python binding auto-retrieval examples in tp-py/README.md +- [X] T038 [P] Update .NET binding auto-retrieval examples in tp-net/README.md +- [X] T039 [P] Update RINF fixture guidance and smoke-test data notes in test-data/README.md +- [X] T040 Update developer quickstart and acceptance scenarios in specs/006-download-rinf-topology/quickstart.md and specs/006-download-rinf-topology/contracts/api.md +- [X] T041 Run `cargo fmt --all -- --check` for Cargo.toml, tp-core/src/, tp-cli/src/, tp-py/src/, tp-net/src/, and tp-webapp/src/ +- [X] T042 Run `cargo clippy --workspace --all-targets --all-features -- -D warnings` for Cargo.toml, tp-core/src/, tp-cli/src/, tp-py/src/, tp-net/src/, and tp-webapp/src/ +- [X] T043 Run `cargo test --workspace` covering tp-core/tests/, tp-cli/tests/, tp-net/src/, and tp-webapp/tests/ +- [X] T044 Run `maturin develop` and `pytest python/tests/ -v` in tp-py/ for tp-py/src/lib.rs and tp-py/python/tests/ +- [X] T045 Run `ruff check tp-py/python` for tp-py/python/ +- [X] T046 Run `dotnet test tp-net/csharp/Tests/TpLib.Tests.csproj -c Debug --verbosity minimal` for tp-net/csharp/Tests/ and tp-net/src/lib.rs + +**Checkpoint**: Documentation is updated across every README surface and the full Rust, Python, and .NET validation matrix passes, including linting. + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies - can start immediately +- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories +- **User Story 1 (Phase 3)**: Depends on Foundational completion - MVP increment +- **User Story 2 (Phase 4)**: Depends on User Story 1 core retrieval path and can start once Phase 3 infrastructure is in place +- **User Story 3 (Phase 5)**: Depends on Foundational completion and should start after User Story 2 outcome shapes are stable +- **Polish (Phase 6)**: Depends on all desired user stories completing + +### User Story Dependencies + +- **User Story 1 (P1)**: First deliverable after Phase 2; no dependency on other stories +- **User Story 2 (P2)**: Builds on User Story 1 retrieval flow to harden validation and messaging +- **User Story 3 (P3)**: Reuses the completed core retrieval and outcome contracts from User Stories 1 and 2 + +### Within Each User Story + +- Tests MUST be written and observed failing before implementation begins +- Core retrieval and validation code in tp-core precedes CLI and binding integration +- CLI and binding adapters should consume typed outcomes rather than reimplement validation logic +- Story checkpoints must pass before starting polish work + +### Parallel Opportunities + +- Setup tasks marked `[P]` can run in parallel because they touch different fixtures and workflow files +- Foundational tasks marked `[P]` can run in parallel once the retrieval module boundaries are agreed +- User Story 1 test tasks can run in parallel across tp-core and tp-cli +- User Story 2 validation tests can run in parallel across tp-core and tp-cli +- User Story 3 binding tests can run in parallel across tp-py and tp-net +- README updates in Phase 6 can run in parallel because each task touches a separate documentation file + +--- + +## Summary + +| Phase | Tasks | User Story | Priority | +|---|---|---|---| +| Phase 1 β€” Setup | T001-T004 | β€” | β€” | +| Phase 2 β€” Foundational | T005-T010 | β€” | β€” | +| Phase 3 β€” Auto Retrieval | T011-T018 | US1 | P1 🎯 MVP | +| Phase 4 β€” Coverage Feedback | T019-T025 | US2 | P2 | +| Phase 5 β€” Language Bindings | T026-T034 | US3 | P3 | +| Phase 6 β€” Polish | T035-T046 | β€” | β€” | + +**Total**: 46 tasks. + +**Task count by user story**: +- **US1**: 8 tasks +- **US2**: 7 tasks +- **US3**: 9 tasks + +**Parallel opportunities identified**: 21 tasks marked `[P]`, plus story-level parallelism between tp-py and tp-net once the tp-core outcome contract is stable. + +**Independent test criteria**: +- **US1**: Covered-area workflow succeeds without `--network` and uses ERA RINF topology +- **US2**: Uncovered, incomplete, invalid, and endpoint-failure scenarios each return a distinct failure outcome +- **US3**: Python and .NET workflows succeed or fail with the same semantics as the core library when topology is omitted + +**Format validation**: Every task uses the required checklist structure with checkbox, task ID, optional `[P]`, required story labels for user-story phases, and explicit file paths. \ No newline at end of file diff --git a/test-data/README.md b/test-data/README.md index c17470b..62bfe20 100644 --- a/test-data/README.md +++ b/test-data/README.md @@ -68,6 +68,7 @@ Each log has its own subdirectory (`log_XXXXX/`) containing the source GNSS CSV - [Original path calculation](#original-path-calculation-1) - [Adding train detections](#adding-train-detections) - [Reviewing train detections](#reviewing-train-detections) + - [Missing network topology](#missing-network-topology) Root folder for release exe: `target/release/` @@ -99,6 +100,14 @@ The three operations available via `tp-cli` are: Common flags: `--gnss `, `--network `, `--crs EPSG:4326`, `--output ` (`.geojson` extension auto-selects GeoJSON format). +If `--network` is omitted, `tp-cli` downloads a RINF topology subset on demand +around the GNSS bounding box (default endpoint +`https://graph.data.era.europa.eu/repositories/rinf-plus`, default buffer +1000 m). The fixtures in this directory remain the recommended source for +offline runs and reproducible regression tests; the auto-retrieval path is +intended for ad-hoc analysis where a curated network file is not yet +available. + ## Simple projection, path calculation & path projection ### L36 track B – log_28876 @@ -737,4 +746,57 @@ A browser window will open and allow the user to modify the calculated train pat ![L36-A to L36C-A to L25N-B (log_28573) - Path review with detection](log_28573-detections/log_28586_L36-A_to_L36C-A_to_L25N-B-path-review.png) ---- \ No newline at end of file +--- + +## Missing network topology + +In case the user doesn't supply a network topology file, an attempt will be made to download the applicable data from [EU Register of railway Infrastructure (RINF)](https://rinf.data.era.europa.eu/). In time, this register should have a micro topology definition of the railway network which is suitable for usage with TP-lib. The availability of micro topology in the RINF will significantly improve the usability of this tool. At the time of writing, not all EU countries have such a detailed description of the network available (though the EU regulation had 31/03/2026 as deadline). + +Since Norway has a very detailed description available, we are going to use a small part of this network to test the RINF topology fetching feature. A [manually created GNSS dataset](rinf-kvg/gnss.geojson) will be used as input. + +The GNSS data simulates a train driving from the SolΓΈrbanen onto the Kongsvingerbanen into the direction of Oslo. Here is an overview: + +![GNSS overview RINF download feature](rinf-kvg/raw-gnss-data.png) + +The train enters the station from the upper left and immediately takes a switch onto track 2: + +![GNSS overview RINF download feature - detail 3](rinf-kvg/raw-gnss-data-detail3.png) + +To make this exercise more challenging, we simulated the train taking another switch to briefly drive on track 3 to later merge back onto track 2: + +![GNSS overview RINF download feature - detail 2](rinf-kvg/raw-gnss-data-detail2.png) + +And finally the train will merge onto track 1 and exit in the Kongsvinger station in the direction of Oslo: + +![GNSS overview RINF download feature - detail 1](rinf-kvg/raw-gnss-data-detail1.png) + +We will first trigger simple fetching of the network topology using the following command: + +```bash +cargo run -- fetch-topology --gnss test-data/rinf-kvg/gnss.geojson -o test-data/rinf-kvg/network.geojson +``` + +This will produce the network.geojson output file that we can visualise together with the raw GNSS positions (red lines are the topology): + +![RINF downloaded topology](rinf-kvg/topology-overview.png) + +![RINF downloaded topology without openstreetmap](rinf-kvg/topology-overview-no-background.png) + +![RINF downloaded topology zoom](rinf-kvg/topology-detail.png) + +And running the complete TP-lib processing on the GNSS data will produce the [expected result](rinf-kvg/path-projected.geojson): + +```bash +cargo run -- --gnss test-data/rinf-kvg/gnss.geojson -o test-data/rinf-kvg/path-projected.geojson + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.37s + Running `target\debug\tp-cli.exe --gnss test-data/rinf-kvg/gnss.geojson -o test-data/rinf-kvg/path-projected.geojson` +Topology source: EraRinf (endpoint=https://graph.data.era.europa.eu/repositories/rinf-plus, buffer_meters=1000) +Topology retrieved: source=EraRinf, netelements=99, netrelations=364 +``` + +Here are detailed screenshots of the result: + + +![Path projected using RINF download feature - detail 1](rinf-kvg/path-projected-detail1.png) +![Path projected using RINF download feature - detail 2](rinf-kvg/path-projected-detail2.png) +![Path projected using RINF download feature - detail 3](rinf-kvg/path-projected-detail3.png) \ No newline at end of file diff --git a/test-data/rinf-kvg/gnss.geojson b/test-data/rinf-kvg/gnss.geojson new file mode 100644 index 0000000..103deb7 --- /dev/null +++ b/test-data/rinf-kvg/gnss.geojson @@ -0,0 +1,156 @@ +{ +"type": "FeatureCollection", +"name": "kvg-rinf-test", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, +"features": [ +{"type":"Feature","properties":{"fid":1,"timestamp":"2026-05-25T13:09:15"},"geometry":{"type":"Point","coordinates":[11.986731368455182,60.190456829671398]}}, +{"type":"Feature","properties":{"fid":2,"timestamp":"2026-05-25T13:09:14"},"geometry":{"type":"Point","coordinates":[11.98694690632874,60.190384377710934]}}, +{"type":"Feature","properties":{"fid":3,"timestamp":"2026-05-25T13:09:13"},"geometry":{"type":"Point","coordinates":[11.987113462024588,60.19031610214823]}}, +{"type":"Feature","properties":{"fid":4,"timestamp":"2026-05-25T13:09:12"},"geometry":{"type":"Point","coordinates":[11.987335713245637,60.190249642980156]}}, +{"type":"Feature","properties":{"fid":5,"timestamp":"2026-05-25T13:09:11"},"geometry":{"type":"Point","coordinates":[11.987521018704937,60.190184016522764]}}, +{"type":"Feature","properties":{"fid":6,"timestamp":"2026-05-25T13:09:10"},"geometry":{"type":"Point","coordinates":[11.987859958664933,60.190179445713461]}}, +{"type":"Feature","properties":{"fid":7,"timestamp":"2026-05-25T13:09:09"},"geometry":{"type":"Point","coordinates":[11.988039941847278,60.190123155755003]}}, +{"type":"Feature","properties":{"fid":8,"timestamp":"2026-05-25T13:09:08"},"geometry":{"type":"Point","coordinates":[11.988304734179561,60.190049590657154]}}, +{"type":"Feature","properties":{"fid":9,"timestamp":"2026-05-25T13:09:07"},"geometry":{"type":"Point","coordinates":[11.988619342631047,60.189981046110866]}}, +{"type":"Feature","properties":{"fid":10,"timestamp":"2026-05-25T13:09:06"},"geometry":{"type":"Point","coordinates":[11.988919961151272,60.189894381956677]}}, +{"type":"Feature","properties":{"fid":11,"timestamp":"2026-05-25T13:09:05"},"geometry":{"type":"Point","coordinates":[11.989232334886477,60.189801307054537]}}, +{"type":"Feature","properties":{"fid":12,"timestamp":"2026-05-25T13:09:04"},"geometry":{"type":"Point","coordinates":[11.989510831735322,60.18974279230423]}}, +{"type":"Feature","properties":{"fid":13,"timestamp":"2026-05-25T13:09:03"},"geometry":{"type":"Point","coordinates":[11.989770576430505,60.189681627711366]}}, +{"type":"Feature","properties":{"fid":14,"timestamp":"2026-05-25T13:09:02"},"geometry":{"type":"Point","coordinates":[11.989958403404312,60.189575999485577]}}, +{"type":"Feature","properties":{"fid":15,"timestamp":"2026-05-25T13:09:01"},"geometry":{"type":"Point","coordinates":[11.990193237512649,60.189512323794752]}}, +{"type":"Feature","properties":{"fid":16,"timestamp":"2026-05-25T13:09:00"},"geometry":{"type":"Point","coordinates":[11.990860478783338,60.189454248989648]}}, +{"type":"Feature","properties":{"fid":17,"timestamp":"2026-05-25T13:08:59"},"geometry":{"type":"Point","coordinates":[11.991105949894129,60.18937189704203]}}, +{"type":"Feature","properties":{"fid":18,"timestamp":"2026-05-25T13:08:58"},"geometry":{"type":"Point","coordinates":[11.991877302019638,60.189305321710314]}}, +{"type":"Feature","properties":{"fid":19,"timestamp":"2026-05-25T13:08:57"},"geometry":{"type":"Point","coordinates":[11.992271674381819,60.189164301462931]}}, +{"type":"Feature","properties":{"fid":20,"timestamp":"2026-05-25T13:08:56"},"geometry":{"type":"Point","coordinates":[11.992834219193007,60.189176174241595]}}, +{"type":"Feature","properties":{"fid":21,"timestamp":"2026-05-25T13:08:55"},"geometry":{"type":"Point","coordinates":[11.993065972018281,60.189078766370805]}}, +{"type":"Feature","properties":{"fid":22,"timestamp":"2026-05-25T13:08:54"},"geometry":{"type":"Point","coordinates":[11.993415537632965,60.189055506616697]}}, +{"type":"Feature","properties":{"fid":23,"timestamp":"2026-05-25T13:08:53"},"geometry":{"type":"Point","coordinates":[11.993578989009663,60.189021089314586]}}, +{"type":"Feature","properties":{"fid":24,"timestamp":"2026-05-25T13:08:52"},"geometry":{"type":"Point","coordinates":[11.993955138421464,60.18901873435717]}}, +{"type":"Feature","properties":{"fid":25,"timestamp":"2026-05-25T13:08:51"},"geometry":{"type":"Point","coordinates":[11.994297707127533,60.188986413123828]}}, +{"type":"Feature","properties":{"fid":26,"timestamp":"2026-05-25T13:08:50"},"geometry":{"type":"Point","coordinates":[11.994889925454023,60.188917723636763]}}, +{"type":"Feature","properties":{"fid":27,"timestamp":"2026-05-25T13:08:49"},"geometry":{"type":"Point","coordinates":[11.995371591505252,60.188854603409574]}}, +{"type":"Feature","properties":{"fid":28,"timestamp":"2026-05-25T13:08:48"},"geometry":{"type":"Point","coordinates":[11.995907272172529,60.188774897879675]}}, +{"type":"Feature","properties":{"fid":29,"timestamp":"2026-05-25T13:08:47"},"geometry":{"type":"Point","coordinates":[11.996598002053236,60.188636379713024]}}, +{"type":"Feature","properties":{"fid":30,"timestamp":"2026-05-25T13:08:46"},"geometry":{"type":"Point","coordinates":[11.997252902274674,60.188510958130919]}}, +{"type":"Feature","properties":{"fid":31,"timestamp":"2026-05-25T13:08:45"},"geometry":{"type":"Point","coordinates":[11.997511503453655,60.188437514165756]}}, +{"type":"Feature","properties":{"fid":32,"timestamp":"2026-05-25T13:08:44"},"geometry":{"type":"Point","coordinates":[11.997868616702345,60.188361840567019]}}, +{"type":"Feature","properties":{"fid":33,"timestamp":"2026-05-25T13:08:43"},"geometry":{"type":"Point","coordinates":[11.998107345124264,60.18827348297139]}}, +{"type":"Feature","properties":{"fid":34,"timestamp":"2026-05-25T13:08:42"},"geometry":{"type":"Point","coordinates":[11.998230206478832,60.188267630138462]}}, +{"type":"Feature","properties":{"fid":35,"timestamp":"2026-05-25T13:08:41"},"geometry":{"type":"Point","coordinates":[11.998366921396649,60.188244565334919]}}, +{"type":"Feature","properties":{"fid":36,"timestamp":"2026-05-25T13:08:40"},"geometry":{"type":"Point","coordinates":[11.998642449697545,60.188187634318709]}}, +{"type":"Feature","properties":{"fid":37,"timestamp":"2026-05-25T13:08:39"},"geometry":{"type":"Point","coordinates":[11.998857945993533,60.188115163270211]}}, +{"type":"Feature","properties":{"fid":38,"timestamp":"2026-05-25T13:08:38"},"geometry":{"type":"Point","coordinates":[11.999018868076259,60.188086941191699]}}, +{"type":"Feature","properties":{"fid":39,"timestamp":"2026-05-25T13:08:37"},"geometry":{"type":"Point","coordinates":[11.999173912409734,60.188028127738797]}}, +{"type":"Feature","properties":{"fid":40,"timestamp":"2026-05-25T13:08:36"},"geometry":{"type":"Point","coordinates":[11.999269205609226,60.187990637565683]}}, +{"type":"Feature","properties":{"fid":41,"timestamp":"2026-05-25T13:08:35"},"geometry":{"type":"Point","coordinates":[11.999519962262298,60.187932729255756]}}, +{"type":"Feature","properties":{"fid":42,"timestamp":"2026-05-25T13:08:34"},"geometry":{"type":"Point","coordinates":[11.999739514153289,60.187904715048347]}}, +{"type":"Feature","properties":{"fid":43,"timestamp":"2026-05-25T13:08:33"},"geometry":{"type":"Point","coordinates":[11.999928980699652,60.187885062788098]}}, +{"type":"Feature","properties":{"fid":44,"timestamp":"2026-05-25T13:08:32"},"geometry":{"type":"Point","coordinates":[12.000061214420343,60.187846735539317]}}, +{"type":"Feature","properties":{"fid":45,"timestamp":"2026-05-25T13:08:31"},"geometry":{"type":"Point","coordinates":[12.000214438623436,60.187835584628083]}}, +{"type":"Feature","properties":{"fid":46,"timestamp":"2026-05-25T13:08:30"},"geometry":{"type":"Point","coordinates":[12.000313928538583,60.187810288337538]}}, +{"type":"Feature","properties":{"fid":47,"timestamp":"2026-05-25T13:08:29"},"geometry":{"type":"Point","coordinates":[12.00055278982272,60.187791052735847]}}, +{"type":"Feature","properties":{"fid":48,"timestamp":"2026-05-25T13:08:28"},"geometry":{"type":"Point","coordinates":[12.000706851771433,60.187755302867259]}}, +{"type":"Feature","properties":{"fid":49,"timestamp":"2026-05-25T13:08:27"},"geometry":{"type":"Point","coordinates":[12.000935916878575,60.187730143594592]}}, +{"type":"Feature","properties":{"fid":50,"timestamp":"2026-05-25T13:08:26"},"geometry":{"type":"Point","coordinates":[12.001125521691941,60.18771202267704]}}, +{"type":"Feature","properties":{"fid":51,"timestamp":"2026-05-25T13:08:25"},"geometry":{"type":"Point","coordinates":[12.001273705788153,60.187679477711782]}}, +{"type":"Feature","properties":{"fid":52,"timestamp":"2026-05-25T13:08:24"},"geometry":{"type":"Point","coordinates":[12.001439662767769,60.187672645553036]}}, +{"type":"Feature","properties":{"fid":53,"timestamp":"2026-05-25T13:08:23"},"geometry":{"type":"Point","coordinates":[12.00161695310457,60.187654802912476]}}, +{"type":"Feature","properties":{"fid":54,"timestamp":"2026-05-25T13:08:22"},"geometry":{"type":"Point","coordinates":[12.001831745032879,60.18764225516334]}}, +{"type":"Feature","properties":{"fid":55,"timestamp":"2026-05-25T13:08:21"},"geometry":{"type":"Point","coordinates":[12.002046816946065,60.187632773146646]}}, +{"type":"Feature","properties":{"fid":56,"timestamp":"2026-05-25T13:08:20"},"geometry":{"type":"Point","coordinates":[12.002363057509998,60.187616389441054]}}, +{"type":"Feature","properties":{"fid":57,"timestamp":"2026-05-25T13:08:19"},"geometry":{"type":"Point","coordinates":[12.002573653414569,60.18762544257288]}}, +{"type":"Feature","properties":{"fid":58,"timestamp":"2026-05-25T13:08:18"},"geometry":{"type":"Point","coordinates":[12.002746046936943,60.187621535318932]}}, +{"type":"Feature","properties":{"fid":59,"timestamp":"2026-05-25T13:08:17"},"geometry":{"type":"Point","coordinates":[12.002990786219515,60.187632886341341]}}, +{"type":"Feature","properties":{"fid":60,"timestamp":"2026-05-25T13:08:16"},"geometry":{"type":"Point","coordinates":[12.003154504875367,60.187635320049303]}}, +{"type":"Feature","properties":{"fid":61,"timestamp":"2026-05-25T13:08:15"},"geometry":{"type":"Point","coordinates":[12.003477046648685,60.187654123780007]}}, +{"type":"Feature","properties":{"fid":62,"timestamp":"2026-05-25T13:08:14"},"geometry":{"type":"Point","coordinates":[12.003826173579569,60.187660034232366]}}, +{"type":"Feature","properties":{"fid":63,"timestamp":"2026-05-25T13:08:13"},"geometry":{"type":"Point","coordinates":[12.004141297420082,60.18766517860653]}}, +{"type":"Feature","properties":{"fid":64,"timestamp":"2026-05-25T13:08:12"},"geometry":{"type":"Point","coordinates":[12.004484127431855,60.187669693900482]}}, +{"type":"Feature","properties":{"fid":65,"timestamp":"2026-05-25T13:08:11"},"geometry":{"type":"Point","coordinates":[12.004782322974959,60.187692119034196]}}, +{"type":"Feature","properties":{"fid":66,"timestamp":"2026-05-25T13:08:10"},"geometry":{"type":"Point","coordinates":[12.005284812286721,60.187688402321825]}}, +{"type":"Feature","properties":{"fid":67,"timestamp":"2026-05-25T13:08:09"},"geometry":{"type":"Point","coordinates":[12.005519202371197,60.187721490488073]}}, +{"type":"Feature","properties":{"fid":68,"timestamp":"2026-05-25T13:08:08"},"geometry":{"type":"Point","coordinates":[12.005694113387289,60.187711377419838]}}, +{"type":"Feature","properties":{"fid":69,"timestamp":"2026-05-25T13:08:07"},"geometry":{"type":"Point","coordinates":[12.005907508595495,60.187717289329974]}}, +{"type":"Feature","properties":{"fid":70,"timestamp":"2026-05-25T13:08:06"},"geometry":{"type":"Point","coordinates":[12.006279027291392,60.187731902579557]}}, +{"type":"Feature","properties":{"fid":71,"timestamp":"2026-05-25T13:08:05"},"geometry":{"type":"Point","coordinates":[12.006498719908953,60.187739206856314]}}, +{"type":"Feature","properties":{"fid":72,"timestamp":"2026-05-25T13:08:04"},"geometry":{"type":"Point","coordinates":[12.006921029434565,60.187735767279158]}}, +{"type":"Feature","properties":{"fid":73,"timestamp":"2026-05-25T13:08:03"},"geometry":{"type":"Point","coordinates":[12.007147580687489,60.18775059589975]}}, +{"type":"Feature","properties":{"fid":74,"timestamp":"2026-05-25T13:08:02"},"geometry":{"type":"Point","coordinates":[12.007400855924823,60.187754063966054]}}, +{"type":"Feature","properties":{"fid":75,"timestamp":"2026-05-25T13:08:01"},"geometry":{"type":"Point","coordinates":[12.00759690362,60.187772656702066]}}, +{"type":"Feature","properties":{"fid":76,"timestamp":"2026-05-25T13:08:00"},"geometry":{"type":"Point","coordinates":[12.007740196720027,60.187787838199483]}}, +{"type":"Feature","properties":{"fid":77,"timestamp":"2026-05-25T13:07:59"},"geometry":{"type":"Point","coordinates":[12.007898180470155,60.187795004913859]}}, +{"type":"Feature","properties":{"fid":78,"timestamp":"2026-05-25T13:07:58"},"geometry":{"type":"Point","coordinates":[12.008047349992694,60.187806980226149]}}, +{"type":"Feature","properties":{"fid":79,"timestamp":"2026-05-25T13:07:57"},"geometry":{"type":"Point","coordinates":[12.008162095112363,60.187813592152793]}}, +{"type":"Feature","properties":{"fid":80,"timestamp":"2026-05-25T13:07:56"},"geometry":{"type":"Point","coordinates":[12.008258229154823,60.187819090365068]}}, +{"type":"Feature","properties":{"fid":81,"timestamp":"2026-05-25T13:07:55"},"geometry":{"type":"Point","coordinates":[12.008394383186747,60.187823679739829]}}, +{"type":"Feature","properties":{"fid":82,"timestamp":"2026-05-25T13:07:54"},"geometry":{"type":"Point","coordinates":[12.008515144960539,60.187828618517614]}}, +{"type":"Feature","properties":{"fid":83,"timestamp":"2026-05-25T13:07:53"},"geometry":{"type":"Point","coordinates":[12.008670752537414,60.187843519369203]}}, +{"type":"Feature","properties":{"fid":84,"timestamp":"2026-05-25T13:07:52"},"geometry":{"type":"Point","coordinates":[12.00886245938146,60.187848382862839]}}, +{"type":"Feature","properties":{"fid":85,"timestamp":"2026-05-25T13:07:51"},"geometry":{"type":"Point","coordinates":[12.00901330386904,60.187844956866464]}}, +{"type":"Feature","properties":{"fid":86,"timestamp":"2026-05-25T13:07:50"},"geometry":{"type":"Point","coordinates":[12.00919689869526,60.187862293874083]}}, +{"type":"Feature","properties":{"fid":87,"timestamp":"2026-05-25T13:07:49"},"geometry":{"type":"Point","coordinates":[12.009397700658935,60.187865413789183]}}, +{"type":"Feature","properties":{"fid":88,"timestamp":"2026-05-25T13:07:48"},"geometry":{"type":"Point","coordinates":[12.009604519136515,60.187866860503235]}}, +{"type":"Feature","properties":{"fid":89,"timestamp":"2026-05-25T13:07:47"},"geometry":{"type":"Point","coordinates":[12.009738016372587,60.187876117515231]}}, +{"type":"Feature","properties":{"fid":90,"timestamp":"2026-05-25T13:07:46"},"geometry":{"type":"Point","coordinates":[12.009976040857527,60.187881463362558]}}, +{"type":"Feature","properties":{"fid":91,"timestamp":"2026-05-25T13:07:45"},"geometry":{"type":"Point","coordinates":[12.010253804446561,60.187882833396166]}}, +{"type":"Feature","properties":{"fid":92,"timestamp":"2026-05-25T13:07:44"},"geometry":{"type":"Point","coordinates":[12.010420743545541,60.187886721192513]}}, +{"type":"Feature","properties":{"fid":93,"timestamp":"2026-05-25T13:07:43"},"geometry":{"type":"Point","coordinates":[12.010705787963094,60.187900214594762]}}, +{"type":"Feature","properties":{"fid":94,"timestamp":"2026-05-25T13:07:42"},"geometry":{"type":"Point","coordinates":[12.010901557267351,60.187915736366023]}}, +{"type":"Feature","properties":{"fid":95,"timestamp":"2026-05-25T13:07:41"},"geometry":{"type":"Point","coordinates":[12.011067934578538,60.187913491236699]}}, +{"type":"Feature","properties":{"fid":96,"timestamp":"2026-05-25T13:07:40"},"geometry":{"type":"Point","coordinates":[12.011299521535685,60.187915908584763]}}, +{"type":"Feature","properties":{"fid":97,"timestamp":"2026-05-25T13:07:39"},"geometry":{"type":"Point","coordinates":[12.011444349598765,60.18791415279339]}}, +{"type":"Feature","properties":{"fid":98,"timestamp":"2026-05-25T13:07:38"},"geometry":{"type":"Point","coordinates":[12.011749116882125,60.187907224767038]}}, +{"type":"Feature","properties":{"fid":99,"timestamp":"2026-05-25T13:07:37"},"geometry":{"type":"Point","coordinates":[12.011967406178742,60.187899189700069]}}, +{"type":"Feature","properties":{"fid":100,"timestamp":"2026-05-25T13:07:36"},"geometry":{"type":"Point","coordinates":[12.012248388886379,60.187902018587906]}}, +{"type":"Feature","properties":{"fid":101,"timestamp":"2026-05-25T13:07:35"},"geometry":{"type":"Point","coordinates":[12.012653072083575,60.187908178852936]}}, +{"type":"Feature","properties":{"fid":102,"timestamp":"2026-05-25T13:07:34"},"geometry":{"type":"Point","coordinates":[12.013056364075426,60.187932804184754]}}, +{"type":"Feature","properties":{"fid":103,"timestamp":"2026-05-25T13:07:33"},"geometry":{"type":"Point","coordinates":[12.013343785220401,60.187938556767087]}}, +{"type":"Feature","properties":{"fid":104,"timestamp":"2026-05-25T13:07:32"},"geometry":{"type":"Point","coordinates":[12.013551025999623,60.187944596387943]}}, +{"type":"Feature","properties":{"fid":105,"timestamp":"2026-05-25T13:07:31"},"geometry":{"type":"Point","coordinates":[12.01381381966339,60.187950908150718]}}, +{"type":"Feature","properties":{"fid":106,"timestamp":"2026-05-25T13:07:30"},"geometry":{"type":"Point","coordinates":[12.014110617057614,60.187957981995581]}}, +{"type":"Feature","properties":{"fid":107,"timestamp":"2026-05-25T13:07:29"},"geometry":{"type":"Point","coordinates":[12.014428823082369,60.187963031833263]}}, +{"type":"Feature","properties":{"fid":108,"timestamp":"2026-05-25T13:07:28"},"geometry":{"type":"Point","coordinates":[12.014855759808462,60.187976360150266]}}, +{"type":"Feature","properties":{"fid":109,"timestamp":"2026-05-25T13:07:27"},"geometry":{"type":"Point","coordinates":[12.015344125222503,60.18798675274013]}}, +{"type":"Feature","properties":{"fid":110,"timestamp":"2026-05-25T13:07:26"},"geometry":{"type":"Point","coordinates":[12.015718447381795,60.187998203361531]}}, +{"type":"Feature","properties":{"fid":111,"timestamp":"2026-05-25T13:07:25"},"geometry":{"type":"Point","coordinates":[12.01631246593629,60.188016940899736]}}, +{"type":"Feature","properties":{"fid":112,"timestamp":"2026-05-25T13:07:24"},"geometry":{"type":"Point","coordinates":[12.016795238423136,60.188033600390234]}}, +{"type":"Feature","properties":{"fid":113,"timestamp":"2026-05-25T13:07:23"},"geometry":{"type":"Point","coordinates":[12.017039703284635,60.188041859368049]}}, +{"type":"Feature","properties":{"fid":114,"timestamp":"2026-05-25T13:07:22"},"geometry":{"type":"Point","coordinates":[12.017274228933028,60.188042663139314]}}, +{"type":"Feature","properties":{"fid":115,"timestamp":"2026-05-25T13:07:21"},"geometry":{"type":"Point","coordinates":[12.017577747798759,60.18805572105498]}}, +{"type":"Feature","properties":{"fid":116,"timestamp":"2026-05-25T13:07:20"},"geometry":{"type":"Point","coordinates":[12.017731248042828,60.188047615962311]}}, +{"type":"Feature","properties":{"fid":117,"timestamp":"2026-05-25T13:07:19"},"geometry":{"type":"Point","coordinates":[12.018054223621538,60.188070983169929]}}, +{"type":"Feature","properties":{"fid":118,"timestamp":"2026-05-25T13:07:18"},"geometry":{"type":"Point","coordinates":[12.018329332707975,60.188077005442693]}}, +{"type":"Feature","properties":{"fid":119,"timestamp":"2026-05-25T13:07:17"},"geometry":{"type":"Point","coordinates":[12.018546232957716,60.188087425877939]}}, +{"type":"Feature","properties":{"fid":120,"timestamp":"2026-05-25T13:07:16"},"geometry":{"type":"Point","coordinates":[12.018759209711609,60.188088718007222]}}, +{"type":"Feature","properties":{"fid":121,"timestamp":"2026-05-25T13:07:15"},"geometry":{"type":"Point","coordinates":[12.019180556023827,60.188108304721318]}}, +{"type":"Feature","properties":{"fid":122,"timestamp":"2026-05-25T13:07:14"},"geometry":{"type":"Point","coordinates":[12.019569850152887,60.188114794614613]}}, +{"type":"Feature","properties":{"fid":123,"timestamp":"2026-05-25T13:07:13"},"geometry":{"type":"Point","coordinates":[12.019847474594011,60.188114611506073]}}, +{"type":"Feature","properties":{"fid":124,"timestamp":"2026-05-25T13:07:12"},"geometry":{"type":"Point","coordinates":[12.020086488366454,60.188130670268357]}}, +{"type":"Feature","properties":{"fid":125,"timestamp":"2026-05-25T13:07:11"},"geometry":{"type":"Point","coordinates":[12.020312624804239,60.188140877273192]}}, +{"type":"Feature","properties":{"fid":126,"timestamp":"2026-05-25T13:07:10"},"geometry":{"type":"Point","coordinates":[12.020468238794104,60.188155764219395]}}, +{"type":"Feature","properties":{"fid":127,"timestamp":"2026-05-25T13:07:09"},"geometry":{"type":"Point","coordinates":[12.020768399387698,60.188165818968486]}}, +{"type":"Feature","properties":{"fid":128,"timestamp":"2026-05-25T13:07:08"},"geometry":{"type":"Point","coordinates":[12.020862585517833,60.188183642311515]}}, +{"type":"Feature","properties":{"fid":129,"timestamp":"2026-05-25T13:07:07"},"geometry":{"type":"Point","coordinates":[12.021098098863725,60.188195170397897]}}, +{"type":"Feature","properties":{"fid":130,"timestamp":"2026-05-25T13:07:06"},"geometry":{"type":"Point","coordinates":[12.021413370281788,60.188201806651549]}}, +{"type":"Feature","properties":{"fid":131,"timestamp":"2026-05-25T13:07:05"},"geometry":{"type":"Point","coordinates":[12.021574718956378,60.188211952839154]}}, +{"type":"Feature","properties":{"fid":132,"timestamp":"2026-05-25T13:07:04"},"geometry":{"type":"Point","coordinates":[12.021827716427524,60.188212327268836]}}, +{"type":"Feature","properties":{"fid":133,"timestamp":"2026-05-25T13:07:03"},"geometry":{"type":"Point","coordinates":[12.022158239366506,60.188217077646541]}}, +{"type":"Feature","properties":{"fid":134,"timestamp":"2026-05-25T13:07:02"},"geometry":{"type":"Point","coordinates":[12.022330092945991,60.188240809335909]}}, +{"type":"Feature","properties":{"fid":135,"timestamp":"2026-05-25T13:07:01"},"geometry":{"type":"Point","coordinates":[12.022472830275783,60.188249842791016]}}, +{"type":"Feature","properties":{"fid":136,"timestamp":"2026-05-25T13:07:00"},"geometry":{"type":"Point","coordinates":[12.022723172680562,60.18825488477367]}}, +{"type":"Feature","properties":{"fid":137,"timestamp":"2026-05-25T13:06:59"},"geometry":{"type":"Point","coordinates":[12.02318804473556,60.188278074394347]}}, +{"type":"Feature","properties":{"fid":138,"timestamp":"2026-05-25T13:06:58"},"geometry":{"type":"Point","coordinates":[12.023350548104363,60.188334278953434]}}, +{"type":"Feature","properties":{"fid":139,"timestamp":"2026-05-25T13:06:57"},"geometry":{"type":"Point","coordinates":[12.023658843219318,60.188365648761383]}}, +{"type":"Feature","properties":{"fid":140,"timestamp":"2026-05-25T13:06:56"},"geometry":{"type":"Point","coordinates":[12.023934404589278,60.188410054856192]}}, +{"type":"Feature","properties":{"fid":141,"timestamp":"2026-05-25T13:06:55"},"geometry":{"type":"Point","coordinates":[12.024284299578296,60.188491169272744]}}, +{"type":"Feature","properties":{"fid":142,"timestamp":"2026-05-25T13:06:54"},"geometry":{"type":"Point","coordinates":[12.024754566038672,60.188606404118538]}}, +{"type":"Feature","properties":{"fid":143,"timestamp":"2026-05-25T13:06:53"},"geometry":{"type":"Point","coordinates":[12.025170693326528,60.188736699665334]}}, +{"type":"Feature","properties":{"fid":144,"timestamp":"2026-05-25T13:06:52"},"geometry":{"type":"Point","coordinates":[12.025903428869627,60.188955009329419]}}, +{"type":"Feature","properties":{"fid":145,"timestamp":"2026-05-25T13:06:51"},"geometry":{"type":"Point","coordinates":[12.026576410853449,60.189160854078402]}}, +{"type":"Feature","properties":{"fid":146,"timestamp":"2026-05-25T13:06:50"},"geometry":{"type":"Point","coordinates":[12.027355260783679,60.189444160470103]}}, +{"type":"Feature","properties":{"fid":147,"timestamp":"2026-05-25T13:06:49"},"geometry":{"type":"Point","coordinates":[12.027908509915092,60.189689604208176]}}, +{"type":"Feature","properties":{"fid":148,"timestamp":"2026-05-25T13:06:48"},"geometry":{"type":"Point","coordinates":[12.028703090228282,60.190009413095055]}}, +{"type":"Feature","properties":{"fid":149,"timestamp":"2026-05-25T13:06:47"},"geometry":{"type":"Point","coordinates":[12.029307438976096,60.190407306400495]}} +] +} diff --git a/test-data/rinf-kvg/network.geojson b/test-data/rinf-kvg/network.geojson new file mode 100644 index 0000000..2cb07cd --- /dev/null +++ b/test-data/rinf-kvg/network.geojson @@ -0,0 +1,14337 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.98732, + 60.190313 + ], + [ + 11.987807, + 60.190143 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_00b6f74b-7ca0-428e-a2d2-c2f5f5452310_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.001636, + 60.187737 + ], + [ + 12.001948, + 60.187735 + ], + [ + 12.00212, + 60.187739 + ], + [ + 12.002429, + 60.187749 + ], + [ + 12.002821, + 60.187759 + ], + [ + 12.003218, + 60.18777 + ], + [ + 12.003491, + 60.187778 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.003491, + 60.187778 + ], + [ + 12.004671, + 60.187812 + ], + [ + 12.004846, + 60.187816 + ], + [ + 12.005038, + 60.187821 + ], + [ + 12.005251, + 60.187828 + ], + [ + 12.005453, + 60.187834 + ], + [ + 12.005665, + 60.187839 + ], + [ + 12.005847, + 60.187845 + ], + [ + 12.006002, + 60.187848 + ], + [ + 12.00621, + 60.187854 + ], + [ + 12.006431, + 60.18786 + ], + [ + 12.006624, + 60.187865 + ], + [ + 12.006824, + 60.18787 + ], + [ + 12.007039, + 60.187877 + ], + [ + 12.007241, + 60.187882 + ], + [ + 12.007443, + 60.187888 + ], + [ + 12.007678, + 60.187895 + ], + [ + 12.007884, + 60.187901 + ], + [ + 12.008059, + 60.187906 + ], + [ + 12.008247, + 60.18791 + ], + [ + 12.008518, + 60.187917 + ], + [ + 12.008714, + 60.187923 + ], + [ + 12.008729, + 60.187923 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.008729, + 60.187923 + ], + [ + 12.008848, + 60.187926 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.4024163" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.999886, + 60.187979 + ], + [ + 12.000071, + 60.187952 + ], + [ + 12.000299, + 60.18792 + ], + [ + 12.000495, + 60.187897 + ], + [ + 12.000674, + 60.18788 + ], + [ + 12.000842, + 60.187867 + ], + [ + 12.000993, + 60.187859 + ], + [ + 12.001216, + 60.187851 + ], + [ + 12.001477, + 60.187845 + ], + [ + 12.001716, + 60.187838 + ], + [ + 12.001979, + 60.187831 + ], + [ + 12.002259, + 60.187823 + ], + [ + 12.002527, + 60.187814 + ], + [ + 12.002774, + 60.187807 + ], + [ + 12.003196, + 60.187806 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_d7a47da7-1b60-401f-987e-1e8341962cec_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.019341, + 60.188184 + ], + [ + 12.019616, + 60.188191 + ], + [ + 12.019878, + 60.188199 + ], + [ + 12.020171, + 60.188208 + ], + [ + 12.020431, + 60.188213 + ], + [ + 12.020918, + 60.188217 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.011075, + 60.188082 + ], + [ + 12.011233, + 60.18809 + ], + [ + 12.01159, + 60.188101 + ], + [ + 12.011666, + 60.188104 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.11971913" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.011666, + 60.188104 + ], + [ + 12.01177, + 60.188107 + ], + [ + 12.011959, + 60.188112 + ], + [ + 12.012091, + 60.188117 + ], + [ + 12.012435, + 60.188128 + ], + [ + 12.012494, + 60.188129 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.18653556" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.012494, + 60.188129 + ], + [ + 12.012601, + 60.188132 + ], + [ + 12.012766, + 60.188137 + ], + [ + 12.012929, + 60.188141 + ], + [ + 12.013078, + 60.188144 + ], + [ + 12.01324, + 60.188149 + ], + [ + 12.013361, + 60.188152 + ], + [ + 12.013509, + 60.188155 + ], + [ + 12.013665, + 60.188158 + ], + [ + 12.013805, + 60.188161 + ], + [ + 12.013941, + 60.188165 + ], + [ + 12.013997, + 60.188166 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.27513392" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.013997, + 60.188166 + ], + [ + 12.014084, + 60.188169 + ], + [ + 12.014201, + 60.188171 + ], + [ + 12.014347, + 60.188175 + ], + [ + 12.014481, + 60.188178 + ], + [ + 12.014612, + 60.188182 + ], + [ + 12.014755, + 60.188185 + ], + [ + 12.014906, + 60.188189 + ], + [ + 12.015094, + 60.188194 + ], + [ + 12.015122, + 60.188195 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.42227724" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.015122, + 60.188195 + ], + [ + 12.015249, + 60.188198 + ], + [ + 12.015401, + 60.188202 + ], + [ + 12.015558, + 60.188206 + ], + [ + 12.01572, + 60.188211 + ], + [ + 12.015891, + 60.188216 + ], + [ + 12.016038, + 60.18822 + ], + [ + 12.016174, + 60.188224 + ], + [ + 12.016312, + 60.188227 + ], + [ + 12.016409, + 60.18823 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.52963664" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.016409, + 60.18823 + ], + [ + 12.016985, + 60.188248 + ], + [ + 12.017146, + 60.188251 + ], + [ + 12.017289, + 60.188255 + ], + [ + 12.017441, + 60.18826 + ], + [ + 12.017587, + 60.188265 + ], + [ + 12.017729, + 60.188269 + ], + [ + 12.017892, + 60.188274 + ], + [ + 12.018039, + 60.188279 + ], + [ + 12.018204, + 60.188283 + ], + [ + 12.018352, + 60.188287 + ], + [ + 12.018524, + 60.188293 + ], + [ + 12.018654, + 60.188297 + ], + [ + 12.018803, + 60.188299 + ], + [ + 12.018871, + 60.1883 + ], + [ + 12.01894, + 60.1883 + ], + [ + 12.019063, + 60.188297 + ], + [ + 12.019178, + 60.188293 + ], + [ + 12.019271, + 60.188288 + ], + [ + 12.019521, + 60.188268 + ], + [ + 12.0199, + 60.188245 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.007156, + 60.188002 + ], + [ + 12.007622, + 60.188026 + ], + [ + 12.007774, + 60.18803 + ], + [ + 12.007962, + 60.188035 + ], + [ + 12.008158, + 60.18804 + ], + [ + 12.008347, + 60.188045 + ], + [ + 12.008521, + 60.18805 + ], + [ + 12.00958, + 60.18808 + ], + [ + 12.00971, + 60.188083 + ], + [ + 12.009897, + 60.188089 + ], + [ + 12.010102, + 60.188094 + ], + [ + 12.010285, + 60.188101 + ], + [ + 12.010456, + 60.188105 + ], + [ + 12.01066, + 60.18811 + ], + [ + 12.01087, + 60.188111 + ], + [ + 12.011044, + 60.188106 + ], + [ + 12.01125, + 60.188101 + ], + [ + 12.011666, + 60.188104 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.011875, + 60.188011 + ], + [ + 12.012043, + 60.188017 + ], + [ + 12.012261, + 60.188022 + ], + [ + 12.012421, + 60.188026 + ], + [ + 12.012583, + 60.188031 + ], + [ + 12.012753, + 60.188036 + ], + [ + 12.012905, + 60.18804 + ], + [ + 12.013065, + 60.188044 + ], + [ + 12.013223, + 60.188048 + ], + [ + 12.013376, + 60.188053 + ], + [ + 12.013549, + 60.188058 + ], + [ + 12.013713, + 60.188063 + ], + [ + 12.013879, + 60.188068 + ], + [ + 12.014068, + 60.188073 + ], + [ + 12.01424, + 60.188078 + ], + [ + 12.014402, + 60.188083 + ], + [ + 12.014567, + 60.188088 + ], + [ + 12.014752, + 60.188092 + ], + [ + 12.014896, + 60.188097 + ], + [ + 12.015041, + 60.188101 + ], + [ + 12.015196, + 60.188105 + ], + [ + 12.015354, + 60.188109 + ], + [ + 12.015515, + 60.188114 + ], + [ + 12.015655, + 60.188118 + ], + [ + 12.015809, + 60.188121 + ], + [ + 12.015971, + 60.188125 + ], + [ + 12.016118, + 60.188129 + ], + [ + 12.01629, + 60.188134 + ], + [ + 12.016467, + 60.188139 + ], + [ + 12.016649, + 60.188144 + ], + [ + 12.01681, + 60.188149 + ], + [ + 12.016981, + 60.188154 + ], + [ + 12.017152, + 60.188159 + ], + [ + 12.017315, + 60.188164 + ], + [ + 12.017494, + 60.188169 + ], + [ + 12.017648, + 60.188173 + ], + [ + 12.01782, + 60.188179 + ], + [ + 12.018001, + 60.188184 + ], + [ + 12.018178, + 60.188187 + ], + [ + 12.018321, + 60.18819 + ], + [ + 12.018445, + 60.18819 + ], + [ + 12.018564, + 60.188189 + ], + [ + 12.018699, + 60.188186 + ], + [ + 12.018816, + 60.188181 + ], + [ + 12.018994, + 60.188179 + ], + [ + 12.019226, + 60.188182 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.983873, + 60.191901 + ], + [ + 11.984022, + 60.19179 + ], + [ + 11.984179, + 60.191677 + ], + [ + 11.984346, + 60.191569 + ], + [ + 11.984531, + 60.191454 + ], + [ + 11.984718, + 60.191348 + ], + [ + 11.984909, + 60.191245 + ], + [ + 11.985067, + 60.191163 + ], + [ + 11.985236, + 60.191085 + ], + [ + 11.985405, + 60.191007 + ], + [ + 11.985635, + 60.190908 + ], + [ + 11.985835, + 60.190824 + ], + [ + 11.985992, + 60.190763 + ], + [ + 11.986179, + 60.190691 + ], + [ + 11.986332, + 60.190633 + ], + [ + 11.98649, + 60.190576 + ], + [ + 11.986689, + 60.190505 + ], + [ + 11.986878, + 60.190439 + ], + [ + 11.987081, + 60.190371 + ], + [ + 11.987285, + 60.190304 + ], + [ + 11.987469, + 60.190245 + ], + [ + 11.987628, + 60.190198 + ], + [ + 11.987801, + 60.190145 + ], + [ + 11.987807, + 60.190143 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.987807, + 60.190143 + ], + [ + 11.987837, + 60.190134 + ], + [ + 11.988032, + 60.190078 + ], + [ + 11.988234, + 60.19002 + ], + [ + 11.988403, + 60.189974 + ], + [ + 11.988579, + 60.189928 + ], + [ + 11.988752, + 60.189884 + ], + [ + 11.988918, + 60.189841 + ], + [ + 11.989083, + 60.1898 + ], + [ + 11.989388, + 60.189731 + ], + [ + 11.989524, + 60.189698 + ], + [ + 11.989761, + 60.189646 + ], + [ + 11.989955, + 60.189605 + ], + [ + 11.990083, + 60.189579 + ], + [ + 11.990243, + 60.189547 + ], + [ + 11.990419, + 60.189512 + ], + [ + 11.990588, + 60.189481 + ], + [ + 11.99077, + 60.189448 + ], + [ + 11.990959, + 60.189414 + ], + [ + 11.99115, + 60.189382 + ], + [ + 11.991336, + 60.189351 + ], + [ + 11.991396, + 60.189342 + ], + [ + 11.991441, + 60.189335 + ], + [ + 11.991498, + 60.189327 + ], + [ + 11.991684, + 60.189299 + ], + [ + 11.991858, + 60.189274 + ], + [ + 11.992075, + 60.189244 + ], + [ + 11.992295, + 60.189215 + ], + [ + 11.99248, + 60.189193 + ], + [ + 11.992668, + 60.189171 + ], + [ + 11.992899, + 60.189145 + ], + [ + 11.993119, + 60.189121 + ], + [ + 11.993306, + 60.189101 + ], + [ + 11.993546, + 60.189077 + ], + [ + 11.993759, + 60.189055 + ], + [ + 11.99398, + 60.189032 + ], + [ + 11.994244, + 60.189006 + ], + [ + 11.994503, + 60.188979 + ], + [ + 11.994718, + 60.188958 + ], + [ + 11.994952, + 60.188934 + ], + [ + 11.995164, + 60.188911 + ], + [ + 11.995395, + 60.188885 + ], + [ + 11.995617, + 60.188859 + ], + [ + 11.995854, + 60.188826 + ], + [ + 11.996005, + 60.188802 + ], + [ + 11.996231, + 60.188766 + ], + [ + 11.996321, + 60.18875 + ], + [ + 11.99639, + 60.188738 + ], + [ + 11.996609, + 60.188695 + ], + [ + 11.996698, + 60.188677 + ], + [ + 11.996872, + 60.188638 + ], + [ + 11.997065, + 60.188591 + ], + [ + 11.997266, + 60.188539 + ], + [ + 11.997509, + 60.188472 + ], + [ + 11.997798, + 60.188389 + ], + [ + 11.998059, + 60.188314 + ], + [ + 11.998286, + 60.188249 + ], + [ + 11.998477, + 60.188199 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.998477, + 60.188199 + ], + [ + 11.998855, + 60.188101 + ], + [ + 11.999227, + 60.188013 + ], + [ + 11.999239, + 60.18801 + ], + [ + 11.999288, + 60.187999 + ], + [ + 11.999545, + 60.187942 + ], + [ + 11.99962, + 60.187925 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.99962, + 60.187925 + ], + [ + 11.999697, + 60.187909 + ], + [ + 11.999912, + 60.187865 + ], + [ + 12.000137, + 60.187821 + ], + [ + 12.000271, + 60.187797 + ], + [ + 12.000487, + 60.187758 + ], + [ + 12.000718, + 60.187718 + ], + [ + 12.000936, + 60.187685 + ], + [ + 12.001059, + 60.187667 + ], + [ + 12.001176, + 60.187653 + ], + [ + 12.001393, + 60.187629 + ], + [ + 12.001612, + 60.18761 + ], + [ + 12.001833, + 60.187596 + ], + [ + 12.002055, + 60.187588 + ], + [ + 12.002278, + 60.187585 + ], + [ + 12.002375, + 60.187585 + ], + [ + 12.002548, + 60.187587 + ], + [ + 12.002669, + 60.187589 + ], + [ + 12.003092, + 60.187601 + ], + [ + 12.003589, + 60.187615 + ], + [ + 12.003776, + 60.18762 + ], + [ + 12.003978, + 60.187627 + ], + [ + 12.004141, + 60.18763 + ], + [ + 12.004295, + 60.187634 + ], + [ + 12.004477, + 60.187639 + ], + [ + 12.004663, + 60.187645 + ], + [ + 12.004869, + 60.187651 + ], + [ + 12.00506, + 60.187656 + ], + [ + 12.005256, + 60.187661 + ], + [ + 12.005454, + 60.187666 + ], + [ + 12.005655, + 60.187672 + ], + [ + 12.005842, + 60.187677 + ], + [ + 12.006039, + 60.187683 + ], + [ + 12.006216, + 60.187688 + ], + [ + 12.00642, + 60.187693 + ], + [ + 12.006632, + 60.187699 + ], + [ + 12.006861, + 60.187705 + ], + [ + 12.00706, + 60.187711 + ], + [ + 12.007271, + 60.187717 + ], + [ + 12.007283, + 60.187717 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.3813903" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.007283, + 60.187717 + ], + [ + 12.007286, + 60.187717 + ], + [ + 12.007312, + 60.187718 + ], + [ + 12.007462, + 60.187724 + ], + [ + 12.007629, + 60.187729 + ], + [ + 12.007784, + 60.187733 + ], + [ + 12.007957, + 60.187737 + ], + [ + 12.008162, + 60.187743 + ], + [ + 12.008359, + 60.187749 + ], + [ + 12.008562, + 60.187754 + ], + [ + 12.008767, + 60.18776 + ], + [ + 12.00896, + 60.187765 + ], + [ + 12.009146, + 60.187771 + ], + [ + 12.009323, + 60.187777 + ], + [ + 12.009506, + 60.187781 + ], + [ + 12.009693, + 60.187786 + ], + [ + 12.009879, + 60.187791 + ], + [ + 12.010077, + 60.187797 + ], + [ + 12.010281, + 60.187803 + ], + [ + 12.010464, + 60.187808 + ], + [ + 12.010639, + 60.187814 + ], + [ + 12.010805, + 60.187819 + ], + [ + 12.01099, + 60.187823 + ], + [ + 12.01118, + 60.187828 + ], + [ + 12.011391, + 60.187834 + ], + [ + 12.011616, + 60.187841 + ], + [ + 12.011838, + 60.187848 + ], + [ + 12.012083, + 60.187855 + ], + [ + 12.01229, + 60.187861 + ], + [ + 12.012474, + 60.187866 + ], + [ + 12.012657, + 60.187872 + ], + [ + 12.012827, + 60.187877 + ], + [ + 12.01299, + 60.187882 + ], + [ + 12.013148, + 60.187886 + ], + [ + 12.013327, + 60.187891 + ], + [ + 12.013505, + 60.187897 + ], + [ + 12.013692, + 60.187902 + ], + [ + 12.013892, + 60.187907 + ], + [ + 12.014078, + 60.187913 + ], + [ + 12.014234, + 60.187917 + ], + [ + 12.014405, + 60.187922 + ], + [ + 12.014587, + 60.187927 + ], + [ + 12.014796, + 60.187934 + ], + [ + 12.014965, + 60.187938 + ], + [ + 12.015159, + 60.187944 + ], + [ + 12.01536, + 60.18795 + ], + [ + 12.015553, + 60.187955 + ], + [ + 12.015743, + 60.187961 + ], + [ + 12.01593, + 60.187966 + ], + [ + 12.016137, + 60.187973 + ], + [ + 12.016319, + 60.187978 + ], + [ + 12.016489, + 60.187983 + ], + [ + 12.01667, + 60.187988 + ], + [ + 12.01768, + 60.188017 + ], + [ + 12.017692, + 60.188018 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.017692, + 60.188018 + ], + [ + 12.018106, + 60.18803 + ], + [ + 12.018289, + 60.188038 + ], + [ + 12.018479, + 60.188049 + ], + [ + 12.018645, + 60.188062 + ], + [ + 12.018843, + 60.188076 + ], + [ + 12.018998, + 60.188086 + ], + [ + 12.019151, + 60.188094 + ], + [ + 12.019295, + 60.1881 + ], + [ + 12.019471, + 60.188106 + ], + [ + 12.019531, + 60.188108 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.76414609" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.019531, + 60.188108 + ], + [ + 12.019827, + 60.188116 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.79667018" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.019827, + 60.188116 + ], + [ + 12.021431, + 60.188161 + ], + [ + 12.021619, + 60.188166 + ], + [ + 12.022497, + 60.188191 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.022497, + 60.188191 + ], + [ + 12.022824, + 60.1882 + ], + [ + 12.023241, + 60.188211 + ], + [ + 12.023498, + 60.188218 + ], + [ + 12.023713, + 60.188223 + ], + [ + 12.023904, + 60.188227 + ], + [ + 12.024453, + 60.188235 + ], + [ + 12.024865, + 60.188237 + ], + [ + 12.025002, + 60.188237 + ], + [ + 12.025304, + 60.188237 + ], + [ + 12.025551, + 60.188235 + ], + [ + 12.0261, + 60.188228 + ], + [ + 12.026649, + 60.188215 + ], + [ + 12.026818, + 60.18821 + ], + [ + 12.026997, + 60.188205 + ], + [ + 12.027189, + 60.188198 + ], + [ + 12.027876, + 60.188172 + ], + [ + 12.028562, + 60.188139 + ], + [ + 12.028797, + 60.188127 + ], + [ + 12.028951, + 60.188118 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.86330064" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.986026, + 60.191262 + ], + [ + 11.986089, + 60.191212 + ], + [ + 11.9862, + 60.191091 + ], + [ + 11.986338, + 60.19093 + ], + [ + 11.9864, + 60.190867 + ], + [ + 11.986464, + 60.190808 + ], + [ + 11.986605, + 60.190703 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.986605, + 60.190703 + ], + [ + 11.986671, + 60.190657 + ], + [ + 11.986774, + 60.190592 + ], + [ + 11.986879, + 60.190532 + ], + [ + 11.986965, + 60.190481 + ], + [ + 11.98732, + 60.190313 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.543595" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.006005, + 60.187946 + ], + [ + 12.00635, + 60.188005 + ], + [ + 12.006504, + 60.188038 + ], + [ + 12.006661, + 60.188075 + ], + [ + 12.00682, + 60.188114 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.00682, + 60.188114 + ], + [ + 12.007166, + 60.188198 + ], + [ + 12.007346, + 60.188238 + ], + [ + 12.007464, + 60.188259 + ], + [ + 12.007636, + 60.18829 + ], + [ + 12.007785, + 60.188313 + ], + [ + 12.007971, + 60.188339 + ], + [ + 12.008127, + 60.188356 + ], + [ + 12.008351, + 60.188373 + ], + [ + 12.00848, + 60.188379 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.14593799" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.00848, + 60.188379 + ], + [ + 12.008736, + 60.188384 + ], + [ + 12.00886, + 60.188384 + ], + [ + 12.00902, + 60.188382 + ], + [ + 12.009263, + 60.188378 + ], + [ + 12.009479, + 60.188377 + ], + [ + 12.0097, + 60.188374 + ], + [ + 12.009886, + 60.188368 + ], + [ + 12.01024, + 60.18836 + ], + [ + 12.010634, + 60.188341 + ], + [ + 12.011042, + 60.188315 + ], + [ + 12.011401, + 60.18828 + ], + [ + 12.011794, + 60.18824 + ], + [ + 12.012019, + 60.188216 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.40408531" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.020419, + 60.188265 + ], + [ + 12.020764, + 60.188303 + ], + [ + 12.020971, + 60.188333 + ], + [ + 12.021138, + 60.188363 + ], + [ + 12.021303, + 60.188396 + ], + [ + 12.021457, + 60.18843 + ], + [ + 12.021643, + 60.188475 + ], + [ + 12.021784, + 60.188513 + ], + [ + 12.021905, + 60.18855 + ], + [ + 12.022031, + 60.188591 + ], + [ + 12.022121, + 60.188624 + ], + [ + 12.022207, + 60.188658 + ], + [ + 12.02231, + 60.188702 + ], + [ + 12.022427, + 60.188757 + ], + [ + 12.022604, + 60.18884 + ], + [ + 12.022724, + 60.188898 + ], + [ + 12.022905, + 60.18898 + ], + [ + 12.02307, + 60.189059 + ], + [ + 12.023249, + 60.189143 + ], + [ + 12.023459, + 60.189241 + ], + [ + 12.023721, + 60.189365 + ], + [ + 12.023924, + 60.189459 + ], + [ + 12.024168, + 60.189574 + ], + [ + 12.024367, + 60.189667 + ], + [ + 12.024564, + 60.189759 + ], + [ + 12.02477, + 60.189856 + ], + [ + 12.025011, + 60.189969 + ], + [ + 12.025295, + 60.190102 + ], + [ + 12.025502, + 60.190201 + ], + [ + 12.02568, + 60.190286 + ], + [ + 12.025848, + 60.190363 + ], + [ + 12.026063, + 60.190464 + ], + [ + 12.026178, + 60.190519 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_14050ace-8df0-4dff-bae1-787efbda8200_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.000639, + 60.187817 + ], + [ + 12.001043, + 60.187789 + ], + [ + 12.001228, + 60.187783 + ], + [ + 12.001424, + 60.187778 + ], + [ + 12.001698, + 60.187776 + ], + [ + 12.001945, + 60.187776 + ], + [ + 12.002201, + 60.187779 + ], + [ + 12.002421, + 60.187785 + ], + [ + 12.002726, + 60.187793 + ], + [ + 12.003196, + 60.187806 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.003196, + 60.187806 + ], + [ + 12.003282, + 60.187808 + ], + [ + 12.003611, + 60.187818 + ], + [ + 12.003795, + 60.187823 + ], + [ + 12.003974, + 60.187827 + ], + [ + 12.004177, + 60.187832 + ], + [ + 12.004518, + 60.187842 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.12461327" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.005523, + 60.18787 + ], + [ + 12.005536, + 60.18787 + ], + [ + 12.005586, + 60.187872 + ], + [ + 12.005928, + 60.187882 + ], + [ + 12.006119, + 60.187886 + ], + [ + 12.006311, + 60.187892 + ], + [ + 12.006505, + 60.187898 + ], + [ + 12.006716, + 60.187903 + ], + [ + 12.006891, + 60.187908 + ], + [ + 12.007132, + 60.187915 + ], + [ + 12.007318, + 60.18792 + ], + [ + 12.007487, + 60.187925 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.24348413" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.007487, + 60.187925 + ], + [ + 12.00752, + 60.187926 + ], + [ + 12.007864, + 60.187936 + ], + [ + 12.008028, + 60.187941 + ], + [ + 12.00821, + 60.187946 + ], + [ + 12.008422, + 60.187952 + ], + [ + 12.008614, + 60.187956 + ], + [ + 12.008812, + 60.187961 + ], + [ + 12.00901, + 60.187967 + ], + [ + 12.009207, + 60.187972 + ], + [ + 12.009393, + 60.187978 + ], + [ + 12.009597, + 60.187983 + ], + [ + 12.009807, + 60.187989 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.33732172" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.009807, + 60.187989 + ], + [ + 12.009971, + 60.187995 + ], + [ + 12.010178, + 60.188 + ], + [ + 12.010345, + 60.188004 + ], + [ + 12.010572, + 60.18801 + ], + [ + 12.010795, + 60.188016 + ], + [ + 12.011019, + 60.188023 + ], + [ + 12.011242, + 60.188031 + ], + [ + 12.011449, + 60.18804 + ], + [ + 12.011623, + 60.188047 + ], + [ + 12.011824, + 60.188057 + ], + [ + 12.011964, + 60.188062 + ], + [ + 12.012162, + 60.188069 + ], + [ + 12.012337, + 60.188075 + ], + [ + 12.01251, + 60.188081 + ], + [ + 12.012682, + 60.188087 + ], + [ + 12.01276, + 60.188089 + ], + [ + 12.012838, + 60.188091 + ], + [ + 12.01298, + 60.188094 + ], + [ + 12.012992, + 60.188094 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.012992, + 60.188094 + ], + [ + 12.013107, + 60.188098 + ], + [ + 12.013278, + 60.188102 + ], + [ + 12.013435, + 60.188106 + ], + [ + 12.013597, + 60.188111 + ], + [ + 12.013748, + 60.188115 + ], + [ + 12.013814, + 60.188117 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.60869272" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.013814, + 60.188117 + ], + [ + 12.013908, + 60.18812 + ], + [ + 12.014044, + 60.188125 + ], + [ + 12.01421, + 60.18813 + ], + [ + 12.014385, + 60.188135 + ], + [ + 12.014561, + 60.18814 + ], + [ + 12.014743, + 60.188145 + ], + [ + 12.01491, + 60.18815 + ], + [ + 12.01506, + 60.188155 + ], + [ + 12.015217, + 60.188158 + ], + [ + 12.015371, + 60.188162 + ], + [ + 12.01556, + 60.188166 + ], + [ + 12.015732, + 60.188172 + ], + [ + 12.01589, + 60.188177 + ], + [ + 12.01604, + 60.188181 + ], + [ + 12.01621, + 60.188185 + ], + [ + 12.016387, + 60.18819 + ], + [ + 12.01657, + 60.188195 + ], + [ + 12.01674, + 60.1882 + ], + [ + 12.016904, + 60.188204 + ], + [ + 12.01707, + 60.188209 + ], + [ + 12.017245, + 60.188214 + ], + [ + 12.01743, + 60.18822 + ], + [ + 12.017618, + 60.188225 + ], + [ + 12.017775, + 60.18823 + ], + [ + 12.017975, + 60.188234 + ], + [ + 12.018167, + 60.18824 + ], + [ + 12.018341, + 60.188245 + ], + [ + 12.018517, + 60.18825 + ], + [ + 12.018689, + 60.188254 + ], + [ + 12.018878, + 60.18826 + ], + [ + 12.019045, + 60.188263 + ], + [ + 12.019175, + 60.188263 + ], + [ + 12.019273, + 60.188262 + ], + [ + 12.019474, + 60.188257 + ], + [ + 12.019639, + 60.188254 + ], + [ + 12.0199, + 60.188245 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.0199, + 60.188245 + ], + [ + 12.019921, + 60.188244 + ], + [ + 12.020108, + 60.188238 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.95000637" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.020108, + 60.188238 + ], + [ + 12.020364, + 60.188232 + ], + [ + 12.020519, + 60.188228 + ], + [ + 12.020918, + 60.188217 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.985786, + 60.191172 + ], + [ + 11.985941, + 60.191049 + ], + [ + 11.986133, + 60.190906 + ], + [ + 11.986232, + 60.190838 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.986232, + 60.190838 + ], + [ + 11.986311, + 60.190786 + ], + [ + 11.986409, + 60.190726 + ], + [ + 11.986504, + 60.190671 + ], + [ + 11.986603, + 60.190619 + ], + [ + 11.986714, + 60.190565 + ], + [ + 11.986843, + 60.190511 + ], + [ + 11.987005, + 60.190439 + ], + [ + 11.98732, + 60.190313 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.34610038" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.02146, + 60.188203 + ], + [ + 12.021727, + 60.188195 + ], + [ + 12.021924, + 60.18819 + ], + [ + 12.022149, + 60.188186 + ], + [ + 12.022497, + 60.188191 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.897492, + 60.201131 + ], + [ + 11.89919, + 60.20173 + ], + [ + 11.900005, + 60.202018 + ], + [ + 11.903583, + 60.20328 + ], + [ + 11.904617, + 60.203645 + ], + [ + 11.905326, + 60.203895 + ], + [ + 11.906232, + 60.204214 + ], + [ + 11.906564, + 60.204331 + ], + [ + 11.906574, + 60.204335 + ], + [ + 11.906787, + 60.204409 + ], + [ + 11.906964, + 60.20447 + ], + [ + 11.907123, + 60.204523 + ], + [ + 11.90727, + 60.204572 + ], + [ + 11.907705, + 60.20471 + ], + [ + 11.908149, + 60.204843 + ], + [ + 11.908599, + 60.20497 + ], + [ + 11.909057, + 60.20509 + ], + [ + 11.909388, + 60.205172 + ], + [ + 11.909521, + 60.205204 + ], + [ + 11.909991, + 60.205312 + ], + [ + 11.91033, + 60.205385 + ], + [ + 11.910467, + 60.205413 + ], + [ + 11.910948, + 60.205507 + ], + [ + 11.911434, + 60.205595 + ], + [ + 11.911925, + 60.205676 + ], + [ + 11.91242, + 60.205751 + ], + [ + 11.912701, + 60.20579 + ], + [ + 11.912958, + 60.205824 + ], + [ + 11.913199, + 60.205855 + ], + [ + 11.913425, + 60.205883 + ], + [ + 11.913641, + 60.20591 + ], + [ + 11.915104, + 60.206093 + ], + [ + 11.915968, + 60.206201 + ], + [ + 11.923611, + 60.207154 + ], + [ + 11.923949, + 60.207196 + ], + [ + 11.924157, + 60.207222 + ], + [ + 11.924331, + 60.207243 + ], + [ + 11.924486, + 60.207261 + ], + [ + 11.925256, + 60.20735 + ], + [ + 11.926028, + 60.207432 + ], + [ + 11.926804, + 60.207508 + ], + [ + 11.927581, + 60.207577 + ], + [ + 11.928361, + 60.207639 + ], + [ + 11.929143, + 60.207695 + ], + [ + 11.929927, + 60.207744 + ], + [ + 11.930713, + 60.207786 + ], + [ + 11.931499, + 60.207822 + ], + [ + 11.931756, + 60.207832 + ], + [ + 11.932287, + 60.207851 + ], + [ + 11.933076, + 60.207874 + ], + [ + 11.933093, + 60.207874 + ], + [ + 11.933865, + 60.20789 + ], + [ + 11.934655, + 60.207899 + ], + [ + 11.935445, + 60.207902 + ], + [ + 11.93575, + 60.207901 + ], + [ + 11.936235, + 60.207897 + ], + [ + 11.937025, + 60.207886 + ], + [ + 11.937814, + 60.207869 + ], + [ + 11.938056, + 60.207862 + ], + [ + 11.938296, + 60.207855 + ], + [ + 11.938534, + 60.207847 + ], + [ + 11.939289, + 60.207818 + ], + [ + 11.940044, + 60.207782 + ], + [ + 11.940797, + 60.20774 + ], + [ + 11.941548, + 60.207691 + ], + [ + 11.942298, + 60.207635 + ], + [ + 11.942399, + 60.207627 + ], + [ + 11.9428, + 60.207595 + ], + [ + 11.943045, + 60.207574 + ], + [ + 11.94379, + 60.207505 + ], + [ + 11.944501, + 60.207434 + ], + [ + 11.944533, + 60.207431 + ], + [ + 11.944757, + 60.207407 + ], + [ + 11.944978, + 60.207383 + ], + [ + 11.945197, + 60.207358 + ], + [ + 11.945413, + 60.207333 + ], + [ + 11.945938, + 60.20727 + ], + [ + 11.946101, + 60.20725 + ], + [ + 11.946328, + 60.207221 + ], + [ + 11.946678, + 60.207175 + ], + [ + 11.946786, + 60.207161 + ], + [ + 11.947467, + 60.207066 + ], + [ + 11.947889, + 60.207004 + ], + [ + 11.948145, + 60.206965 + ], + [ + 11.94882, + 60.206858 + ], + [ + 11.94949, + 60.206745 + ], + [ + 11.950157, + 60.206627 + ], + [ + 11.950819, + 60.206502 + ], + [ + 11.951477, + 60.206372 + ], + [ + 11.951614, + 60.206344 + ], + [ + 11.951753, + 60.206316 + ], + [ + 11.951893, + 60.206286 + ], + [ + 11.952218, + 60.206218 + ], + [ + 11.95257, + 60.206142 + ], + [ + 11.953242, + 60.205991 + ], + [ + 11.953308, + 60.205976 + ], + [ + 11.953909, + 60.205834 + ], + [ + 11.954571, + 60.205672 + ], + [ + 11.955226, + 60.205505 + ], + [ + 11.955876, + 60.205332 + ], + [ + 11.95601, + 60.205295 + ], + [ + 11.956141, + 60.205259 + ], + [ + 11.95627, + 60.205223 + ], + [ + 11.956918, + 60.205038 + ], + [ + 11.957559, + 60.204847 + ], + [ + 11.958193, + 60.204651 + ], + [ + 11.958819, + 60.204448 + ], + [ + 11.959437, + 60.204239 + ], + [ + 11.960047, + 60.204025 + ], + [ + 11.960649, + 60.203805 + ], + [ + 11.961242, + 60.203579 + ], + [ + 11.961827, + 60.203348 + ], + [ + 11.962069, + 60.20325 + ], + [ + 11.962285, + 60.203161 + ], + [ + 11.962484, + 60.203079 + ], + [ + 11.962671, + 60.203001 + ], + [ + 11.964602, + 60.2022 + ], + [ + 11.967111, + 60.20116 + ], + [ + 11.967484, + 60.201005 + ], + [ + 11.970581, + 60.19972 + ], + [ + 11.977317, + 60.196926 + ], + [ + 11.977681, + 60.196775 + ], + [ + 11.978048, + 60.196622 + ], + [ + 11.978274, + 60.196526 + ], + [ + 11.97846, + 60.196446 + ], + [ + 11.978624, + 60.196373 + ], + [ + 11.978772, + 60.196306 + ], + [ + 11.979088, + 60.196156 + ], + [ + 11.979395, + 60.196001 + ], + [ + 11.979407, + 60.195995 + ], + [ + 11.979692, + 60.195842 + ], + [ + 11.979937, + 60.195702 + ], + [ + 11.979978, + 60.195678 + ], + [ + 11.980255, + 60.195509 + ], + [ + 11.98052, + 60.195337 + ], + [ + 11.980775, + 60.19516 + ], + [ + 11.981019, + 60.19498 + ], + [ + 11.981251, + 60.194796 + ], + [ + 11.981472, + 60.194608 + ], + [ + 11.981681, + 60.194417 + ], + [ + 11.981878, + 60.194223 + ], + [ + 11.981964, + 60.194134 + ], + [ + 11.982062, + 60.194027 + ], + [ + 11.982235, + 60.193827 + ], + [ + 11.982358, + 60.193673 + ], + [ + 11.982466, + 60.193531 + ], + [ + 11.982563, + 60.193399 + ], + [ + 11.982653, + 60.193273 + ], + [ + 11.98271, + 60.193193 + ], + [ + 11.982738, + 60.193154 + ], + [ + 11.982876, + 60.19296 + ], + [ + 11.982903, + 60.192923 + ], + [ + 11.983009, + 60.192781 + ], + [ + 11.983101, + 60.192663 + ], + [ + 11.983124, + 60.192633 + ], + [ + 11.983186, + 60.192559 + ], + [ + 11.983268, + 60.192464 + ], + [ + 11.98331, + 60.192417 + ], + [ + 11.983347, + 60.192377 + ], + [ + 11.983489, + 60.192234 + ], + [ + 11.983641, + 60.192093 + ], + [ + 11.983804, + 60.191956 + ], + [ + 11.983856, + 60.191914 + ], + [ + 11.983873, + 60.191901 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_1b975e4c-b7ee-4386-b8b2-425e1b24e386_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.009807, + 60.187989 + ], + [ + 12.010141, + 60.188008 + ], + [ + 12.010319, + 60.188024 + ], + [ + 12.01049, + 60.188039 + ], + [ + 12.010642, + 60.188053 + ], + [ + 12.01079, + 60.188066 + ], + [ + 12.011075, + 60.188082 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.008608, + 60.187797 + ], + [ + 12.008946, + 60.187817 + ], + [ + 12.009102, + 60.18783 + ], + [ + 12.009264, + 60.187845 + ], + [ + 12.009749, + 60.187875 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.011062, + 60.187954 + ], + [ + 12.01108, + 60.187954 + ], + [ + 12.011353, + 60.187961 + ], + [ + 12.011562, + 60.187966 + ], + [ + 12.011739, + 60.187971 + ], + [ + 12.011952, + 60.187977 + ], + [ + 12.012137, + 60.187983 + ], + [ + 12.012321, + 60.187989 + ], + [ + 12.012513, + 60.187994 + ], + [ + 12.012678, + 60.187998 + ], + [ + 12.012841, + 60.188003 + ], + [ + 12.013008, + 60.188008 + ], + [ + 12.013171, + 60.188012 + ], + [ + 12.013338, + 60.188018 + ], + [ + 12.01351, + 60.188021 + ], + [ + 12.013661, + 60.188026 + ], + [ + 12.013816, + 60.18803 + ], + [ + 12.013981, + 60.188035 + ], + [ + 12.014141, + 60.188039 + ], + [ + 12.0143, + 60.188044 + ], + [ + 12.014478, + 60.188049 + ], + [ + 12.014638, + 60.188054 + ], + [ + 12.014802, + 60.188057 + ], + [ + 12.01496, + 60.188062 + ], + [ + 12.015095, + 60.188066 + ], + [ + 12.01524, + 60.18807 + ], + [ + 12.015406, + 60.188074 + ], + [ + 12.015566, + 60.188079 + ], + [ + 12.01573, + 60.188084 + ], + [ + 12.015895, + 60.188089 + ], + [ + 12.016069, + 60.188093 + ], + [ + 12.01624, + 60.188098 + ], + [ + 12.016414, + 60.188102 + ], + [ + 12.016551, + 60.188106 + ], + [ + 12.016701, + 60.18811 + ], + [ + 12.016867, + 60.188115 + ], + [ + 12.016998, + 60.188119 + ], + [ + 12.017149, + 60.188123 + ], + [ + 12.017297, + 60.188127 + ], + [ + 12.017455, + 60.188132 + ], + [ + 12.017594, + 60.188136 + ], + [ + 12.017749, + 60.18814 + ], + [ + 12.017883, + 60.188144 + ], + [ + 12.018044, + 60.188148 + ], + [ + 12.018194, + 60.188153 + ], + [ + 12.018355, + 60.188157 + ], + [ + 12.018505, + 60.188161 + ], + [ + 12.018658, + 60.188165 + ], + [ + 12.018803, + 60.188169 + ], + [ + 12.019226, + 60.188182 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.999816, + 60.188037 + ], + [ + 12.000024, + 60.188004 + ], + [ + 12.000236, + 60.187973 + ], + [ + 12.000373, + 60.187957 + ], + [ + 12.000419, + 60.187952 + ], + [ + 12.000553, + 60.187938 + ], + [ + 12.000713, + 60.187923 + ], + [ + 12.000779, + 60.187919 + ], + [ + 12.000905, + 60.18791 + ], + [ + 12.001025, + 60.187902 + ], + [ + 12.001241, + 60.187893 + ], + [ + 12.001448, + 60.187888 + ], + [ + 12.001652, + 60.187887 + ], + [ + 12.001862, + 60.18789 + ], + [ + 12.00215, + 60.187888 + ], + [ + 12.002416, + 60.187889 + ], + [ + 12.002715, + 60.18789 + ], + [ + 12.003045, + 60.18789 + ], + [ + 12.003347, + 60.187889 + ], + [ + 12.003522, + 60.187887 + ], + [ + 12.003897, + 60.187877 + ], + [ + 12.004065, + 60.187872 + ], + [ + 12.004244, + 60.187867 + ], + [ + 12.004421, + 60.187861 + ], + [ + 12.004594, + 60.187857 + ], + [ + 12.00501, + 60.187855 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_956249db-173f-4328-b790-e8fec5844933_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.020108, + 60.188238 + ], + [ + 12.020375, + 60.188261 + ], + [ + 12.020419, + 60.188265 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.08950671" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.009739, + 60.187914 + ], + [ + 12.009845, + 60.187917 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.46861837" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.016677, + 60.187951 + ], + [ + 12.01715, + 60.187963 + ], + [ + 12.01734, + 60.187968 + ], + [ + 12.017519, + 60.187973 + ], + [ + 12.017686, + 60.187978 + ], + [ + 12.017846, + 60.187983 + ], + [ + 12.018006, + 60.187988 + ], + [ + 12.018192, + 60.187992 + ], + [ + 12.018351, + 60.187997 + ], + [ + 12.018519, + 60.188001 + ], + [ + 12.018669, + 60.188005 + ], + [ + 12.018842, + 60.18801 + ], + [ + 12.019012, + 60.188015 + ], + [ + 12.019142, + 60.188019 + ], + [ + 12.019278, + 60.188023 + ], + [ + 12.019558, + 60.18803 + ], + [ + 12.0197, + 60.188034 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_a54d0a9c-d1f1-4e14-8528-6042a2967135_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.008602, + 60.187798 + ], + [ + 12.008608, + 60.187797 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.4553507" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.009845, + 60.187917 + ], + [ + 12.009865, + 60.187918 + ], + [ + 12.009883, + 60.187918 + ], + [ + 12.01029, + 60.18793 + ], + [ + 12.010473, + 60.187935 + ], + [ + 12.010886, + 60.187949 + ], + [ + 12.011062, + 60.187954 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.47492909" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.020939, + 60.188189 + ], + [ + 12.020941, + 60.188189 + ], + [ + 12.02122, + 60.188197 + ], + [ + 12.021423, + 60.188201 + ], + [ + 12.02146, + 60.188203 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.006414, + 60.187937 + ], + [ + 12.006782, + 60.187968 + ], + [ + 12.006959, + 60.187984 + ], + [ + 12.007124, + 60.187999 + ], + [ + 12.007156, + 60.188002 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_4626ff0b-5193-4f4a-847f-936894bcdfc6_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.00501, + 60.187855 + ], + [ + 12.00537, + 60.187866 + ], + [ + 12.005523, + 60.18787 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_49dc2660-09f0-41e1-82f9-f683d464847d_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.005523, + 60.18787 + ], + [ + 12.005848, + 60.187887 + ], + [ + 12.005992, + 60.1879 + ], + [ + 12.006135, + 60.187913 + ], + [ + 12.006287, + 60.187926 + ], + [ + 12.006414, + 60.187937 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_c0220d76-9945-458d-b27f-f06dd10ec53c_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.012228, + 60.187904 + ], + [ + 12.019531, + 60.188108 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.007156, + 60.188002 + ], + [ + 12.007608, + 60.188041 + ], + [ + 12.007787, + 60.188058 + ], + [ + 12.007958, + 60.188071 + ], + [ + 12.008115, + 60.188083 + ], + [ + 12.008258, + 60.188089 + ], + [ + 12.008408, + 60.188094 + ], + [ + 12.008529, + 60.188096 + ], + [ + 12.009576, + 60.188127 + ], + [ + 12.010092, + 60.188143 + ], + [ + 12.010268, + 60.188148 + ], + [ + 12.01059, + 60.188152 + ], + [ + 12.010703, + 60.188153 + ], + [ + 12.010852, + 60.188154 + ], + [ + 12.011036, + 60.188153 + ], + [ + 12.011156, + 60.188152 + ], + [ + 12.011322, + 60.188149 + ], + [ + 12.011468, + 60.188147 + ], + [ + 12.011704, + 60.188141 + ], + [ + 12.011864, + 60.188137 + ], + [ + 12.01203, + 60.188131 + ], + [ + 12.012176, + 60.188127 + ], + [ + 12.012494, + 60.188129 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.019341, + 60.188184 + ], + [ + 12.019796, + 60.188213 + ], + [ + 12.020108, + 60.188238 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.020419, + 60.188265 + ], + [ + 12.020858, + 60.188301 + ], + [ + 12.02096, + 60.18831 + ], + [ + 12.021088, + 60.188323 + ], + [ + 12.021202, + 60.188338 + ], + [ + 12.021335, + 60.18836 + ], + [ + 12.021464, + 60.188382 + ], + [ + 12.021584, + 60.188404 + ], + [ + 12.021707, + 60.188432 + ], + [ + 12.021857, + 60.188472 + ], + [ + 12.021998, + 60.188513 + ], + [ + 12.022128, + 60.188555 + ], + [ + 12.022277, + 60.188612 + ], + [ + 12.022375, + 60.188651 + ], + [ + 12.022506, + 60.188711 + ], + [ + 12.022687, + 60.188795 + ], + [ + 12.022897, + 60.188893 + ], + [ + 12.023101, + 60.18899 + ], + [ + 12.023336, + 60.1891 + ], + [ + 12.023511, + 60.189182 + ], + [ + 12.023751, + 60.189294 + ], + [ + 12.023975, + 60.1894 + ], + [ + 12.024206, + 60.189507 + ], + [ + 12.024385, + 60.189592 + ], + [ + 12.024538, + 60.189663 + ], + [ + 12.024703, + 60.18974 + ], + [ + 12.024886, + 60.189826 + ], + [ + 12.025048, + 60.189903 + ], + [ + 12.025237, + 60.189992 + ], + [ + 12.025391, + 60.190063 + ], + [ + 12.025527, + 60.190127 + ], + [ + 12.025654, + 60.190187 + ], + [ + 12.025811, + 60.19026 + ], + [ + 12.025966, + 60.190333 + ], + [ + 12.026118, + 60.190403 + ], + [ + 12.026258, + 60.190469 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.12767833" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.00848, + 60.188379 + ], + [ + 12.008937, + 60.188401 + ], + [ + 12.009244, + 60.188416 + ], + [ + 12.009451, + 60.188423 + ], + [ + 12.009679, + 60.188427 + ], + [ + 12.009928, + 60.188429 + ], + [ + 12.010119, + 60.188426 + ], + [ + 12.010305, + 60.188422 + ], + [ + 12.01059, + 60.18841 + ], + [ + 12.010838, + 60.188394 + ], + [ + 12.010962, + 60.188386 + ], + [ + 12.011516, + 60.188329 + ], + [ + 12.012353, + 60.188243 + ], + [ + 12.012606, + 60.188215 + ], + [ + 12.012845, + 60.188192 + ], + [ + 12.013035, + 60.188182 + ], + [ + 12.01324, + 60.188174 + ], + [ + 12.013547, + 60.188166 + ], + [ + 12.013997, + 60.188166 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.017311, + 60.188297 + ], + [ + 12.01773, + 60.188352 + ], + [ + 12.017858, + 60.188368 + ], + [ + 12.018003, + 60.188387 + ], + [ + 12.018157, + 60.188408 + ], + [ + 12.018308, + 60.188428 + ], + [ + 12.018466, + 60.18845 + ], + [ + 12.01862, + 60.188472 + ], + [ + 12.018762, + 60.188492 + ], + [ + 12.018905, + 60.188512 + ], + [ + 12.019039, + 60.188532 + ], + [ + 12.019167, + 60.188549 + ], + [ + 12.019272, + 60.188564 + ], + [ + 12.019526, + 60.188602 + ], + [ + 12.019752, + 60.188636 + ], + [ + 12.019949, + 60.188666 + ], + [ + 12.020176, + 60.188701 + ], + [ + 12.0203, + 60.18872 + ], + [ + 12.020739, + 60.188786 + ], + [ + 12.020957, + 60.188825 + ], + [ + 12.021142, + 60.188863 + ], + [ + 12.021358, + 60.188919 + ], + [ + 12.021556, + 60.188979 + ], + [ + 12.0218, + 60.189062 + ], + [ + 12.022003, + 60.189147 + ], + [ + 12.02232, + 60.189293 + ], + [ + 12.022562, + 60.18941 + ], + [ + 12.02278, + 60.189512 + ], + [ + 12.022973, + 60.189605 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.1532445" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.007471, + 60.187763 + ], + [ + 12.008602, + 60.187798 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.017311, + 60.188297 + ], + [ + 12.017733, + 60.188366 + ], + [ + 12.017849, + 60.188391 + ], + [ + 12.017951, + 60.188415 + ], + [ + 12.018072, + 60.188446 + ], + [ + 12.018177, + 60.188475 + ], + [ + 12.018285, + 60.188505 + ], + [ + 12.018403, + 60.188538 + ], + [ + 12.018516, + 60.18857 + ], + [ + 12.018529, + 60.188574 + ], + [ + 12.01855, + 60.18858 + ], + [ + 12.018584, + 60.18859 + ], + [ + 12.018657, + 60.188611 + ], + [ + 12.018776, + 60.188645 + ], + [ + 12.018886, + 60.188678 + ], + [ + 12.018998, + 60.18871 + ], + [ + 12.019108, + 60.188743 + ], + [ + 12.019226, + 60.188776 + ], + [ + 12.019327, + 60.188805 + ], + [ + 12.019656, + 60.1889 + ], + [ + 12.019745, + 60.188926 + ], + [ + 12.020106, + 60.18903 + ], + [ + 12.020408, + 60.189119 + ], + [ + 12.020704, + 60.1892 + ], + [ + 12.020897, + 60.189254 + ], + [ + 12.021173, + 60.189334 + ], + [ + 12.02136, + 60.189386 + ], + [ + 12.021485, + 60.189421 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_7146f562-e13b-4592-93c2-e49e88de3b09_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.007487, + 60.187925 + ], + [ + 12.007885, + 60.187928 + ], + [ + 12.00806, + 60.187924 + ], + [ + 12.008232, + 60.187919 + ], + [ + 12.008729, + 60.187923 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.006005, + 60.187946 + ], + [ + 12.006393, + 60.187999 + ], + [ + 12.006614, + 60.188029 + ], + [ + 12.006824, + 60.188057 + ], + [ + 12.007016, + 60.188083 + ], + [ + 12.007172, + 60.188103 + ], + [ + 12.007347, + 60.188126 + ], + [ + 12.007527, + 60.188151 + ], + [ + 12.007687, + 60.188173 + ], + [ + 12.007852, + 60.188194 + ], + [ + 12.007973, + 60.188212 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_9b7bb57f-92ee-4836-9d11-258ef154a18d_0.28937834" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.028951, + 60.188118 + ], + [ + 12.029246, + 60.188102 + ], + [ + 12.029592, + 60.188081 + ], + [ + 12.02993, + 60.188059 + ], + [ + 12.030612, + 60.18801 + ], + [ + 12.030702, + 60.188004 + ], + [ + 12.030972, + 60.187983 + ], + [ + 12.031292, + 60.187957 + ], + [ + 12.031652, + 60.187926 + ], + [ + 12.031954, + 60.187898 + ], + [ + 12.032223, + 60.187871 + ], + [ + 12.03247, + 60.187845 + ], + [ + 12.0327, + 60.187818 + ], + [ + 12.032916, + 60.187791 + ], + [ + 12.033292, + 60.187739 + ], + [ + 12.033665, + 60.187681 + ], + [ + 12.034033, + 60.187617 + ], + [ + 12.034107, + 60.187603 + ], + [ + 12.034398, + 60.187547 + ], + [ + 12.034757, + 60.187471 + ], + [ + 12.034885, + 60.187442 + ], + [ + 12.035111, + 60.187389 + ], + [ + 12.035459, + 60.187301 + ], + [ + 12.035802, + 60.187208 + ], + [ + 12.036138, + 60.187109 + ], + [ + 12.036468, + 60.187004 + ], + [ + 12.03679, + 60.186895 + ], + [ + 12.037105, + 60.18678 + ], + [ + 12.037412, + 60.18666 + ], + [ + 12.037712, + 60.186535 + ], + [ + 12.038002, + 60.186405 + ], + [ + 12.038285, + 60.186271 + ], + [ + 12.038558, + 60.186132 + ], + [ + 12.038776, + 60.186014 + ], + [ + 12.038973, + 60.185903 + ], + [ + 12.039155, + 60.185798 + ], + [ + 12.039325, + 60.185697 + ], + [ + 12.039486, + 60.1856 + ], + [ + 12.03964, + 60.185507 + ], + [ + 12.042496, + 60.183778 + ], + [ + 12.042822, + 60.183579 + ], + [ + 12.04302, + 60.183456 + ], + [ + 12.043181, + 60.183351 + ], + [ + 12.043321, + 60.183257 + ], + [ + 12.043446, + 60.18317 + ], + [ + 12.043559, + 60.183087 + ], + [ + 12.043734, + 60.182949 + ], + [ + 12.043898, + 60.182808 + ], + [ + 12.043984, + 60.182728 + ], + [ + 12.044051, + 60.182663 + ], + [ + 12.044192, + 60.182516 + ], + [ + 12.044322, + 60.182366 + ], + [ + 12.04436, + 60.182318 + ], + [ + 12.044439, + 60.182214 + ], + [ + 12.044545, + 60.18206 + ], + [ + 12.044639, + 60.181903 + ], + [ + 12.04472, + 60.181745 + ], + [ + 12.044788, + 60.181586 + ], + [ + 12.044844, + 60.181425 + ], + [ + 12.044887, + 60.181263 + ], + [ + 12.044917, + 60.181101 + ], + [ + 12.044935, + 60.18095 + ], + [ + 12.044943, + 60.180812 + ], + [ + 12.044946, + 60.180683 + ], + [ + 12.044946, + 60.180562 + ], + [ + 12.044945, + 60.180446 + ], + [ + 12.044943, + 60.180225 + ], + [ + 12.044946, + 60.180089 + ], + [ + 12.044952, + 60.179975 + ], + [ + 12.044961, + 60.179874 + ], + [ + 12.044973, + 60.179782 + ], + [ + 12.045003, + 60.179627 + ], + [ + 12.045045, + 60.179473 + ], + [ + 12.045098, + 60.179319 + ], + [ + 12.04513, + 60.179241 + ], + [ + 12.045163, + 60.179167 + ], + [ + 12.04524, + 60.179016 + ], + [ + 12.045328, + 60.178867 + ], + [ + 12.045427, + 60.178719 + ], + [ + 12.045538, + 60.178574 + ], + [ + 12.045656, + 60.178435 + ], + [ + 12.04566, + 60.17843 + ], + [ + 12.045784, + 60.178298 + ], + [ + 12.045906, + 60.178177 + ], + [ + 12.046027, + 60.178064 + ], + [ + 12.046145, + 60.177957 + ], + [ + 12.046261, + 60.177855 + ], + [ + 12.046374, + 60.177757 + ], + [ + 12.046484, + 60.177663 + ], + [ + 12.048098, + 60.17628 + ], + [ + 12.048245, + 60.176154 + ], + [ + 12.048935, + 60.175563 + ], + [ + 12.049144, + 60.175383 + ], + [ + 12.049273, + 60.175271 + ], + [ + 12.049379, + 60.175178 + ], + [ + 12.049472, + 60.175095 + ], + [ + 12.049777, + 60.174815 + ], + [ + 12.050069, + 60.174532 + ], + [ + 12.050349, + 60.174245 + ], + [ + 12.0504, + 60.174191 + ], + [ + 12.050616, + 60.173955 + ], + [ + 12.050807, + 60.173737 + ], + [ + 12.050869, + 60.173663 + ], + [ + 12.05111, + 60.173367 + ], + [ + 12.051338, + 60.173069 + ], + [ + 12.051552, + 60.172769 + ], + [ + 12.051752, + 60.172467 + ], + [ + 12.051939, + 60.172162 + ], + [ + 12.052113, + 60.171855 + ], + [ + 12.052272, + 60.171546 + ], + [ + 12.052418, + 60.171236 + ], + [ + 12.052551, + 60.170924 + ], + [ + 12.052669, + 60.170611 + ], + [ + 12.052773, + 60.170297 + ], + [ + 12.052863, + 60.169981 + ], + [ + 12.052939, + 60.169665 + ], + [ + 12.053001, + 60.169347 + ], + [ + 12.053049, + 60.169029 + ], + [ + 12.053082, + 60.168711 + ], + [ + 12.053102, + 60.168393 + ], + [ + 12.053107, + 60.168074 + ], + [ + 12.053098, + 60.167755 + ], + [ + 12.053074, + 60.167437 + ], + [ + 12.053037, + 60.167119 + ], + [ + 12.053024, + 60.167027 + ], + [ + 12.053009, + 60.166938 + ], + [ + 12.052994, + 60.16685 + ], + [ + 12.052939, + 60.166577 + ], + [ + 12.052872, + 60.166305 + ], + [ + 12.052793, + 60.166034 + ], + [ + 12.052702, + 60.165763 + ], + [ + 12.052599, + 60.165494 + ], + [ + 12.052551, + 60.165379 + ], + [ + 12.052537, + 60.165347 + ], + [ + 12.052484, + 60.165226 + ], + [ + 12.052358, + 60.164959 + ], + [ + 12.05222, + 60.164694 + ], + [ + 12.052156, + 60.164578 + ], + [ + 12.052096, + 60.164474 + ], + [ + 12.052038, + 60.164378 + ], + [ + 12.051981, + 60.164289 + ], + [ + 12.05186, + 60.164113 + ], + [ + 12.051728, + 60.163938 + ], + [ + 12.051587, + 60.163766 + ], + [ + 12.051435, + 60.163595 + ], + [ + 12.051265, + 60.16342 + ], + [ + 12.051104, + 60.163261 + ], + [ + 12.050948, + 60.163115 + ], + [ + 12.050799, + 60.162977 + ], + [ + 12.050656, + 60.162847 + ], + [ + 12.050373, + 60.162587 + ], + [ + 12.050202, + 60.162425 + ], + [ + 12.050063, + 60.16229 + ], + [ + 12.049944, + 60.162168 + ], + [ + 12.049838, + 60.162056 + ], + [ + 12.049745, + 60.16195 + ], + [ + 12.049609, + 60.161786 + ], + [ + 12.049485, + 60.16162 + ], + [ + 12.049371, + 60.161451 + ], + [ + 12.049269, + 60.161281 + ], + [ + 12.049179, + 60.161109 + ], + [ + 12.0491, + 60.160936 + ], + [ + 12.049032, + 60.160762 + ], + [ + 12.048979, + 60.160596 + ], + [ + 12.048937, + 60.16044 + ], + [ + 12.048903, + 60.160293 + ], + [ + 12.048875, + 60.160153 + ], + [ + 12.04885, + 60.160018 + ], + [ + 12.048829, + 60.159888 + ], + [ + 12.048808, + 60.159763 + ], + [ + 12.048496, + 60.157824 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_eacc17c5-7cb5-4e88-88d1-d0363912e4e5_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.998477, + 60.188199 + ], + [ + 11.998688, + 60.188145 + ], + [ + 11.998903, + 60.188096 + ], + [ + 11.999122, + 60.188052 + ], + [ + 11.999324, + 60.188016 + ], + [ + 11.999345, + 60.188013 + ], + [ + 11.999436, + 60.187998 + ], + [ + 12.000049, + 60.187896 + ], + [ + 12.00013, + 60.187883 + ], + [ + 12.000244, + 60.187866 + ], + [ + 12.000297, + 60.187859 + ], + [ + 12.000441, + 60.18784 + ], + [ + 12.000639, + 60.187817 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.011062, + 60.187954 + ], + [ + 12.011436, + 60.187974 + ], + [ + 12.011611, + 60.187988 + ], + [ + 12.011875, + 60.188011 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.01342, + 60.187863 + ], + [ + 12.01356, + 60.187865 + ], + [ + 12.013706, + 60.187869 + ], + [ + 12.013869, + 60.187874 + ], + [ + 12.014017, + 60.187878 + ], + [ + 12.014165, + 60.187882 + ], + [ + 12.014326, + 60.187886 + ], + [ + 12.014497, + 60.187891 + ], + [ + 12.014655, + 60.187896 + ], + [ + 12.014817, + 60.187901 + ], + [ + 12.014988, + 60.187905 + ], + [ + 12.015139, + 60.187909 + ], + [ + 12.015287, + 60.187914 + ], + [ + 12.015428, + 60.187917 + ], + [ + 12.015583, + 60.187922 + ], + [ + 12.015757, + 60.187927 + ], + [ + 12.015902, + 60.187933 + ], + [ + 12.016059, + 60.187938 + ], + [ + 12.016232, + 60.187943 + ], + [ + 12.016369, + 60.187945 + ], + [ + 12.016519, + 60.187949 + ], + [ + 12.016639, + 60.187951 + ], + [ + 12.016677, + 60.187951 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.016677, + 60.187951 + ], + [ + 12.016788, + 60.187955 + ], + [ + 12.016967, + 60.187968 + ], + [ + 12.017114, + 60.18798 + ], + [ + 12.01727, + 60.187994 + ], + [ + 12.017692, + 60.188018 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.003491, + 60.187778 + ], + [ + 12.003821, + 60.187796 + ], + [ + 12.003984, + 60.18781 + ], + [ + 12.004135, + 60.187822 + ], + [ + 12.004518, + 60.187842 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.011875, + 60.188011 + ], + [ + 12.012042, + 60.188027 + ], + [ + 12.0122, + 60.18804 + ], + [ + 12.012344, + 60.188051 + ], + [ + 12.012506, + 60.188064 + ], + [ + 12.012992, + 60.188094 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.007283, + 60.187717 + ], + [ + 12.00783, + 60.187747 + ], + [ + 12.007963, + 60.187759 + ], + [ + 12.008095, + 60.187771 + ], + [ + 12.008602, + 60.187798 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.020918, + 60.188217 + ], + [ + 12.020945, + 60.188217 + ], + [ + 12.021163, + 60.188211 + ], + [ + 12.021278, + 60.188207 + ], + [ + 12.02146, + 60.188203 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.00501, + 60.187855 + ], + [ + 12.005366, + 60.187875 + ], + [ + 12.00553, + 60.187889 + ], + [ + 12.005719, + 60.187908 + ], + [ + 12.005877, + 60.187928 + ], + [ + 12.006005, + 60.187946 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_8eee1418-ee14-410b-b5af-0a44d63a03ee_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.030919, + 60.19171 + ], + [ + 12.030985, + 60.191779 + ], + [ + 12.03106, + 60.191868 + ], + [ + 12.031159, + 60.191995 + ], + [ + 12.031238, + 60.192106 + ], + [ + 12.031307, + 60.192213 + ], + [ + 12.031374, + 60.19233 + ], + [ + 12.031442, + 60.192459 + ], + [ + 12.031495, + 60.19258 + ], + [ + 12.031543, + 60.192705 + ], + [ + 12.031578, + 60.19281 + ], + [ + 12.031616, + 60.192958 + ], + [ + 12.031651, + 60.193124 + ], + [ + 12.031679, + 60.193275 + ], + [ + 12.031716, + 60.1935 + ], + [ + 12.031721, + 60.193525 + ], + [ + 12.031725, + 60.193549 + ], + [ + 12.031729, + 60.193573 + ], + [ + 12.031755, + 60.193702 + ], + [ + 12.031778, + 60.193831 + ], + [ + 12.031807, + 60.193981 + ], + [ + 12.031831, + 60.194119 + ], + [ + 12.031857, + 60.194266 + ], + [ + 12.031888, + 60.194425 + ], + [ + 12.031893, + 60.194453 + ], + [ + 12.031897, + 60.194475 + ], + [ + 12.031902, + 60.194503 + ], + [ + 12.031922, + 60.194621 + ], + [ + 12.031943, + 60.194739 + ], + [ + 12.031948, + 60.194765 + ], + [ + 12.03197, + 60.194892 + ], + [ + 12.031997, + 60.19504 + ], + [ + 12.032025, + 60.195191 + ], + [ + 12.032055, + 60.195356 + ], + [ + 12.032083, + 60.195509 + ], + [ + 12.032109, + 60.195666 + ], + [ + 12.032137, + 60.195811 + ], + [ + 12.032162, + 60.195956 + ], + [ + 12.032185, + 60.196081 + ], + [ + 12.032212, + 60.196224 + ], + [ + 12.032243, + 60.196394 + ], + [ + 12.03227, + 60.196539 + ], + [ + 12.032296, + 60.196694 + ], + [ + 12.032327, + 60.196852 + ], + [ + 12.032357, + 60.197014 + ], + [ + 12.032384, + 60.197169 + ], + [ + 12.032412, + 60.197318 + ], + [ + 12.032439, + 60.197465 + ], + [ + 12.032465, + 60.19761 + ], + [ + 12.032487, + 60.197734 + ], + [ + 12.032515, + 60.197884 + ], + [ + 12.03254, + 60.198031 + ], + [ + 12.032572, + 60.198204 + ], + [ + 12.032599, + 60.198349 + ], + [ + 12.032629, + 60.198523 + ], + [ + 12.032655, + 60.198668 + ], + [ + 12.032681, + 60.198812 + ], + [ + 12.032708, + 60.198958 + ], + [ + 12.032743, + 60.199147 + ], + [ + 12.032769, + 60.199286 + ], + [ + 12.032796, + 60.199435 + ], + [ + 12.032818, + 60.199557 + ], + [ + 12.03284, + 60.199673 + ], + [ + 12.032861, + 60.19979 + ], + [ + 12.032882, + 60.199911 + ], + [ + 12.032904, + 60.200028 + ], + [ + 12.03291, + 60.200063 + ], + [ + 12.032914, + 60.200084 + ], + [ + 12.032917, + 60.200106 + ], + [ + 12.032924, + 60.200142 + ], + [ + 12.032946, + 60.200267 + ], + [ + 12.032972, + 60.200403 + ], + [ + 12.032997, + 60.200548 + ], + [ + 12.033024, + 60.200691 + ], + [ + 12.033042, + 60.200805 + ], + [ + 12.033062, + 60.200915 + ], + [ + 12.033081, + 60.201035 + ], + [ + 12.033101, + 60.201146 + ], + [ + 12.033117, + 60.201251 + ], + [ + 12.033133, + 60.201363 + ], + [ + 12.033148, + 60.201474 + ], + [ + 12.033161, + 60.201584 + ], + [ + 12.033177, + 60.20171 + ], + [ + 12.033194, + 60.201837 + ], + [ + 12.033208, + 60.201959 + ], + [ + 12.033226, + 60.202106 + ], + [ + 12.033239, + 60.202237 + ], + [ + 12.03325, + 60.202355 + ], + [ + 12.033265, + 60.20249 + ], + [ + 12.033273, + 60.202594 + ], + [ + 12.03328, + 60.202685 + ], + [ + 12.033288, + 60.202782 + ], + [ + 12.033295, + 60.202883 + ], + [ + 12.033304, + 60.202992 + ], + [ + 12.033312, + 60.203112 + ], + [ + 12.033316, + 60.203202 + ], + [ + 12.033321, + 60.203296 + ], + [ + 12.033326, + 60.203418 + ], + [ + 12.033331, + 60.203533 + ], + [ + 12.033336, + 60.203634 + ], + [ + 12.033341, + 60.203748 + ], + [ + 12.033344, + 60.203866 + ], + [ + 12.033346, + 60.203972 + ], + [ + 12.033349, + 60.204086 + ], + [ + 12.033351, + 60.204185 + ], + [ + 12.033353, + 60.204301 + ], + [ + 12.033353, + 60.204418 + ], + [ + 12.033354, + 60.204525 + ], + [ + 12.033355, + 60.20464 + ], + [ + 12.033357, + 60.2047 + ], + [ + 12.033357, + 60.20474 + ], + [ + 12.033357, + 60.204823 + ], + [ + 12.033359, + 60.204923 + ], + [ + 12.03336, + 60.205032 + ], + [ + 12.033361, + 60.205129 + ], + [ + 12.033361, + 60.205254 + ], + [ + 12.033363, + 60.205381 + ], + [ + 12.033364, + 60.205498 + ], + [ + 12.033365, + 60.205645 + ], + [ + 12.033368, + 60.205772 + ], + [ + 12.033366, + 60.205853 + ], + [ + 12.033367, + 60.205964 + ], + [ + 12.03337, + 60.206168 + ], + [ + 12.033372, + 60.206283 + ], + [ + 12.033373, + 60.206403 + ], + [ + 12.033375, + 60.206531 + ], + [ + 12.033377, + 60.20667 + ], + [ + 12.03338, + 60.206813 + ], + [ + 12.033379, + 60.20695 + ], + [ + 12.033381, + 60.207088 + ], + [ + 12.033381, + 60.207229 + ], + [ + 12.033383, + 60.207401 + ], + [ + 12.033385, + 60.207532 + ], + [ + 12.033385, + 60.207668 + ], + [ + 12.033387, + 60.207797 + ], + [ + 12.033389, + 60.207961 + ], + [ + 12.033388, + 60.208078 + ], + [ + 12.033391, + 60.208201 + ], + [ + 12.033393, + 60.208397 + ], + [ + 12.033396, + 60.208516 + ], + [ + 12.0334, + 60.20892 + ], + [ + 12.033402, + 60.2091 + ], + [ + 12.033404, + 60.209347 + ], + [ + 12.033405, + 60.209543 + ], + [ + 12.033407, + 60.209728 + ], + [ + 12.033408, + 60.209914 + ], + [ + 12.033409, + 60.210087 + ], + [ + 12.033411, + 60.210254 + ], + [ + 12.033414, + 60.210434 + ], + [ + 12.033416, + 60.21062 + ], + [ + 12.033419, + 60.210821 + ], + [ + 12.033419, + 60.211 + ], + [ + 12.033422, + 60.211265 + ], + [ + 12.033423, + 60.211463 + ], + [ + 12.033427, + 60.211761 + ], + [ + 12.033431, + 60.212056 + ], + [ + 12.033433, + 60.212374 + ], + [ + 12.033436, + 60.212754 + ], + [ + 12.03344, + 60.212974 + ], + [ + 12.03344, + 60.213162 + ], + [ + 12.033444, + 60.21343 + ], + [ + 12.033445, + 60.213596 + ], + [ + 12.033447, + 60.213798 + ], + [ + 12.03345, + 60.214082 + ], + [ + 12.033455, + 60.214352 + ], + [ + 12.033457, + 60.214603 + ], + [ + 12.033461, + 60.214941 + ], + [ + 12.033464, + 60.215262 + ], + [ + 12.033465, + 60.215436 + ], + [ + 12.033467, + 60.215636 + ], + [ + 12.033469, + 60.215891 + ], + [ + 12.033473, + 60.216152 + ], + [ + 12.033476, + 60.216393 + ], + [ + 12.033479, + 60.216665 + ], + [ + 12.03348, + 60.216802 + ], + [ + 12.03348, + 60.216814 + ], + [ + 12.03348, + 60.216826 + ], + [ + 12.03348, + 60.216872 + ], + [ + 12.033484, + 60.217225 + ], + [ + 12.033488, + 60.217458 + ], + [ + 12.033489, + 60.217559 + ], + [ + 12.033489, + 60.217726 + ], + [ + 12.033491, + 60.217858 + ], + [ + 12.033493, + 60.218025 + ], + [ + 12.033494, + 60.218148 + ], + [ + 12.033497, + 60.218305 + ], + [ + 12.033497, + 60.218415 + ], + [ + 12.033499, + 60.218577 + ], + [ + 12.0335, + 60.218711 + ], + [ + 12.033501, + 60.218878 + ], + [ + 12.033504, + 60.218952 + ], + [ + 12.033504, + 60.218968 + ], + [ + 12.033504, + 60.218979 + ], + [ + 12.033505, + 60.219133 + ], + [ + 12.033507, + 60.21929 + ], + [ + 12.033508, + 60.219453 + ], + [ + 12.033511, + 60.219682 + ], + [ + 12.033512, + 60.219826 + ], + [ + 12.033515, + 60.219957 + ], + [ + 12.033515, + 60.220121 + ], + [ + 12.033517, + 60.220256 + ], + [ + 12.03352, + 60.22044 + ], + [ + 12.033522, + 60.220578 + ], + [ + 12.033525, + 60.220821 + ], + [ + 12.033525, + 60.220974 + ], + [ + 12.033528, + 60.221139 + ], + [ + 12.03353, + 60.221311 + ], + [ + 12.033533, + 60.221547 + ], + [ + 12.033535, + 60.221722 + ], + [ + 12.033535, + 60.221745 + ], + [ + 12.033537, + 60.221903 + ], + [ + 12.03354, + 60.222114 + ], + [ + 12.033544, + 60.222229 + ], + [ + 12.033555, + 60.222499 + ], + [ + 12.033571, + 60.222719 + ], + [ + 12.033603, + 60.223002 + ], + [ + 12.033632, + 60.223192 + ], + [ + 12.033676, + 60.223432 + ], + [ + 12.033717, + 60.223617 + ], + [ + 12.033761, + 60.223793 + ], + [ + 12.033839, + 60.224074 + ], + [ + 12.033901, + 60.224261 + ], + [ + 12.03396, + 60.224434 + ], + [ + 12.034058, + 60.224689 + ], + [ + 12.034148, + 60.224907 + ], + [ + 12.034302, + 60.225239 + ], + [ + 12.034438, + 60.225529 + ], + [ + 12.034513, + 60.225679 + ], + [ + 12.03461, + 60.225878 + ], + [ + 12.034723, + 60.22611 + ], + [ + 12.035012, + 60.226707 + ], + [ + 12.035131, + 60.226953 + ], + [ + 12.035254, + 60.227204 + ], + [ + 12.035387, + 60.22748 + ], + [ + 12.035538, + 60.227794 + ], + [ + 12.035679, + 60.228085 + ], + [ + 12.035816, + 60.228368 + ], + [ + 12.035915, + 60.228572 + ], + [ + 12.036017, + 60.228784 + ], + [ + 12.036171, + 60.229099 + ], + [ + 12.036317, + 60.229404 + ], + [ + 12.036446, + 60.229668 + ], + [ + 12.036593, + 60.229971 + ], + [ + 12.03676, + 60.230313 + ], + [ + 12.036877, + 60.230552 + ], + [ + 12.036974, + 60.230754 + ], + [ + 12.037028, + 60.230863 + ], + [ + 12.037161, + 60.231137 + ], + [ + 12.037268, + 60.231357 + ], + [ + 12.037335, + 60.231498 + ], + [ + 12.037385, + 60.231601 + ], + [ + 12.037428, + 60.231691 + ], + [ + 12.037475, + 60.231788 + ], + [ + 12.037534, + 60.231909 + ], + [ + 12.03759, + 60.232024 + ], + [ + 12.037655, + 60.232159 + ], + [ + 12.03771, + 60.232272 + ], + [ + 12.037771, + 60.232398 + ], + [ + 12.037825, + 60.23251 + ], + [ + 12.037891, + 60.232644 + ], + [ + 12.037951, + 60.232771 + ], + [ + 12.038005, + 60.232881 + ], + [ + 12.038055, + 60.232985 + ], + [ + 12.038183, + 60.23325 + ], + [ + 12.038251, + 60.233384 + ], + [ + 12.038313, + 60.233513 + ], + [ + 12.038364, + 60.233618 + ], + [ + 12.038425, + 60.233743 + ], + [ + 12.03849, + 60.233877 + ], + [ + 12.038546, + 60.233995 + ], + [ + 12.038607, + 60.234118 + ], + [ + 12.038656, + 60.234216 + ], + [ + 12.038713, + 60.234337 + ], + [ + 12.038779, + 60.234473 + ], + [ + 12.038828, + 60.234574 + ], + [ + 12.038893, + 60.234706 + ], + [ + 12.038953, + 60.234836 + ], + [ + 12.03904, + 60.235016 + ], + [ + 12.039106, + 60.235149 + ], + [ + 12.03914, + 60.235215 + ], + [ + 12.039204, + 60.235352 + ], + [ + 12.039277, + 60.2355 + ], + [ + 12.039331, + 60.235613 + ], + [ + 12.03934, + 60.235631 + ], + [ + 12.039345, + 60.235641 + ], + [ + 12.03935, + 60.235652 + ], + [ + 12.039392, + 60.23574 + ], + [ + 12.039473, + 60.235907 + ], + [ + 12.039554, + 60.236073 + ], + [ + 12.039651, + 60.236273 + ], + [ + 12.039741, + 60.236454 + ], + [ + 12.039821, + 60.236623 + ], + [ + 12.0399, + 60.236784 + ], + [ + 12.039983, + 60.236957 + ], + [ + 12.040073, + 60.237138 + ], + [ + 12.04015, + 60.237297 + ], + [ + 12.040247, + 60.237496 + ], + [ + 12.040301, + 60.237611 + ], + [ + 12.04037, + 60.237749 + ], + [ + 12.040446, + 60.237908 + ], + [ + 12.040526, + 60.238071 + ], + [ + 12.040598, + 60.23822 + ], + [ + 12.040672, + 60.238372 + ], + [ + 12.040744, + 60.238525 + ], + [ + 12.040827, + 60.238689 + ], + [ + 12.040899, + 60.23884 + ], + [ + 12.04097, + 60.238989 + ], + [ + 12.041051, + 60.239155 + ], + [ + 12.041123, + 60.239301 + ], + [ + 12.041201, + 60.239456 + ], + [ + 12.041294, + 60.239632 + ], + [ + 12.041368, + 60.239769 + ], + [ + 12.041433, + 60.239885 + ], + [ + 12.041518, + 60.240031 + ], + [ + 12.041595, + 60.240162 + ], + [ + 12.04168, + 60.240305 + ], + [ + 12.041758, + 60.240426 + ], + [ + 12.041821, + 60.240525 + ], + [ + 12.041914, + 60.240667 + ], + [ + 12.042049, + 60.240869 + ], + [ + 12.042167, + 60.241033 + ], + [ + 12.042315, + 60.241235 + ], + [ + 12.042443, + 60.241403 + ], + [ + 12.042559, + 60.241552 + ], + [ + 12.042702, + 60.241732 + ], + [ + 12.042828, + 60.241887 + ], + [ + 12.042855, + 60.241918 + ], + [ + 12.042865, + 60.241929 + ], + [ + 12.042876, + 60.241942 + ], + [ + 12.042893, + 60.241961 + ], + [ + 12.043025, + 60.242118 + ], + [ + 12.043198, + 60.242315 + ], + [ + 12.043362, + 60.242496 + ], + [ + 12.043501, + 60.242649 + ], + [ + 12.043643, + 60.242805 + ], + [ + 12.043784, + 60.242959 + ], + [ + 12.043928, + 60.243116 + ], + [ + 12.044073, + 60.243276 + ], + [ + 12.04421, + 60.243425 + ], + [ + 12.04435, + 60.243578 + ], + [ + 12.044477, + 60.243719 + ], + [ + 12.044617, + 60.243871 + ], + [ + 12.044738, + 60.244006 + ], + [ + 12.044889, + 60.24417 + ], + [ + 12.045053, + 60.244348 + ], + [ + 12.045205, + 60.244517 + ], + [ + 12.045405, + 60.244736 + ], + [ + 12.045592, + 60.244939 + ], + [ + 12.045602, + 60.244951 + ], + [ + 12.045706, + 60.245064 + ], + [ + 12.045763, + 60.245126 + ], + [ + 12.045949, + 60.245331 + ], + [ + 12.046107, + 60.245505 + ], + [ + 12.046245, + 60.245657 + ], + [ + 12.046405, + 60.245833 + ], + [ + 12.046565, + 60.246004 + ], + [ + 12.046741, + 60.2462 + ], + [ + 12.046861, + 60.246332 + ], + [ + 12.047061, + 60.246554 + ], + [ + 12.047262, + 60.24677 + ], + [ + 12.047403, + 60.246928 + ], + [ + 12.047534, + 60.247069 + ], + [ + 12.047635, + 60.24718 + ], + [ + 12.047848, + 60.247411 + ], + [ + 12.048053, + 60.247635 + ], + [ + 12.048228, + 60.247829 + ], + [ + 12.048434, + 60.248056 + ], + [ + 12.048661, + 60.248305 + ], + [ + 12.048829, + 60.248489 + ], + [ + 12.049071, + 60.248757 + ], + [ + 12.049366, + 60.249079 + ], + [ + 12.049675, + 60.249418 + ], + [ + 12.049942, + 60.24971 + ], + [ + 12.050077, + 60.249856 + ], + [ + 12.050276, + 60.250075 + ], + [ + 12.05044, + 60.250257 + ], + [ + 12.050565, + 60.250394 + ], + [ + 12.050698, + 60.25054 + ], + [ + 12.050846, + 60.250702 + ], + [ + 12.050996, + 60.250866 + ], + [ + 12.05112, + 60.251003 + ], + [ + 12.051236, + 60.251131 + ], + [ + 12.05139, + 60.251305 + ], + [ + 12.051551, + 60.251498 + ], + [ + 12.051672, + 60.251651 + ], + [ + 12.05176, + 60.251769 + ], + [ + 12.051857, + 60.251909 + ], + [ + 12.052012, + 60.25216 + ], + [ + 12.052178, + 60.252466 + ], + [ + 12.052321, + 60.252799 + ], + [ + 12.052431, + 60.253139 + ], + [ + 12.052481, + 60.253332 + ], + [ + 12.052489, + 60.253362 + ], + [ + 12.052496, + 60.253394 + ], + [ + 12.052503, + 60.253427 + ], + [ + 12.052533, + 60.253563 + ], + [ + 12.052601, + 60.25389 + ], + [ + 12.052688, + 60.25432 + ], + [ + 12.052771, + 60.25473 + ], + [ + 12.052779, + 60.254774 + ], + [ + 12.052821, + 60.254964 + ], + [ + 12.05286, + 60.255163 + ], + [ + 12.0529, + 60.255355 + ], + [ + 12.05295, + 60.255594 + ], + [ + 12.052987, + 60.255771 + ], + [ + 12.053021, + 60.255943 + ], + [ + 12.053057, + 60.256109 + ], + [ + 12.053084, + 60.256238 + ], + [ + 12.053107, + 60.256356 + ], + [ + 12.053131, + 60.256472 + ], + [ + 12.053154, + 60.256594 + ], + [ + 12.053177, + 60.256701 + ], + [ + 12.05321, + 60.25686 + ], + [ + 12.053247, + 60.257041 + ], + [ + 12.053279, + 60.257204 + ], + [ + 12.05333, + 60.257447 + ], + [ + 12.053373, + 60.257667 + ], + [ + 12.053417, + 60.257883 + ], + [ + 12.053463, + 60.258093 + ], + [ + 12.053501, + 60.258282 + ], + [ + 12.053519, + 60.258359 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_c6dddb62-f3e5-4587-93fa-5747ef4c4919_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.019226, + 60.188182 + ], + [ + 12.019315, + 60.188183 + ], + [ + 12.019341, + 60.188184 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.86458099" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.019827, + 60.188116 + ], + [ + 12.020317, + 60.188148 + ], + [ + 12.020517, + 60.188163 + ], + [ + 12.020939, + 60.188189 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.02146, + 60.188203 + ], + [ + 12.02177, + 60.188213 + ], + [ + 12.022016, + 60.188218 + ], + [ + 12.022192, + 60.188225 + ], + [ + 12.022234, + 60.188227 + ], + [ + 12.022333, + 60.188232 + ], + [ + 12.02241, + 60.188236 + ], + [ + 12.02243, + 60.188237 + ], + [ + 12.02248, + 60.188239 + ], + [ + 12.022588, + 60.188245 + ], + [ + 12.022741, + 60.188255 + ], + [ + 12.02287, + 60.188265 + ], + [ + 12.023016, + 60.188279 + ], + [ + 12.023193, + 60.188298 + ], + [ + 12.023328, + 60.188315 + ], + [ + 12.023431, + 60.188329 + ], + [ + 12.023579, + 60.188351 + ], + [ + 12.023676, + 60.188367 + ], + [ + 12.023866, + 60.188401 + ], + [ + 12.024078, + 60.188444 + ], + [ + 12.024278, + 60.188489 + ], + [ + 12.024443, + 60.188529 + ], + [ + 12.024625, + 60.188578 + ], + [ + 12.024857, + 60.188641 + ], + [ + 12.025015, + 60.188686 + ], + [ + 12.02522, + 60.188745 + ], + [ + 12.02538, + 60.188794 + ], + [ + 12.025525, + 60.188839 + ], + [ + 12.025698, + 60.188894 + ], + [ + 12.025874, + 60.188952 + ], + [ + 12.02601, + 60.188998 + ], + [ + 12.026234, + 60.189076 + ], + [ + 12.026446, + 60.189153 + ], + [ + 12.026596, + 60.189207 + ], + [ + 12.026754, + 60.189267 + ], + [ + 12.026928, + 60.189335 + ], + [ + 12.027093, + 60.189401 + ], + [ + 12.027259, + 60.189469 + ], + [ + 12.027416, + 60.189535 + ], + [ + 12.027596, + 60.189613 + ], + [ + 12.027773, + 60.189693 + ], + [ + 12.02794, + 60.18977 + ], + [ + 12.028055, + 60.189824 + ], + [ + 12.028218, + 60.189903 + ], + [ + 12.028353, + 60.189971 + ], + [ + 12.028505, + 60.190048 + ], + [ + 12.028644, + 60.190122 + ], + [ + 12.028772, + 60.19019 + ], + [ + 12.02885, + 60.190233 + ], + [ + 12.0289, + 60.190261 + ], + [ + 12.028919, + 60.190271 + ], + [ + 12.028935, + 60.190281 + ], + [ + 12.028997, + 60.190315 + ], + [ + 12.029155, + 60.190406 + ], + [ + 12.029283, + 60.190482 + ], + [ + 12.029419, + 60.190564 + ], + [ + 12.029558, + 60.190651 + ], + [ + 12.029702, + 60.190742 + ], + [ + 12.029842, + 60.190835 + ], + [ + 12.029976, + 60.190928 + ], + [ + 12.03009, + 60.191009 + ], + [ + 12.030219, + 60.191101 + ], + [ + 12.030337, + 60.191193 + ], + [ + 12.030457, + 60.191288 + ], + [ + 12.030565, + 60.191377 + ], + [ + 12.030673, + 60.191472 + ], + [ + 12.030782, + 60.191573 + ], + [ + 12.030893, + 60.191682 + ], + [ + 12.030919, + 60.19171 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 11.99962, + 60.187925 + ], + [ + 11.999828, + 60.187885 + ], + [ + 12.000031, + 60.187851 + ], + [ + 12.00017, + 60.187831 + ], + [ + 12.000238, + 60.187823 + ], + [ + 12.000442, + 60.187798 + ], + [ + 12.000562, + 60.187784 + ], + [ + 12.001091, + 60.187719 + ], + [ + 12.001098, + 60.187718 + ], + [ + 12.001119, + 60.187716 + ], + [ + 12.001311, + 60.187693 + ], + [ + 12.00143, + 60.187679 + ], + [ + 12.001529, + 60.187669 + ], + [ + 12.001618, + 60.18766 + ], + [ + 12.0017, + 60.187653 + ], + [ + 12.001768, + 60.187647 + ], + [ + 12.001908, + 60.187638 + ], + [ + 12.002118, + 60.187628 + ], + [ + 12.002297, + 60.187624 + ], + [ + 12.002328, + 60.187624 + ], + [ + 12.002466, + 60.187624 + ], + [ + 12.002591, + 60.187626 + ], + [ + 12.002708, + 60.187628 + ], + [ + 12.002818, + 60.187631 + ], + [ + 12.002923, + 60.187634 + ], + [ + 12.004054, + 60.187666 + ], + [ + 12.004067, + 60.187666 + ], + [ + 12.006646, + 60.187739 + ], + [ + 12.007445, + 60.187763 + ], + [ + 12.007471, + 60.187763 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.008608, + 60.187797 + ], + [ + 12.008746, + 60.187802 + ], + [ + 12.009174, + 60.187815 + ], + [ + 12.009357, + 60.187821 + ], + [ + 12.009544, + 60.187827 + ], + [ + 12.009717, + 60.187833 + ], + [ + 12.009929, + 60.187838 + ], + [ + 12.010164, + 60.187845 + ], + [ + 12.010378, + 60.187851 + ], + [ + 12.010629, + 60.187859 + ], + [ + 12.010815, + 60.187864 + ], + [ + 12.011033, + 60.18787 + ], + [ + 12.011235, + 60.187876 + ], + [ + 12.011441, + 60.187882 + ], + [ + 12.011625, + 60.187887 + ], + [ + 12.011807, + 60.187893 + ], + [ + 12.011959, + 60.187896 + ], + [ + 12.012017, + 60.187898 + ], + [ + 12.012188, + 60.187903 + ], + [ + 12.012202, + 60.187903 + ], + [ + 12.012228, + 60.187904 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.011133, + 60.187913 + ], + [ + 12.01116, + 60.187914 + ], + [ + 12.011527, + 60.187927 + ], + [ + 12.011659, + 60.187929 + ], + [ + 12.01181, + 60.187933 + ], + [ + 12.011958, + 60.187937 + ], + [ + 12.012101, + 60.187941 + ], + [ + 12.012237, + 60.187945 + ], + [ + 12.012372, + 60.187949 + ], + [ + 12.012515, + 60.187953 + ], + [ + 12.012639, + 60.187957 + ], + [ + 12.012762, + 60.18796 + ], + [ + 12.012888, + 60.187964 + ], + [ + 12.013035, + 60.187968 + ], + [ + 12.013157, + 60.187972 + ], + [ + 12.013293, + 60.187975 + ], + [ + 12.013422, + 60.187979 + ], + [ + 12.013555, + 60.187982 + ], + [ + 12.013684, + 60.187985 + ], + [ + 12.013823, + 60.18799 + ], + [ + 12.01395, + 60.187993 + ], + [ + 12.014079, + 60.187997 + ], + [ + 12.014212, + 60.188001 + ], + [ + 12.014354, + 60.188005 + ], + [ + 12.014506, + 60.188009 + ], + [ + 12.014634, + 60.188012 + ], + [ + 12.014773, + 60.188016 + ], + [ + 12.014919, + 60.18802 + ], + [ + 12.015062, + 60.188024 + ], + [ + 12.015198, + 60.188028 + ], + [ + 12.015339, + 60.188031 + ], + [ + 12.015474, + 60.188036 + ], + [ + 12.015596, + 60.188039 + ], + [ + 12.015741, + 60.188043 + ], + [ + 12.015884, + 60.188047 + ], + [ + 12.016031, + 60.188051 + ], + [ + 12.016175, + 60.188055 + ], + [ + 12.01631, + 60.188059 + ], + [ + 12.016456, + 60.188063 + ], + [ + 12.016588, + 60.188067 + ], + [ + 12.016742, + 60.188071 + ], + [ + 12.016889, + 60.188075 + ], + [ + 12.017028, + 60.188079 + ], + [ + 12.01718, + 60.188083 + ], + [ + 12.017319, + 60.188087 + ], + [ + 12.01745, + 60.188091 + ], + [ + 12.017575, + 60.188095 + ], + [ + 12.017698, + 60.188098 + ], + [ + 12.017836, + 60.188102 + ], + [ + 12.017975, + 60.188106 + ], + [ + 12.018116, + 60.18811 + ], + [ + 12.018258, + 60.188114 + ], + [ + 12.018394, + 60.188118 + ], + [ + 12.018521, + 60.188121 + ], + [ + 12.018678, + 60.188125 + ], + [ + 12.018827, + 60.188129 + ], + [ + 12.018902, + 60.188131 + ], + [ + 12.018978, + 60.188133 + ], + [ + 12.019109, + 60.188137 + ], + [ + 12.019308, + 60.188142 + ], + [ + 12.019401, + 60.188144 + ], + [ + 12.019621, + 60.188151 + ], + [ + 12.019886, + 60.188158 + ], + [ + 12.020095, + 60.188165 + ], + [ + 12.020317, + 60.188171 + ], + [ + 12.020501, + 60.188176 + ], + [ + 12.020939, + 60.188189 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.013814, + 60.188117 + ], + [ + 12.014197, + 60.188145 + ], + [ + 12.014411, + 60.188158 + ], + [ + 12.014614, + 60.188175 + ], + [ + 12.01476, + 60.188181 + ], + [ + 12.015122, + 60.188195 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.005784, + 60.187758 + ], + [ + 12.005933, + 60.187762 + ], + [ + 12.006112, + 60.187767 + ], + [ + 12.006286, + 60.187772 + ], + [ + 12.006477, + 60.187777 + ], + [ + 12.006657, + 60.187782 + ], + [ + 12.006841, + 60.187788 + ], + [ + 12.007042, + 60.187794 + ], + [ + 12.007219, + 60.187801 + ], + [ + 12.007383, + 60.187807 + ], + [ + 12.007577, + 60.187815 + ], + [ + 12.007758, + 60.187821 + ], + [ + 12.007967, + 60.187827 + ], + [ + 12.008121, + 60.187831 + ], + [ + 12.008277, + 60.187836 + ], + [ + 12.008521, + 60.187841 + ], + [ + 12.008634, + 60.187845 + ], + [ + 12.008651, + 60.187846 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.001636, + 60.187737 + ], + [ + 12.00182, + 60.187731 + ], + [ + 12.002037, + 60.187725 + ], + [ + 12.00223, + 60.187719 + ], + [ + 12.002438, + 60.187718 + ], + [ + 12.002689, + 60.187719 + ], + [ + 12.003148, + 60.187732 + ], + [ + 12.003605, + 60.187746 + ], + [ + 12.005034, + 60.187785 + ], + [ + 12.005195, + 60.18779 + ], + [ + 12.00537, + 60.187795 + ], + [ + 12.005562, + 60.1878 + ], + [ + 12.005759, + 60.187805 + ], + [ + 12.005955, + 60.187812 + ], + [ + 12.006171, + 60.187817 + ], + [ + 12.006364, + 60.187822 + ], + [ + 12.006553, + 60.187827 + ], + [ + 12.00673, + 60.187832 + ], + [ + 12.006949, + 60.187838 + ], + [ + 12.007136, + 60.187843 + ], + [ + 12.007316, + 60.187848 + ], + [ + 12.007508, + 60.187853 + ], + [ + 12.0077, + 60.187859 + ], + [ + 12.007913, + 60.187865 + ], + [ + 12.008141, + 60.187871 + ], + [ + 12.008355, + 60.187877 + ], + [ + 12.00856, + 60.187882 + ], + [ + 12.008743, + 60.187887 + ], + [ + 12.008929, + 60.187893 + ], + [ + 12.009134, + 60.187898 + ], + [ + 12.009316, + 60.187902 + ], + [ + 12.009539, + 60.18791 + ], + [ + 12.009732, + 60.187914 + ], + [ + 12.009739, + 60.187914 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.000639, + 60.187817 + ], + [ + 12.000869, + 60.187794 + ], + [ + 12.001023, + 60.187779 + ], + [ + 12.001153, + 60.187766 + ], + [ + 12.001271, + 60.187757 + ], + [ + 12.001433, + 60.187746 + ], + [ + 12.001592, + 60.187739 + ], + [ + 12.001636, + 60.187737 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.08928393" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.008848, + 60.187926 + ], + [ + 12.009041, + 60.187933 + ], + [ + 12.00917, + 60.187937 + ], + [ + 12.009393, + 60.187943 + ], + [ + 12.009584, + 60.187948 + ], + [ + 12.009802, + 60.187955 + ], + [ + 12.010047, + 60.18796 + ], + [ + 12.010265, + 60.187967 + ], + [ + 12.010487, + 60.187973 + ], + [ + 12.010675, + 60.187979 + ], + [ + 12.010876, + 60.187984 + ], + [ + 12.011092, + 60.187989 + ], + [ + 12.011295, + 60.187994 + ], + [ + 12.011493, + 60.187999 + ], + [ + 12.011567, + 60.188001 + ], + [ + 12.011875, + 60.188011 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.00682, + 60.188114 + ], + [ + 12.007128, + 60.188198 + ], + [ + 12.007263, + 60.188232 + ], + [ + 12.007409, + 60.188272 + ], + [ + 12.007538, + 60.188305 + ], + [ + 12.007621, + 60.188324 + ], + [ + 12.007742, + 60.188348 + ], + [ + 12.007867, + 60.188369 + ], + [ + 12.008054, + 60.188397 + ], + [ + 12.00823, + 60.18842 + ], + [ + 12.008403, + 60.188435 + ], + [ + 12.008606, + 60.188446 + ], + [ + 12.008875, + 60.188458 + ], + [ + 12.009091, + 60.188467 + ], + [ + 12.009295, + 60.188476 + ], + [ + 12.009482, + 60.188483 + ], + [ + 12.009718, + 60.188487 + ], + [ + 12.009975, + 60.188486 + ], + [ + 12.010306, + 60.188482 + ], + [ + 12.010543, + 60.188472 + ], + [ + 12.010792, + 60.188458 + ], + [ + 12.011155, + 60.188423 + ], + [ + 12.011532, + 60.188385 + ], + [ + 12.012018, + 60.18833 + ], + [ + 12.012735, + 60.188263 + ], + [ + 12.012931, + 60.188241 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_aa747288-7a54-4d2e-bf85-42ebe9b73c89_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.006414, + 60.187937 + ], + [ + 12.006776, + 60.187958 + ], + [ + 12.006951, + 60.187964 + ], + [ + 12.007125, + 60.187968 + ], + [ + 12.007315, + 60.187972 + ], + [ + 12.007532, + 60.187977 + ], + [ + 12.007738, + 60.187982 + ], + [ + 12.007933, + 60.187986 + ], + [ + 12.008128, + 60.187992 + ], + [ + 12.008343, + 60.187998 + ], + [ + 12.008533, + 60.188002 + ], + [ + 12.009686, + 60.188036 + ], + [ + 12.009836, + 60.188041 + ], + [ + 12.010075, + 60.188049 + ], + [ + 12.010298, + 60.188058 + ], + [ + 12.010506, + 60.188064 + ], + [ + 12.010721, + 60.188071 + ], + [ + 12.011075, + 60.188082 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.011133, + 60.187913 + ], + [ + 12.01158, + 60.187912 + ], + [ + 12.011752, + 60.187907 + ], + [ + 12.012228, + 60.187904 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.016409, + 60.18823 + ], + [ + 12.016983, + 60.188261 + ], + [ + 12.017109, + 60.188274 + ], + [ + 12.017233, + 60.188288 + ], + [ + 12.017311, + 60.188297 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.008651, + 60.187846 + ], + [ + 12.008692, + 60.187847 + ], + [ + 12.008887, + 60.187853 + ], + [ + 12.009098, + 60.187858 + ], + [ + 12.009268, + 60.187865 + ], + [ + 12.009415, + 60.187867 + ], + [ + 12.009592, + 60.187871 + ], + [ + 12.009732, + 60.187874 + ], + [ + 12.009749, + 60.187875 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.009749, + 60.187875 + ], + [ + 12.00992, + 60.187881 + ], + [ + 12.010111, + 60.187886 + ], + [ + 12.010277, + 60.18789 + ], + [ + 12.010452, + 60.187894 + ], + [ + 12.010881, + 60.187907 + ], + [ + 12.010932, + 60.187908 + ], + [ + 12.010952, + 60.187909 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.010952, + 60.187909 + ], + [ + 12.011062, + 60.187912 + ], + [ + 12.011133, + 60.187913 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.34029524" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.004518, + 60.187842 + ], + [ + 12.004573, + 60.187844 + ], + [ + 12.00501, + 60.187855 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_eea127ef-584e-4de6-a558-2738b992f74b_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.009845, + 60.187917 + ], + [ + 12.010223, + 60.187917 + ], + [ + 12.010382, + 60.187916 + ], + [ + 12.010565, + 60.187912 + ], + [ + 12.010952, + 60.187909 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.007471, + 60.187763 + ], + [ + 12.007943, + 60.187794 + ], + [ + 12.00809, + 60.187808 + ], + [ + 12.008257, + 60.187824 + ], + [ + 12.008651, + 60.187846 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.008848, + 60.187926 + ], + [ + 12.009069, + 60.187925 + ], + [ + 12.009236, + 60.187919 + ], + [ + 12.009435, + 60.187917 + ], + [ + 12.009739, + 60.187914 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 12.005046, + 60.187857 + ], + [ + 12.004974, + 60.187855 + ] + ] + }, + "properties": { + "crs": "EPSG:4326", + "id": "_netelement_62769c33-c4ff-4cda-958c-3775012825d2-link_0.0" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_539ca81d-10cc-4c41-a39c-ced47b2f7be6", + "navigability": "both", + "netelementA": "_netelement_00b6f74b-7ca0-428e-a2d2-c2f5f5452310_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0a4319bf-b89f-4302-85db-dee03d887fde", + "navigability": "both", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.0", + "netelementB": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_32805aa3-03d5-46b6-9733-aa6d385a938c", + "navigability": "both", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.0", + "netelementB": "_netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.0_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151", + "navigability": "none", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0a7290b0-8d44-4c63-939d-b6360ed53752", + "navigability": "both", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652", + "netelementB": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.4024163", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "navigability": "none", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652", + "netelementB": "_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_5715701f-374a-4acb-b097-73673983df35", + "navigability": "both", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.4024163", + "netelementB": "_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_926997d9-f5ac-4112-a41d-d29a2d2fe4d7", + "navigability": "both", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.4024163", + "netelementB": "_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_c4e6f7ce-282d-41d6-b872-0c2f0a676941", + "navigability": "both", + "netelementA": "_netelement_d7a47da7-1b60-401f-987e-1e8341962cec_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.12461327", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4eb1d58f-1ac3-4244-a3cb-a97592188785", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "navigability": "none", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_2b27597a-fa5d-4401-8ba4-d1601d034518", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.11971913", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.18653556", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.11971913_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "navigability": "none", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.11971913", + "netelementB": "_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_732f881f-ef3c-46d9-9f07-b5d9671eef0a", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.18653556", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.27513392", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.18653556_netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0", + "navigability": "none", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.18653556", + "netelementB": "_netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4d5ea433-03e0-4eaa-8c4a-8a354bd1f394", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.27513392", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.42227724", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.27513392_netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0", + "navigability": "none", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.27513392", + "netelementB": "_netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_36aa6037-7e47-4b1c-a028-786a27a8fe33", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.42227724", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.52963664", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_65b83a8c-bd39-4719-b82d-7a526e953adc", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.52963664", + "netelementB": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_d3c8360c-887e-49b2-a169-3dc96e6bae72", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.52963664", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_7a514313-0dd2-4bdb-b5c2-b9778fc85275", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.95000637", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_ef62f922-455d-4955-8644-64115c996c28", + "navigability": "both", + "netelementA": "_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.18653556", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_95a8a5b0-1d97-452e-8cf3-834d01ff5010", + "navigability": "both", + "netelementA": "_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.86458099", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_52a60e14-9cf4-496e-9675-09cacb4850a9", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0_netelement_00b6f74b-7ca0-428e-a2d2-c2f5f5452310_0.0", + "navigability": "none", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0", + "netelementB": "_netelement_00b6f74b-7ca0-428e-a2d2-c2f5f5452310_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_on_node_ef55eda9-aac9-47f3-98ec-243eb06d0665", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0", + "netelementB": "_netelement_1b975e4c-b7ee-4386-b8b2-425e1b24e386_0.0", + "positionOnA": 0, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_24057e4e-4714-444e-9352-4db9f1c14841", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_3d314460-aaa5-4d88-a43a-0828bda00543", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0f6453b8-7291-409b-a6cf-81fce1620a70", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_b7f3ba57-b3ec-42f5-8ede-e8bc963b13ba", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.3813903", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_1768146f-404b-49f4-aac4-0ec5cdc41bb2", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.3813903", + "netelementB": "_netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_54c0293a-6e41-4dd8-8f60-61a2f0e0c9b6", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.3813903", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4072a4d7-e972-42d8-ba6d-daa67dbc3756", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.76414609", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_acf20bd1-8905-44ad-ae0a-3e599be34f0c", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.76414609", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.79667018", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.76414609_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "navigability": "none", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.76414609", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_50f768c0-02b8-41c2-805f-d182f64fc8e8", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.79667018", + "netelementB": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_795b478a-6de1-4000-bc6b-803bc1291b9c", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.79667018", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_ac5a9840-16e0-4278-a572-09fea0801947", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.86330064", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "navigability": "none", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_on_node_4bd14100-c80d-4ac4-8ecd-d63ba677c510", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.86330064", + "netelementB": "_netelement_eacc17c5-7cb5-4e88-88d1-d0363912e4e5_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_on_node_8b48cbec-1e11-4afa-901a-1fa6dd10a3ba", + "navigability": "both", + "netelementA": "_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.0", + "netelementB": "_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.543595", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_1d40468d-3bb7-4553-b5ca-6df87cfac3e9", + "navigability": "both", + "netelementA": "_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.543595", + "netelementB": "_netelement_00b6f74b-7ca0-428e-a2d2-c2f5f5452310_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0e46bc24-387b-4b3f-a313-add4a1e503c5", + "navigability": "both", + "netelementA": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.0", + "netelementB": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.14593799", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_63676b94-073b-4b45-a1af-c123f9a8750a", + "navigability": "both", + "netelementA": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.0", + "netelementB": "_netelement_aa747288-7a54-4d2e-bf85-42ebe9b73c89_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.0_netelement_9b7bb57f-92ee-4836-9d11-258ef154a18d_0.28937834", + "navigability": "none", + "netelementA": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.0", + "netelementB": "_netelement_9b7bb57f-92ee-4836-9d11-258ef154a18d_0.28937834", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_686e3867-e53a-4005-af38-f60fa7fd25d7", + "navigability": "both", + "netelementA": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.14593799", + "netelementB": "_netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_70cf621f-1509-42bf-b0fb-7de15667ba30", + "navigability": "both", + "netelementA": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.14593799", + "netelementB": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.40408531", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_14050ace-8df0-4dff-bae1-787efbda8200_0.0_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.12767833", + "navigability": "none", + "netelementA": "_netelement_14050ace-8df0-4dff-bae1-787efbda8200_0.0", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.12767833", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0edbe82d-c894-4ee4-9722-1868ebd0d8da", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.12461327", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0_netelement_d7a47da7-1b60-401f-987e-1e8341962cec_0.0", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0", + "netelementB": "_netelement_d7a47da7-1b60-401f-987e-1e8341962cec_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.08928393", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.08928393", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4b592ee9-a62f-4172-aa83-58baf07e7714", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.12461327", + "netelementB": "_netelement_eea127ef-584e-4de6-a558-2738b992f74b_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_5d27260f-0fd5-4a48-a5df-fd13d4af8632", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.24348413", + "netelementB": "_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_83a91d05-b047-4a90-8d11-135588e2abbf", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.24348413", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.33732172", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_168a243e-d902-4016-88a6-353750bbf8cd", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.33732172", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_a4675d80-1ff5-4090-893a-29da2ac9a0ef", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.33732172", + "netelementB": "_netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.33732172_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.33732172", + "netelementB": "_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_314802a1-914e-47e7-9431-7bb61e22c6f3", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.60869272", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_3c973a6b-e5d0-48cb-964f-891edde9a76c", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.60869272", + "netelementB": "_netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_75bde0b8-f209-4502-ae33-7adb375313d3", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.60869272", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_d6e5e5b3-a5c0-4604-8057-a0a06dc88117", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.95000637", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_307a204f-ebcc-4eed-974b-594c80d4e090", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.95000637", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_for_crossing_first_branch_e14ba638-9251-4b83-8729-136da5c725ec_307a204f-ebcc-4eed-974b-594c80d4e090_branch_e14ba638-9251-4b83-8729-136da5c725ec_cf6db05c-9711-460a-a784-6e573387ce62", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.95000637", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_for_crossing_third_branch_e14ba638-9251-4b83-8729-136da5c725ec_307a204f-ebcc-4eed-974b-594c80d4e090_branch_e14ba638-9251-4b83-8729-136da5c725ec_cf6db05c-9711-460a-a784-6e573387ce62", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.95000637", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.08950671", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4ee6801e-22fe-421a-83a2-55e9f7686523", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_for_crossing_fourth_branch_e14ba638-9251-4b83-8729-136da5c725ec_307a204f-ebcc-4eed-974b-594c80d4e090_branch_e14ba638-9251-4b83-8729-136da5c725ec_cf6db05c-9711-460a-a784-6e573387ce62", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0", + "positionOnA": 0, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_for_crossing_second_branch_e14ba638-9251-4b83-8729-136da5c725ec_307a204f-ebcc-4eed-974b-594c80d4e090_branch_e14ba638-9251-4b83-8729-136da5c725ec_cf6db05c-9711-460a-a784-6e573387ce62", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.08950671", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_on_node_b0e1946c-d991-4fe2-9efc-0b8acc725969", + "navigability": "both", + "netelementA": "_netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.0", + "netelementB": "_netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.34610038", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_b90e196f-105a-49db-a783-0f1944bc7eea", + "navigability": "both", + "netelementA": "_netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.34610038", + "netelementB": "_netelement_00b6f74b-7ca0-428e-a2d2-c2f5f5452310_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.34610038_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.543595", + "navigability": "none", + "netelementA": "_netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.34610038", + "netelementB": "_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.543595", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_7da06e7c-abb0-4a2d-b013-a8faa3a2c6aa", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.86330064", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_60fe15c0-106a-416c-bb63-e064ec638d38", + "navigability": "both", + "netelementA": "_netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.11971913", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "navigability": "none", + "netelementA": "_netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "navigability": "none", + "netelementA": "_netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0", + "netelementB": "_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_19e0c426-2844-4fdc-a5e4-d3c9aa075777", + "navigability": "both", + "netelementA": "_netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904", + "navigability": "none", + "netelementA": "_netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "navigability": "none", + "netelementA": "_netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0fc2d6f6-5349-4121-ae91-cedc362d853f", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.86458099", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "navigability": "none", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093", + "netelementB": "_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_45328533-ac8e-47bf-9ccf-323eae5da92a", + "navigability": "both", + "netelementA": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.08950671", + "netelementB": "_netelement_14050ace-8df0-4dff-bae1-787efbda8200_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_ca6825e1-e876-4d8e-b150-5ab218a65b20", + "navigability": "both", + "netelementA": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.08950671", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.12767833", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0364a341-f1bc-415f-9474-05cdea78c0aa", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.46861837", + "netelementB": "_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_315832c9-1165-4cb9-9d71-cfd10d757a8c", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.46861837", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.47492909", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_14bc3806-136c-4b1d-b53e-29cf4f2a7bdf", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.4553507", + "netelementB": "_netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_d0ae2ddc-728a-44c0-a728-238b22d7f689", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.4553507", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_cea51723-19c9-42ba-9dd6-009a23c5b147", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.47492909", + "netelementB": "_netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_e63e4446-23de-43c3-b235-b19306aca95e", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.47492909", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.47492909_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "navigability": "none", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.47492909", + "netelementB": "_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_57d9b84d-7537-4cdc-9abd-9fee71bfafef", + "navigability": "both", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_f3a79890-475a-4790-b88f-07e0e79bb1b4", + "navigability": "both", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "netelementB": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "navigability": "none", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_1abe77ee-831e-4644-a001-8a9f8c412a09", + "navigability": "both", + "netelementA": "_netelement_4626ff0b-5193-4f4a-847f-936894bcdfc6_0.0", + "netelementB": "_netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_b36d76a5-340f-4320-98aa-c2af303eccad", + "navigability": "both", + "netelementA": "_netelement_4626ff0b-5193-4f4a-847f-936894bcdfc6_0.0", + "netelementB": "_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_4626ff0b-5193-4f4a-847f-936894bcdfc6_0.0_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "navigability": "none", + "netelementA": "_netelement_4626ff0b-5193-4f4a-847f-936894bcdfc6_0.0", + "netelementB": "_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_3e53d8e8-d6a2-45ae-9339-cd5f52073ef6", + "navigability": "both", + "netelementA": "_netelement_49dc2660-09f0-41e1-82f9-f683d464847d_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.24348413", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_474118d8-6ce6-4276-9b97-15bcc55a2567", + "navigability": "both", + "netelementA": "_netelement_49dc2660-09f0-41e1-82f9-f683d464847d_0.0", + "netelementB": "_netelement_c0220d76-9945-458d-b27f-f06dd10ec53c_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_62769c33-c4ff-4cda-958c-3775012825d2-f2", + "navigability": "both", + "netelementA": "_netelement_49dc2660-09f0-41e1-82f9-f683d464847d_0.0", + "netelementB": "_netelement_62769c33-c4ff-4cda-958c-3775012825d2-link_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_18f241da-14a6-4346-9ef6-f1c1b20c9b22", + "navigability": "both", + "netelementA": "_netelement_c0220d76-9945-458d-b27f-f06dd10ec53c_0.0", + "netelementB": "_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_6c137ae7-22af-470b-8a4c-87f2389f46ca", + "navigability": "both", + "netelementA": "_netelement_c0220d76-9945-458d-b27f-f06dd10ec53c_0.0", + "netelementB": "_netelement_4626ff0b-5193-4f4a-847f-936894bcdfc6_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_c0220d76-9945-458d-b27f-f06dd10ec53c_0.0_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.24348413", + "navigability": "none", + "netelementA": "_netelement_c0220d76-9945-458d-b27f-f06dd10ec53c_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.24348413", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_1e48c811-e164-4679-aa33-a1c941063b97", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.79667018", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_f0739fc4-a96a-4854-b86f-67ba0db75770", + "navigability": "both", + "netelementA": "_netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.27513392", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "navigability": "none", + "netelementA": "_netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0", + "netelementB": "_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_cf6db05c-9711-460a-a784-6e573387ce62", + "navigability": "both", + "netelementA": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.08950671", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838", + "navigability": "none", + "netelementA": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_456b2831-6c24-4f09-821e-43c38d9cb5b8", + "navigability": "both", + "netelementA": "_netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.42227724", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.40408531", + "navigability": "none", + "netelementA": "_netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0", + "netelementB": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.40408531", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_27e4d452-3672-4fa4-8911-38e743d1a0f0", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.4553507", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_7146f562-e13b-4592-93c2-e49e88de3b09_0.0_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.1532445", + "navigability": "none", + "netelementA": "_netelement_7146f562-e13b-4592-93c2-e49e88de3b09_0.0", + "netelementB": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.1532445", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_5b9d107b-1b10-41da-b9e9-bbb38e486709", + "navigability": "both", + "netelementA": "_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "netelementB": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.4024163", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_175e0c15-3b4d-4a30-bb48-41ada3077352", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_260a9532-c311-4966-892d-95af1b78e7b4", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.08928393", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.0_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "navigability": "none", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_b3f1fa22-5114-4a4d-b5d1-9b907a342970", + "navigability": "both", + "netelementA": "_netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0", + "netelementB": "_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_f0e39ac0-aba4-40b7-a17f-e88ea648cc66", + "navigability": "both", + "netelementA": "_netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0", + "netelementB": "_netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "navigability": "none", + "netelementA": "_netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0", + "netelementB": "_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093", + "navigability": "none", + "netelementA": "_netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4ed3b377-823f-4329-ab62-93bba704f3d0", + "navigability": "both", + "netelementA": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.0", + "netelementB": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_a9efe211-1d76-4611-bfae-0a8eb7078e07", + "navigability": "both", + "netelementA": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.0", + "netelementB": "_netelement_a54d0a9c-d1f1-4e14-8528-6042a2967135_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_d09ee2b1-c333-4c4e-bca0-949a30251d32", + "navigability": "both", + "netelementA": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.76414609", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "navigability": "none", + "netelementA": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109_netelement_a54d0a9c-d1f1-4e14-8528-6042a2967135_0.0", + "navigability": "none", + "netelementA": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109", + "netelementB": "_netelement_a54d0a9c-d1f1-4e14-8528-6042a2967135_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_6b5981ab-c009-4220-b20e-12e2c0e0b298", + "navigability": "both", + "netelementA": "_netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0", + "netelementB": "_netelement_eea127ef-584e-4de6-a558-2738b992f74b_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652", + "navigability": "none", + "netelementA": "_netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0", + "netelementB": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.12461327", + "navigability": "none", + "netelementA": "_netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.12461327", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_9f80b3a0-3c7d-4cac-8242-46c956d5b2e6", + "navigability": "both", + "netelementA": "_netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.60869272", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "navigability": "none", + "netelementA": "_netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "navigability": "none", + "netelementA": "_netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0", + "netelementB": "_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_c7636416-9027-4234-bd60-b9e77c9c1b9a", + "navigability": "both", + "netelementA": "_netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.4553507", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "navigability": "none", + "netelementA": "_netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "navigability": "none", + "netelementA": "_netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_756bf129-d1bc-48f2-a1b4-1d1611784b79", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_a5c0bdbb-892b-46cf-96cf-ca18215f7eb3", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "netelementB": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_518d3541-9d0f-424e-af5c-9a5863061412", + "navigability": "both", + "netelementA": "_netelement_8eee1418-ee14-410b-b5af-0a44d63a03ee_0.0", + "netelementB": "_netelement_9b7bb57f-92ee-4836-9d11-258ef154a18d_0.28937834", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_62769c33-c4ff-4cda-958c-3775012825d2-f1", + "navigability": "both", + "netelementA": "_netelement_8eee1418-ee14-410b-b5af-0a44d63a03ee_0.0", + "netelementB": "_netelement_62769c33-c4ff-4cda-958c-3775012825d2-link_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_631e257e-78c8-4e3b-a1c5-c844a4b393a1", + "navigability": "both", + "netelementA": "_netelement_8eee1418-ee14-410b-b5af-0a44d63a03ee_0.0", + "netelementB": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_8eee1418-ee14-410b-b5af-0a44d63a03ee_0.0_netelement_49dc2660-09f0-41e1-82f9-f683d464847d_0.0", + "navigability": "none", + "netelementA": "_netelement_8eee1418-ee14-410b-b5af-0a44d63a03ee_0.0", + "netelementB": "_netelement_49dc2660-09f0-41e1-82f9-f683d464847d_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0be1857f-936f-4dc4-b9b8-865146c21728", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.86458099", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_9de80c99-c1c0-4d7a-8f8d-0924cc448531", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.86458099", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_3361fbcc-bf38-4939-a4dd-6ba2ba10d473", + "navigability": "both", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "netelementB": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011", + "navigability": "none", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879", + "navigability": "none", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "navigability": "none", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_on_node_4210dda2-0ad6-4978-82a6-0eb11f3b7439", + "navigability": "both", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "netelementB": "_netelement_c6dddb62-f3e5-4587-93fa-5747ef4c4919_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_bfea7479-6842-4afe-9c64-0a1b8c6e08c1", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "netelementB": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_e7e4e8ee-2ea3-42f4-a6bd-048cfb3022cf", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.3813903", + "navigability": "none", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.3813903", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_cad908f5-64a5-4b27-8632-36f6e5145ab1", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "navigability": "none", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904", + "netelementB": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_71d81434-c1e8-4f81-b5a8-f2900a7b9f59", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879", + "netelementB": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "navigability": "none", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879", + "netelementB": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_c2071e62-bc4c-47bd-aa6f-431d31c36930", + "navigability": "both", + "netelementA": "_netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.52963664", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.42227724", + "navigability": "none", + "netelementA": "_netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.42227724", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445", + "navigability": "none", + "netelementA": "_netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_c5422cd9-ac57-416d-a7e3-7a32c66e492f", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_bda2933f-7b8e-4719-8826-639fc54e3e76", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.46861837", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "navigability": "none", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151", + "netelementB": "_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_1cd2b7a9-4a3b-4fb1-b7e8-323aa24b5735", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.08928393", + "netelementB": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_5d0a6e63-72cd-4d8a-98ee-56f83b48f64f", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.08928393", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_8be03b2e-80c4-4a7d-a62d-ccec435612b8", + "navigability": "both", + "netelementA": "_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "netelementB": "_netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_cb4c9eeb-3ee0-41d8-a165-cd51a318c9ae", + "navigability": "both", + "netelementA": "_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "netelementB": "_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "navigability": "none", + "netelementA": "_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "netelementB": "_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_aa747288-7a54-4d2e-bf85-42ebe9b73c89_0.0_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.14593799", + "navigability": "none", + "netelementA": "_netelement_aa747288-7a54-4d2e-bf85-42ebe9b73c89_0.0", + "netelementB": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.14593799", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_dc23207c-aa9d-432a-b72e-c9ca87454135", + "navigability": "both", + "netelementA": "_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.11971913", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_d51dbff9-aa36-421e-a9fa-728624e5f691", + "navigability": "both", + "netelementA": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_414dca2e-d3d0-4073-bd8e-04bd59159fec", + "navigability": "both", + "netelementA": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.0", + "netelementB": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.1532445", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_47e373dc-162d-47fc-8371-74ba2d190806", + "navigability": "both", + "netelementA": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.0", + "netelementB": "_netelement_7146f562-e13b-4592-93c2-e49e88de3b09_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.0_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "navigability": "none", + "netelementA": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_200dbdf0-5a35-472d-94cf-2687b48d94ee", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_81df38fd-c364-4034-b8b1-6cffd164dfd1", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.34029524", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "navigability": "none", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "netelementB": "_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_40333632-aaf6-4991-9857-7a904f4a5487", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.34029524", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_a7c73ded-19ec-40cd-97b3-23203398b07f", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.34029524", + "netelementB": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_eea127ef-584e-4de6-a558-2738b992f74b_0.0_netelement_956249db-173f-4328-b790-e8fec5844933_0.0", + "navigability": "none", + "netelementA": "_netelement_eea127ef-584e-4de6-a558-2738b992f74b_0.0", + "netelementB": "_netelement_956249db-173f-4328-b790-e8fec5844933_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0278a6a4-9077-48d5-812d-5bc37494b1bd", + "navigability": "both", + "netelementA": "_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.34029524", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_9308ceeb-9455-435f-a6d8-3ded24ab6210", + "navigability": "both", + "netelementA": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "navigability": "none", + "netelementA": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.0", + "navigability": "none", + "netelementA": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_42bdad43-1355-4b97-af5a-9a6e132acf8c", + "navigability": "both", + "netelementA": "_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.46861837", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_62769c33-c4ff-4cda-958c-3775012825d2-f3", + "navigability": "both", + "netelementA": "_netelement_62769c33-c4ff-4cda-958c-3775012825d2-link_0.0", + "netelementB": "_netelement_eea127ef-584e-4de6-a558-2738b992f74b_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_62769c33-c4ff-4cda-958c-3775012825d2-f4", + "navigability": "both", + "netelementA": "_netelement_62769c33-c4ff-4cda-958c-3775012825d2-link_0.0", + "netelementB": "_netelement_956249db-173f-4328-b790-e8fec5844933_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_1d40468d-3bb7-4553-b5ca-6df87cfac3e9", + "navigability": "both", + "netelementA": "_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.543595", + "netelementB": "_netelement_00b6f74b-7ca0-428e-a2d2-c2f5f5452310_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_b90e196f-105a-49db-a783-0f1944bc7eea", + "navigability": "both", + "netelementA": "_netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.34610038", + "netelementB": "_netelement_00b6f74b-7ca0-428e-a2d2-c2f5f5452310_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0_netelement_00b6f74b-7ca0-428e-a2d2-c2f5f5452310_0.0", + "navigability": "none", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0", + "netelementB": "_netelement_00b6f74b-7ca0-428e-a2d2-c2f5f5452310_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_1cd2b7a9-4a3b-4fb1-b7e8-323aa24b5735", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.08928393", + "netelementB": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0a4319bf-b89f-4302-85db-dee03d887fde", + "navigability": "both", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.0", + "netelementB": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652", + "navigability": "none", + "netelementA": "_netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0", + "netelementB": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0a7290b0-8d44-4c63-939d-b6360ed53752", + "navigability": "both", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652", + "netelementB": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.4024163", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_5b9d107b-1b10-41da-b9e9-bbb38e486709", + "navigability": "both", + "netelementA": "_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "netelementB": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.4024163", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0_netelement_d7a47da7-1b60-401f-987e-1e8341962cec_0.0", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0", + "netelementB": "_netelement_d7a47da7-1b60-401f-987e-1e8341962cec_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_9de80c99-c1c0-4d7a-8f8d-0924cc448531", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.86458099", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838", + "navigability": "none", + "netelementA": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_60fe15c0-106a-416c-bb63-e064ec638d38", + "navigability": "both", + "netelementA": "_netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.11971913", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_dc23207c-aa9d-432a-b72e-c9ca87454135", + "navigability": "both", + "netelementA": "_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.11971913", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_2b27597a-fa5d-4401-8ba4-d1601d034518", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.11971913", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.18653556", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_ef62f922-455d-4955-8644-64115c996c28", + "navigability": "both", + "netelementA": "_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.18653556", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_732f881f-ef3c-46d9-9f07-b5d9671eef0a", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.18653556", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.27513392", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_f0739fc4-a96a-4854-b86f-67ba0db75770", + "navigability": "both", + "netelementA": "_netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.27513392", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_456b2831-6c24-4f09-821e-43c38d9cb5b8", + "navigability": "both", + "netelementA": "_netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.42227724", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4d5ea433-03e0-4eaa-8c4a-8a354bd1f394", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.27513392", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.42227724", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.42227724", + "navigability": "none", + "netelementA": "_netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.42227724", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_36aa6037-7e47-4b1c-a028-786a27a8fe33", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.42227724", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.52963664", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_c2071e62-bc4c-47bd-aa6f-431d31c36930", + "navigability": "both", + "netelementA": "_netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.52963664", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_d3c8360c-887e-49b2-a169-3dc96e6bae72", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.52963664", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.0_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "navigability": "none", + "netelementA": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.0", + "netelementB": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_b36d76a5-340f-4320-98aa-c2af303eccad", + "navigability": "both", + "netelementA": "_netelement_4626ff0b-5193-4f4a-847f-936894bcdfc6_0.0", + "netelementB": "_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.11971913_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "navigability": "none", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.11971913", + "netelementB": "_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "navigability": "none", + "netelementA": "_netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0", + "netelementB": "_netelement_2782db00-5a8e-42d2-aaf9-840ad42c1d52_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_b3f1fa22-5114-4a4d-b5d1-9b907a342970", + "navigability": "both", + "netelementA": "_netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0", + "netelementB": "_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_cb4c9eeb-3ee0-41d8-a165-cd51a318c9ae", + "navigability": "both", + "netelementA": "_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "netelementB": "_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "navigability": "none", + "netelementA": "_netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0", + "netelementB": "_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "navigability": "none", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093", + "netelementB": "_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_52a60e14-9cf4-496e-9675-09cacb4850a9", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_539ca81d-10cc-4c41-a39c-ced47b2f7be6", + "navigability": "both", + "netelementA": "_netelement_00b6f74b-7ca0-428e-a2d2-c2f5f5452310_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_24057e4e-4714-444e-9352-4db9f1c14841", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.0_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "navigability": "none", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_b7f3ba57-b3ec-42f5-8ede-e8bc963b13ba", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.3813903", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.3813903", + "navigability": "none", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.3813903", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_54c0293a-6e41-4dd8-8f60-61a2f0e0c9b6", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.3813903", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "navigability": "none", + "netelementA": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "navigability": "none", + "netelementA": "_netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4072a4d7-e972-42d8-ba6d-daa67dbc3756", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.54706999", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.76414609", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_d09ee2b1-c333-4c4e-bca0-949a30251d32", + "navigability": "both", + "netelementA": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.76414609", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_1e48c811-e164-4679-aa33-a1c941063b97", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.79667018", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_acf20bd1-8905-44ad-ae0a-3e599be34f0c", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.76414609", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.79667018", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_795b478a-6de1-4000-bc6b-803bc1291b9c", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.79667018", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011", + "navigability": "none", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_7da06e7c-abb0-4a2d-b013-a8faa3a2c6aa", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.86330064", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_ac5a9840-16e0-4278-a572-09fea0801947", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011", + "netelementB": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.86330064", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.34610038_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.543595", + "navigability": "none", + "netelementA": "_netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.34610038", + "netelementB": "_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.543595", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_on_node_8b48cbec-1e11-4afa-901a-1fa6dd10a3ba", + "navigability": "both", + "netelementA": "_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.0", + "netelementB": "_netelement_130241a2-8c75-4a60-8a5c-3cafa496c3ae_0.543595", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_631e257e-78c8-4e3b-a1c5-c844a4b393a1", + "navigability": "both", + "netelementA": "_netelement_8eee1418-ee14-410b-b5af-0a44d63a03ee_0.0", + "netelementB": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0e46bc24-387b-4b3f-a313-add4a1e503c5", + "navigability": "both", + "netelementA": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.0", + "netelementB": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.14593799", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_aa747288-7a54-4d2e-bf85-42ebe9b73c89_0.0_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.14593799", + "navigability": "none", + "netelementA": "_netelement_aa747288-7a54-4d2e-bf85-42ebe9b73c89_0.0", + "netelementB": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.14593799", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_70cf621f-1509-42bf-b0fb-7de15667ba30", + "navigability": "both", + "netelementA": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.14593799", + "netelementB": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.40408531", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.40408531", + "navigability": "none", + "netelementA": "_netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0", + "netelementB": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.40408531", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_45328533-ac8e-47bf-9ccf-323eae5da92a", + "navigability": "both", + "netelementA": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.08950671", + "netelementB": "_netelement_14050ace-8df0-4dff-bae1-787efbda8200_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_175e0c15-3b4d-4a30-bb48-41ada3077352", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0edbe82d-c894-4ee4-9722-1868ebd0d8da", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.12461327", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_c4e6f7ce-282d-41d6-b872-0c2f0a676941", + "navigability": "both", + "netelementA": "_netelement_d7a47da7-1b60-401f-987e-1e8341962cec_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.12461327", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.12461327", + "navigability": "none", + "netelementA": "_netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.12461327", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_3e53d8e8-d6a2-45ae-9339-cd5f52073ef6", + "navigability": "both", + "netelementA": "_netelement_49dc2660-09f0-41e1-82f9-f683d464847d_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.24348413", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_c0220d76-9945-458d-b27f-f06dd10ec53c_0.0_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.24348413", + "navigability": "none", + "netelementA": "_netelement_c0220d76-9945-458d-b27f-f06dd10ec53c_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.24348413", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_83a91d05-b047-4a90-8d11-135588e2abbf", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.24348413", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.33732172", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_168a243e-d902-4016-88a6-353750bbf8cd", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.33732172", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "navigability": "none", + "netelementA": "_netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "navigability": "none", + "netelementA": "_netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_314802a1-914e-47e7-9431-7bb61e22c6f3", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.45187372", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.60869272", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_9f80b3a0-3c7d-4cac-8242-46c956d5b2e6", + "navigability": "both", + "netelementA": "_netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.60869272", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_75bde0b8-f209-4502-ae33-7adb375313d3", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.60869272", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445", + "navigability": "none", + "netelementA": "_netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_7a514313-0dd2-4bdb-b5c2-b9778fc85275", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.66405391", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.95000637", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_d6e5e5b3-a5c0-4604-8057-a0a06dc88117", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.66125445", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.95000637", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_307a204f-ebcc-4eed-974b-594c80d4e090", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.95000637", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "navigability": "none", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838", + "netelementB": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_on_node_b0e1946c-d991-4fe2-9efc-0b8acc725969", + "navigability": "both", + "netelementA": "_netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.0", + "netelementB": "_netelement_1552ce6d-4eee-48b4-8480-bb7324cbf110_0.34610038", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_57d9b84d-7537-4cdc-9abd-9fee71bfafef", + "navigability": "both", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_756bf129-d1bc-48f2-a1b4-1d1611784b79", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "navigability": "none", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.80837011", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "navigability": "none", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.96284825", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_on_node_ef55eda9-aac9-47f3-98ec-243eb06d0665", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0", + "netelementB": "_netelement_1b975e4c-b7ee-4386-b8b2-425e1b24e386_0.0", + "positionOnA": 0, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_a4675d80-1ff5-4090-893a-29da2ac9a0ef", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.33732172", + "netelementB": "_netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_14bc3806-136c-4b1d-b53e-29cf4f2a7bdf", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.4553507", + "netelementB": "_netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_e63e4446-23de-43c3-b235-b19306aca95e", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.47492909", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093", + "navigability": "none", + "netelementA": "_netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_62769c33-c4ff-4cda-958c-3775012825d2-f4", + "navigability": "both", + "netelementA": "_netelement_62769c33-c4ff-4cda-958c-3775012825d2-link_0.0", + "netelementB": "_netelement_956249db-173f-4328-b790-e8fec5844933_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_eea127ef-584e-4de6-a558-2738b992f74b_0.0_netelement_956249db-173f-4328-b790-e8fec5844933_0.0", + "navigability": "none", + "netelementA": "_netelement_eea127ef-584e-4de6-a558-2738b992f74b_0.0", + "netelementB": "_netelement_956249db-173f-4328-b790-e8fec5844933_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_cf6db05c-9711-460a-a784-6e573387ce62", + "navigability": "both", + "netelementA": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.08950671", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_for_crossing_second_branch_e14ba638-9251-4b83-8729-136da5c725ec_307a204f-ebcc-4eed-974b-594c80d4e090_branch_e14ba638-9251-4b83-8729-136da5c725ec_cf6db05c-9711-460a-a784-6e573387ce62", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.08950671", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_for_crossing_third_branch_e14ba638-9251-4b83-8729-136da5c725ec_307a204f-ebcc-4eed-974b-594c80d4e090_branch_e14ba638-9251-4b83-8729-136da5c725ec_cf6db05c-9711-460a-a784-6e573387ce62", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.95000637", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.08950671", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_42bdad43-1355-4b97-af5a-9a6e132acf8c", + "navigability": "both", + "netelementA": "_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.46861837", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_bda2933f-7b8e-4719-8826-639fc54e3e76", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.46861837", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_a9efe211-1d76-4611-bfae-0a8eb7078e07", + "navigability": "both", + "netelementA": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.0", + "netelementB": "_netelement_a54d0a9c-d1f1-4e14-8528-6042a2967135_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109_netelement_a54d0a9c-d1f1-4e14-8528-6042a2967135_0.0", + "navigability": "none", + "netelementA": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109", + "netelementB": "_netelement_a54d0a9c-d1f1-4e14-8528-6042a2967135_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_27e4d452-3672-4fa4-8911-38e743d1a0f0", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.4553507", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_c7636416-9027-4234-bd60-b9e77c9c1b9a", + "navigability": "both", + "netelementA": "_netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.4553507", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_315832c9-1165-4cb9-9d71-cfd10d757a8c", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.46861837", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.47492909", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_3361fbcc-bf38-4939-a4dd-6ba2ba10d473", + "navigability": "both", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "netelementB": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_71d81434-c1e8-4f81-b5a8-f2900a7b9f59", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879", + "netelementB": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_6c137ae7-22af-470b-8a4c-87f2389f46ca", + "navigability": "both", + "netelementA": "_netelement_c0220d76-9945-458d-b27f-f06dd10ec53c_0.0", + "netelementB": "_netelement_4626ff0b-5193-4f4a-847f-936894bcdfc6_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_8eee1418-ee14-410b-b5af-0a44d63a03ee_0.0_netelement_49dc2660-09f0-41e1-82f9-f683d464847d_0.0", + "navigability": "none", + "netelementA": "_netelement_8eee1418-ee14-410b-b5af-0a44d63a03ee_0.0", + "netelementB": "_netelement_49dc2660-09f0-41e1-82f9-f683d464847d_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_474118d8-6ce6-4276-9b97-15bcc55a2567", + "navigability": "both", + "netelementA": "_netelement_49dc2660-09f0-41e1-82f9-f683d464847d_0.0", + "netelementB": "_netelement_c0220d76-9945-458d-b27f-f06dd10ec53c_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_cad908f5-64a5-4b27-8632-36f6e5145ab1", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_d51dbff9-aa36-421e-a9fa-728624e5f691", + "navigability": "both", + "netelementA": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.76414609_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "navigability": "none", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.76414609", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_1abe77ee-831e-4644-a001-8a9f8c412a09", + "navigability": "both", + "netelementA": "_netelement_4626ff0b-5193-4f4a-847f-936894bcdfc6_0.0", + "netelementB": "_netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.18653556_netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0", + "navigability": "none", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.18653556", + "netelementB": "_netelement_718b9616-34cb-4dcd-b6be-914d3379c149_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0be1857f-936f-4dc4-b9b8-865146c21728", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.86458099", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_for_crossing_first_branch_e14ba638-9251-4b83-8729-136da5c725ec_307a204f-ebcc-4eed-974b-594c80d4e090_branch_e14ba638-9251-4b83-8729-136da5c725ec_cf6db05c-9711-460a-a784-6e573387ce62", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.95000637", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_for_crossing_fourth_branch_e14ba638-9251-4b83-8729-136da5c725ec_307a204f-ebcc-4eed-974b-594c80d4e090_branch_e14ba638-9251-4b83-8729-136da5c725ec_cf6db05c-9711-460a-a784-6e573387ce62", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.0", + "positionOnA": 0, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_ca6825e1-e876-4d8e-b150-5ab218a65b20", + "navigability": "both", + "netelementA": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.08950671", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.12767833", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_14050ace-8df0-4dff-bae1-787efbda8200_0.0_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.12767833", + "navigability": "none", + "netelementA": "_netelement_14050ace-8df0-4dff-bae1-787efbda8200_0.0", + "netelementB": "_netelement_513d28f4-b864-4171-a553-9574c542ca2c_0.12767833", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_686e3867-e53a-4005-af38-f60fa7fd25d7", + "navigability": "both", + "netelementA": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.14593799", + "netelementB": "_netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.27513392_netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0", + "navigability": "none", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.27513392", + "netelementB": "_netelement_d7c13ace-efcb-467d-bd7b-02077fb3f247_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_414dca2e-d3d0-4073-bd8e-04bd59159fec", + "navigability": "both", + "netelementA": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.0", + "netelementB": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.1532445", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_7146f562-e13b-4592-93c2-e49e88de3b09_0.0_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.1532445", + "navigability": "none", + "netelementA": "_netelement_7146f562-e13b-4592-93c2-e49e88de3b09_0.0", + "netelementB": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.1532445", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_e7e4e8ee-2ea3-42f4-a6bd-048cfb3022cf", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "navigability": "none", + "netelementA": "_netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "navigability": "none", + "netelementA": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.39951469", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_47e373dc-162d-47fc-8371-74ba2d190806", + "navigability": "both", + "netelementA": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.0", + "netelementB": "_netelement_7146f562-e13b-4592-93c2-e49e88de3b09_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_5d27260f-0fd5-4a48-a5df-fd13d4af8632", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.24348413", + "netelementB": "_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "navigability": "none", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.11093652", + "netelementB": "_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.33732172_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.33732172", + "netelementB": "_netelement_5811e448-ce92-4a2a-8d7d-4b60a4607a39_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_518d3541-9d0f-424e-af5c-9a5863061412", + "navigability": "both", + "netelementA": "_netelement_8eee1418-ee14-410b-b5af-0a44d63a03ee_0.0", + "netelementB": "_netelement_9b7bb57f-92ee-4836-9d11-258ef154a18d_0.28937834", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.0_netelement_9b7bb57f-92ee-4836-9d11-258ef154a18d_0.28937834", + "navigability": "none", + "netelementA": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.0", + "netelementB": "_netelement_9b7bb57f-92ee-4836-9d11-258ef154a18d_0.28937834", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_on_node_4bd14100-c80d-4ac4-8ecd-d63ba677c510", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.86330064", + "netelementB": "_netelement_eacc17c5-7cb5-4e88-88d1-d0363912e4e5_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_3d314460-aaa5-4d88-a43a-0828bda00543", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_cea51723-19c9-42ba-9dd6-009a23c5b147", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.47492909", + "netelementB": "_netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4ed3b377-823f-4329-ab62-93bba704f3d0", + "navigability": "both", + "netelementA": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.0", + "netelementB": "_netelement_7326b7a0-3031-45ea-949b-198241c2bd70_0.76311109", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_32805aa3-03d5-46b6-9733-aa6d385a938c", + "navigability": "both", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.0", + "netelementB": "_netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_8be03b2e-80c4-4a7d-a62d-ccec435612b8", + "navigability": "both", + "netelementA": "_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "netelementB": "_netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_f0e39ac0-aba4-40b7-a17f-e88ea648cc66", + "navigability": "both", + "netelementA": "_netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0", + "netelementB": "_netelement_81fd6446-78ff-4b48-a517-f4023e185feb_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_1768146f-404b-49f4-aac4-0ec5cdc41bb2", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.3813903", + "netelementB": "_netelement_bea5804e-a3d7-4f67-8787-b4112c31212c_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4eb1d58f-1ac3-4244-a3cb-a97592188785", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.8714838", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4ee6801e-22fe-421a-83a2-55e9f7686523", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.96186922", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "navigability": "none", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_on_node_4210dda2-0ad6-4978-82a6-0eb11f3b7439", + "navigability": "both", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "netelementB": "_netelement_c6dddb62-f3e5-4587-93fa-5747ef4c4919_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0fc2d6f6-5349-4121-ae91-cedc362d853f", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.52496093", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.86458099", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_95a8a5b0-1d97-452e-8cf3-834d01ff5010", + "navigability": "both", + "netelementA": "_netelement_aae4e474-79be-4ed1-a966-a1fbe021cdae_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.86458099", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_50f768c0-02b8-41c2-805f-d182f64fc8e8", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.79667018", + "netelementB": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_a5c0bdbb-892b-46cf-96cf-ca18215f7eb3", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.93523613", + "netelementB": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_f3a79890-475a-4790-b88f-07e0e79bb1b4", + "navigability": "both", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "netelementB": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0f6453b8-7291-409b-a6cf-81fce1620a70", + "navigability": "both", + "netelementA": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_d0ae2ddc-728a-44c0-a728-238b22d7f689", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.4553507", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904", + "navigability": "none", + "netelementA": "_netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0", + "netelementB": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_40333632-aaf6-4991-9857-7a904f4a5487", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.34029524", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879", + "navigability": "none", + "netelementA": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_3c973a6b-e5d0-48cb-964f-891edde9a76c", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.60869272", + "netelementB": "_netelement_a90fec12-25bc-4e28-84d3-31596e0fcc6c_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.0", + "navigability": "none", + "netelementA": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_5d0a6e63-72cd-4d8a-98ee-56f83b48f64f", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.08928393", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.0_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151", + "navigability": "none", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_260a9532-c311-4966-892d-95af1b78e7b4", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.08928393", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.08928393", + "navigability": "none", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.0", + "netelementB": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.08928393", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_926997d9-f5ac-4112-a41d-d29a2d2fe4d7", + "navigability": "both", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.4024163", + "netelementB": "_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "navigability": "none", + "netelementA": "_netelement_6ffafb60-df00-4dba-805b-c460839400d8_0.0", + "netelementB": "_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_63676b94-073b-4b45-a1af-c123f9a8750a", + "navigability": "both", + "netelementA": "_netelement_139385a4-d343-441f-a6de-d6f52fee0dce_0.0", + "netelementB": "_netelement_aa747288-7a54-4d2e-bf85-42ebe9b73c89_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_18f241da-14a6-4346-9ef6-f1c1b20c9b22", + "navigability": "both", + "netelementA": "_netelement_c0220d76-9945-458d-b27f-f06dd10ec53c_0.0", + "netelementB": "_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "navigability": "none", + "netelementA": "_netelement_178a77fe-1208-4b8e-9359-f56b6ed686e4_0.0", + "netelementB": "_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_4626ff0b-5193-4f4a-847f-936894bcdfc6_0.0_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "navigability": "none", + "netelementA": "_netelement_4626ff0b-5193-4f4a-847f-936894bcdfc6_0.0", + "netelementB": "_netelement_b82917a8-0272-46d2-8e7f-9303e9496ada_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_a7c73ded-19ec-40cd-97b3-23203398b07f", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.34029524", + "netelementB": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "navigability": "none", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.46909904", + "netelementB": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "navigability": "none", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.35539879", + "netelementB": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_65b83a8c-bd39-4719-b82d-7a526e953adc", + "navigability": "both", + "netelementA": "_netelement_083b4f07-1323-4fa1-b1b9-64e8ce068884_0.52963664", + "netelementB": "_netelement_bcafadd3-6f40-4562-8ce5-4f06f31da62c_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_9308ceeb-9455-435f-a6d8-3ded24ab6210", + "navigability": "both", + "netelementA": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_c5422cd9-ac57-416d-a7e3-7a32c66e492f", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "navigability": "none", + "netelementA": "_netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_19e0c426-2844-4fdc-a5e4-d3c9aa075777", + "navigability": "both", + "netelementA": "_netelement_bb2f5bf3-d844-45d1-88e2-b19f78e837aa_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_200dbdf0-5a35-472d-94cf-2687b48d94ee", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0278a6a4-9077-48d5-812d-5bc37494b1bd", + "navigability": "both", + "netelementA": "_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.34029524", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_81df38fd-c364-4034-b8b1-6cffd164dfd1", + "navigability": "both", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "netelementB": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.34029524", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_4b592ee9-a62f-4172-aa83-58baf07e7714", + "navigability": "both", + "netelementA": "_netelement_1515717a-0bb3-4161-9b0b-229abc1fcc2b_0.12461327", + "netelementB": "_netelement_eea127ef-584e-4de6-a558-2738b992f74b_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_62769c33-c4ff-4cda-958c-3775012825d2-f3", + "navigability": "both", + "netelementA": "_netelement_62769c33-c4ff-4cda-958c-3775012825d2-link_0.0", + "netelementB": "_netelement_eea127ef-584e-4de6-a558-2738b992f74b_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_6b5981ab-c009-4220-b20e-12e2c0e0b298", + "navigability": "both", + "netelementA": "_netelement_ff1fd959-89d3-4ff4-abe1-6564e4fca9dc_0.0", + "netelementB": "_netelement_eea127ef-584e-4de6-a558-2738b992f74b_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_0364a341-f1bc-415f-9474-05cdea78c0aa", + "navigability": "both", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.46861837", + "netelementB": "_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "navigability": "none", + "netelementA": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "netelementB": "_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.47492909_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "navigability": "none", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.47492909", + "netelementB": "_netelement_e238e806-82cf-48c6-bc96-4204ef09eccb_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_bfea7479-6842-4afe-9c64-0a1b8c6e08c1", + "navigability": "both", + "netelementA": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "netelementB": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_5715701f-374a-4acb-b097-73673983df35", + "navigability": "both", + "netelementA": "_netelement_01fadff1-852a-4137-b141-867cfc91d09a_0.4024163", + "netelementB": "_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "positionOnA": 1, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "navigability": "none", + "netelementA": "_netelement_d5b44b60-686f-465f-b044-f7ca3acb0362_0.0", + "netelementB": "_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_not_navigable__netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "navigability": "none", + "netelementA": "_netelement_f9c4d558-c756-469d-a9f9-f6ce4c4506c2_0.13198151", + "netelementB": "_netelement_f23eb963-e5cd-4465-a88c-3fda763244d9_0.0", + "positionOnA": 1, + "positionOnB": 1, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_62769c33-c4ff-4cda-958c-3775012825d2-f1", + "navigability": "both", + "netelementA": "_netelement_8eee1418-ee14-410b-b5af-0a44d63a03ee_0.0", + "netelementB": "_netelement_62769c33-c4ff-4cda-958c-3775012825d2-link_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + }, + { + "type": "Feature", + "geometry": null, + "properties": { + "id": "_netrelation_62769c33-c4ff-4cda-958c-3775012825d2-f2", + "navigability": "both", + "netelementA": "_netelement_49dc2660-09f0-41e1-82f9-f683d464847d_0.0", + "netelementB": "_netelement_62769c33-c4ff-4cda-958c-3775012825d2-link_0.0", + "positionOnA": 0, + "positionOnB": 0, + "type": "netrelation" + } + } + ] +} \ No newline at end of file diff --git a/test-data/rinf-kvg/path-projected-detail1.png b/test-data/rinf-kvg/path-projected-detail1.png new file mode 100644 index 0000000..f7501f8 Binary files /dev/null and b/test-data/rinf-kvg/path-projected-detail1.png differ diff --git a/test-data/rinf-kvg/path-projected-detail2.png b/test-data/rinf-kvg/path-projected-detail2.png new file mode 100644 index 0000000..feb5137 Binary files /dev/null and b/test-data/rinf-kvg/path-projected-detail2.png differ diff --git a/test-data/rinf-kvg/path-projected-detail3.png b/test-data/rinf-kvg/path-projected-detail3.png new file mode 100644 index 0000000..02b22f1 Binary files /dev/null and b/test-data/rinf-kvg/path-projected-detail3.png differ diff --git a/test-data/rinf-kvg/path-projected.geojson b/test-data/rinf-kvg/path-projected.geojson new file mode 100644 index 0000000..9d68c90 --- /dev/null +++ b/test-data/rinf-kvg/path-projected.geojson @@ -0,0 +1,2985 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.986762947020871, + 60.190479177230806 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 226.34478451836716, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0", + "original_fid": "1", + "original_lat": 60.1904568296714, + "original_lon": 11.986731368455182, + "original_time": "2026-05-25T13:09:15+02:00", + "projection_distance_meters": 3.0367649549132123 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.986976308433164, + 60.19040606909628 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 240.66968923426245, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0", + "original_fid": "2", + "original_lat": 60.190384377710934, + "original_lon": 11.98694690632874, + "original_time": "2026-05-25T13:09:14+02:00", + "projection_distance_meters": 2.908456266043556 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.98715438808299, + 60.19034689705117 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 252.51004446206707, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0", + "original_fid": "3", + "original_lat": 60.19031610214823, + "original_lon": 11.987113462024588, + "original_time": "2026-05-25T13:09:13+02:00", + "projection_distance_meters": 4.104066127106043 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.987370619805679, + 60.190276545823174 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 266.79528933772355, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0", + "original_fid": "4", + "original_lat": 60.19024964298016, + "original_lon": 11.987335713245637, + "original_time": "2026-05-25T13:09:12+02:00", + "projection_distance_meters": 3.5597756661466873 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.987561320424305, + 60.190217710314826 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 279.20390449373866, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.0", + "original_fid": "5", + "original_lat": 60.19018401652277, + "original_lon": 11.987521018704935, + "original_time": "2026-05-25T13:09:11+02:00", + "projection_distance_meters": 4.358887257972013 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.987813389244302, + 60.190141083226706 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 0.41251029168573056, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "6", + "original_lat": 60.19017944571346, + "original_lon": 11.987859958664933, + "original_time": "2026-05-25T13:09:10+02:00", + "projection_distance_meters": 4.982279993057557 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.987998611061654, + 60.19008758861819 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 12.253886387270144, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "7", + "original_lat": 60.190123155755, + "original_lon": 11.988039941847278, + "original_time": "2026-05-25T13:09:09+02:00", + "projection_distance_meters": 4.567371738737249 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.988263345881025, + 60.190012012363745 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 29.129598215511034, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "8", + "original_lat": 60.19004959065715, + "original_lon": 11.98830473417956, + "original_time": "2026-05-25T13:09:08+02:00", + "projection_distance_meters": 4.763856214751768 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.988566654609865, + 60.18993122663606 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 48.151370773285365, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "9", + "original_lat": 60.189981046110866, + "original_lon": 11.988619342631049, + "original_time": "2026-05-25T13:09:07+02:00", + "projection_distance_meters": 6.258645279680024 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.98887553755228, + 60.18985199930875 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 67.36473667791908, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "10", + "original_lat": 60.18989438195668, + "original_lon": 11.988919961151272, + "original_time": "2026-05-25T13:09:06+02:00", + "projection_distance_meters": 5.314147803871315 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.989205723472905, + 60.18977223632908 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 87.66171066505417, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "11", + "original_lat": 60.18980130705454, + "original_lon": 11.989232334886475, + "original_time": "2026-05-25T13:09:05+02:00", + "projection_distance_meters": 3.5514973082433507 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.989477848382995, + 60.18970919855413 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 104.25862844078794, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "12", + "original_lat": 60.18974279230423, + "original_lon": 11.989510831735322, + "original_time": "2026-05-25T13:09:04+02:00", + "projection_distance_meters": 4.156676208491586 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.98974254170069, + 60.189650049922214 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 120.30242327482313, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "13", + "original_lat": 60.189681627711366, + "original_lon": 11.989770576430503, + "original_time": "2026-05-25T13:09:03+02:00", + "projection_distance_meters": 3.8380741492962986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.989978342063862, + 60.18960025864328 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 134.464468049879, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "14", + "original_lat": 60.18957599948558, + "original_lon": 11.989958403404312, + "original_time": "2026-05-25T13:09:02+02:00", + "projection_distance_meters": 2.913984188258286 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.990224322380822, + 60.18955073552383 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 149.134718780753, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "15", + "original_lat": 60.18951232379475, + "original_lon": 11.990193237512647, + "original_time": "2026-05-25T13:09:01+02:00", + "projection_distance_meters": 4.6038863801769425 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.990845980863819, + 60.18943433148481 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 185.85966463323837, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "16", + "original_lat": 60.18945424898965, + "original_lon": 11.990860478783338, + "original_time": "2026-05-25T13:09:00+02:00", + "projection_distance_meters": 2.35527271261282 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.991116592950387, + 60.18938759699261 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 201.69667279665313, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "17", + "original_lat": 60.18937189704203, + "original_lon": 11.991105949894129, + "original_time": "2026-05-25T13:08:59+02:00", + "projection_distance_meters": 1.842229287284795 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.991859653254918, + 60.18927377143941 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 244.68477503098563, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "18", + "original_lat": 60.189305321710314, + "original_lon": 11.991877302019638, + "original_time": "2026-05-25T13:08:58+02:00", + "projection_distance_meters": 3.6413627450824966 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.99229601105337, + 60.189214879766624 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 269.67983425795603, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "19", + "original_lat": 60.18916430146293, + "original_lon": 11.99227167438182, + "original_time": "2026-05-25T13:08:57+02:00", + "projection_distance_meters": 5.782724469862908 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.992823872895704, + 60.189153455864556 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 299.6487057822699, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "20", + "original_lat": 60.189176174241595, + "original_lon": 11.992834219193009, + "original_time": "2026-05-25T13:08:56+02:00", + "projection_distance_meters": 2.59010708083909 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.993086193654324, + 60.18912457887407 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 314.50094688806485, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "21", + "original_lat": 60.189078766370805, + "original_lon": 11.99306597201828, + "original_time": "2026-05-25T13:08:55+02:00", + "projection_distance_meters": 5.215330851188183 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.993428969444441, + 60.18908870305555 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 333.8652114676557, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "22", + "original_lat": 60.1890555066167, + "original_lon": 11.993415537632965, + "original_time": "2026-05-25T13:08:54+02:00", + "projection_distance_meters": 3.765217688672978 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.99360002289827, + 60.18907142017013 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 343.51430669783997, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "23", + "original_lat": 60.189021089314586, + "original_lon": 11.993578989009665, + "original_time": "2026-05-25T13:08:53+02:00", + "projection_distance_meters": 5.716053758625855 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.99396153375347, + 60.18903392182656 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 363.92880624870764, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "24", + "original_lat": 60.18901873435717, + "original_lon": 11.993955138421464, + "original_time": "2026-05-25T13:08:52+02:00", + "projection_distance_meters": 1.7253795595973735 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.994303358765112, + 60.188999812020626 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 383.2018445702051, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "25", + "original_lat": 60.18898641312382, + "original_lon": 11.994297707127531, + "original_time": "2026-05-25T13:08:51+02:00", + "projection_distance_meters": 1.5222952419626707 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.994898938389257, + 60.18893944221649 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 416.80315626931656, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "26", + "original_lat": 60.18891772363676, + "original_lon": 11.994889925454023, + "original_time": "2026-05-25T13:08:50+02:00", + "projection_distance_meters": 2.465858212051658 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.995385900717288, + 60.18888602416169 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 444.3703397690224, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "27", + "original_lat": 60.188854603409574, + "original_lon": 11.995371591505252, + "original_time": "2026-05-25T13:08:49+02:00", + "projection_distance_meters": 3.582257229800133 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.995932147630041, + 60.18881357918463 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 475.6350295215603, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "28", + "original_lat": 60.188774897879675, + "original_lon": 11.995907272172529, + "original_time": "2026-05-25T13:08:48+02:00", + "projection_distance_meters": 4.5156403302439365 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.99664072137395, + 60.18868858444122 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 517.2161957534472, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "29", + "original_lat": 60.188636379713024, + "original_lon": 11.996598002053236, + "original_time": "2026-05-25T13:08:47+02:00", + "projection_distance_meters": 6.266880744788485 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.997279907290528, + 60.188535165479564 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 556.4709206370492, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "30", + "original_lat": 60.18851095813092, + "original_lon": 11.997252902274674, + "original_time": "2026-05-25T13:08:46+02:00", + "projection_distance_meters": 3.077994178794078 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.997540922704705, + 60.18846283188758 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 572.9905588085688, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "31", + "original_lat": 60.18843751416576, + "original_lon": 11.997511503453657, + "original_time": "2026-05-25T13:08:45+02:00", + "projection_distance_meters": 3.2511957016699657 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.997874601427961, + 60.18836698809541 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 594.2939011770561, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "32", + "original_lat": 60.18836184056702, + "original_lon": 11.997868616702345, + "original_time": "2026-05-25T13:08:44+02:00", + "projection_distance_meters": 0.6611153278216484 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.998130549720036, + 60.188293512194704 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 610.6323803442685, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "33", + "original_lat": 60.18827348297139, + "original_lon": 11.998107345124264, + "original_time": "2026-05-25T13:08:43+02:00", + "projection_distance_meters": 2.570154999731738 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.998227897634512, + 60.188265637241216 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 616.8426522057093, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "34", + "original_lat": 60.18826763013846, + "original_lon": 11.998230206478832, + "original_time": "2026-05-25T13:08:42+02:00", + "projection_distance_meters": 0.2557290884457992 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.998353032594585, + 60.18823145220037 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 624.7372197319565, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.11291494", + "original_fid": "35", + "original_lat": 60.18824456533492, + "original_lon": 11.998366921396649, + "original_time": "2026-05-25T13:08:41+02:00", + "projection_distance_meters": 1.6479066228256625 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.998616448682652, + 60.18816284663783 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 8.694113284619743, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "original_fid": "36", + "original_lat": 60.18818763431871, + "original_lon": 11.998642449697543, + "original_time": "2026-05-25T13:08:40+02:00", + "projection_distance_meters": 3.1085401617183366 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.998845636028515, + 60.18810342769631 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 22.98310268514554, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "original_fid": "37", + "original_lat": 60.18811516327021, + "original_lon": 11.998857945993532, + "original_time": "2026-05-25T13:08:39+02:00", + "projection_distance_meters": 1.4717183182875133 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.998999587583429, + 60.18806679648564 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 32.41860050796677, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "original_fid": "38", + "original_lat": 60.1880869411917, + "original_lon": 11.99901886807626, + "original_time": "2026-05-25T13:08:38+02:00", + "projection_distance_meters": 2.480644302171445 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.999171907249726, + 60.188026032693614 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 42.968054252861236, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "original_fid": "39", + "original_lat": 60.1880281277388, + "original_lon": 11.999173912409734, + "original_time": "2026-05-25T13:08:37+02:00", + "projection_distance_meters": 0.25798638607432445 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.999278697613907, + 60.188001088290754 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 49.491263714696544, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "original_fid": "40", + "original_lat": 60.18799063756568, + "original_lon": 11.999269205609226, + "original_time": "2026-05-25T13:08:36+02:00", + "projection_distance_meters": 1.2750476651172171 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.999531056309424, + 60.18794509256951 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 64.76841777438818, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.35659538", + "original_fid": "41", + "original_lat": 60.18793272925576, + "original_lon": 11.999519962262298, + "original_time": "2026-05-25T13:08:35+02:00", + "projection_distance_meters": 1.5053367822960113 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.99973768781332, + 60.187902367728206 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 6.975718681764187, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "42", + "original_lat": 60.18790471504835, + "original_lon": 11.999739514153289, + "original_time": "2026-05-25T13:08:34+02:00", + "projection_distance_meters": 0.2798569624625333 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.999918649823341, + 60.18786981727097 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 17.61679275510239, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "43", + "original_lat": 60.1878850627881, + "original_lon": 11.999928980699652, + "original_time": "2026-05-25T13:08:33+02:00", + "projection_distance_meters": 1.788841811466635 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.00006116987052, + 60.18784665901144 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 25.90694248940722, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "44", + "original_lat": 60.18784673553932, + "original_lon": 12.000061214420343, + "original_time": "2026-05-25T13:08:32+02:00", + "projection_distance_meters": 0.008858740450698996 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.000210015619222, + 60.18782629228009 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 34.44335766883631, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "45", + "original_lat": 60.18783558462808, + "original_lon": 12.000214438623436, + "original_time": "2026-05-25T13:08:31+02:00", + "projection_distance_meters": 1.0617997061478457 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.000315520876914, + 60.187813499892535 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 40.44685653949886, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "46", + "original_lat": 60.18781028833754, + "original_lon": 12.000313928538583, + "original_time": "2026-05-25T13:08:30+02:00", + "projection_distance_meters": 0.36779841023008164 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.00055011529372, + 60.187785386549066 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 53.78726223388744, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "47", + "original_lat": 60.18779105273585, + "original_lon": 12.00055278982272, + "original_time": "2026-05-25T13:08:29+02:00", + "projection_distance_meters": 0.6471676330151832 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.000711957902258, + 60.187765574170804 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 63.00147762579323, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "48", + "original_lat": 60.18775530286726, + "original_lon": 12.000706851771431, + "original_time": "2026-05-25T13:08:28+02:00", + "projection_distance_meters": 1.1764837079394705 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.000939623700868, + 60.18773760011237 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 75.9659203415338, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "49", + "original_lat": 60.18773014359459, + "original_lon": 12.000935916878577, + "original_time": "2026-05-25T13:08:27+02:00", + "projection_distance_meters": 0.8540757539483143 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.001126985695732, + 60.1877150433802 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 86.62480747138511, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "50", + "original_lat": 60.18771202267704, + "original_lon": 12.00112552169194, + "original_time": "2026-05-25T13:08:26+02:00", + "projection_distance_meters": 0.34550021596550345 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.001281946236652, + 60.187696480398735 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 95.4364672341243, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "51", + "original_lat": 60.18767947771178, + "original_lon": 12.001273705788153, + "original_time": "2026-05-25T13:08:25+02:00", + "projection_distance_meters": 1.944723235638264 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.00144177361991, + 60.18767781074546 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 104.51295820113884, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "52", + "original_lat": 60.18767264555304, + "original_lon": 12.001439662767767, + "original_time": "2026-05-25T13:08:24+02:00", + "projection_distance_meters": 0.5860784716532157 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.001618726622711, + 60.18765993797123 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 114.4950968908243, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "53", + "original_lat": 60.18765480291248, + "original_lon": 12.00161695310457, + "original_time": "2026-05-25T13:08:23+02:00", + "projection_distance_meters": 0.5793494741818854 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.001831910527535, + 60.18764289146608 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 126.4342101391019, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "54", + "original_lat": 60.18764225516334, + "original_lon": 12.00183174503288, + "original_time": "2026-05-25T13:08:22+02:00", + "projection_distance_meters": 0.07134278165240121 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.002046552831652, + 60.18763140224611 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 138.37011389125922, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "55", + "original_lat": 60.187632773146646, + "original_lon": 12.002046816946065, + "original_time": "2026-05-25T13:08:21+02:00", + "projection_distance_meters": 0.15313504192982696 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.002363057509998, + 60.187624 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 155.89515967179332, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "56", + "original_lat": 60.187616389441054, + "original_lon": 12.002363057509998, + "original_time": "2026-05-25T13:08:20+02:00", + "projection_distance_meters": 0.8462567125907303 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.002573635315748, + 60.18762572216505 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 167.53938142825342, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "57", + "original_lat": 60.18762544257288, + "original_lon": 12.002573653414569, + "original_time": "2026-05-25T13:08:19+02:00", + "projection_distance_meters": 0.031105369790024776 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.002745221612642, + 60.18762901513489 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 177.0324099079725, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "58", + "original_lat": 60.187621535318925, + "original_lon": 12.002746046936943, + "original_time": "2026-05-25T13:08:18+02:00", + "projection_distance_meters": 0.8329692321843535 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.00299044031496, + 60.18763590812563 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 190.6102198718549, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "59", + "original_lat": 60.18763288634134, + "original_lon": 12.002990786219517, + "original_time": "2026-05-25T13:08:17+02:00", + "projection_distance_meters": 0.33655123209793664 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.003153908123265, + 60.1876405332095 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 199.66164607823538, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "60", + "original_lat": 60.1876353200493, + "original_lon": 12.003154504875369, + "original_time": "2026-05-25T13:08:16+02:00", + "projection_distance_meters": 0.5806157295683853 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.003477554150773, + 60.18764969030312 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 217.5823496428601, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "61", + "original_lat": 60.18765412378001, + "original_lon": 12.003477046648683, + "original_time": "2026-05-25T13:08:15+02:00", + "projection_distance_meters": 0.4937785000484728 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.003826228376496, + 60.1876595555332 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 236.8888972557337, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "62", + "original_lat": 60.187660034232366, + "original_lon": 12.003826173579569, + "original_time": "2026-05-25T13:08:14+02:00", + "projection_distance_meters": 0.053315120644676084 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.004140963600024, + 60.187668093579994 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 254.31503383232555, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "63", + "original_lat": 60.18766517860653, + "original_lon": 12.004141297420082, + "original_time": "2026-05-25T13:08:13+02:00", + "projection_distance_meters": 0.3246556220221595 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.004483201326762, + 60.187677780805295 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 273.26517982696424, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "64", + "original_lat": 60.18766969390048, + "original_lon": 12.004484127431857, + "original_time": "2026-05-25T13:08:12+02:00", + "projection_distance_meters": 0.900680278909856 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.004782993193983, + 60.187686266577415 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 289.86504362919766, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "65", + "original_lat": 60.18769211903419, + "original_lon": 12.00478232297496, + "original_time": "2026-05-25T13:08:11+02:00", + "projection_distance_meters": 0.6518182834998503 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.005283434674798, + 60.18770043184617 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 317.5751362996632, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "66", + "original_lat": 60.187688402321825, + "original_lon": 12.00528481228672, + "original_time": "2026-05-25T13:08:10+02:00", + "projection_distance_meters": 1.3397901431268913 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.005520844423401, + 60.18770715185844 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 330.72082143342055, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "67", + "original_lat": 60.18772149048807, + "original_lon": 12.005519202371197, + "original_time": "2026-05-25T13:08:09+02:00", + "projection_distance_meters": 1.5969671110531025 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.005694035890018, + 60.18771205413725 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 340.31065715578086, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "68", + "original_lat": 60.18771137741984, + "original_lon": 12.005694113387287, + "original_time": "2026-05-25T13:08:08+02:00", + "projection_distance_meters": 0.07536950723161172 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.005907416446266, + 60.18771809399014 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 352.1258148003478, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "69", + "original_lat": 60.187717289329974, + "original_lon": 12.005907508595495, + "original_time": "2026-05-25T13:08:07+02:00", + "projection_distance_meters": 0.08961915155036816 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.006279402836686, + 60.187728623267574 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 372.7231828160261, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "70", + "original_lat": 60.18773190257956, + "original_lon": 12.006279027291392, + "original_time": "2026-05-25T13:08:06+02:00", + "projection_distance_meters": 0.3652338835767991 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.006499219393167, + 60.18773484529496 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 384.894710125601, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "71", + "original_lat": 60.187739206856314, + "original_lon": 12.006498719908953, + "original_time": "2026-05-25T13:08:05+02:00", + "projection_distance_meters": 0.4857695762501077 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.006919637692517, + 60.18774721940503 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 408.17688025795087, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "72", + "original_lat": 60.18773576727916, + "original_lon": 12.006921029434563, + "original_time": "2026-05-25T13:08:04+02:00", + "projection_distance_meters": 1.275742165825519 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.007147160479489, + 60.18775405363142 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 420.77765967883954, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "73", + "original_lat": 60.18775059589975, + "original_lon": 12.007147580687487, + "original_time": "2026-05-25T13:08:03+02:00", + "projection_distance_meters": 0.38518386455208453 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.007399934460729, + 60.18776164634175 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 434.7769132188431, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.0", + "original_fid": "74", + "original_lat": 60.187754063966054, + "original_lon": 12.007400855924825, + "original_time": "2026-05-25T13:08:02+02:00", + "projection_distance_meters": 0.8446603292357897 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.007597266014645, + 60.18777129289503 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 7.0408385974729955, + "netelement_id": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "original_fid": "75", + "original_lat": 60.187772656702066, + "original_lon": 12.00759690362, + "original_time": "2026-05-25T13:08:01+02:00", + "projection_distance_meters": 0.15296620536064526 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.007742066115854, + 60.18778080307116 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 15.115173915376335, + "netelement_id": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "original_fid": "76", + "original_lat": 60.18778783819948, + "original_lon": 12.007740196720029, + "original_time": "2026-05-25T13:08:00+02:00", + "projection_distance_meters": 0.7890682949321483 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.007899211699389, + 60.18779112407347 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 23.87791734301249, + "netelement_id": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "original_fid": "77", + "original_lat": 60.18779500491386, + "original_lon": 12.007898180470155, + "original_time": "2026-05-25T13:07:59+02:00", + "projection_distance_meters": 0.4352796392620827 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.008048480691368, + 60.187804045780126 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 32.256808720446806, + "netelement_id": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "original_fid": "78", + "original_lat": 60.18780698022615, + "original_lon": 12.008047349992694, + "original_time": "2026-05-25T13:07:58+02:00", + "projection_distance_meters": 0.33222907374849403 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.00816160357454, + 60.18781486022271 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 38.62498877456138, + "netelement_id": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "original_fid": "79", + "original_lat": 60.18781359215279, + "original_lon": 12.008162095112365, + "original_time": "2026-05-25T13:07:57+02:00", + "projection_distance_meters": 0.14359754741500058 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.00825635018125, + 60.18782393774192 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 43.95910337288218, + "netelement_id": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "original_fid": "80", + "original_lat": 60.187819090365075, + "original_lon": 12.008258229154825, + "original_time": "2026-05-25T13:07:56+02:00", + "projection_distance_meters": 0.5489219641784993 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.008392600332222, + 60.187831571592156 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 51.53901476765445, + "netelement_id": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "original_fid": "81", + "original_lat": 60.18782367973983, + "original_lon": 12.008394383186747, + "original_time": "2026-05-25T13:07:55+02:00", + "projection_distance_meters": 0.8830525555288571 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.008512959579935, + 60.187838292159284 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 58.23449403664283, + "netelement_id": "_netelement_e6eca9f0-5bc0-4e4f-b06b-8ff9861ae24a_0.0", + "original_fid": "82", + "original_lat": 60.187828618517614, + "original_lon": 12.00851514496054, + "original_time": "2026-05-25T13:07:54+02:00", + "projection_distance_meters": 1.0824244622975603 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.008670460911004, + 60.18784647465637 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 1.077122738501975, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "original_fid": "83", + "original_lat": 60.1878435193692, + "original_lon": 12.008670752537414, + "original_time": "2026-05-25T13:07:53+02:00", + "projection_distance_meters": 0.3290086121152251 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.008861980437887, + 60.187852230167316 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 11.684024981737382, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "original_fid": "84", + "original_lat": 60.18784838286284, + "original_lon": 12.00886245938146, + "original_time": "2026-05-25T13:07:52+02:00", + "projection_distance_meters": 0.4286198724728204 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.00901224819625, + 60.18785596796673 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 20.001561022494833, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "original_fid": "85", + "original_lat": 60.18784495686646, + "original_lon": 12.00901330386904, + "original_time": "2026-05-25T13:07:51+02:00", + "projection_distance_meters": 1.2257702133295698 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.009196935356922, + 60.187862073808816 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 30.235454133613604, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "original_fid": "86", + "original_lat": 60.18786229387408, + "original_lon": 12.00919689869526, + "original_time": "2026-05-25T13:07:50+02:00", + "projection_distance_meters": 0.024553961544634656 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.009397626356192, + 60.187866763623894 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 41.3460870895633, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "original_fid": "87", + "original_lat": 60.18786541378918, + "original_lon": 12.009397700658935, + "original_time": "2026-05-25T13:07:49+02:00", + "projection_distance_meters": 0.15015117329038175 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.00960413770453, + 60.18787126009367 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 52.77342807130051, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "original_fid": "88", + "original_lat": 60.187866860503235, + "original_lon": 12.009604519136516, + "original_time": "2026-05-25T13:07:48+02:00", + "projection_distance_meters": 0.4896670296563457 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.009738430303624, + 60.187874378253156 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 60.20636435701181, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.18844755", + "original_fid": "89", + "original_lat": 60.18787611751523, + "original_lon": 12.009738016372587, + "original_time": "2026-05-25T13:07:47+02:00", + "projection_distance_meters": 0.19474641905701212 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.0099759348493, + 60.18788246426307 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 12.57308993847608, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "original_fid": "90", + "original_lat": 60.18788146336256, + "original_lon": 12.009976040857527, + "original_time": "2026-05-25T13:07:46+02:00", + "projection_distance_meters": 0.111449393688098 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.010253161768325, + 60.18788942558478 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 27.918167982454154, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "original_fid": "91", + "original_lat": 60.187882833396166, + "original_lon": 12.01025380444656, + "original_time": "2026-05-25T13:07:45+02:00", + "projection_distance_meters": 0.7338794322522192 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.010420137771838, + 60.1878932717205 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 37.158749120862204, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "original_fid": "92", + "original_lat": 60.18788672119251, + "original_lon": 12.01042074354554, + "original_time": "2026-05-25T13:07:44+02:00", + "projection_distance_meters": 0.7291558961041181 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.010705607678094, + 60.18790168508116 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 52.967815385226814, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "original_fid": "93", + "original_lat": 60.18790021459476, + "original_lon": 12.010705787963094, + "original_time": "2026-05-25T13:07:43+02:00", + "projection_distance_meters": 0.16381431062135152 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.010902217327983, + 60.18790741602604 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 63.85558516583937, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.26099764", + "original_fid": "94", + "original_lat": 60.18791573636602, + "original_lon": 12.010901557267353, + "original_time": "2026-05-25T13:07:42+02:00", + "projection_distance_meters": 0.9259001558589317 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.011068014728094, + 60.187912084714476 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 6.422734999488096, + "netelement_id": "_netelement_d244111e-0214-4502-ac70-2528adf87d2b_0.34029524", + "original_fid": "95", + "original_lat": 60.1879134912367, + "original_lon": 12.011067934578538, + "original_time": "2026-05-25T13:07:41+02:00", + "projection_distance_meters": 0.15646110109388353 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.011299491838297, + 60.187912627535034 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 9.203997536732517, + "netelement_id": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "original_fid": "96", + "original_lat": 60.18791590858476, + "original_lon": 12.011299521535683, + "original_time": "2026-05-25T13:07:40+02:00", + "projection_distance_meters": 0.3648402815651593 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.011444332860522, + 60.187912303505904 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 17.211095214322878, + "netelement_id": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "original_fid": "97", + "original_lat": 60.18791415279339, + "original_lon": 12.011444349598763, + "original_time": "2026-05-25T13:07:39+02:00", + "projection_distance_meters": 0.20563375223973876 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.011749100360456, + 60.18790708429185 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 34.07511738023972, + "netelement_id": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "original_fid": "98", + "original_lat": 60.18790722476704, + "original_lon": 12.011749116882124, + "original_time": "2026-05-25T13:07:38+02:00", + "projection_distance_meters": 0.015646829352601817 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.011967570691032, + 60.18790564136119 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 46.15370021876744, + "netelement_id": "_netelement_bb0dad71-6258-4846-825f-c0f4b0f563eb_0.0", + "original_fid": "99", + "original_lat": 60.18789918970007, + "original_lon": 12.011967406178742, + "original_time": "2026-05-25T13:07:37+02:00", + "projection_distance_meters": 0.7174506196235 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.01224810149454, + 60.18790456150964 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 1.1129885873634044, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "100", + "original_lat": 60.187902018587906, + "original_lon": 12.01224838888638, + "original_time": "2026-05-25T13:07:36+02:00", + "projection_distance_meters": 0.28320636742967437 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.012652205159633, + 60.18791584963064 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 23.487582002112774, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "101", + "original_lat": 60.187908178852936, + "original_lon": 12.012653072083577, + "original_time": "2026-05-25T13:07:35+02:00", + "projection_distance_meters": 0.854298054307386 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.013057002287209, + 60.18792715712264 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 45.90057135946927, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "102", + "original_lat": 60.187932804184754, + "original_lon": 12.013056364075426, + "original_time": "2026-05-25T13:07:34+02:00", + "projection_distance_meters": 0.6289159169014421 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.01334416699787, + 60.18793517870294 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 61.80043616927656, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "103", + "original_lat": 60.18793855676709, + "original_lon": 12.0133437852204, + "original_time": "2026-05-25T13:07:33+02:00", + "projection_distance_meters": 0.37621656440617623 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.013551436011634, + 60.18794096849875 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 73.27660011194853, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "104", + "original_lat": 60.18794459638794, + "original_lon": 12.013551025999623, + "original_time": "2026-05-25T13:07:32+02:00", + "projection_distance_meters": 0.4040396958424204 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.013814113742438, + 60.187948306066474 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 87.82065881161881, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "105", + "original_lat": 60.18795090815072, + "original_lon": 12.01381381966339, + "original_time": "2026-05-25T13:07:31+02:00", + "projection_distance_meters": 0.289795324852898 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.01411077404938, + 60.18795659289417 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 104.24628006556526, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "106", + "original_lat": 60.18795798199558, + "original_lon": 12.014110617057614, + "original_time": "2026-05-25T13:07:30+02:00", + "projection_distance_meters": 0.15470486650829812 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.014428547585833, + 60.18796546949302 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 121.84090810363097, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "107", + "original_lat": 60.18796303183326, + "original_lon": 12.014428823082367, + "original_time": "2026-05-25T13:07:29+02:00", + "projection_distance_meters": 0.27148329425281087 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.014855642305964, + 60.18797739983985 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 145.48848059057534, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "108", + "original_lat": 60.187976360150266, + "original_lon": 12.014855759808462, + "original_time": "2026-05-25T13:07:28+02:00", + "projection_distance_meters": 0.11579070996342347 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.015343641652287, + 60.18799103147981 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 172.5082478033687, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "109", + "original_lat": 60.18798675274013, + "original_lon": 12.015344125222503, + "original_time": "2026-05-25T13:07:27+02:00", + "projection_distance_meters": 0.4765252172159142 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.015718075842715, + 60.18800149082185 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 193.24008840551107, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "110", + "original_lat": 60.18799820336153, + "original_lon": 12.015718447381795, + "original_time": "2026-05-25T13:07:26+02:00", + "projection_distance_meters": 0.36612597634661576 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.016312335986639, + 60.1880180907218 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 226.14335123498154, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "111", + "original_lat": 60.188016940899736, + "original_lon": 12.01631246593629, + "original_time": "2026-05-25T13:07:25+02:00", + "projection_distance_meters": 0.12805621519385088 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.016795466045153, + 60.18803158634441 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 252.89351353116436, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "112", + "original_lat": 60.188033600390234, + "original_lon": 12.016795238423136, + "original_time": "2026-05-25T13:07:24+02:00", + "projection_distance_meters": 0.22430521624981986 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.017040092031381, + 60.18803841965965 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 266.4380751210746, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "113", + "original_lat": 60.18804185936805, + "original_lon": 12.017039703284636, + "original_time": "2026-05-25T13:07:23+02:00", + "projection_distance_meters": 0.38308191766690197 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.017273970167567, + 60.18804495274739 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 279.3875449175466, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "114", + "original_lat": 60.18804266313931, + "original_lon": 12.017274228933028, + "original_time": "2026-05-25T13:07:22+02:00", + "projection_distance_meters": 0.25499471183694594 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.017578004968026, + 60.18805344557216 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 296.2214804441107, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "115", + "original_lat": 60.18805572105498, + "original_lon": 12.01757774779876, + "original_time": "2026-05-25T13:07:21+02:00", + "projection_distance_meters": 0.25342157594620707 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.017730109003544, + 60.18805769440459 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 304.64324506899646, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "116", + "original_lat": 60.18804761596231, + "original_lon": 12.017731248042828, + "original_time": "2026-05-25T13:07:20+02:00", + "projection_distance_meters": 1.1224407843364677 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.018054700748714, + 60.18806676146142 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 322.61538675273283, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "117", + "original_lat": 60.18807098316993, + "original_lon": 12.018054223621538, + "original_time": "2026-05-25T13:07:19+02:00", + "projection_distance_meters": 0.47017363312767746 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.018329622529354, + 60.18807444105107 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 337.8373795086615, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "118", + "original_lat": 60.18807700544269, + "original_lon": 12.018329332707976, + "original_time": "2026-05-25T13:07:18+02:00", + "projection_distance_meters": 0.2855974835347245 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.018547014166183, + 60.188080513609464 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 349.8740173964011, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "119", + "original_lat": 60.18808742587794, + "original_lon": 12.018546232957716, + "original_time": "2026-05-25T13:07:17+02:00", + "projection_distance_meters": 0.769822544900786 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.018759466241361, + 60.18808644818749 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 361.6371594138204, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "120", + "original_lat": 60.18808871800722, + "original_lon": 12.018759209711607, + "original_time": "2026-05-25T13:07:16+02:00", + "projection_distance_meters": 0.2527908758408338 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.019181693227244, + 60.188098242560365 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 385.01521300238466, + "netelement_id": "_netelement_95f349c4-215f-4a67-a958-a1aceb553028_0.63930502", + "original_fid": "121", + "original_lat": 60.18810830472132, + "original_lon": 12.019180556023828, + "original_time": "2026-05-25T13:07:15+02:00", + "projection_distance_meters": 1.1206275330648723 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.019570476471685, + 60.188109066931666 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 2.1855243363313357, + "netelement_id": "_netelement_0fa90553-f256-4ef2-ad39-f27ef2de68a5_0.79667018", + "original_fid": "122", + "original_lat": 60.18811479461461, + "original_lon": 12.019569850152887, + "original_time": "2026-05-25T13:07:14+02:00", + "projection_distance_meters": 0.6378305999482283 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.019846766638723, + 60.18811729088253 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 1.1021078184948692, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "original_fid": "123", + "original_lat": 60.18811461150607, + "original_lon": 12.019847474594012, + "original_time": "2026-05-25T13:07:13+02:00", + "projection_distance_meters": 0.30049297227308713 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.02008589721644, + 60.18813290757332 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 14.43506154008209, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "original_fid": "124", + "original_lat": 60.18813067026836, + "original_lon": 12.020086488366454, + "original_time": "2026-05-25T13:07:12+02:00", + "projection_distance_meters": 0.2509145072126909 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.020310848945895, + 60.18814759829851 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 26.977460036595506, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "original_fid": "125", + "original_lat": 60.18814087727319, + "original_lon": 12.02031262480424, + "original_time": "2026-05-25T13:07:11+02:00", + "projection_distance_meters": 0.7537652625224194 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.020467177021315, + 60.1881592632766 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 35.71629262551427, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "original_fid": "126", + "original_lat": 60.188155764219395, + "original_lon": 12.020468238794104, + "original_time": "2026-05-25T13:07:10+02:00", + "projection_distance_meters": 0.39348043561748036 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.020765288810637, + 60.18817829741487 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 52.3323678408688, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "original_fid": "127", + "original_lat": 60.188165818968486, + "original_lon": 12.020768399387698, + "original_time": "2026-05-25T13:07:09+02:00", + "projection_distance_meters": 1.3981563411716444 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.020862426015904, + 60.1881842821716 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 57.74328539705317, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.0", + "original_fid": "128", + "original_lat": 60.188183642311515, + "original_lon": 12.020862585517833, + "original_time": "2026-05-25T13:07:08+02:00", + "projection_distance_meters": 0.07169357584979644 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.021098291473846, + 60.18819351014979 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 8.820233257751502, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "original_fid": "129", + "original_lat": 60.1881951703979, + "original_lon": 12.021098098863725, + "original_time": "2026-05-25T13:07:07+02:00", + "projection_distance_meters": 0.1849182248755714 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.021413449593322, + 60.188200811814646 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 26.26204693192542, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.07907821", + "original_fid": "130", + "original_lat": 60.18820180665155, + "original_lon": 12.021413370281788, + "original_time": "2026-05-25T13:07:06+02:00", + "projection_distance_meters": 0.11070782232995041 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.021575401574815, + 60.18820672263145 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 6.3929201114292376, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "131", + "original_lat": 60.188211952839154, + "original_lon": 12.021574718956378, + "original_time": "2026-05-25T13:07:05+02:00", + "projection_distance_meters": 0.5827963347683475 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.021827564889394, + 60.18821417001808 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 20.35801873149825, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "132", + "original_lat": 60.18821232726883, + "original_lon": 12.021827716427524, + "original_time": "2026-05-25T13:07:04+02:00", + "projection_distance_meters": 0.2050758219394244 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.022157187319525, + 60.18822361540476 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 38.61348203498862, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "133", + "original_lat": 60.18821707764654, + "original_lon": 12.022158239366506, + "original_time": "2026-05-25T13:07:03+02:00", + "projection_distance_meters": 0.7292891941817821 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.022331904365192, + 60.188231944664906 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 48.316626148021555, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "134", + "original_lat": 60.18824080933591, + "original_lon": 12.022330092945992, + "original_time": "2026-05-25T13:07:02+02:00", + "projection_distance_meters": 0.9907811352483149 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.022474619884884, + 60.188238784795395 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 56.24309935850334, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "135", + "original_lat": 60.188249842791016, + "original_lon": 12.022472830275785, + "original_time": "2026-05-25T13:07:01+02:00", + "projection_distance_meters": 1.2335682059632065 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.022723445616206, + 60.188253852654654 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 70.10095016021583, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "136", + "original_lat": 60.18825488477367, + "original_lon": 12.022723172680562, + "original_time": "2026-05-25T13:07:00+02:00", + "projection_distance_meters": 0.11575410862931543 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.023179997000545, + 60.1882966041978 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 95.79168394556316, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "137", + "original_lat": 60.18827807439435, + "original_lon": 12.02318804473556, + "original_time": "2026-05-25T13:06:59+02:00", + "projection_distance_meters": 2.1079055040225447 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.02335884474491, + 60.1883191924896 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 105.99334842782262, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "138", + "original_lat": 60.188334278953434, + "original_lon": 12.023350548104364, + "original_time": "2026-05-25T13:06:58+02:00", + "projection_distance_meters": 1.739108063469145 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.023659732238352, + 60.188364316657875 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 123.37013971616452, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "139", + "original_lat": 60.18836564876138, + "original_lon": 12.023658843219318, + "original_time": "2026-05-25T13:06:57+02:00", + "projection_distance_meters": 0.15606349372068778 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.023931013745466, + 60.18841418675026 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 139.36225617421678, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "140", + "original_lat": 60.18841005485619, + "original_lon": 12.023934404589278, + "original_time": "2026-05-25T13:06:56+02:00", + "projection_distance_meters": 0.4962132298027234 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.02428480839808, + 60.18849065052074 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 160.69240395889545, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "141", + "original_lat": 60.188491169272744, + "original_lon": 12.024284299578296, + "original_time": "2026-05-25T13:06:55+02:00", + "projection_distance_meters": 0.06417524915937471 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.024748828784181, + 60.18861162591984 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 189.66312382818884, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "142", + "original_lat": 60.18860640411854, + "original_lon": 12.024754566038672, + "original_time": "2026-05-25T13:06:54+02:00", + "projection_distance_meters": 0.661611401560871 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.025175830770955, + 60.18873228788042 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 216.81589845939607, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "143", + "original_lat": 60.188736699665334, + "original_lon": 12.025170693326528, + "original_time": "2026-05-25T13:06:53+02:00", + "projection_distance_meters": 0.5668444562926674 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.025896932184393, + 60.188959756474134 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 264.03237750022726, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "144", + "original_lat": 60.18895500932942, + "original_lon": 12.025903428869627, + "original_time": "2026-05-25T13:06:52+02:00", + "projection_distance_meters": 0.6384456689660215 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.026539054738247, + 60.18918649970577 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 307.5749983933766, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "145", + "original_lat": 60.1891608540784, + "original_lon": 12.026576410853448, + "original_time": "2026-05-25T13:06:51+02:00", + "projection_distance_meters": 3.5208399356149918 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.027290491270982, + 60.1894822383687 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 360.563973107638, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "146", + "original_lat": 60.1894441604701, + "original_lon": 12.02735526078368, + "original_time": "2026-05-25T13:06:50+02:00", + "projection_distance_meters": 5.5449544887945565 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.027842440527452, + 60.189725017488705 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 401.30861065790134, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "147", + "original_lat": 60.189689604208176, + "original_lon": 12.027908509915092, + "original_time": "2026-05-25T13:06:49+02:00", + "projection_distance_meters": 5.370732357376866 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.028558551526551, + 60.19007650944579 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 456.94935503912916, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "148", + "original_lat": 60.190009413095055, + "original_lon": 12.028703090228282, + "original_time": "2026-05-25T13:06:48+02:00", + "projection_distance_meters": 10.931604172586342 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.029219114866644, + 60.190444068202076 + ] + }, + "properties": { + "crs": "EPSG:4326", + "measure_meters": 511.76781701972243, + "netelement_id": "_netelement_94cd1b3d-9ef1-42dc-b7f6-32932adf7d6f_0.12594356", + "original_fid": "149", + "original_lat": 60.190407306400495, + "original_lon": 12.029307438976096, + "original_time": "2026-05-25T13:06:47+02:00", + "projection_distance_meters": 6.367611749925647 + } + } + ] +} \ No newline at end of file diff --git a/test-data/rinf-kvg/raw-gnss-data-detail1.png b/test-data/rinf-kvg/raw-gnss-data-detail1.png new file mode 100644 index 0000000..f610ab3 Binary files /dev/null and b/test-data/rinf-kvg/raw-gnss-data-detail1.png differ diff --git a/test-data/rinf-kvg/raw-gnss-data-detail2.png b/test-data/rinf-kvg/raw-gnss-data-detail2.png new file mode 100644 index 0000000..a876ccc Binary files /dev/null and b/test-data/rinf-kvg/raw-gnss-data-detail2.png differ diff --git a/test-data/rinf-kvg/raw-gnss-data-detail3.png b/test-data/rinf-kvg/raw-gnss-data-detail3.png new file mode 100644 index 0000000..8fc4040 Binary files /dev/null and b/test-data/rinf-kvg/raw-gnss-data-detail3.png differ diff --git a/test-data/rinf-kvg/raw-gnss-data.png b/test-data/rinf-kvg/raw-gnss-data.png new file mode 100644 index 0000000..b6b0bc0 Binary files /dev/null and b/test-data/rinf-kvg/raw-gnss-data.png differ diff --git a/test-data/rinf-kvg/topology-detail.png b/test-data/rinf-kvg/topology-detail.png new file mode 100644 index 0000000..9deccf0 Binary files /dev/null and b/test-data/rinf-kvg/topology-detail.png differ diff --git a/test-data/rinf-kvg/topology-overview-no-background.png b/test-data/rinf-kvg/topology-overview-no-background.png new file mode 100644 index 0000000..624593d Binary files /dev/null and b/test-data/rinf-kvg/topology-overview-no-background.png differ diff --git a/test-data/rinf-kvg/topology-overview.png b/test-data/rinf-kvg/topology-overview.png new file mode 100644 index 0000000..66eb03e Binary files /dev/null and b/test-data/rinf-kvg/topology-overview.png differ diff --git a/test-data/rinf_empty_gnss.geojson b/test-data/rinf_empty_gnss.geojson new file mode 100644 index 0000000..7f24caa --- /dev/null +++ b/test-data/rinf_empty_gnss.geojson @@ -0,0 +1,4 @@ +{ + "type": "FeatureCollection", + "features": [] +} diff --git a/test-data/rinf_smoke_gnss.geojson b/test-data/rinf_smoke_gnss.geojson new file mode 100644 index 0000000..54c977c --- /dev/null +++ b/test-data/rinf_smoke_gnss.geojson @@ -0,0 +1,20 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [11.50, 60.00]}, + "properties": {"timestamp": "2026-05-13T08:00:00+00:00"} + }, + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [11.55, 60.05]}, + "properties": {"timestamp": "2026-05-13T08:00:10+00:00"} + }, + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [11.60, 60.10]}, + "properties": {"timestamp": "2026-05-13T08:00:20+00:00"} + } + ] +} diff --git a/test-data/rinf_uncovered_gnss.geojson b/test-data/rinf_uncovered_gnss.geojson new file mode 100644 index 0000000..27b8e63 --- /dev/null +++ b/test-data/rinf_uncovered_gnss.geojson @@ -0,0 +1,15 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-30.0, 45.0]}, + "properties": {"timestamp": "2026-05-13T08:00:00+00:00"} + }, + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-30.01, 45.01]}, + "properties": {"timestamp": "2026-05-13T08:00:10+00:00"} + } + ] +} diff --git a/tp-cli/README.md b/tp-cli/README.md index 40df64f..5e0156c 100644 --- a/tp-cli/README.md +++ b/tp-cli/README.md @@ -68,9 +68,24 @@ tp-cli --gnss positions.csv \ --debug ``` +### Automatic RINF Topology Retrieval + +```bash +# Omit --network to download a RINF topology subset on demand +tp-cli --gnss positions.csv \ + --crs EPSG:4326 \ + --output projected.geojson \ + --rinf-endpoint https://graph.data.era.europa.eu/repositories/rinf-plus \ + --rinf-buffer-meters 1000 +``` + +Flags `--rinf-endpoint` and `--rinf-buffer-meters` are optional and fall back to +the documented defaults. RINF-specific failures map to dedicated exit codes +(4=invalid GNSS, 5=missing coverage, 6=incomplete topology, 7=endpoint failure). + ## Commands -`tp-cli` has three modes of operation: +`tp-cli` has four modes of operation: ### Default (no subcommand) @@ -96,6 +111,33 @@ Legacy nearest-netelement projection (feature 001 behavior). Projects each GNSS tp-cli simple-projection --gnss --network --output [OPTIONS] ``` +### `fetch-topology` + +Inspection helper: load a GNSS file, query the RINF SPARQL endpoint for the +topology covering those positions, and write the retrieved netelements + +netrelations to a GeoJSON file that round-trips through the standard +`--network` input. + +```bash +tp-cli fetch-topology --gnss --output [OPTIONS] +``` + +Options: + +- `--crs ` β€” CRS of the GNSS input (defaults to `EPSG:4326`). +- `--lat-col`, `--lon-col`, `--time-col` β€” CSV column names (defaults + `latitude`, `longitude`, `timestamp`). +- `--rinf-endpoint ` β€” override the SPARQL endpoint. +- `--rinf-buffer-meters ` β€” override the buffer around the GNSS hull. + +The retrieved topology is **always written to disk**, even when validation +reports issues such as `RinfIncompleteTopology` (coarse geometries) or +`RinfMissingCoverage`. The same validation errors are still surfaced through +the process exit code (4–7), so the file can be inspected in QGIS while the +caller can detect that the data is not fit for production use. Network or +endpoint failures (exit 7) abort before any file is written, since there is +no topology to dump in that case. + ## Arguments ### Required Arguments @@ -115,7 +157,7 @@ latitude,longitude,timestamp,altitude,hdop **Requirements:** - CSV must have latitude, longitude, and timestamp columns -- Timestamps must be RFC3339 format with timezone (e.g., `2025-12-09T14:30:00+01:00`) +- Timestamps may be RFC3339 with timezone (e.g., `2025-12-09T14:30:00+01:00`, `2025-12-09T14:30:00Z`) or naive ISO 8601 (e.g., `2025-12-09T14:30:00`, `2025-12-09 14:30:00`); naive values are interpreted in the host's **local** timezone. All emitted timestamps are RFC3339 with an explicit timezone offset. - Additional columns preserved as metadata #### `--network ` (or `-n`) @@ -449,18 +491,18 @@ tp-cli --gnss data.geojson --network network.geojson --output out.csv ### Error: "Failed to load GNSS data: Invalid timestamp" -**Problem:** Timestamps not in RFC3339 format or missing timezone. - -**Required format:** `YYYY-MM-DDTHH:MM:SSΒ±HH:MM` +**Problem:** Timestamps not in RFC3339 nor a recognised naive ISO 8601 form. -**Examples:** +Accepted formats: -- βœ… `2025-12-09T14:30:00+01:00` (Brussels time) -- βœ… `2025-12-09T13:30:00Z` (UTC) -- ❌ `2025-12-09 14:30:00` (missing T and timezone) -- ❌ `2025-12-09T14:30:00` (missing timezone) +- βœ… `2025-12-09T14:30:00+01:00` (RFC3339 with offset) +- βœ… `2025-12-09T14:30:00Z` (RFC3339 UTC) +- βœ… `2025-12-09T14:30:00` (naive, interpreted as local time) +- βœ… `2025-12-09 14:30:00` (naive with space separator, local time) +- ❌ `09/12/2025 14:30` (locale-specific date format) +- ❌ `14:30:00` (time only) -**Solution:** Fix timestamps in input CSV to include timezone. +**Solution:** Reformat timestamps as one of the accepted forms above. Naive timestamps are always interpreted in the machine's local timezone; emit with an explicit offset (e.g., `+01:00` / `Z`) if you need deterministic cross-host behaviour. ### Error: "Failed to load network: Invalid geometry" diff --git a/tp-cli/src/main.rs b/tp-cli/src/main.rs index 3d998ad..d26aeb4 100644 --- a/tp-cli/src/main.rs +++ b/tp-cli/src/main.rs @@ -13,8 +13,11 @@ use std::process; use tp_lib_core::{ calculate_train_path, export_all_debug_info, parse_gnss_csv, parse_gnss_geojson, parse_network_geojson, parse_trainpath_csv, parse_trainpath_geojson, project_gnss, - project_onto_path, write_csv, write_geojson, write_trainpath_csv, write_trainpath_geojson, - Netelement, PathConfig, PathConfigBuilder, ProjectionConfig, RailwayNetwork, + project_onto_path, resolve_topology, write_csv, write_geojson, write_network_geojson, + write_trainpath_csv, write_trainpath_geojson, GnssPosition, NetRelation, Netelement, + PathConfig, PathConfigBuilder, ProjectionConfig, ProjectionError, RailwayNetwork, + RetrievalConfig, RetrievalStatus, TopologySource, UreqSparqlClient, WorkflowKind, + DEFAULT_RETRIEVAL_BUFFER_METERS, DEFAULT_RINF_ENDPOINT, }; #[cfg(feature = "webapp")] use tp_webapp::{run_webapp_integrated, run_webapp_standalone, WebConfirmResult}; @@ -158,6 +161,16 @@ struct Cli { )] detection_cutoff: f64, + /// ERA RINF SPARQL endpoint URL used when no network file is supplied + /// (feature 006). Defaults to the production RINF-plus repository. + #[arg(long = "rinf-endpoint", value_name = "URL", global = true)] + rinf_endpoint: Option, + + /// Buffer distance in meters added around the GNSS bounding box when + /// downloading topology automatically (feature 006). Defaults to 1000. + #[arg(long = "rinf-buffer-meters", value_name = "METERS", global = true)] + rinf_buffer_meters: Option, + // Legacy argument for backward compatibility /// [DEPRECATED] Path to GNSS input file - use --gnss instead #[arg(long = "gnss-file", value_name = "FILE", hide = true)] @@ -185,9 +198,11 @@ enum Commands { #[arg(long = "crs", value_name = "CRS")] gnss_crs: Option, - /// Path to railway network GeoJSON file + /// Path to railway network GeoJSON file. If omitted, the topology + /// is downloaded automatically from the ERA RINF SPARQL endpoint + /// (feature 006) using the bounding box of the GNSS positions. #[arg(short = 'n', long = "network", value_name = "FILE")] - network_file: String, + network_file: Option, /// Output file path for train path #[arg(short = 'o', long = "output", value_name = "FILE")] @@ -243,9 +258,11 @@ enum Commands { #[arg(long = "crs", value_name = "CRS")] gnss_crs: Option, - /// Path to railway network GeoJSON file + /// Path to railway network GeoJSON file. If omitted, the topology + /// is downloaded automatically from the ERA RINF SPARQL endpoint + /// (feature 006) using the bounding box of the GNSS positions. #[arg(short = 'n', long = "network", value_name = "FILE")] - network_file: String, + network_file: Option, /// Output file path #[arg(short = 'o', long = "output", value_name = "FILE")] @@ -270,6 +287,37 @@ enum Commands { time_col: String, }, + /// Fetch the RINF topology (netelements + netrelations) for the area covered + /// by a GNSS input file, and write it to a GeoJSON file. + /// + /// This is an inspection helper: it performs only the RINF retrieval step + /// of the path-calculation pipeline so the resulting topology can be + /// examined when an unexpected error occurs. + #[command(name = "fetch-topology")] + FetchTopology { + /// Path to GNSS input file (CSV or GeoJSON) + #[arg(short = 'g', long = "gnss", value_name = "FILE")] + gnss_file: String, + + /// CRS of GNSS data (required for CSV input) + #[arg(long = "crs", value_name = "CRS")] + gnss_crs: Option, + + /// Output GeoJSON file for the retrieved topology + #[arg(short = 'o', long = "output", value_name = "FILE")] + output_file: String, + + /// Latitude column name for CSV + #[arg(long = "lat-col", default_value = "latitude")] + lat_col: String, + /// Longitude column name for CSV + #[arg(long = "lon-col", default_value = "longitude")] + lon_col: String, + /// Timestamp column name for CSV + #[arg(long = "time-col", default_value = "timestamp")] + time_col: String, + }, + /// Launch the webapp to visually review and edit a pre-calculated train path. /// /// In standalone mode (default) the user saves the edited path to a CSV file. @@ -355,7 +403,7 @@ fn main() { run_calculate_path( &gnss_file, gnss_crs.as_deref(), - &network_file, + network_file.as_deref(), &output_file, &format, distance_scale, @@ -373,6 +421,8 @@ fn main() { cli.punctual_detections.as_deref(), cli.linear_detections.as_deref(), cli.detection_cutoff, + cli.rinf_endpoint.as_deref(), + cli.rinf_buffer_meters, ) } Some(Commands::SimpleProjection { @@ -388,13 +438,32 @@ fn main() { }) => run_simple_projection( &gnss_file, gnss_crs.as_deref(), - &network_file, + network_file.as_deref(), &output_file, &format, warning_threshold, &lat_col, &lon_col, &time_col, + cli.rinf_endpoint.as_deref(), + cli.rinf_buffer_meters, + ), + Some(Commands::FetchTopology { + gnss_file, + gnss_crs, + output_file, + lat_col, + lon_col, + time_col, + }) => run_fetch_topology( + &gnss_file, + gnss_crs.as_deref(), + &output_file, + &lat_col, + &lon_col, + &time_col, + cli.rinf_endpoint.as_deref(), + cli.rinf_buffer_meters, ), #[cfg(feature = "webapp")] Some(Commands::Webapp { @@ -421,15 +490,28 @@ fn main() { let network_file = cli.network_file.or(cli.legacy_network_file); let format = cli.legacy_output_format.unwrap_or(cli.format); - // Check if we have the required arguments - let (gnss, network, output) = match (gnss_file, network_file, cli.output_file.clone()) { - (Some(g), Some(n), Some(o)) => (g, n, o), - (Some(g), Some(n), None) => { + // Check if we have the required arguments. Network may be omitted, + // in which case topology is auto-retrieved from ERA RINF. + let (gnss, output) = match (gnss_file, cli.output_file.clone()) { + (Some(g), Some(o)) => (g, o), + (Some(g), None) => { + // Legacy mode requires an explicit network file because it + // writes projection output to stdout. + let n = match network_file.as_deref() { + Some(path) => path, + None => { + eprintln!( + "Error: Missing required arguments. Use --gnss and --output (and optionally --network)\n\ + Run with --help for usage information." + ); + process::exit(1); + } + }; // Legacy mode: output to stdout run_legacy_pipeline( &g, cli.gnss_crs.as_deref(), - &n, + n, &format, cli.warning_threshold, &cli.lat_col, @@ -440,7 +522,7 @@ fn main() { } _ => { eprintln!( - "Error: Missing required arguments. Use --gnss, --network, and --output\n\ + "Error: Missing required arguments. Use --gnss and --output (and optionally --network)\n\ Run with --help for usage information." ); process::exit(1); @@ -454,7 +536,7 @@ fn main() { run_default_command( &gnss, cli.gnss_crs.as_deref(), - &network, + network_file.as_deref(), &output, cli.train_path_file.as_deref(), cli.save_path_file.as_deref(), @@ -485,6 +567,8 @@ fn main() { cli.punctual_detections.as_deref(), cli.linear_detections.as_deref(), cli.detection_cutoff, + cli.rinf_endpoint.as_deref(), + cli.rinf_buffer_meters, ) } }; @@ -499,6 +583,10 @@ fn main() { PipelineError::Validation(_) => 1, PipelineError::Processing(_) => 2, PipelineError::Io(_) => 3, + PipelineError::RinfInvalidInput(_) => 4, + PipelineError::RinfMissingCoverage(_) => 5, + PipelineError::RinfIncompleteTopology(_) => 6, + PipelineError::RinfEndpointFailure(_) => 7, }; tracing::error!(error = %e, exit_code = exit_code, "Pipeline failed"); eprintln!("Error: {}", e); @@ -512,6 +600,10 @@ enum PipelineError { Validation(String), Processing(String), Io(String), + RinfInvalidInput(String), + RinfMissingCoverage(String), + RinfIncompleteTopology(String), + RinfEndpointFailure(String), } impl std::fmt::Display for PipelineError { @@ -520,6 +612,191 @@ impl std::fmt::Display for PipelineError { PipelineError::Validation(msg) => write!(f, "Validation error: {}", msg), PipelineError::Processing(msg) => write!(f, "Processing error: {}", msg), PipelineError::Io(msg) => write!(f, "I/O error: {}", msg), + PipelineError::RinfInvalidInput(msg) => write!(f, "RINF invalid input: {}", msg), + PipelineError::RinfMissingCoverage(msg) => { + write!(f, "RINF missing coverage: {}", msg) + } + PipelineError::RinfIncompleteTopology(msg) => { + write!(f, "RINF incomplete topology: {}", msg) + } + PipelineError::RinfEndpointFailure(msg) => { + write!(f, "RINF endpoint failure: {}", msg) + } + } + } +} + +/// Resolve the topology either from a supplied GeoJSON file or by retrieving +/// it automatically from the ERA RINF SPARQL endpoint (feature 006). +fn resolve_cli_topology( + workflow_kind: WorkflowKind, + network_file: Option<&str>, + gnss_positions: &[GnssPosition], + rinf_endpoint: Option<&str>, + rinf_buffer_meters: Option, +) -> Result<(Vec, Vec), PipelineError> { + if let Some(path) = network_file { + tracing::info!(network_file = %path, "Loading railway network"); + let (netelements, netrelations) = parse_network_geojson(path) + .map_err(|e| PipelineError::Io(format!("Failed to load network: {}", e)))?; + tracing::info!( + netelement_count = netelements.len(), + netrelation_count = netrelations.len(), + "Railway network loaded" + ); + println!("Topology source: SuppliedTopology ({})", path); + return Ok((netelements, netrelations)); + } + + let endpoint = rinf_endpoint + .map(|s| s.to_string()) + .unwrap_or_else(|| DEFAULT_RINF_ENDPOINT.to_string()); + let buffer = rinf_buffer_meters.unwrap_or(DEFAULT_RETRIEVAL_BUFFER_METERS); + let config = RetrievalConfig::default() + .with_endpoint(endpoint.clone()) + .with_buffer_meters(buffer); + let client = UreqSparqlClient::default(); + + tracing::info!( + endpoint = %endpoint, + buffer_meters = buffer, + "Auto-retrieving topology from ERA RINF" + ); + println!( + "Topology source: EraRinf (endpoint={}, buffer_meters={})", + endpoint, buffer + ); + + let (topology, outcome) = + resolve_topology(workflow_kind, gnss_positions, None, &config, &client).map_err( + |e| match e { + ProjectionError::InvalidGnssInput(m) => PipelineError::RinfInvalidInput(m), + ProjectionError::RinfMissingCoverage(m) => PipelineError::RinfMissingCoverage(m), + ProjectionError::RinfIncompleteTopology(m) => { + PipelineError::RinfIncompleteTopology(m) + } + ProjectionError::RinfRetrievalFailed(m) => PipelineError::RinfEndpointFailure(m), + other => PipelineError::Processing(other.to_string()), + }, + )?; + + match outcome.status { + RetrievalStatus::Success => { + let source = match outcome.source_used { + TopologySource::EraRinf => "EraRinf", + TopologySource::SuppliedTopology => "SuppliedTopology", + }; + println!( + "Topology retrieved: source={}, netelements={}, netrelations={}", + source, + topology.netelements.len(), + topology.netrelations.len() + ); + Ok((topology.netelements, topology.netrelations)) + } + RetrievalStatus::InvalidInput => { + Err(PipelineError::RinfInvalidInput(outcome.detail_message)) + } + RetrievalStatus::MissingCoverage => { + Err(PipelineError::RinfMissingCoverage(outcome.detail_message)) + } + RetrievalStatus::IncompleteTopology => Err(PipelineError::RinfIncompleteTopology( + outcome.detail_message, + )), + RetrievalStatus::EndpointFailure => { + Err(PipelineError::RinfEndpointFailure(outcome.detail_message)) + } + } +} + +/// Handle the `fetch-topology` subcommand: load GNSS positions, retrieve the +/// RINF topology that covers them, and write the result to a GeoJSON file +/// that round-trips through [`parse_network_geojson`]. +/// +/// The topology file is written even when validation fails (e.g. coarse +/// geometries or incomplete coverage) so the user can inspect what RINF +/// returned. The validation error, if any, is still propagated as the +/// command's exit status after the file has been written. +#[allow(clippy::too_many_arguments)] +fn run_fetch_topology( + gnss_file: &str, + gnss_crs: Option<&str>, + output_file: &str, + lat_col: &str, + lon_col: &str, + time_col: &str, + rinf_endpoint: Option<&str>, + rinf_buffer_meters: Option, +) -> Result<(), PipelineError> { + let gnss = load_gnss_positions(gnss_file, gnss_crs, lat_col, lon_col, time_col)?; + + let endpoint = rinf_endpoint + .map(|s| s.to_string()) + .unwrap_or_else(|| DEFAULT_RINF_ENDPOINT.to_string()); + let buffer = rinf_buffer_meters.unwrap_or(DEFAULT_RETRIEVAL_BUFFER_METERS); + let config = RetrievalConfig::default() + .with_endpoint(endpoint.clone()) + .with_buffer_meters(buffer); + let client = UreqSparqlClient::default(); + + println!( + "Topology source: EraRinf (endpoint={}, buffer_meters={})", + endpoint, buffer + ); + + let (topology, outcome) = + resolve_topology(WorkflowKind::PathCalculation, &gnss, None, &config, &client).map_err( + |e| match e { + ProjectionError::InvalidGnssInput(m) => PipelineError::RinfInvalidInput(m), + ProjectionError::RinfMissingCoverage(m) => PipelineError::RinfMissingCoverage(m), + ProjectionError::RinfIncompleteTopology(m) => { + PipelineError::RinfIncompleteTopology(m) + } + ProjectionError::RinfRetrievalFailed(m) => PipelineError::RinfEndpointFailure(m), + other => PipelineError::Processing(other.to_string()), + }, + )?; + + let file = File::create(output_file) + .map_err(|e| PipelineError::Io(format!("Failed to create {}: {}", output_file, e)))?; + let mut writer = BufWriter::new(file); + write_network_geojson(&topology.netelements, &topology.netrelations, &mut writer) + .map_err(|e| PipelineError::Io(format!("Failed to write topology GeoJSON: {}", e)))?; + + println!( + "Topology written: file={}, netelements={}, netrelations={}, status={:?}", + output_file, + topology.netelements.len(), + topology.netrelations.len(), + outcome.status, + ); + if !topology.validation_report.coarse_geometry_ids.is_empty() { + println!( + "Coarse netelement geometries: {} (ids: {})", + topology.validation_report.coarse_geometry_ids.len(), + topology.validation_report.coarse_geometry_ids.join(", "), + ); + } + if !topology.validation_report.uncovered_gnss_indices.is_empty() { + println!( + "Uncovered GNSS positions: {} indices", + topology.validation_report.uncovered_gnss_indices.len(), + ); + } + + match outcome.status { + RetrievalStatus::Success => Ok(()), + RetrievalStatus::InvalidInput => { + Err(PipelineError::RinfInvalidInput(outcome.detail_message)) + } + RetrievalStatus::MissingCoverage => { + Err(PipelineError::RinfMissingCoverage(outcome.detail_message)) + } + RetrievalStatus::IncompleteTopology => Err(PipelineError::RinfIncompleteTopology( + outcome.detail_message, + )), + RetrievalStatus::EndpointFailure => { + Err(PipelineError::RinfEndpointFailure(outcome.detail_message)) } } } @@ -578,7 +855,7 @@ fn derive_path_output(output_file: &str) -> String { fn run_default_command( gnss_file: &str, gnss_crs: Option<&str>, - network_file: &str, + network_file: Option<&str>, output_file: &str, train_path_file: Option<&str>, save_path_file: Option<&str>, @@ -600,6 +877,8 @@ fn run_default_command( punctual_detections: Option<&str>, linear_detections: Option<&str>, detection_cutoff: f64, + rinf_endpoint: Option<&str>, + rinf_buffer_meters: Option, ) -> Result<(), PipelineError> { // Suppress unused warning when webapp feature is disabled #[cfg(not(feature = "webapp"))] @@ -607,16 +886,22 @@ fn run_default_command( let output_format = determine_format(format, output_file)?; - // Load network - tracing::info!(network_file = %network_file, "Loading railway network"); - let (netelements, netrelations) = parse_network_geojson(network_file) - .map_err(|e| PipelineError::Io(format!("Failed to load network: {}", e)))?; + // Load GNSS positions first (needed for auto-retrieval bounding box). + let gnss_positions = load_gnss_positions(gnss_file, gnss_crs, lat_col, lon_col, time_col)?; tracing::info!( - netelement_count = netelements.len(), - netrelation_count = netrelations.len(), - "Railway network loaded" + position_count = gnss_positions.len(), + "GNSS positions loaded" ); + // Resolve topology (file or auto-retrieved from RINF). + let (netelements, netrelations) = resolve_cli_topology( + WorkflowKind::PathCalculation, + network_file, + &gnss_positions, + rinf_endpoint, + rinf_buffer_meters, + )?; + let network = RailwayNetwork::new(netelements.clone()) .map_err(|e| PipelineError::Processing(format!("Failed to build network index: {}", e)))?; @@ -626,13 +911,6 @@ fn run_default_command( .map(|ne| (ne.id.clone(), ne.clone())) .collect(); - // Load GNSS positions - let gnss_positions = load_gnss_positions(gnss_file, gnss_crs, lat_col, lon_col, time_col)?; - tracing::info!( - position_count = gnss_positions.len(), - "GNSS positions loaded" - ); - // Get or calculate train path let mut detection_provenance: Vec = Vec::new(); let mut train_path = if let Some(path_file) = train_path_file { @@ -795,7 +1073,7 @@ fn run_default_command( fn run_calculate_path( gnss_file: &str, gnss_crs: Option<&str>, - network_file: &str, + network_file: Option<&str>, output_file: &str, format: &str, distance_scale: f64, @@ -813,26 +1091,27 @@ fn run_calculate_path( punctual_detections: Option<&str>, linear_detections: Option<&str>, detection_cutoff: f64, + rinf_endpoint: Option<&str>, + rinf_buffer_meters: Option, ) -> Result<(), PipelineError> { let output_format = determine_format(format, output_file)?; - // Load network - tracing::info!(network_file = %network_file, "Loading railway network"); - let (netelements, netrelations) = parse_network_geojson(network_file) - .map_err(|e| PipelineError::Io(format!("Failed to load network: {}", e)))?; - tracing::info!( - netelement_count = netelements.len(), - netrelation_count = netrelations.len(), - "Railway network loaded" - ); - - // Load GNSS positions + // Load GNSS positions first (needed for auto-retrieval bounding box). let gnss_positions = load_gnss_positions(gnss_file, gnss_crs, lat_col, lon_col, time_col)?; tracing::info!( position_count = gnss_positions.len(), "GNSS positions loaded" ); + // Resolve topology (file or auto-retrieved from RINF). + let (netelements, netrelations) = resolve_cli_topology( + WorkflowKind::PathCalculation, + network_file, + &gnss_positions, + rinf_endpoint, + rinf_buffer_meters, + )?; + // Build path config let prepared_detections = prepare_detection_anchors( punctual_detections, @@ -909,28 +1188,18 @@ fn run_calculate_path( fn run_simple_projection( gnss_file: &str, gnss_crs: Option<&str>, - network_file: &str, + network_file: Option<&str>, output_file: &str, format: &str, warning_threshold: f64, lat_col: &str, lon_col: &str, time_col: &str, + rinf_endpoint: Option<&str>, + rinf_buffer_meters: Option, ) -> Result<(), PipelineError> { let output_format = determine_format(format, output_file)?; - // Load network (ignore netrelations for simple projection) - tracing::info!(network_file = %network_file, "Loading railway network"); - let (netelements, _netrelations) = parse_network_geojson(network_file) - .map_err(|e| PipelineError::Io(format!("Failed to load network: {}", e)))?; - tracing::info!( - netelement_count = netelements.len(), - "Railway network loaded" - ); - - let network = RailwayNetwork::new(netelements) - .map_err(|e| PipelineError::Processing(format!("Failed to build network index: {}", e)))?; - // Load GNSS positions let gnss_positions = load_gnss_positions(gnss_file, gnss_crs, lat_col, lon_col, time_col)?; tracing::info!( @@ -938,6 +1207,19 @@ fn run_simple_projection( "GNSS positions loaded" ); + // Resolve topology (file or auto-retrieved from RINF). Netrelations are + // ignored by simple projection, but loaded for consistent topology handling. + let (netelements, _netrelations) = resolve_cli_topology( + WorkflowKind::Projection, + network_file, + &gnss_positions, + rinf_endpoint, + rinf_buffer_meters, + )?; + + let network = RailwayNetwork::new(netelements) + .map_err(|e| PipelineError::Processing(format!("Failed to build network index: {}", e)))?; + // Project using simple nearest-netelement method let config = ProjectionConfig { projection_distance_warning_threshold: warning_threshold, diff --git a/tp-cli/tests/cli_integration_test.rs b/tp-cli/tests/cli_integration_test.rs index 6887423..f6e2abe 100644 --- a/tp-cli/tests/cli_integration_test.rs +++ b/tp-cli/tests/cli_integration_test.rs @@ -767,3 +767,135 @@ fn test_simple_projection_real_fixture() { "Output should contain at least one projected feature" ); } + +/// T018: calculate-path with supplied --network prints SuppliedTopology source line. +#[test] +fn test_calculate_path_supplied_topology_source_message() { + let temp_dir = TempDir::new().unwrap(); + let gnss_csv = create_path_test_gnss(&temp_dir); + let network_geojson = create_path_test_network(&temp_dir); + let output_file = temp_dir.path().join("path.geojson"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("tp-cli")); + cmd.arg("calculate-path") + .arg("--gnss") + .arg(&gnss_csv) + .arg("--crs") + .arg("EPSG:4326") + .arg("--network") + .arg(&network_geojson) + .arg("--output") + .arg(&output_file); + + cmd.assert() + .success() + .stdout(predicate::str::contains("SuppliedTopology")); +} + +/// T020: auto-retrieval against unreachable endpoint exits with code 7 (RinfEndpointFailure). +#[test] +fn test_calculate_path_unreachable_rinf_endpoint_exit_code_7() { + let temp_dir = TempDir::new().unwrap(); + let gnss_csv = create_path_test_gnss(&temp_dir); + let output_file = temp_dir.path().join("path.geojson"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("tp-cli")); + cmd.arg("--rinf-endpoint") + .arg("http://127.0.0.1:1/sparql") + .arg("--rinf-buffer-meters") + .arg("500") + .arg("calculate-path") + .arg("--gnss") + .arg(&gnss_csv) + .arg("--crs") + .arg("EPSG:4326") + .arg("--output") + .arg(&output_file); + + cmd.assert().code(7); +} + +/// T020: simple-projection without --network should auto-retrieve topology; +/// against an unreachable endpoint this exits with code 7 (RinfEndpointFailure). +#[test] +fn test_simple_projection_without_network_unreachable_rinf_endpoint_exit_code_7() { + let temp_dir = TempDir::new().unwrap(); + let gnss_csv = create_path_test_gnss(&temp_dir); + let output_file = temp_dir.path().join("projected.geojson"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("tp-cli")); + cmd.arg("--rinf-endpoint") + .arg("http://127.0.0.1:1/sparql") + .arg("--rinf-buffer-meters") + .arg("500") + .arg("simple-projection") + .arg("--gnss") + .arg(&gnss_csv) + .arg("--crs") + .arg("EPSG:4326") + .arg("--output") + .arg(&output_file); + + cmd.assert().code(7); +} + +/// T020: auto-retrieval with empty GNSS file should exit with code 4 (RinfInvalidInput) +/// or another input-related code; we accept >=4 and <=7 for the RINF error band. +#[test] +fn test_calculate_path_empty_gnss_rinf_invalid_input() { + let temp_dir = TempDir::new().unwrap(); + let gnss_csv = temp_dir.path().join("empty.csv"); + fs::write(&gnss_csv, "timestamp,latitude,longitude,altitude,hdop\n").unwrap(); + let output_file = temp_dir.path().join("path.geojson"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("tp-cli")); + cmd.arg("--rinf-endpoint") + .arg("http://127.0.0.1:1/sparql") + .arg("calculate-path") + .arg("--gnss") + .arg(&gnss_csv) + .arg("--crs") + .arg("EPSG:4326") + .arg("--output") + .arg(&output_file); + + let output = cmd.output().expect("cli should run"); + let code = output.status.code().unwrap_or(-1); + assert!( + (1..=7).contains(&code), + "expected non-zero exit (1..=7), got {code}; stderr={}", + String::from_utf8_lossy(&output.stderr) + ); +} + +/// T012: live RINF auto-retrieval against ERA endpoint. Ignored by default +/// (network access + slow). Run with `cargo test --test cli_integration_test -- --ignored`. +#[ignore] +#[test] +fn test_calculate_path_auto_retrieval_live_endpoint() { + let temp_dir = TempDir::new().unwrap(); + // Brussels area, small fix set; coverage expected in RINF. + let gnss_csv = temp_dir.path().join("gnss.csv"); + fs::write( + &gnss_csv, + "timestamp,latitude,longitude,altitude,hdop\n\ + 2025-12-09T14:30:00+01:00,50.8503,4.3517,100.0,2.0\n\ + 2025-12-09T14:30:01+01:00,50.8504,4.3518,100.5,2.1\n\ + 2025-12-09T14:30:02+01:00,50.8505,4.3519,101.0,2.0\n", + ) + .unwrap(); + let output_file = temp_dir.path().join("path.geojson"); + + let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("tp-cli")); + cmd.arg("calculate-path") + .arg("--gnss") + .arg(&gnss_csv) + .arg("--crs") + .arg("EPSG:4326") + .arg("--output") + .arg(&output_file); + + cmd.assert() + .success() + .stdout(predicate::str::contains("EraRinf")); +} diff --git a/tp-core/Cargo.toml b/tp-core/Cargo.toml index 070e43c..7c32e13 100644 --- a/tp-core/Cargo.toml +++ b/tp-core/Cargo.toml @@ -27,6 +27,10 @@ chrono.workspace = true thiserror.workspace = true tracing.workspace = true +# RINF SPARQL retrieval (feature 006) +ureq.workspace = true +url.workspace = true + [features] default = [] diff --git a/tp-core/src/errors.rs b/tp-core/src/errors.rs index cb51f81..bfd3b0f 100644 --- a/tp-core/src/errors.rs +++ b/tp-core/src/errors.rs @@ -43,4 +43,21 @@ pub enum ProjectionError { #[error("Invalid netrelation: {0}")] InvalidNetRelation(String), + + /// GNSS input was empty, unparseable, or otherwise unusable before any + /// retrieval attempt could be made. + #[error("Invalid GNSS input: {0}")] + InvalidGnssInput(String), + + /// The RINF SPARQL endpoint could not be reached or returned an unusable response. + #[error("RINF retrieval failed: {0}")] + RinfRetrievalFailed(String), + + /// The retrieval region produced zero netelements. + #[error("RINF coverage missing for area: {0}")] + RinfMissingCoverage(String), + + /// The retrieval returned netelements but no netrelations, or coarse geometries. + #[error("RINF topology incomplete: {0}")] + RinfIncompleteTopology(String), } diff --git a/tp-core/src/io.rs b/tp-core/src/io.rs index e9391ce..301e546 100644 --- a/tp-core/src/io.rs +++ b/tp-core/src/io.rs @@ -3,11 +3,14 @@ pub mod arrow; pub mod csv; pub mod geojson; +pub mod rinf; pub use csv::{ parse_gnss_csv, parse_gnss_csv_str, parse_trainpath_csv, write_csv, write_trainpath_csv, }; pub use geojson::{ parse_gnss_geojson, parse_gnss_geojson_str, parse_netrelations_geojson, parse_network_geojson, - parse_network_geojson_str, parse_trainpath_geojson, write_geojson, write_trainpath_geojson, + parse_network_geojson_str, parse_trainpath_geojson, write_geojson, write_network_geojson, + write_trainpath_geojson, }; +pub use rinf::{build_netelements_query, build_netrelations_query, SparqlClient, UreqSparqlClient}; diff --git a/tp-core/src/io/csv.rs b/tp-core/src/io/csv.rs index fdec3bf..53e5893 100644 --- a/tp-core/src/io/csv.rs +++ b/tp-core/src/io/csv.rs @@ -2,7 +2,8 @@ use crate::errors::ProjectionError; use crate::models::{AssociatedNetElement, GnssPosition, ProjectedPosition, TrainPath}; -use chrono::{DateTime, FixedOffset, NaiveDateTime, TimeZone, Utc}; +use crate::temporal::parse_timestamp_flexible_str; +use chrono::{DateTime, FixedOffset}; use polars::prelude::*; use std::collections::HashMap; @@ -24,23 +25,10 @@ const COL_END_INTRINSIC: &str = "end_intrinsic"; const COL_GNSS_START_INDEX: &str = "gnss_start_index"; const COL_GNSS_END_INDEX: &str = "gnss_end_index"; -/// Parse a timestamp string, accepting RFC3339 (with timezone) or ISO 8601 without timezone -/// (assumed to be UTC). +/// Parse a timestamp string, accepting RFC3339 (with timezone) or a naive +/// ISO 8601 datetime without timezone (interpreted as local time). fn parse_timestamp(s: &str) -> Result, String> { - // First try full RFC3339 (includes timezone) - if let Ok(dt) = DateTime::parse_from_rfc3339(s) { - return Ok(dt); - } - // Fall back: treat timezone-less ISO 8601 datetime as UTC - let naive = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") - .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")) - .map_err(|e| { - format!( - "{} (expected RFC3339 with timezone, e.g., 2025-12-09T14:30:00+01:00, or ISO 8601 without timezone assumed UTC)", - e - ) - })?; - Ok(Utc.from_utc_datetime(&naive).fixed_offset()) + parse_timestamp_flexible_str(s) } /// Parse GNSS positions from CSV file @@ -411,7 +399,7 @@ pub fn parse_trainpath_csv(path: &str) -> Result { if let Some(value) = comment.strip_prefix("overall_probability:") { overall_probability = value.trim().parse().ok(); } else if let Some(value) = comment.strip_prefix("calculated_at:") { - if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(value.trim()) { + if let Ok(dt) = parse_timestamp_flexible_str(value.trim()) { calculated_at = Some(dt.with_timezone(&chrono::Utc)); } } diff --git a/tp-core/src/io/csv/detections.rs b/tp-core/src/io/csv/detections.rs index e434c5f..9b71850 100644 --- a/tp-core/src/io/csv/detections.rs +++ b/tp-core/src/io/csv/detections.rs @@ -94,10 +94,10 @@ fn parse_timestamp( source_file: &str, source_row: usize, ) -> Result, DetectionError> { - DateTime::parse_from_rfc3339(s).map_err(|e| DetectionError::InvalidTimestamp { + crate::temporal::parse_timestamp_flexible_str(s).map_err(|e| DetectionError::InvalidTimestamp { source_file: source_file.to_string(), source_row, - message: format!("'{s}': {e}; RFC3339 with explicit timezone offset required"), + message: format!("'{s}': {e}"), }) } diff --git a/tp-core/src/io/csv/tests.rs b/tp-core/src/io/csv/tests.rs index 8d44f8a..028a9ab 100644 --- a/tp-core/src/io/csv/tests.rs +++ b/tp-core/src/io/csv/tests.rs @@ -549,32 +549,6 @@ fn test_parse_trainpath_csv_with_comments() { // Additional edge case tests for improved coverage -#[test] -fn test_parse_gnss_csv_timezone_validation_missing_utc() { - let mut file = NamedTempFile::new().unwrap(); - writeln!( - file, - "latitude,longitude,timestamp\n50.8503,4.3517,2025-12-09T14:30:00" - ) - .unwrap(); - - let result = parse_gnss_csv( - file.path().to_str().unwrap(), - "EPSG:4326", - "latitude", - "longitude", - "timestamp", - ); - - // Should succeed - timezone-less timestamps are assumed to be UTC - assert!(result.is_ok()); - if let Ok(positions) = result { - assert_eq!(positions.len(), 1); - // Verify timestamp was parsed as UTC (offset 0) - assert_eq!(positions[0].timestamp.offset().local_minus_utc(), 0); - } -} - #[test] fn test_parse_gnss_csv_empty_string_values() { let mut file = NamedTempFile::new().unwrap(); diff --git a/tp-core/src/io/geojson.rs b/tp-core/src/io/geojson.rs index 9ed2905..f62952e 100644 --- a/tp-core/src/io/geojson.rs +++ b/tp-core/src/io/geojson.rs @@ -2,7 +2,7 @@ use crate::errors::ProjectionError; use crate::models::{GnssPosition, NetRelation, Netelement, ProjectedPosition}; -use chrono::DateTime; +use crate::temporal::parse_timestamp_flexible; use geo::{Coord, LineString}; use geojson::{Feature, GeoJson, Value}; use std::fs; @@ -272,8 +272,9 @@ fn parse_gnss_feature( )) })?; - // Parse timestamp with timezone - let timestamp = DateTime::parse_from_rfc3339(timestamp_str).map_err(|e| { + // Parse timestamp; accept RFC3339 with timezone or naive ISO 8601 + // datetime interpreted as the host's local timezone. + let timestamp = parse_timestamp_flexible(timestamp_str).map_err(|e| { ProjectionError::InvalidTimestamp(format!( "Feature {}: invalid timestamp '{}': {}", idx, timestamp_str, e @@ -599,6 +600,92 @@ fn parse_netrelation_feature( Ok(netrelation) } +/// Write a railway network (netelements + netrelations) as a GeoJSON +/// FeatureCollection that round-trips through [`parse_network_geojson_str`]. +/// +/// Netelements are written as LineString features with an `id` property. +/// Netrelations are written as geometry-less features tagged with +/// `type="netrelation"` and the topology properties consumed by +/// [`parse_netrelation_feature`]. +pub fn write_network_geojson( + netelements: &[Netelement], + netrelations: &[NetRelation], + writer: &mut impl std::io::Write, +) -> Result<(), ProjectionError> { + use geojson::{Feature, FeatureCollection, Geometry, Value}; + use serde_json::{Map, Value as JsonValue}; + + let mut features: Vec = Vec::with_capacity(netelements.len() + netrelations.len()); + + for ne in netelements { + let coords: Vec> = ne.geometry.coords().map(|c| vec![c.x, c.y]).collect(); + let geometry = Geometry::new(Value::LineString(coords)); + + let mut properties = Map::new(); + properties.insert("id".to_string(), JsonValue::from(ne.id.clone())); + properties.insert("crs".to_string(), JsonValue::from(ne.crs.clone())); + + features.push(Feature { + bbox: None, + geometry: Some(geometry), + id: None, + properties: Some(properties), + foreign_members: None, + }); + } + + for nr in netrelations { + let navigability = match (nr.navigable_forward, nr.navigable_backward) { + (true, true) => "both", + (true, false) => "AB", + (false, true) => "BA", + (false, false) => "none", + }; + + let mut properties = Map::new(); + properties.insert("type".to_string(), JsonValue::from("netrelation")); + properties.insert("id".to_string(), JsonValue::from(nr.id.clone())); + properties.insert( + "netelementA".to_string(), + JsonValue::from(nr.from_netelement_id.clone()), + ); + properties.insert( + "netelementB".to_string(), + JsonValue::from(nr.to_netelement_id.clone()), + ); + properties.insert( + "positionOnA".to_string(), + JsonValue::from(nr.position_on_a as u64), + ); + properties.insert( + "positionOnB".to_string(), + JsonValue::from(nr.position_on_b as u64), + ); + properties.insert("navigability".to_string(), JsonValue::from(navigability)); + + features.push(Feature { + bbox: None, + geometry: None, + id: None, + properties: Some(properties), + foreign_members: None, + }); + } + + let feature_collection = FeatureCollection { + bbox: None, + features, + foreign_members: None, + }; + + let json = serde_json::to_string_pretty(&feature_collection).map_err(|e| { + ProjectionError::GeoJsonError(format!("Failed to serialize network GeoJSON: {}", e)) + })?; + + writer.write_all(json.as_bytes())?; + Ok(()) +} + /// Write projected positions as GeoJSON FeatureCollection pub fn write_geojson( positions: &[ProjectedPosition], @@ -715,7 +802,7 @@ pub fn parse_trainpath_geojson(path: &str) -> Result Result, DetectionError> { - DateTime::parse_from_rfc3339(s).map_err(|e| DetectionError::InvalidTimestamp { + crate::temporal::parse_timestamp_flexible_str(s).map_err(|e| DetectionError::InvalidTimestamp { source_file: source_file.to_string(), source_row, message: format!("'{s}': {e}"), diff --git a/tp-core/src/io/geojson/tests.rs b/tp-core/src/io/geojson/tests.rs index 55e13d4..b2c2c11 100644 --- a/tp-core/src/io/geojson/tests.rs +++ b/tp-core/src/io/geojson/tests.rs @@ -796,3 +796,154 @@ fn test_parse_netrelations_geojson_with_netelement_a_and_netelement_b() { } } } + +#[test] +fn test_parse_network_geojson_str_basic() { + let content = r#"{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "id": "NE001" }, + "geometry": { + "type": "LineString", + "coordinates": [[4.3517, 50.8503], [4.3527, 50.8513]] + } + } + ] + }"#; + + let (netelements, netrelations) = parse_network_geojson_str(content).unwrap(); + assert_eq!(netelements.len(), 1); + assert_eq!(netrelations.len(), 0); + assert_eq!(netelements[0].id, "NE001"); +} + +#[test] +fn test_parse_gnss_geojson_str_accepts_naive_iso_timestamp() { + let content = r#"{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "timestamp": "2026-05-25T12:34:56" }, + "geometry": { + "type": "Point", + "coordinates": [4.3517, 50.8503] + } + } + ] + }"#; + + let positions = parse_gnss_geojson_str(content, "EPSG:4326").unwrap(); + assert_eq!(positions.len(), 1); + assert_eq!(positions[0].latitude, 50.8503); + assert_eq!(positions[0].longitude, 4.3517); +} + +#[test] +fn test_write_network_geojson_round_trips_all_navigabilities() { + let netelements = vec![ + Netelement::new( + "NE_A".to_string(), + LineString::from(vec![(4.0, 50.0), (4.1, 50.1)]), + "EPSG:4326".to_string(), + ) + .unwrap(), + Netelement::new( + "NE_B".to_string(), + LineString::from(vec![(4.1, 50.1), (4.2, 50.2)]), + "EPSG:4326".to_string(), + ) + .unwrap(), + ]; + + let netrelations = vec![ + NetRelation::new( + "NR_BOTH".to_string(), + "NE_A".to_string(), + "NE_B".to_string(), + 0, + 1, + true, + true, + ) + .unwrap(), + NetRelation::new( + "NR_AB".to_string(), + "NE_A".to_string(), + "NE_B".to_string(), + 1, + 0, + true, + false, + ) + .unwrap(), + NetRelation::new( + "NR_BA".to_string(), + "NE_A".to_string(), + "NE_B".to_string(), + 1, + 1, + false, + true, + ) + .unwrap(), + NetRelation::new( + "NR_NONE".to_string(), + "NE_A".to_string(), + "NE_B".to_string(), + 0, + 0, + false, + false, + ) + .unwrap(), + ]; + + let mut out = Vec::new(); + write_network_geojson(&netelements, &netrelations, &mut out).unwrap(); + let content = String::from_utf8(out).unwrap(); + + let (parsed_nes, parsed_nrs) = parse_network_geojson_str(&content).unwrap(); + assert_eq!(parsed_nes.len(), 2); + assert_eq!(parsed_nrs.len(), 4); + + let ids: std::collections::HashSet = parsed_nrs.into_iter().map(|r| r.id).collect(); + assert!(ids.contains("NR_BOTH")); + assert!(ids.contains("NR_AB")); + assert!(ids.contains("NR_BA")); + assert!(ids.contains("NR_NONE")); +} + +#[test] +fn test_parse_trainpath_geojson_accepts_naive_calculated_at() { + let mut file = NamedTempFile::new().unwrap(); + let content = r#"{ + "type": "FeatureCollection", + "properties": { + "overall_probability": 0.75, + "calculated_at": "2026-05-25T12:34:56" + }, + "features": [ + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[4.0, 50.0], [4.1, 50.1]]}, + "properties": { + "netelement_id": "NE_A", + "probability": 0.75, + "start_intrinsic": 0.0, + "end_intrinsic": 1.0, + "gnss_start_index": 0, + "gnss_end_index": 1 + } + } + ] + }"#; + file.write_all(content.as_bytes()).unwrap(); + + let parsed = parse_trainpath_geojson(file.path().to_str().unwrap()).unwrap(); + assert_eq!(parsed.overall_probability, 0.75); + assert!(parsed.calculated_at.is_some()); + assert_eq!(parsed.segments.len(), 1); +} diff --git a/tp-core/src/io/rinf.rs b/tp-core/src/io/rinf.rs new file mode 100644 index 0000000..a09d8f0 --- /dev/null +++ b/tp-core/src/io/rinf.rs @@ -0,0 +1,603 @@ +//! ERA RINF SPARQL retrieval module (feature 006). +//! +//! Provides: +//! - The [`SparqlClient`] trait so production code can hit the real endpoint +//! via [`UreqSparqlClient`] while tests inject deterministic fixtures. +//! - A tiny inline WKT `LINESTRING` parser (avoids pulling a wkt crate). +//! - Builders for the two SPARQL queries documented in +//! `specs/006-download-rinf-topology/research.md`. +//! - Row-to-core-model mappers that produce [`Netelement`] / +//! [`NetRelation`] instances ready for downstream workflows. + +use std::time::Duration; + +use chrono::NaiveDate; +use geo::{LineString, Point}; +use serde_json::Value; + +use crate::errors::ProjectionError; +use crate::models::{ + NetRelation, Netelement, RinfNavigability, RinfNetelementRow, RinfNetrelationRow, +}; + +/// Pluggable SPARQL transport β€” production uses ureq, tests use mocks. +pub trait SparqlClient: Send + Sync { + /// Execute a SPARQL query and return parsed JSON (SPARQL-Results 1.1 shape). + fn query(&self, endpoint_url: &str, sparql: &str) -> Result; +} + +/// Default blocking SPARQL client backed by [`ureq`]. +pub struct UreqSparqlClient { + timeout: Duration, +} + +impl Default for UreqSparqlClient { + fn default() -> Self { + Self { + timeout: Duration::from_secs(60), + } + } +} + +impl UreqSparqlClient { + pub fn new(timeout: Duration) -> Self { + Self { timeout } + } +} + +impl SparqlClient for UreqSparqlClient { + fn query(&self, endpoint_url: &str, sparql: &str) -> Result { + let agent = ureq::AgentBuilder::new() + .timeout(self.timeout) + .user_agent("tp-lib/006-rinf-retrieval") + .build(); + let response = agent + .post(endpoint_url) + .set("Accept", "application/sparql-results+json") + .set("Content-Type", "application/sparql-query") + .send_string(sparql) + .map_err(|e| ProjectionError::RinfRetrievalFailed(format!("HTTP error: {e}")))?; + let json: Value = response + .into_json() + .map_err(|e| ProjectionError::RinfRetrievalFailed(format!("JSON parse error: {e}")))?; + Ok(json) + } +} + +/// Build the netelements SPARQL query for a given closed WGS84 polygon WKT. +pub fn build_netelements_query(polygon_wkt: &str) -> String { + format!( + r#"PREFIX era: +PREFIX gsp: +PREFIX geof: +PREFIX time: +PREFIX xsd: + +SELECT ?netelement ?netelement_wkt ?valid_from_date ?valid_to_date +WHERE {{ + ?netelement a era:LinearElement ; + era:validity/time:hasBeginning/time:inXSDDate ?valid_from_date ; + gsp:hasGeometry/gsp:asWKT ?netelement_wkt . + FILTER(geof:sfIntersects( + ?netelement_wkt, + "{polygon}"^^gsp:wktLiteral + )) + OPTIONAL {{ + ?netelement era:validity/time:hasEnd/time:inXSDDate ?valid_to_date . + FILTER (xsd:date(now()) >= ?valid_to_date) + }} + FILTER (xsd:date(now()) >= ?valid_from_date && !BOUND(?valid_to_date)) +}}"#, + polygon = polygon_wkt + ) +} + +/// Build the netrelations SPARQL query for a list of seed element IRIs. +pub fn build_netrelations_query(seed_iris: &[String]) -> String { + let values = seed_iris + .iter() + .map(|iri| format!("<{}>", iri)) + .collect::>() + .join(" "); + format!( + r#"PREFIX era: +PREFIX xsd: +PREFIX time: + +SELECT ?netrelation ?netelementA ?netelementB ?isOnOriginOfElementA ?isOnOriginOfElementB ?navigability ?valid_from_date ?valid_to_date +WHERE {{ + VALUES ?seed_element {{ {values} }} + {{ + BIND(?seed_element AS ?netelementA) + ?netrelation a era:NetRelation ; + era:elementA ?netelementA ; + era:elementB ?netelementB ; + era:isOnOriginOfElementA ?isOnOriginOfElementA ; + era:isOnOriginOfElementB ?isOnOriginOfElementB ; + era:navigability ?navigability ; + era:validity/time:hasBeginning/time:inXSDDate ?valid_from_date . + OPTIONAL {{ + ?netrelation era:validity/time:hasEnd/time:inXSDDate ?valid_to_date . + FILTER (xsd:date(now()) >= ?valid_to_date) + }} + FILTER (xsd:date(now()) >= ?valid_from_date && !BOUND(?valid_to_date)) + }} + UNION + {{ + BIND(?seed_element AS ?netelementB) + ?netrelation a era:NetRelation ; + era:elementA ?netelementA ; + era:elementB ?netelementB ; + era:isOnOriginOfElementA ?isOnOriginOfElementA ; + era:isOnOriginOfElementB ?isOnOriginOfElementB ; + era:navigability ?navigability ; + era:validity/time:hasBeginning/time:inXSDDate ?valid_from_date . + OPTIONAL {{ + ?netrelation era:validity/time:hasEnd/time:inXSDDate ?valid_to_date . + FILTER (xsd:date(now()) >= ?valid_to_date) + }} + FILTER (xsd:date(now()) >= ?valid_from_date && !BOUND(?valid_to_date)) + }} +}}"# + ) +} + +/// Parse a `LINESTRING(...)` WKT into a [`LineString`]. +/// +/// Inline parser to avoid pulling a wkt-crate dependency. Accepts upper or +/// lower-case keyword and any whitespace between tokens. Does NOT handle Z/M. +pub fn parse_wkt_linestring(wkt: &str) -> Result, ProjectionError> { + let trimmed = wkt.trim(); + let upper = trimmed.to_ascii_uppercase(); + let body = if let Some(rest) = upper.strip_prefix("LINESTRING") { + rest + } else { + return Err(ProjectionError::RinfIncompleteTopology(format!( + "WKT is not a LINESTRING: {trimmed}" + ))); + }; + // Use the original (un-uppercased) string for coordinate parsing β€” but + // since `body` was sliced from `upper`, recompute the same slice on the + // original. Easiest: just lowercase numbers are identical in either case. + let body = body.trim(); + let inner = body + .strip_prefix('(') + .and_then(|s| s.strip_suffix(')')) + .ok_or_else(|| { + ProjectionError::RinfIncompleteTopology(format!( + "Malformed LINESTRING parentheses: {trimmed}" + )) + })?; + let mut coords: Vec<(f64, f64)> = Vec::new(); + for pair in inner.split(',') { + let mut nums = pair.split_whitespace(); + let lon = nums + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| { + ProjectionError::RinfIncompleteTopology(format!( + "Missing or invalid longitude in WKT: {trimmed}" + )) + })?; + let lat = nums + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| { + ProjectionError::RinfIncompleteTopology(format!( + "Missing or invalid latitude in WKT: {trimmed}" + )) + })?; + coords.push((lon, lat)); + } + if coords.len() < 2 { + return Err(ProjectionError::RinfIncompleteTopology(format!( + "LINESTRING needs >=2 points: {trimmed}" + ))); + } + Ok(LineString::from(coords)) +} + +/// Approximate length of a WGS84 LineString in meters (great-circle, equirectangular). +pub fn linestring_length_meters(ls: &LineString) -> f64 { + let pts: Vec> = ls.points().collect(); + let mut total = 0.0; + for w in pts.windows(2) { + let (a, b) = (w[0], w[1]); + let lat_mid = (a.y() + b.y()) / 2.0; + let dx = (b.x() - a.x()) * 111_320.0 * lat_mid.to_radians().cos(); + let dy = (b.y() - a.y()) * 111_320.0; + total += (dx * dx + dy * dy).sqrt(); + } + total +} + +/// Extract the IRI tail to use as a stable id. +fn iri_to_id(iri: &str) -> String { + iri.rsplit(['/', '#']).next().unwrap_or(iri).to_string() +} + +fn binding_value<'a>(row: &'a Value, key: &str) -> Option<&'a str> { + row.get(key)?.get("value")?.as_str() +} + +fn parse_bool(s: &str) -> bool { + matches!(s.trim().to_ascii_lowercase().as_str(), "true" | "1") +} + +fn parse_navigability(iri_or_label: &str) -> RinfNavigability { + let tail = iri_to_id(iri_or_label).to_ascii_lowercase(); + match tail.as_str() { + "both" => RinfNavigability::Both, + "ab" | "atob" | "anbi" => RinfNavigability::AB, + "ba" | "btoa" | "binba" => RinfNavigability::BA, + "none" | "non-navigable" => RinfNavigability::None, + _ => RinfNavigability::Both, + } +} + +/// Parse the SPARQL-JSON response for the netelements query. +pub fn parse_netelements_response(json: &Value) -> Result, ProjectionError> { + let bindings = json + .get("results") + .and_then(|r| r.get("bindings")) + .and_then(|b| b.as_array()) + .ok_or_else(|| { + ProjectionError::RinfRetrievalFailed( + "Netelements response missing results.bindings array".to_string(), + ) + })?; + + let mut out = Vec::with_capacity(bindings.len()); + for row in bindings { + let iri = binding_value(row, "netelement").ok_or_else(|| { + ProjectionError::RinfRetrievalFailed("Missing ?netelement binding".to_string()) + })?; + let wkt = binding_value(row, "netelement_wkt").ok_or_else(|| { + ProjectionError::RinfRetrievalFailed("Missing ?netelement_wkt binding".to_string()) + })?; + let ls = parse_wkt_linestring(wkt)?; + let count = ls.coords().count(); + let length = linestring_length_meters(&ls); + out.push(RinfNetelementRow { + netelement_iri: iri.to_string(), + netelement_id: iri_to_id(iri), + wkt: wkt.to_string(), + geometry_point_count: count, + length_meters: length, + }); + } + Ok(out) +} + +/// Parse the SPARQL-JSON response for the netrelations query. +pub fn parse_netrelations_response( + json: &Value, +) -> Result, ProjectionError> { + let bindings = json + .get("results") + .and_then(|r| r.get("bindings")) + .and_then(|b| b.as_array()) + .ok_or_else(|| { + ProjectionError::RinfRetrievalFailed( + "Netrelations response missing results.bindings array".to_string(), + ) + })?; + let today = chrono::Utc::now().date_naive(); + let mut out = Vec::with_capacity(bindings.len()); + for row in bindings { + let iri = binding_value(row, "netrelation").ok_or_else(|| { + ProjectionError::RinfRetrievalFailed("Missing ?netrelation binding".to_string()) + })?; + let a = binding_value(row, "netelementA").ok_or_else(|| { + ProjectionError::RinfRetrievalFailed("Missing ?netelementA binding".to_string()) + })?; + let b = binding_value(row, "netelementB").ok_or_else(|| { + ProjectionError::RinfRetrievalFailed("Missing ?netelementB binding".to_string()) + })?; + let on_a = binding_value(row, "isOnOriginOfElementA") + .map(parse_bool) + .unwrap_or(false); + let on_b = binding_value(row, "isOnOriginOfElementB") + .map(parse_bool) + .unwrap_or(false); + let nav = binding_value(row, "navigability") + .map(parse_navigability) + .unwrap_or(RinfNavigability::Both); + let valid_on_date = binding_value(row, "valid_from_date") + .and_then(|s| NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()) + .unwrap_or(today); + out.push(RinfNetrelationRow { + netrelation_iri: iri.to_string(), + element_a_id: iri_to_id(a), + element_b_id: iri_to_id(b), + is_on_origin_of_element_a: on_a, + is_on_origin_of_element_b: on_b, + navigability: nav, + valid_on_date, + }); + } + Ok(out) +} + +/// Map parsed netelement rows to core [`Netelement`] structs. +/// +/// Returns the netelements plus a parallel `(id, length_meters, point_count)` +/// vector used by the validator to detect coarse geometries. +#[allow(clippy::type_complexity)] +pub fn map_netelements_to_core( + rows: &[RinfNetelementRow], +) -> Result<(Vec, Vec<(String, f64, usize)>), ProjectionError> { + let mut nes = Vec::with_capacity(rows.len()); + let mut lengths = Vec::with_capacity(rows.len()); + for r in rows { + let ls = parse_wkt_linestring(&r.wkt)?; + let length = linestring_length_meters(&ls); + let count = ls.coords().count(); + let ne = Netelement::new(r.netelement_id.clone(), ls, "EPSG:4326".to_string())?; + lengths.push((r.netelement_id.clone(), length, count)); + nes.push(ne); + } + Ok((nes, lengths)) +} + +/// Map parsed netrelation rows to core [`NetRelation`] structs. +/// +/// Drops rows whose endpoints don't reference loaded netelements. +pub fn map_netrelations_to_core( + rows: &[RinfNetrelationRow], + netelements: &[Netelement], +) -> Result, ProjectionError> { + use std::collections::HashSet; + let known: HashSet<&str> = netelements.iter().map(|n| n.id.as_str()).collect(); + let mut out = Vec::with_capacity(rows.len()); + for r in rows { + if !known.contains(r.element_a_id.as_str()) || !known.contains(r.element_b_id.as_str()) { + continue; + } + if r.element_a_id == r.element_b_id { + continue; + } + let (fwd, bwd) = match r.navigability { + RinfNavigability::Both => (true, true), + RinfNavigability::AB => (true, false), + RinfNavigability::BA => (false, true), + RinfNavigability::None => (false, false), + }; + let pos_a: u8 = if r.is_on_origin_of_element_a { 0 } else { 1 }; + let pos_b: u8 = if r.is_on_origin_of_element_b { 0 } else { 1 }; + let id = iri_to_id(&r.netrelation_iri); + let nr = NetRelation::new( + id, + r.element_a_id.clone(), + r.element_b_id.clone(), + pos_a, + pos_b, + fwd, + bwd, + )?; + out.push(nr); + } + Ok(out) +} + +/// High-level helper: fetch + parse netelements for a search polygon. +pub fn fetch_netelements( + client: &dyn SparqlClient, + endpoint_url: &str, + polygon_wkt: &str, +) -> Result, ProjectionError> { + let query = build_netelements_query(polygon_wkt); + let json = client.query(endpoint_url, &query)?; + parse_netelements_response(&json) +} + +/// High-level helper: fetch + parse netrelations for the given seed IRIs. +pub fn fetch_netrelations( + client: &dyn SparqlClient, + endpoint_url: &str, + seed_iris: &[String], +) -> Result, ProjectionError> { + if seed_iris.is_empty() { + return Ok(Vec::new()); + } + let query = build_netrelations_query(seed_iris); + let json = client.query(endpoint_url, &query)?; + parse_netrelations_response(&json) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::sync::atomic::{AtomicUsize, Ordering}; + + #[test] + fn wkt_linestring_parses_basic() { + let ls = parse_wkt_linestring("LINESTRING(11.0 60.0, 11.1 60.1)").unwrap(); + assert_eq!(ls.coords().count(), 2); + } + + #[test] + fn wkt_linestring_rejects_single_point() { + assert!(parse_wkt_linestring("LINESTRING(11.0 60.0)").is_err()); + } + + #[test] + fn iri_tail_is_used_as_id() { + assert_eq!( + iri_to_id("http://data.europa.eu/949/linearElement/SMOKE-A"), + "SMOKE-A" + ); + } + + #[test] + fn netelements_query_contains_polygon() { + let q = build_netelements_query("POLYGON((1 2, 3 4))"); + assert!(q.contains("POLYGON((1 2, 3 4))")); + assert!(q.contains("sfIntersects")); + } + + #[test] + fn wkt_linestring_rejects_non_linestring() { + let err = parse_wkt_linestring("POINT(11.0 60.0)").unwrap_err(); + assert!(err.to_string().contains("not a LINESTRING")); + } + + #[test] + fn wkt_linestring_rejects_malformed_coordinates() { + let err = parse_wkt_linestring("LINESTRING(11.0, 11.1 60.1)").unwrap_err(); + assert!(err.to_string().contains("longitude") || err.to_string().contains("latitude")); + } + + #[test] + fn parse_netelements_response_rejects_missing_bindings() { + let err = parse_netelements_response(&json!({"results": {}})).unwrap_err(); + assert!(err.to_string().contains("results.bindings")); + } + + #[test] + fn parse_netelements_response_maps_rows() { + let input = json!({ + "results": { + "bindings": [ + { + "netelement": {"value": "http://example/linearElement/NE-A"}, + "netelement_wkt": {"value": "LINESTRING(11.0 60.0, 11.1 60.1)"} + } + ] + } + }); + + let rows = parse_netelements_response(&input).unwrap(); + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].netelement_id, "NE-A"); + assert_eq!(rows[0].geometry_point_count, 2); + assert!(rows[0].length_meters > 0.0); + } + + #[test] + fn parse_netrelations_response_uses_defaults() { + let input = json!({ + "results": { + "bindings": [ + { + "netrelation": {"value": "http://example/netRelation/NR-1"}, + "netelementA": {"value": "http://example/linearElement/NE-A"}, + "netelementB": {"value": "http://example/linearElement/NE-B"} + } + ] + } + }); + + let rows = parse_netrelations_response(&input).unwrap(); + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].element_a_id, "NE-A"); + assert_eq!(rows[0].element_b_id, "NE-B"); + assert_eq!(rows[0].navigability, RinfNavigability::Both); + assert!(!rows[0].is_on_origin_of_element_a); + assert!(!rows[0].is_on_origin_of_element_b); + } + + #[test] + fn map_netrelations_to_core_filters_unknown_and_self_loops() { + let ne_a = Netelement::new( + "NE-A".to_string(), + parse_wkt_linestring("LINESTRING(11.0 60.0, 11.1 60.1)").unwrap(), + "EPSG:4326".to_string(), + ) + .unwrap(); + let ne_b = Netelement::new( + "NE-B".to_string(), + parse_wkt_linestring("LINESTRING(11.1 60.1, 11.2 60.2)").unwrap(), + "EPSG:4326".to_string(), + ) + .unwrap(); + + let rows = vec![ + RinfNetrelationRow { + netrelation_iri: "http://example/netRelation/NR-valid".to_string(), + element_a_id: "NE-A".to_string(), + element_b_id: "NE-B".to_string(), + is_on_origin_of_element_a: true, + is_on_origin_of_element_b: false, + navigability: RinfNavigability::AB, + valid_on_date: chrono::Utc::now().date_naive(), + }, + RinfNetrelationRow { + netrelation_iri: "http://example/netRelation/NR-unknown".to_string(), + element_a_id: "NE-A".to_string(), + element_b_id: "NE-X".to_string(), + is_on_origin_of_element_a: true, + is_on_origin_of_element_b: false, + navigability: RinfNavigability::Both, + valid_on_date: chrono::Utc::now().date_naive(), + }, + RinfNetrelationRow { + netrelation_iri: "http://example/netRelation/NR-self".to_string(), + element_a_id: "NE-A".to_string(), + element_b_id: "NE-A".to_string(), + is_on_origin_of_element_a: true, + is_on_origin_of_element_b: false, + navigability: RinfNavigability::Both, + valid_on_date: chrono::Utc::now().date_naive(), + }, + ]; + + let mapped = map_netrelations_to_core(&rows, &[ne_a, ne_b]).unwrap(); + assert_eq!(mapped.len(), 1); + assert_eq!(mapped[0].id, "NR-valid"); + assert!(mapped[0].navigable_forward); + assert!(!mapped[0].navigable_backward); + } + + struct CountingClient { + calls: AtomicUsize, + payload: Value, + } + + impl SparqlClient for CountingClient { + fn query(&self, _endpoint_url: &str, _sparql: &str) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); + Ok(self.payload.clone()) + } + } + + #[test] + fn fetch_netrelations_with_empty_seeds_short_circuits() { + let client = CountingClient { + calls: AtomicUsize::new(0), + payload: json!({"results": {"bindings": []}}), + }; + + let out = fetch_netrelations(&client, "https://example.invalid", &[]).unwrap(); + assert!(out.is_empty()); + assert_eq!(client.calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn fetch_netelements_executes_query_and_parses() { + let client = CountingClient { + calls: AtomicUsize::new(0), + payload: json!({ + "results": { + "bindings": [ + { + "netelement": {"value": "http://example/linearElement/NE-A"}, + "netelement_wkt": {"value": "LINESTRING(11.0 60.0, 11.1 60.1)"} + } + ] + } + }), + }; + + let out = fetch_netelements( + &client, + "https://example.invalid", + "POLYGON((0 0,1 0,1 1,0 1,0 0))", + ) + .unwrap(); + assert_eq!(out.len(), 1); + assert_eq!(out[0].netelement_id, "NE-A"); + assert_eq!(client.calls.load(Ordering::SeqCst), 1); + } +} diff --git a/tp-core/src/lib.rs b/tp-core/src/lib.rs index 40c62bf..4bd4e25 100644 --- a/tp-core/src/lib.rs +++ b/tp-core/src/lib.rs @@ -47,6 +47,7 @@ pub mod models; pub mod path; pub mod projection; pub mod temporal; +pub mod workflow; // Re-export main types for convenience pub use detections::{ @@ -54,10 +55,11 @@ pub use detections::{ }; pub use errors::ProjectionError; pub use io::{ - parse_gnss_csv, parse_gnss_csv_str, parse_gnss_geojson, parse_gnss_geojson_str, - parse_netrelations_geojson, parse_network_geojson, parse_network_geojson_str, - parse_trainpath_csv, parse_trainpath_geojson, write_csv, write_geojson, write_trainpath_csv, - write_trainpath_geojson, + build_netelements_query, build_netrelations_query, parse_gnss_csv, parse_gnss_csv_str, + parse_gnss_geojson, parse_gnss_geojson_str, parse_netrelations_geojson, parse_network_geojson, + parse_network_geojson_str, parse_trainpath_csv, parse_trainpath_geojson, write_csv, + write_geojson, write_network_geojson, write_trainpath_csv, write_trainpath_geojson, + SparqlClient, UreqSparqlClient, }; pub use models::{ AssociatedNetElement, @@ -79,9 +81,23 @@ pub use models::{ ProjectedPosition, PunctualDetection, ResolvedAnchor, + // Feature 006: RINF retrieval + RetrievalArea, + RetrievalOutcome, + RetrievalStatus, + RetrievedTopology, + RinfNavigability, + RinfNetelementRow, + RinfNetrelationRow, SegmentDiagnostic, TimestampOrRange, + TopologySource, + TopologyValidationReport, + TopologyValidationStatus, TrainPath, + WorkflowKind, + DEFAULT_RETRIEVAL_BUFFER_METERS, + DEFAULT_RINF_ENDPOINT, }; pub use path::{ calculate_mean_spacing, @@ -101,6 +117,7 @@ pub use path::{ PositionCandidates, TransitionProbabilityEntry, }; +pub use workflow::{build_retrieval_area, resolve_topology, validate_topology, RetrievalConfig}; /// Result type alias using ProjectionError pub type Result = std::result::Result; diff --git a/tp-core/src/models.rs b/tp-core/src/models.rs index e61491f..401a583 100644 --- a/tp-core/src/models.rs +++ b/tp-core/src/models.rs @@ -10,6 +10,7 @@ pub mod netrelation; pub mod path_metadata; pub mod path_origin; pub mod projected_position; +pub mod retrieval; pub mod train_path; pub use associated_net_element::AssociatedNetElement; @@ -27,4 +28,11 @@ pub use netrelation::NetRelation; pub use path_metadata::{PathDiagnosticInfo, PathMetadata, SegmentDiagnostic}; pub use path_origin::PathOrigin; pub use projected_position::ProjectedPosition; +pub use retrieval::{ + AutoTopologyRequest, RetrievalArea, RetrievalOutcome, RetrievalStatus, RetrievedTopology, + RinfNavigability, RinfNetelementRow, RinfNetrelationRow, TopologySource, + TopologyValidationReport, TopologyValidationStatus, WorkflowKind, + COARSE_GEOMETRY_LENGTH_THRESHOLD_METERS, DEFAULT_RETRIEVAL_BUFFER_METERS, + DEFAULT_RINF_ENDPOINT, +}; pub use train_path::TrainPath; diff --git a/tp-core/src/models/retrieval.rs b/tp-core/src/models/retrieval.rs new file mode 100644 index 0000000..d1c3da4 --- /dev/null +++ b/tp-core/src/models/retrieval.rs @@ -0,0 +1,160 @@ +//! Retrieval domain types for automatic ERA RINF topology download (feature 006). +//! +//! See `specs/006-download-rinf-topology/data-model.md` for the canonical +//! definitions. Types here describe spatial search regions, request envelopes, +//! typed SPARQL rows, the assembled topology bundle, its validation report, +//! and the caller-visible outcome. + +use chrono::{DateTime, NaiveDate, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::models::{NetRelation, Netelement}; + +/// Default expansion (meters) around the GNSS envelope. +pub const DEFAULT_RETRIEVAL_BUFFER_METERS: f64 = 1000.0; + +/// Default RINF SPARQL endpoint. +pub const DEFAULT_RINF_ENDPOINT: &str = "https://graph.data.era.europa.eu/repositories/rinf-plus"; + +/// Coarse-geometry threshold (meters) above which more than two points are required. +pub const COARSE_GEOMETRY_LENGTH_THRESHOLD_METERS: f64 = 250.0; + +/// Spatial search region sent to the RINF SPARQL endpoint. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct RetrievalArea { + pub min_longitude: f64, + pub max_longitude: f64, + pub min_latitude: f64, + pub max_latitude: f64, + pub expansion_meters: f64, + pub polygon_wkt: String, + pub source_crs: String, +} + +/// Which workflow triggered the automatic-retrieval decision. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum WorkflowKind { + Projection, + PathCalculation, + DetectionPreparation, + PathReview, +} + +/// Workflow invocation that may require automatic topology retrieval. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AutoTopologyRequest { + pub workflow_kind: WorkflowKind, + pub supplied_topology_present: bool, + pub rinf_endpoint_url: String, + pub retrieval_area: Option, + pub requested_at: DateTime, +} + +/// One parsed netelement row from the SPARQL response. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RinfNetelementRow { + pub netelement_iri: String, + pub netelement_id: String, + pub wkt: String, + pub geometry_point_count: usize, + pub length_meters: f64, +} + +/// Navigability classification as encoded by ERA. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "PascalCase")] +pub enum RinfNavigability { + Both, + /// Navigable only from element A to element B. + AB, + /// Navigable only from element B to element A. + BA, + None, +} + +/// One parsed netrelation row from the SPARQL response. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RinfNetrelationRow { + pub netrelation_iri: String, + pub element_a_id: String, + pub element_b_id: String, + pub is_on_origin_of_element_a: bool, + pub is_on_origin_of_element_b: bool, + pub navigability: RinfNavigability, + pub valid_on_date: NaiveDate, +} + +/// Validation status for a retrieved topology bundle. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TopologyValidationStatus { + Valid, + MissingCoverage, + IncompleteTopology, + InvalidInput, + EndpointFailure, +} + +/// Explains whether a downloaded topology is usable. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TopologyValidationReport { + pub status: TopologyValidationStatus, + pub netelement_count: usize, + pub netrelation_count: usize, + pub coarse_geometry_ids: Vec, + pub uncovered_gnss_indices: Vec, + pub message: String, +} + +/// Normalized topology bundle ready for downstream workflows. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RetrievedTopology { + pub netelements: Vec, + pub netrelations: Vec, + pub retrieval_area: RetrievalArea, + pub endpoint_url: String, + pub retrieved_at: DateTime, + pub validation_report: TopologyValidationReport, +} + +/// Which source supplied the topology used by a workflow. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TopologySource { + SuppliedTopology, + EraRinf, +} + +/// Outcome status surfaced to callers. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum RetrievalStatus { + Success, + InvalidInput, + MissingCoverage, + IncompleteTopology, + EndpointFailure, +} + +/// Caller-visible outcome of source selection and validation. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RetrievalOutcome { + pub source_used: TopologySource, + pub status: RetrievalStatus, + pub detail_message: String, + pub diagnostic_area_wkt: Option, + pub affected_gnss_indices: Vec, +} + +impl RetrievalOutcome { + pub fn supplied_success() -> Self { + Self { + source_used: TopologySource::SuppliedTopology, + status: RetrievalStatus::Success, + detail_message: "Using supplied topology".to_string(), + diagnostic_area_wkt: None, + affected_gnss_indices: Vec::new(), + } + } +} diff --git a/tp-core/src/temporal.rs b/tp-core/src/temporal.rs index f3beb50..3326088 100644 --- a/tp-core/src/temporal.rs +++ b/tp-core/src/temporal.rs @@ -1,9 +1,46 @@ //! Temporal utilities for timezone handling use crate::errors::ProjectionError; -use chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone}; -/// Parse RFC3339 timestamp with timezone validation +/// Parse a timestamp accepting either RFC3339 (with timezone) or a naive +/// ISO 8601 datetime without timezone. Naive datetimes are interpreted as +/// the host's local timezone and returned with that offset attached, so +/// downstream code always works on `DateTime` with explicit +/// timezone information. +pub fn parse_timestamp_flexible(s: &str) -> Result, ProjectionError> { + parse_timestamp_flexible_str(s).map_err(ProjectionError::InvalidTimestamp) +} + +/// Same as [`parse_timestamp_flexible`] but returns the raw error string so +/// callers using their own error types (e.g. `DetectionError`) can wrap it. +pub fn parse_timestamp_flexible_str(s: &str) -> Result, String> { + if let Ok(dt) = DateTime::parse_from_rfc3339(s) { + return Ok(dt); + } + let naive = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") + .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")) + .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f")) + .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")) + .map_err(|e| { + format!( + "{} (expected RFC3339 with timezone, e.g. 2025-12-09T14:30:00+01:00, \ + or ISO 8601 without timezone interpreted as local time)", + e + ) + })?; + Local + .from_local_datetime(&naive) + .single() + .map(|dt| dt.fixed_offset()) + .ok_or_else(|| format!("ambiguous or non-existent local time: '{}'", s)) +} + +/// Parse RFC3339 timestamp with strict timezone validation. +/// +/// Kept for callers that explicitly require timezone-bearing input. Prefer +/// [`parse_timestamp_flexible`] for user-facing inputs (CSV/GeoJSON files, +/// detection feeds, etc.) where a naive datetime should be accepted. pub fn parse_rfc3339_with_timezone(s: &str) -> Result, ProjectionError> { DateTime::parse_from_rfc3339(s) .map_err(|e| ProjectionError::MissingTimezone(format!("Invalid timestamp: {}", e))) diff --git a/tp-core/src/workflow.rs b/tp-core/src/workflow.rs new file mode 100644 index 0000000..48fd1aa --- /dev/null +++ b/tp-core/src/workflow.rs @@ -0,0 +1,451 @@ +//! Source selection and retrieval-area construction for automatic topology +//! retrieval (feature 006). +//! +//! Combines GNSS-derived bounding boxes, the RINF SPARQL client trait, and the +//! validation pipeline that produces a [`RetrievedTopology`] ready for use by +//! the existing path/projection/detection algorithms. + +use chrono::Utc; + +use crate::errors::ProjectionError; +use crate::io::rinf::{ + fetch_netelements, fetch_netrelations, map_netelements_to_core, map_netrelations_to_core, + SparqlClient, +}; +use crate::models::{ + AutoTopologyRequest, GnssPosition, NetRelation, Netelement, RetrievalArea, RetrievalOutcome, + RetrievalStatus, RetrievedTopology, TopologySource, TopologyValidationReport, + TopologyValidationStatus, WorkflowKind, COARSE_GEOMETRY_LENGTH_THRESHOLD_METERS, + DEFAULT_RETRIEVAL_BUFFER_METERS, DEFAULT_RINF_ENDPOINT, +}; + +/// Retrieval configuration shared by the CLI and language bindings. +#[derive(Debug, Clone)] +pub struct RetrievalConfig { + pub endpoint_url: String, + pub buffer_meters: f64, +} + +impl Default for RetrievalConfig { + fn default() -> Self { + Self { + endpoint_url: DEFAULT_RINF_ENDPOINT.to_string(), + buffer_meters: DEFAULT_RETRIEVAL_BUFFER_METERS, + } + } +} + +impl RetrievalConfig { + pub fn with_endpoint(mut self, endpoint: impl Into) -> Self { + self.endpoint_url = endpoint.into(); + self + } + + pub fn with_buffer_meters(mut self, buffer_meters: f64) -> Self { + self.buffer_meters = buffer_meters; + self + } +} + +/// Build a 1 km-expanded (configurable) WGS84 axis-aligned search polygon from +/// GNSS positions. +/// +/// Returns `Err(InvalidGnssInput)` if no usable WGS84 coordinates are present. +pub fn build_retrieval_area( + positions: &[GnssPosition], + buffer_meters: f64, +) -> Result { + if positions.is_empty() { + return Err(ProjectionError::InvalidGnssInput( + "GNSS dataset is empty".to_string(), + )); + } + + let mut min_lon = f64::INFINITY; + let mut max_lon = f64::NEG_INFINITY; + let mut min_lat = f64::INFINITY; + let mut max_lat = f64::NEG_INFINITY; + let mut count = 0usize; + + for p in positions { + let lat = p.latitude; + let lon = p.longitude; + if !lat.is_finite() || !lon.is_finite() { + continue; + } + if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) { + continue; + } + min_lon = min_lon.min(lon); + max_lon = max_lon.max(lon); + min_lat = min_lat.min(lat); + max_lat = max_lat.max(lat); + count += 1; + } + + if count == 0 { + return Err(ProjectionError::InvalidGnssInput( + "No usable WGS84 coordinates in GNSS dataset".to_string(), + )); + } + + let center_lat = (min_lat + max_lat) / 2.0; + let lat_expand = buffer_meters / 111_320.0; + let lon_expand = buffer_meters / (111_320.0 * center_lat.to_radians().cos().max(1e-6)); + + let exp_min_lon = min_lon - lon_expand; + let exp_max_lon = max_lon + lon_expand; + let exp_min_lat = min_lat - lat_expand; + let exp_max_lat = max_lat + lat_expand; + + let polygon_wkt = format!( + "POLYGON(({lo1} {la1}, {lo2} {la1}, {lo2} {la2}, {lo1} {la2}, {lo1} {la1}))", + lo1 = exp_min_lon, + lo2 = exp_max_lon, + la1 = exp_min_lat, + la2 = exp_max_lat, + ); + + Ok(RetrievalArea { + min_longitude: exp_min_lon, + max_longitude: exp_max_lon, + min_latitude: exp_min_lat, + max_latitude: exp_max_lat, + expansion_meters: buffer_meters, + polygon_wkt, + source_crs: "EPSG:4326".to_string(), + }) +} + +/// Find indices of GNSS positions that fall outside the bounding box of the +/// retrieved netelements (used for `uncovered_gnss_indices` diagnostics). +pub fn uncovered_gnss_indices( + positions: &[GnssPosition], + netelements: &[Netelement], +) -> Vec { + if netelements.is_empty() { + return (0..positions.len()).collect(); + } + let mut min_lon = f64::INFINITY; + let mut max_lon = f64::NEG_INFINITY; + let mut min_lat = f64::INFINITY; + let mut max_lat = f64::NEG_INFINITY; + for ne in netelements { + for c in ne.geometry.coords() { + min_lon = min_lon.min(c.x); + max_lon = max_lon.max(c.x); + min_lat = min_lat.min(c.y); + max_lat = max_lat.max(c.y); + } + } + positions + .iter() + .enumerate() + .filter_map(|(i, p)| { + let inside = p.longitude >= min_lon + && p.longitude <= max_lon + && p.latitude >= min_lat + && p.latitude <= max_lat; + if inside { + None + } else { + Some(i) + } + }) + .collect() +} + +/// Validate a topology bundle produced from RINF. +pub fn validate_topology( + netelements: &[Netelement], + netrelations: &[NetRelation], + netelement_lengths: &[(String, f64, usize)], + positions: &[GnssPosition], +) -> TopologyValidationReport { + if netelements.is_empty() { + return TopologyValidationReport { + status: TopologyValidationStatus::MissingCoverage, + netelement_count: 0, + netrelation_count: 0, + coarse_geometry_ids: Vec::new(), + uncovered_gnss_indices: (0..positions.len()).collect(), + message: "No netelements returned for the search area".to_string(), + }; + } + + let coarse_ids: Vec = netelement_lengths + .iter() + .filter_map(|(id, length, points)| { + if *length > COARSE_GEOMETRY_LENGTH_THRESHOLD_METERS && *points <= 2 { + Some(id.clone()) + } else { + None + } + }) + .collect(); + + // Only treat the topology as incomplete when *every* netelement is coarse. + // A few 2-point segments are expected (straight sections, short links) and + // are not a reason to reject the whole bundle. + if !coarse_ids.is_empty() && coarse_ids.len() == netelements.len() { + return TopologyValidationReport { + status: TopologyValidationStatus::IncompleteTopology, + netelement_count: netelements.len(), + netrelation_count: netrelations.len(), + coarse_geometry_ids: coarse_ids, + uncovered_gnss_indices: Vec::new(), + message: "Retrieved topology contains only coarse netelement geometries".to_string(), + }; + } + + if netrelations.is_empty() { + return TopologyValidationReport { + status: TopologyValidationStatus::IncompleteTopology, + netelement_count: netelements.len(), + netrelation_count: 0, + coarse_geometry_ids: Vec::new(), + uncovered_gnss_indices: Vec::new(), + message: "Retrieved topology has zero netrelations".to_string(), + }; + } + + let uncovered = uncovered_gnss_indices(positions, netelements); + + TopologyValidationReport { + status: TopologyValidationStatus::Valid, + netelement_count: netelements.len(), + netrelation_count: netrelations.len(), + coarse_geometry_ids: coarse_ids, + uncovered_gnss_indices: uncovered, + message: "Topology validated successfully".to_string(), + } +} + +/// Resolve the topology for a workflow. Returns the bundle plus an outcome +/// summary suitable for surfacing to callers. +/// +/// If `supplied` is `Some`, it is used verbatim and no retrieval is performed. +/// Otherwise the SPARQL client is invoked with a polygon derived from `positions`. +pub fn resolve_topology( + workflow_kind: WorkflowKind, + positions: &[GnssPosition], + supplied: Option<(Vec, Vec)>, + config: &RetrievalConfig, + client: &dyn SparqlClient, +) -> Result<(RetrievedTopology, RetrievalOutcome), ProjectionError> { + if let Some((nes, nrs)) = supplied { + let area = RetrievalArea { + min_longitude: 0.0, + max_longitude: 0.0, + min_latitude: 0.0, + max_latitude: 0.0, + expansion_meters: 0.0, + polygon_wkt: String::new(), + source_crs: "EPSG:4326".to_string(), + }; + let report = TopologyValidationReport { + status: TopologyValidationStatus::Valid, + netelement_count: nes.len(), + netrelation_count: nrs.len(), + coarse_geometry_ids: Vec::new(), + uncovered_gnss_indices: Vec::new(), + message: "Supplied topology".to_string(), + }; + let topology = RetrievedTopology { + netelements: nes, + netrelations: nrs, + retrieval_area: area, + endpoint_url: String::new(), + retrieved_at: Utc::now(), + validation_report: report, + }; + return Ok((topology, RetrievalOutcome::supplied_success())); + } + + let area = build_retrieval_area(positions, config.buffer_meters)?; + + let _request = AutoTopologyRequest { + workflow_kind, + supplied_topology_present: false, + rinf_endpoint_url: config.endpoint_url.clone(), + retrieval_area: Some(area.clone()), + requested_at: Utc::now(), + }; + + let netelement_rows = fetch_netelements(client, &config.endpoint_url, &area.polygon_wkt) + .map_err(|e| ProjectionError::RinfRetrievalFailed(e.to_string()))?; + + if netelement_rows.is_empty() { + let report = TopologyValidationReport { + status: TopologyValidationStatus::MissingCoverage, + netelement_count: 0, + netrelation_count: 0, + coarse_geometry_ids: Vec::new(), + uncovered_gnss_indices: (0..positions.len()).collect(), + message: "No netelements returned for the search area".to_string(), + }; + let outcome = RetrievalOutcome { + source_used: TopologySource::EraRinf, + status: RetrievalStatus::MissingCoverage, + detail_message: report.message.clone(), + diagnostic_area_wkt: Some(area.polygon_wkt.clone()), + affected_gnss_indices: report.uncovered_gnss_indices.clone(), + }; + let topology = RetrievedTopology { + netelements: Vec::new(), + netrelations: Vec::new(), + retrieval_area: area, + endpoint_url: config.endpoint_url.clone(), + retrieved_at: Utc::now(), + validation_report: report, + }; + return Ok((topology, outcome)); + } + + let (netelements, lengths) = map_netelements_to_core(&netelement_rows)?; + + let seed_iris: Vec = netelement_rows + .iter() + .map(|r| r.netelement_iri.clone()) + .collect(); + let netrelation_rows = fetch_netrelations(client, &config.endpoint_url, &seed_iris) + .map_err(|e| ProjectionError::RinfRetrievalFailed(e.to_string()))?; + let netrelations = map_netrelations_to_core(&netrelation_rows, &netelements)?; + + let report = validate_topology(&netelements, &netrelations, &lengths, positions); + let status = match report.status { + TopologyValidationStatus::Valid => RetrievalStatus::Success, + TopologyValidationStatus::MissingCoverage => RetrievalStatus::MissingCoverage, + TopologyValidationStatus::IncompleteTopology => RetrievalStatus::IncompleteTopology, + TopologyValidationStatus::EndpointFailure => RetrievalStatus::EndpointFailure, + TopologyValidationStatus::InvalidInput => RetrievalStatus::InvalidInput, + }; + let outcome = RetrievalOutcome { + source_used: TopologySource::EraRinf, + status, + detail_message: report.message.clone(), + diagnostic_area_wkt: Some(area.polygon_wkt.clone()), + affected_gnss_indices: report.uncovered_gnss_indices.clone(), + }; + + let topology = RetrievedTopology { + netelements, + netrelations, + retrieval_area: area, + endpoint_url: config.endpoint_url.clone(), + retrieved_at: Utc::now(), + validation_report: report, + }; + + Ok((topology, outcome)) +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{DateTime, FixedOffset}; + use geo::LineString; + use std::collections::HashMap; + + fn gnss(lat: f64, lon: f64) -> GnssPosition { + let ts: DateTime = + DateTime::parse_from_rfc3339("2026-05-13T08:00:00+00:00").unwrap(); + GnssPosition { + latitude: lat, + longitude: lon, + timestamp: ts, + crs: "EPSG:4326".to_string(), + metadata: HashMap::new(), + heading: None, + distance: None, + } + } + + fn ne(id: &str, wkt: &str) -> Netelement { + Netelement::new( + id.to_string(), + crate::io::rinf::parse_wkt_linestring(wkt).unwrap(), + "EPSG:4326".to_string(), + ) + .unwrap() + } + + #[test] + fn retrieval_config_builder_methods_override_defaults() { + let cfg = RetrievalConfig::default() + .with_endpoint("https://example.invalid/sparql") + .with_buffer_meters(250.0); + assert_eq!(cfg.endpoint_url, "https://example.invalid/sparql"); + assert_eq!(cfg.buffer_meters, 250.0); + } + + #[test] + fn build_retrieval_area_skips_non_finite_and_out_of_range_points() { + let positions = vec![ + gnss(f64::NAN, 4.0), + gnss(200.0, 4.0), + gnss(50.0, 4.0), + gnss(50.1, 4.2), + ]; + + let area = build_retrieval_area(&positions, 100.0).unwrap(); + assert!(area.min_latitude < 50.0); + assert!(area.max_latitude > 50.1); + assert!(area.min_longitude < 4.0); + assert!(area.max_longitude > 4.2); + } + + #[test] + fn build_retrieval_area_rejects_when_all_points_invalid() { + let positions = vec![gnss(f64::NAN, 4.0), gnss(95.0, 4.0), gnss(40.0, 190.0)]; + let err = build_retrieval_area(&positions, 100.0).unwrap_err(); + assert!(err.to_string().contains("No usable WGS84 coordinates")); + } + + #[test] + fn uncovered_gnss_indices_returns_all_when_no_netelements() { + let positions = vec![gnss(50.0, 4.0), gnss(50.1, 4.1)]; + let uncovered = uncovered_gnss_indices(&positions, &[]); + assert_eq!(uncovered, vec![0, 1]); + } + + #[test] + fn uncovered_gnss_indices_marks_outside_points() { + let positions = vec![gnss(50.0, 4.0), gnss(51.0, 5.0)]; + let netelements = vec![ne("NE-1", "LINESTRING(3.9 49.9, 4.2 50.2)")]; + let uncovered = uncovered_gnss_indices(&positions, &netelements); + assert_eq!(uncovered, vec![1]); + } + + #[test] + fn validate_topology_returns_missing_coverage_for_empty_netelements() { + let report = validate_topology(&[], &[], &[], &[gnss(50.0, 4.0)]); + assert_eq!(report.status, TopologyValidationStatus::MissingCoverage); + assert_eq!(report.uncovered_gnss_indices, vec![0]); + } + + #[test] + fn validate_topology_returns_incomplete_when_all_netelements_coarse() { + let netelements = vec![ne("NE-1", "LINESTRING(4.0 50.0, 4.2 50.0)")]; + let report = validate_topology( + &netelements, + &[], + &[("NE-1".to_string(), 20_000.0, 2)], + &[gnss(50.0, 4.0)], + ); + assert_eq!(report.status, TopologyValidationStatus::IncompleteTopology); + assert_eq!(report.coarse_geometry_ids, vec!["NE-1".to_string()]); + } + + #[test] + fn validate_topology_returns_incomplete_when_no_netrelations() { + let netelements = vec![Netelement::new( + "NE-1".to_string(), + LineString::from(vec![(4.0, 50.0), (4.0001, 50.0001), (4.0002, 50.0002)]), + "EPSG:4326".to_string(), + ) + .unwrap()]; + let report = validate_topology(&netelements, &[], &[("NE-1".to_string(), 100.0, 3)], &[]); + assert_eq!(report.status, TopologyValidationStatus::IncompleteTopology); + } +} diff --git a/tp-core/tests/contract.rs b/tp-core/tests/contract.rs index 9c147c6..069dee5 100644 --- a/tp-core/tests/contract.rs +++ b/tp-core/tests/contract.rs @@ -1,2 +1,5 @@ #[path = "contract/lib_api_stability_test.rs"] pub mod lib_api_stability_test; + +#[path = "contract/rinf_query_shape_test.rs"] +pub mod rinf_query_shape_test; diff --git a/tp-core/tests/contract/rinf_query_shape_test.rs b/tp-core/tests/contract/rinf_query_shape_test.rs new file mode 100644 index 0000000..280ccd0 --- /dev/null +++ b/tp-core/tests/contract/rinf_query_shape_test.rs @@ -0,0 +1,46 @@ +//! T013 - Contract assertions for RINF SPARQL query shape and relation mapping. + +use tp_lib_core::{build_netelements_query, build_netrelations_query}; + +#[test] +fn netelements_query_targets_linear_elements_with_polygon_filter() { + let polygon = "POLYGON((4.0 50.0, 4.1 50.0, 4.1 50.1, 4.0 50.1, 4.0 50.0))"; + let q = build_netelements_query(polygon); + assert!(q.contains("PREFIX era:"), "missing era prefix: {q}"); + assert!(q.contains("PREFIX gsp:"), "missing gsp prefix: {q}"); + assert!(q.contains("era:LinearElement"), "must select LinearElement"); + assert!(q.contains("gsp:hasGeometry"), "must traverse hasGeometry"); + assert!(q.contains("geof:sfIntersects"), "must intersect polygon"); + assert!(q.contains(polygon), "must embed polygon WKT literal"); + assert!(q.contains("gsp:wktLiteral"), "polygon must be wktLiteral"); +} + +#[test] +fn netrelations_query_filters_by_seed_iris_and_navigability() { + let seeds = vec![ + "http://data.europa.eu/949/functionalInfrastructure/netElements/A".to_string(), + "http://data.europa.eu/949/functionalInfrastructure/netElements/B".to_string(), + ]; + let q = build_netrelations_query(&seeds); + assert!(q.contains("era:NetRelation"), "must select NetRelation"); + assert!(q.contains("era:elementA"), "must include elementA"); + assert!(q.contains("era:elementB"), "must include elementB"); + assert!(q.contains("era:navigability"), "must include navigability"); + assert!( + q.contains("?valid_from_date"), + "must project valid_from_date" + ); + assert!(q.contains("VALUES ?seed_element"), "must use VALUES clause"); + for iri in &seeds { + assert!(q.contains(iri), "must embed seed IRI {iri}"); + } +} + +#[test] +fn netrelations_query_handles_empty_seed_list() { + let q = build_netrelations_query(&[]); + assert!( + q.contains("VALUES ?seed_element"), + "must still have VALUES clause" + ); +} diff --git a/tp-core/tests/detections_load.rs b/tp-core/tests/detections_load.rs index 5962a7b..8e34ffc 100644 --- a/tp-core/tests/detections_load.rs +++ b/tp-core/tests/detections_load.rs @@ -105,7 +105,7 @@ fn invalid_schema_punctual_kind_mismatch() { #[test] fn invalid_timestamp_rejected() { - let csv = "timestamp,netelement_id\n2026-05-01 08:15:30,NE-1\n"; + let csv = "timestamp,netelement_id\n2026-05-01 08:75:30,NE-1\n"; let f = write_named(csv, ".csv"); let err = load_detections(f.path(), DetectionKind::Punctual).unwrap_err(); assert!(matches!(err, DetectionError::InvalidTimestamp { .. })); diff --git a/tp-core/tests/fixtures/rinf_smoke_netelements.json b/tp-core/tests/fixtures/rinf_smoke_netelements.json new file mode 100644 index 0000000..29d88a6 --- /dev/null +++ b/tp-core/tests/fixtures/rinf_smoke_netelements.json @@ -0,0 +1,31 @@ +{ + "head": { + "vars": ["netelement", "netelement_wkt"] + }, + "results": { + "bindings": [ + { + "netelement": { + "type": "uri", + "value": "http://data.europa.eu/949/linearElement/SMOKE-A" + }, + "netelement_wkt": { + "type": "literal", + "datatype": "http://www.opengis.net/ont/geosparql#wktLiteral", + "value": "LINESTRING(11.50 60.00, 11.51 60.005, 11.52 60.01, 11.53 60.015, 11.54 60.02)" + } + }, + { + "netelement": { + "type": "uri", + "value": "http://data.europa.eu/949/linearElement/SMOKE-B" + }, + "netelement_wkt": { + "type": "literal", + "datatype": "http://www.opengis.net/ont/geosparql#wktLiteral", + "value": "LINESTRING(11.54 60.02, 11.55 60.025, 11.56 60.03, 11.57 60.035, 11.58 60.04, 11.59 60.045, 11.60 60.05)" + } + } + ] + } +} diff --git a/tp-core/tests/fixtures/rinf_smoke_netrelations.json b/tp-core/tests/fixtures/rinf_smoke_netrelations.json new file mode 100644 index 0000000..862e56f --- /dev/null +++ b/tp-core/tests/fixtures/rinf_smoke_netrelations.json @@ -0,0 +1,42 @@ +{ + "head": { + "vars": ["netrelation", "netelementA", "netelementB", "isOnOriginOfElementA", "isOnOriginOfElementB", "navigability", "valid_from_date"] + }, + "results": { + "bindings": [ + { + "netrelation": { + "type": "uri", + "value": "http://data.europa.eu/949/netRelation/SMOKE-AB" + }, + "netelementA": { + "type": "uri", + "value": "http://data.europa.eu/949/linearElement/SMOKE-A" + }, + "netelementB": { + "type": "uri", + "value": "http://data.europa.eu/949/linearElement/SMOKE-B" + }, + "isOnOriginOfElementA": { + "type": "literal", + "datatype": "http://www.w3.org/2001/XMLSchema#boolean", + "value": "false" + }, + "isOnOriginOfElementB": { + "type": "literal", + "datatype": "http://www.w3.org/2001/XMLSchema#boolean", + "value": "true" + }, + "navigability": { + "type": "uri", + "value": "http://data.europa.eu/949/concepts/navigabilities/Both" + }, + "valid_from_date": { + "type": "literal", + "datatype": "http://www.w3.org/2001/XMLSchema#date", + "value": "2026-01-01" + } + } + ] + } +} diff --git a/tp-core/tests/rinf_topology.rs b/tp-core/tests/rinf_topology.rs new file mode 100644 index 0000000..58d7d45 --- /dev/null +++ b/tp-core/tests/rinf_topology.rs @@ -0,0 +1,248 @@ +//! Feature 006 β€” RINF topology retrieval integration tests. +//! +//! These tests use the [`MockSparqlClient`] to feed canned JSON responses into +//! [`resolve_topology`], so they run offline and deterministically. + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Mutex; + +use chrono::{DateTime, FixedOffset}; +use serde_json::Value; + +use tp_lib_core::io::rinf::SparqlClient; +use tp_lib_core::workflow::{build_retrieval_area, resolve_topology, RetrievalConfig}; +use tp_lib_core::{ + GnssPosition, ProjectionError, RetrievalStatus, TopologySource, TopologyValidationStatus, + WorkflowKind, +}; + +/// In-memory SPARQL client used by all feature-006 tests. +/// +/// Returns a queued response per call. If the queue is empty, returns an +/// error so tests can verify endpoint-failure paths. +struct MockSparqlClient { + queue: Mutex>, +} + +enum MockResponse { + Ok(Value), + Err(String), +} + +impl MockSparqlClient { + fn new(responses: Vec) -> Self { + Self { + queue: Mutex::new(responses), + } + } +} + +impl SparqlClient for MockSparqlClient { + fn query(&self, _endpoint_url: &str, _sparql: &str) -> Result { + let mut q = self.queue.lock().unwrap(); + if q.is_empty() { + return Err(ProjectionError::RinfRetrievalFailed( + "MockSparqlClient queue is empty".to_string(), + )); + } + match q.remove(0) { + MockResponse::Ok(v) => Ok(v), + MockResponse::Err(m) => Err(ProjectionError::RinfRetrievalFailed(m)), + } + } +} + +fn load_fixture(name: &str) -> Value { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/fixtures"); + path.push(name); + let raw = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read fixture {path:?}: {e}")); + serde_json::from_str(&raw).expect("fixture must be valid JSON") +} + +fn gnss(lat: f64, lon: f64) -> GnssPosition { + let ts: DateTime = + DateTime::parse_from_rfc3339("2026-05-13T08:00:00+00:00").unwrap(); + GnssPosition { + latitude: lat, + longitude: lon, + timestamp: ts, + crs: "EPSG:4326".to_string(), + metadata: HashMap::new(), + heading: None, + distance: None, + } +} + +fn empty_results() -> Value { + serde_json::json!({ + "head": {"vars": []}, + "results": {"bindings": []} + }) +} + +#[test] +fn build_retrieval_area_rejects_empty_input() { + let err = build_retrieval_area(&[], 1000.0).unwrap_err(); + matches!(err, ProjectionError::InvalidGnssInput(_)); +} + +#[test] +fn build_retrieval_area_expands_by_one_kilometer() { + let positions = vec![gnss(60.00, 11.50), gnss(60.10, 11.60)]; + let area = build_retrieval_area(&positions, 1000.0).unwrap(); + // Latitude expanded by ~0.00898 deg, longitude by more (cosine factor). + assert!(area.max_latitude > 60.10); + assert!(area.min_latitude < 60.00); + assert!(area.max_longitude > 11.60); + assert!(area.min_longitude < 11.50); + assert!(area.polygon_wkt.starts_with("POLYGON((")); + assert_eq!(area.source_crs, "EPSG:4326"); +} + +#[test] +fn supplied_topology_short_circuits_retrieval() { + let positions = vec![gnss(60.00, 11.50)]; + let client = MockSparqlClient::new(vec![]); + let config = RetrievalConfig::default(); + let (topology, outcome) = resolve_topology( + WorkflowKind::PathCalculation, + &positions, + Some((Vec::new(), Vec::new())), + &config, + &client, + ) + .unwrap(); + assert_eq!(outcome.source_used, TopologySource::SuppliedTopology); + assert_eq!(outcome.status, RetrievalStatus::Success); + assert_eq!( + topology.validation_report.status, + TopologyValidationStatus::Valid + ); +} + +#[test] +fn covered_area_retrieves_and_validates_topology() { + let positions = vec![gnss(60.00, 11.50), gnss(60.02, 11.54)]; + let netelements = load_fixture("rinf_smoke_netelements.json"); + let netrelations = load_fixture("rinf_smoke_netrelations.json"); + let client = MockSparqlClient::new(vec![ + MockResponse::Ok(netelements), + MockResponse::Ok(netrelations), + ]); + let config = RetrievalConfig::default(); + let (topology, outcome) = resolve_topology( + WorkflowKind::PathCalculation, + &positions, + None, + &config, + &client, + ) + .unwrap(); + assert_eq!(outcome.source_used, TopologySource::EraRinf); + assert_eq!(outcome.status, RetrievalStatus::Success); + assert_eq!(topology.netelements.len(), 2); + assert_eq!(topology.netrelations.len(), 1); + assert_eq!(topology.netrelations[0].from_netelement_id, "SMOKE-A"); + assert_eq!(topology.netrelations[0].to_netelement_id, "SMOKE-B"); + assert!(topology.netrelations[0].navigable_forward); + assert!(topology.netrelations[0].navigable_backward); + assert_eq!( + topology.validation_report.status, + TopologyValidationStatus::Valid + ); +} + +#[test] +fn missing_coverage_yields_missing_coverage_outcome() { + let positions = vec![gnss(45.0, -30.0)]; + let client = MockSparqlClient::new(vec![MockResponse::Ok(empty_results())]); + let (_topology, outcome) = resolve_topology( + WorkflowKind::PathCalculation, + &positions, + None, + &RetrievalConfig::default(), + &client, + ) + .unwrap(); + assert_eq!(outcome.status, RetrievalStatus::MissingCoverage); + assert_eq!(outcome.source_used, TopologySource::EraRinf); + assert!(outcome.diagnostic_area_wkt.is_some()); +} + +#[test] +fn netelements_with_no_netrelations_yields_incomplete_topology() { + let positions = vec![gnss(60.0, 11.5)]; + let netelements = load_fixture("rinf_smoke_netelements.json"); + let client = MockSparqlClient::new(vec![ + MockResponse::Ok(netelements), + MockResponse::Ok(empty_results()), + ]); + let (_topology, outcome) = resolve_topology( + WorkflowKind::PathCalculation, + &positions, + None, + &RetrievalConfig::default(), + &client, + ) + .unwrap(); + assert_eq!(outcome.status, RetrievalStatus::IncompleteTopology); +} + +#[test] +fn coarse_geometry_yields_incomplete_topology() { + // 1 km-long element with only 2 points -> should be flagged coarse. + let positions = vec![gnss(60.0, 11.5)]; + let coarse = serde_json::json!({ + "head": {"vars": ["netelement", "netelement_wkt"]}, + "results": {"bindings": [{ + "netelement": {"type": "uri", "value": "http://example/coarse/COARSE-X"}, + "netelement_wkt": {"type": "literal", "value": "LINESTRING(11.50 60.00, 11.52 60.00)"} + }]} + }); + let client = MockSparqlClient::new(vec![ + MockResponse::Ok(coarse), + MockResponse::Ok(empty_results()), + ]); + let (_topology, outcome) = resolve_topology( + WorkflowKind::PathCalculation, + &positions, + None, + &RetrievalConfig::default(), + &client, + ) + .unwrap(); + assert_eq!(outcome.status, RetrievalStatus::IncompleteTopology); + assert!(outcome.detail_message.to_lowercase().contains("coarse")); +} + +#[test] +fn endpoint_failure_propagates_as_retrieval_error() { + let positions = vec![gnss(60.0, 11.5)]; + let client = MockSparqlClient::new(vec![MockResponse::Err("503 Service Unavailable".into())]); + let err = resolve_topology( + WorkflowKind::PathCalculation, + &positions, + None, + &RetrievalConfig::default(), + &client, + ) + .unwrap_err(); + matches!(err, ProjectionError::RinfRetrievalFailed(_)); +} + +#[test] +fn empty_gnss_yields_invalid_input_error() { + let client = MockSparqlClient::new(vec![]); + let err = resolve_topology( + WorkflowKind::PathCalculation, + &[], + None, + &RetrievalConfig::default(), + &client, + ) + .unwrap_err(); + matches!(err, ProjectionError::InvalidGnssInput(_)); +} diff --git a/tp-net/README.md b/tp-net/README.md index cedc63c..0f65349 100644 --- a/tp-net/README.md +++ b/tp-net/README.md @@ -41,6 +41,40 @@ if (result.HasPath) } ``` +## Automatic RINF Topology Retrieval + +When you do not have a local network GeoJSON, omit it and let the library +download a bounding-box subset of the ERA RINF topology on demand: + +```csharp +using TpLib; + +var gnss = GnssInput.FromGeoJson(File.ReadAllText("gnss.geojson")); +var rinf = new RinfRetrievalOptions +{ + EndpointUrl = "https://graph.data.era.europa.eu/repositories/rinf-plus", + BufferMeters = 1000.0, +}; + +// Pass null for the network to trigger auto-retrieval. +var projections = Projection.ProjectGnssAuto(network: null, gnss, rinfOptions: rinf); +var path = PathCalculation.CalculateTrainPathAuto(network: null, gnss, rinfOptions: rinf); +``` + +Typed exceptions are raised for retrieval failures: +`TpLibInvalidGnssInputException`, `TpLibRinfMissingCoverageException`, +`TpLibRinfIncompleteTopologyException`, `TpLibRinfRetrievalFailedException`. + +## Timestamps + +Timestamps inside GeoJSON / CSV input may be RFC3339 with an explicit +offset (e.g. `2025-12-09T14:30:00+01:00`, `2025-12-09T14:30:00Z`) or +naive ISO 8601 (e.g. `2025-12-09T14:30:00`, `2025-12-09 14:30:00`). +Naive values are interpreted in the host's **local** timezone by the +native library. All managed records expose timestamps as +`DateTimeOffset`, and any timestamps emitted by the library are +RFC3339 strings with an explicit offset. + ## Supported platforms | RID | OS / Architecture | Native library | diff --git a/tp-net/csharp/Exceptions.cs b/tp-net/csharp/Exceptions.cs index 1bc2917..8942967 100644 --- a/tp-net/csharp/Exceptions.cs +++ b/tp-net/csharp/Exceptions.cs @@ -54,3 +54,38 @@ public sealed class TpLibDetectionException : TpLibException public TpLibDetectionException(string message) : base(message) { } public TpLibDetectionException(string message, Exception inner) : base(message, inner) { } } + +/// Base class for ERA RINF topology retrieval failures (feature 006). +public class TpLibRinfException : TpLibException +{ + public TpLibRinfException(string message) : base(message) { } + public TpLibRinfException(string message, Exception inner) : base(message, inner) { } +} + +/// GNSS input was invalid (empty, mixed CRS, etc.) for auto retrieval. +public sealed class TpLibInvalidGnssInputException : TpLibRinfException +{ + public TpLibInvalidGnssInputException(string message) : base(message) { } + public TpLibInvalidGnssInputException(string message, Exception inner) : base(message, inner) { } +} + +/// The SPARQL endpoint returned an HTTP / transport / parse failure. +public sealed class TpLibRinfRetrievalFailedException : TpLibRinfException +{ + public TpLibRinfRetrievalFailedException(string message) : base(message) { } + public TpLibRinfRetrievalFailedException(string message, Exception inner) : base(message, inner) { } +} + +/// Some GNSS positions fall outside the retrieved RINF coverage. +public sealed class TpLibRinfMissingCoverageException : TpLibRinfException +{ + public TpLibRinfMissingCoverageException(string message) : base(message) { } + public TpLibRinfMissingCoverageException(string message, Exception inner) : base(message, inner) { } +} + +/// Retrieved topology is structurally incomplete (e.g. coarse geometry, zero netrelations). +public sealed class TpLibRinfIncompleteTopologyException : TpLibRinfException +{ + public TpLibRinfIncompleteTopologyException(string message) : base(message) { } + public TpLibRinfIncompleteTopologyException(string message, Exception inner) : base(message, inner) { } +} diff --git a/tp-net/csharp/Models.cs b/tp-net/csharp/Models.cs index 666810d..7dfc669 100644 --- a/tp-net/csharp/Models.cs +++ b/tp-net/csharp/Models.cs @@ -32,6 +32,16 @@ public sealed record PathConfig public double DetectionCutoffDistanceMeters { get; init; } = 2.5; } +/// +/// Options for automatic ERA RINF topology retrieval (feature 006). Used when +/// the caller supplies no network file. Defaults mirror the Rust core. +/// +public sealed record RinfRetrievalOptions +{ + public string EndpointUrl { get; init; } = "https://graph.data.era.europa.eu/repositories/rinf-plus"; + public double BufferMeters { get; init; } = 1000.0; +} + // --------------------------------------------------------------------------- // Output records (deserialized from FFI JSON β€” property names match Rust // snake_case via [JsonPropertyName]). diff --git a/tp-net/csharp/NativeMethods.g.cs b/tp-net/csharp/NativeMethods.g.cs index da2be09..c350098 100644 --- a/tp-net/csharp/NativeMethods.g.cs +++ b/tp-net/csharp/NativeMethods.g.cs @@ -61,6 +61,37 @@ internal static unsafe partial class NativeMethods [DllImport(__DllName, EntryPoint = "tp_net_prepare_detections", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern ByteBuffer tp_net_prepare_detections(byte* network_ptr, int network_len, byte* gnss_ptr, int gnss_len, byte* detections_geojson_ptr, int detections_geojson_len, byte kind_is_linear, double cutoff_distance_meters); + /// + /// Calculate a train path with optional RINF auto-retrieval. + /// + /// When `network_ptr` is null or `network_len <= 0`, the railway topology is + /// retrieved from the configured ERA RINF SPARQL endpoint based on the GNSS + /// positions. Otherwise the supplied network GeoJSON is used as-is (RINF + /// options are ignored). + /// + /// On error, returns a JSON `ByteBuffer` whose payload contains an + /// `{"__error": "...", "message": "..."}` envelope rather than the FFI + /// null-error sentinel, so the C# layer can raise a typed RINF exception. + /// + /// # Safety + /// All non-null pointers must reference valid UTF-8 byte slices of the + /// indicated length. + /// + [DllImport(__DllName, EntryPoint = "tp_net_calculate_train_path_auto", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern ByteBuffer tp_net_calculate_train_path_auto(byte* network_ptr, int network_len, byte* gnss_ptr, int gnss_len, byte* prepared_detections_ptr, int prepared_detections_len, byte* rinf_endpoint_ptr, int rinf_endpoint_len, double rinf_buffer_meters, PathConfigFfi config); + + /// + /// Project GNSS positions with optional RINF auto-retrieval. + /// + /// See [`tp_net_calculate_train_path_auto`] for the auto-retrieval contract. + /// + /// # Safety + /// All non-null pointers must reference valid UTF-8 byte slices of the + /// indicated length. + /// + [DllImport(__DllName, EntryPoint = "tp_net_project_gnss_auto", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern ByteBuffer tp_net_project_gnss_auto(byte* network_ptr, int network_len, byte* gnss_ptr, int gnss_len, byte* rinf_endpoint_ptr, int rinf_endpoint_len, double rinf_buffer_meters, ProjectionConfigFfi config); + /// /// Free a [`ByteBuffer`] previously returned by any `tp_net_*` function. /// diff --git a/tp-net/csharp/Tests/PathCalculationTests.cs b/tp-net/csharp/Tests/PathCalculationTests.cs index 5b1a12d..f7a4b22 100644 --- a/tp-net/csharp/Tests/PathCalculationTests.cs +++ b/tp-net/csharp/Tests/PathCalculationTests.cs @@ -53,4 +53,36 @@ public void CalculateTrainPath_MalformedGeoJson_ThrowsParse() Assert.Throws(() => PathCalculation.CalculateTrainPath(network, gnss)); } + + [Fact] + public void CalculateTrainPathAuto_SuppliedNetwork_TakesPrecedence() + { + // Supplying a network must short-circuit RINF retrieval, even with an + // intentionally unreachable endpoint configured. + var network = NetworkInput.FromGeoJson(TestData.Read("sample_network.geojson")); + var gnss = GnssInput.FromGeoJson(TestData.Read("sample_gnss.geojson")); + var rinf = new RinfRetrievalOptions + { + EndpointUrl = "http://127.0.0.1:1/never", + BufferMeters = 250.0, + }; + + var result = PathCalculation.CalculateTrainPathAuto(network, gnss, rinfOptions: rinf); + + Assert.NotNull(result); + } + + [Fact] + public void CalculateTrainPathAuto_NullNetworkUnreachableEndpoint_ThrowsRetrievalFailed() + { + var gnss = GnssInput.FromGeoJson(TestData.Read("sample_gnss.geojson")); + var rinf = new RinfRetrievalOptions + { + EndpointUrl = "http://127.0.0.1:1/never", + BufferMeters = 250.0, + }; + + Assert.Throws( + () => PathCalculation.CalculateTrainPathAuto(null, gnss, rinfOptions: rinf)); + } } diff --git a/tp-net/csharp/Tests/ProjectionTests.cs b/tp-net/csharp/Tests/ProjectionTests.cs index 516585b..3a707d3 100644 --- a/tp-net/csharp/Tests/ProjectionTests.cs +++ b/tp-net/csharp/Tests/ProjectionTests.cs @@ -103,4 +103,34 @@ public void ProjectOntoPath_RoundTrip_PopulatesIntrinsic() } }); } + + [Fact] + public void ProjectGnssAuto_SuppliedNetworkTakesPrecedence() + { + var network = NetworkInput.FromGeoJson(TestData.Read("sample_network.geojson")); + var gnss = GnssInput.FromGeoJson(TestData.Read("sample_gnss.geojson")); + var rinf = new RinfRetrievalOptions + { + EndpointUrl = "http://127.0.0.1:1/never", + BufferMeters = 250.0, + }; + + var result = Projection.ProjectGnssAuto(network, gnss, rinfOptions: rinf); + + Assert.NotEmpty(result); + } + + [Fact] + public void ProjectGnssAuto_NullNetworkUnreachableEndpoint_ThrowsRetrievalFailed() + { + var gnss = GnssInput.FromGeoJson(TestData.Read("sample_gnss.geojson")); + var rinf = new RinfRetrievalOptions + { + EndpointUrl = "http://127.0.0.1:1/never", + BufferMeters = 250.0, + }; + + Assert.Throws( + () => Projection.ProjectGnssAuto(null, gnss, rinfOptions: rinf)); + } } diff --git a/tp-net/csharp/TpLib.cs b/tp-net/csharp/TpLib.cs index 718d62f..f45cc14 100644 --- a/tp-net/csharp/TpLib.cs +++ b/tp-net/csharp/TpLib.cs @@ -90,6 +90,57 @@ public static IReadOnlyList ProjectGnss( string networkGeoJson, string gnssGeoJson, ProjectionConfig? config = null) => ProjectGnss(NetworkInput.FromGeoJson(networkGeoJson), GnssInput.FromGeoJson(gnssGeoJson), config); + /// + /// Project GNSS positions with optional automatic ERA RINF topology + /// retrieval. When is null the railway + /// topology is retrieved from the ERA RINF SPARQL endpoint configured by + /// (feature 006). + /// + public static IReadOnlyList ProjectGnssAuto( + NetworkInput? network, + GnssInput gnss, + ProjectionConfig? config = null, + RinfRetrievalOptions? rinfOptions = null) + { + ArgumentNullException.ThrowIfNull(gnss); + config ??= new ProjectionConfig(); + TpLibNative.EnsureInitialized(); + + var netBytes = network is null + ? Array.Empty() + : Encoding.UTF8.GetBytes(network.AsJson()); + var gnssBytes = Encoding.UTF8.GetBytes(gnss.AsJson()); + var endpointBytes = rinfOptions is null + ? Array.Empty() + : Encoding.UTF8.GetBytes(rinfOptions.EndpointUrl); + var bufferMeters = rinfOptions?.BufferMeters ?? 0.0; + + unsafe + { + fixed (byte* netPtr = netBytes) + fixed (byte* gnssPtr = gnssBytes) + fixed (byte* epPtr = endpointBytes) + { + var cfg = new ProjectionConfigFfi + { + max_search_radius_meters = config.MaxSearchRadiusMeters, + projection_distance_warning_threshold = config.ProjectionDistanceWarningThreshold, + suppress_warnings = (byte)(config.SuppressWarnings ? 1 : 0), + }; + byte* netArg = netBytes.Length == 0 ? null : netPtr; + byte* epArg = endpointBytes.Length == 0 ? null : epPtr; + var buf = NativeMethods.tp_net_project_gnss_auto( + netArg, netBytes.Length, + gnssPtr, gnssBytes.Length, + epArg, endpointBytes.Length, + bufferMeters, + cfg); + return TpLibFfi.DeserializeAutoOrThrow>( + buf, "ProjectGnss failed."); + } + } + } + public static IReadOnlyList ProjectOntoPath( string networkGeoJson, GnssInput gnss, TrainPath path, PathConfig? config = null) => ProjectOntoPath(NetworkInput.FromGeoJson(networkGeoJson), gnss, path, config); @@ -151,6 +202,59 @@ public static PathResult CalculateTrainPath( public static PathResult CalculateTrainPath( string networkGeoJson, string gnssGeoJson, PathConfig? config = null, PreparedDetections? detections = null) => CalculateTrainPath(NetworkInput.FromGeoJson(networkGeoJson), GnssInput.FromGeoJson(gnssGeoJson), config, detections); + + /// + /// Calculate a train path with optional automatic ERA RINF topology + /// retrieval. When is null the topology + /// is fetched from the SPARQL endpoint configured by + /// (feature 006). + /// + public static PathResult CalculateTrainPathAuto( + NetworkInput? network, + GnssInput gnss, + PathConfig? config = null, + PreparedDetections? detections = null, + RinfRetrievalOptions? rinfOptions = null) + { + ArgumentNullException.ThrowIfNull(gnss); + config ??= new PathConfig(); + TpLibNative.EnsureInitialized(); + + var netBytes = network is null + ? Array.Empty() + : Encoding.UTF8.GetBytes(network.AsJson()); + var gnssBytes = Encoding.UTF8.GetBytes(gnss.AsJson()); + var detBytes = detections is null + ? Array.Empty() + : JsonSerializer.SerializeToUtf8Bytes(detections, TpLibJson.Options); + var endpointBytes = rinfOptions is null + ? Array.Empty() + : Encoding.UTF8.GetBytes(rinfOptions.EndpointUrl); + var bufferMeters = rinfOptions?.BufferMeters ?? 0.0; + + unsafe + { + fixed (byte* netPtr = netBytes) + fixed (byte* gnssPtr = gnssBytes) + fixed (byte* detPtr = detBytes) + fixed (byte* epPtr = endpointBytes) + { + var cfg = TpLibFfi.ToFfi(config); + byte* netArg = netBytes.Length == 0 ? null : netPtr; + byte* detArg = detBytes.Length == 0 ? null : detPtr; + byte* epArg = endpointBytes.Length == 0 ? null : epPtr; + var buf = NativeMethods.tp_net_calculate_train_path_auto( + netArg, netBytes.Length, + gnssPtr, gnssBytes.Length, + detArg, detBytes.Length, + epArg, endpointBytes.Length, + bufferMeters, + cfg); + return TpLibFfi.DeserializeAutoOrThrow( + buf, "CalculateTrainPath failed."); + } + } + } } /// @@ -399,4 +503,69 @@ internal static unsafe T DeserializeOrThrow( } } } + + /// + /// Deserialize a buffer produced by the *_auto FFI entry points. These + /// encode typed RINF retrieval failures as a JSON `{"__error": "...", + /// "message": "..."}` envelope rather than the null-buffer sentinel. + /// + internal static unsafe T DeserializeAutoOrThrow( + ByteBuffer buf, + string genericMessage) + { + try + { + if (buf.ptr == null || buf.len < 0) + { + throw new TpLibException(genericMessage); + } + if (buf.len == 0) + { + throw new TpLibException(genericMessage + " (empty buffer)"); + } + var span = new ReadOnlySpan(buf.ptr, buf.len); + + // Peek for the error envelope. + using (var doc = JsonDocument.Parse(span.ToArray())) + { + if (doc.RootElement.ValueKind == JsonValueKind.Object + && doc.RootElement.TryGetProperty("__error", out var cat)) + { + var category = cat.GetString() ?? "Generic"; + var msg = doc.RootElement.TryGetProperty("message", out var m) + ? (m.GetString() ?? genericMessage) + : genericMessage; + throw category switch + { + "InvalidGnssInput" => new TpLibInvalidGnssInputException(msg), + "RinfRetrievalFailed" => new TpLibRinfRetrievalFailedException(msg), + "RinfMissingCoverage" => new TpLibRinfMissingCoverageException(msg), + "RinfIncompleteTopology" => new TpLibRinfIncompleteTopologyException(msg), + _ => new TpLibException(msg), + }; + } + } + + try + { + var value = JsonSerializer.Deserialize(span, TpLibJson.Options); + if (value is null) + { + throw new TpLibException(genericMessage + " (null payload)"); + } + return value; + } + catch (JsonException jex) + { + throw new TpLibException(genericMessage, jex); + } + } + finally + { + if (buf.ptr != null) + { + TpLibNative.FreeByteBuffer(buf); + } + } + } } diff --git a/tp-net/src/lib.rs b/tp-net/src/lib.rs index e903123..09026ab 100644 --- a/tp-net/src/lib.rs +++ b/tp-net/src/lib.rs @@ -1,223 +1,398 @@ -//! C#/.NET bindings for tp-lib-core. -//! -//! All public FFI symbols are declared with `extern "C"` and `#[no_mangle]`. -//! Data crosses the boundary as JSON in heap-allocated byte buffers; configs -//! are flat `#[repr(C)]` structs. - -pub mod ffi; -pub mod marshal; - -use ffi::{ByteBuffer, PathConfigFfi, ProjectionConfigFfi}; -use marshal::{from_json_bytes, to_json_bytes}; -use serde::Deserialize; -use tp_lib_core::{ - calculate_train_path, parse_gnss_csv_str, parse_gnss_geojson_str, parse_network_geojson_str, - prepare_detections_from_loaded, project_gnss, project_onto_path, DetectionKind, PathConfig, - RailwayNetwork, ResolvedAnchor, TrainPath, -}; - -const WGS84: &str = "EPSG:4326"; -const CSV_LAT_COL: &str = "latitude"; -const CSV_LON_COL: &str = "longitude"; -const CSV_TIME_COL: &str = "timestamp"; - -/// Partial mirror of `tp_lib_core::PreparedDetections` used only to recover the -/// `anchors` for path calculation. Remaining fields (records, warnings) are -/// ignored on input. -#[derive(Deserialize)] -struct PreparedDetectionsInput { - #[serde(default)] - anchors: Vec, -} - -unsafe fn load_network( - ptr: *const u8, - len: i32, -) -> Option<( - RailwayNetwork, - Vec, - Vec, -)> { - if ptr.is_null() || len < 0 { - return None; - } - let bytes = std::slice::from_raw_parts(ptr, len as usize); - let text = std::str::from_utf8(bytes).ok()?; - let (netelements, netrelations) = parse_network_geojson_str(text).ok()?; - let net_clone = netelements.clone(); - let network = RailwayNetwork::new(netelements).ok()?; - Some((network, netrelations, net_clone)) -} - -unsafe fn load_gnss(ptr: *const u8, len: i32) -> Option> { - if ptr.is_null() || len < 0 { - return None; - } - let bytes = std::slice::from_raw_parts(ptr, len as usize); - let text = std::str::from_utf8(bytes).ok()?; - if text.trim_start().starts_with('{') { - parse_gnss_geojson_str(text, WGS84).ok() - } else { - parse_gnss_csv_str(text, WGS84, CSV_LAT_COL, CSV_LON_COL, CSV_TIME_COL).ok() - } -} - -/// Project GNSS positions onto the nearest network segments. -/// -/// # Safety -/// All pointers must reference valid UTF-8 byte slices of the indicated length. -#[no_mangle] -pub unsafe extern "C" fn tp_net_project_gnss( - network_ptr: *const u8, - network_len: i32, - gnss_ptr: *const u8, - gnss_len: i32, - config: ProjectionConfigFfi, -) -> ByteBuffer { - let Some((network, _, _)) = load_network(network_ptr, network_len) else { - return ByteBuffer::null_error(); - }; - let Some(gnss) = load_gnss(gnss_ptr, gnss_len) else { - return ByteBuffer::null_error(); - }; - let core_config: tp_lib_core::ProjectionConfig = config.into(); - match project_gnss(&gnss, &network, &core_config) { - Ok(projected) => to_json_bytes(&projected), - Err(_) => ByteBuffer::null_error(), - } -} - -/// Project GNSS positions onto a previously computed train path. -/// -/// # Safety -/// All pointers must reference valid UTF-8 byte slices of the indicated length. -#[no_mangle] -pub unsafe extern "C" fn tp_net_project_onto_path( - network_ptr: *const u8, - network_len: i32, - gnss_ptr: *const u8, - gnss_len: i32, - train_path_ptr: *const u8, - train_path_len: i32, - config: PathConfigFfi, -) -> ByteBuffer { - let Some((_, _, netelements)) = load_network(network_ptr, network_len) else { - return ByteBuffer::null_error(); - }; - let Some(gnss) = load_gnss(gnss_ptr, gnss_len) else { - return ByteBuffer::null_error(); - }; - let Ok(train_path) = from_json_bytes::(train_path_ptr, train_path_len) else { - return ByteBuffer::null_error(); - }; - let core_config: PathConfig = config.into(); - match project_onto_path(&gnss, &train_path, &netelements, &core_config) { - Ok(projected) => to_json_bytes(&projected), - Err(_) => ByteBuffer::null_error(), - } -} - -/// Calculate a train path from GNSS positions and a railway network. -/// -/// `prepared_detections_ptr` may be null (`prepared_detections_len == 0`). -/// When provided, the JSON must include an `anchors` array of [`ResolvedAnchor`]. -/// -/// # Safety -/// All non-null pointers must reference valid UTF-8 byte slices of the -/// indicated length. -#[no_mangle] -pub unsafe extern "C" fn tp_net_calculate_train_path( - network_ptr: *const u8, - network_len: i32, - gnss_ptr: *const u8, - gnss_len: i32, - prepared_detections_ptr: *const u8, - prepared_detections_len: i32, - config: PathConfigFfi, -) -> ByteBuffer { - let Some((_, netrelations, netelements)) = load_network(network_ptr, network_len) else { - return ByteBuffer::null_error(); - }; - let Some(gnss) = load_gnss(gnss_ptr, gnss_len) else { - return ByteBuffer::null_error(); - }; - let mut core_config: PathConfig = config.into(); - if !prepared_detections_ptr.is_null() && prepared_detections_len > 0 { - match from_json_bytes::( - prepared_detections_ptr, - prepared_detections_len, - ) { - Ok(pd) => core_config.anchors = pd.anchors, - Err(_) => return ByteBuffer::null_error(), - } - } - match calculate_train_path(&gnss, &netelements, &netrelations, &core_config) { - Ok(result) => to_json_bytes(&result), - Err(_) => ByteBuffer::null_error(), - } -} - -/// Validate, time-filter and resolve detections into [`ResolvedAnchor`]s for -/// path calculation. -/// -/// `kind_is_linear == 0` β‡’ `Punctual`; non-zero β‡’ `Linear`. -/// -/// # Safety -/// All pointers must reference valid UTF-8 byte slices of the indicated length. -#[no_mangle] -pub unsafe extern "C" fn tp_net_prepare_detections( - network_ptr: *const u8, - network_len: i32, - gnss_ptr: *const u8, - gnss_len: i32, - detections_geojson_ptr: *const u8, - detections_geojson_len: i32, - kind_is_linear: u8, - cutoff_distance_meters: f64, -) -> ByteBuffer { - let Some((_, _, netelements)) = load_network(network_ptr, network_len) else { - return ByteBuffer::null_error(); - }; - let Some(gnss) = load_gnss(gnss_ptr, gnss_len) else { - return ByteBuffer::null_error(); - }; - if detections_geojson_ptr.is_null() || detections_geojson_len < 0 { - return ByteBuffer::null_error(); - } - let det_bytes = - std::slice::from_raw_parts(detections_geojson_ptr, detections_geojson_len as usize); - let Ok(det_text) = std::str::from_utf8(det_bytes) else { - return ByteBuffer::null_error(); - }; - let kind = if kind_is_linear != 0 { - DetectionKind::Linear - } else { - DetectionKind::Punctual - }; - let detections = - match tp_lib_core::io::geojson::detections::load_str(det_text, "", kind) { - Ok(d) => d, - Err(_) => return ByteBuffer::null_error(), - }; - let prepared = match prepare_detections_from_loaded( - detections, - &gnss, - &netelements, - cutoff_distance_meters, - ) { - Ok(p) => p, - Err(_) => return ByteBuffer::null_error(), - }; - #[derive(serde::Serialize)] - struct PreparedDetectionsDto<'a> { - anchors: &'a [ResolvedAnchor], - records: &'a [tp_lib_core::DetectionRecord], - warnings: &'a [String], - } - let dto = PreparedDetectionsDto { - anchors: &prepared.anchors, - records: &prepared.records, - warnings: &prepared.warnings, - }; - to_json_bytes(&dto) -} +//! C#/.NET bindings for tp-lib-core. +//! +//! All public FFI symbols are declared with `extern "C"` and `#[no_mangle]`. +//! Data crosses the boundary as JSON in heap-allocated byte buffers; configs +//! are flat `#[repr(C)]` structs. + +pub mod ffi; +pub mod marshal; + +use ffi::{ByteBuffer, PathConfigFfi, ProjectionConfigFfi}; +use marshal::{from_json_bytes, to_json_bytes}; +use serde::Deserialize; +use tp_lib_core::{ + calculate_train_path, parse_gnss_csv_str, parse_gnss_geojson_str, parse_network_geojson_str, + prepare_detections_from_loaded, project_gnss, project_onto_path, resolve_topology, + DetectionKind, PathConfig, ProjectionError, RailwayNetwork, ResolvedAnchor, RetrievalConfig, + TrainPath, UreqSparqlClient, WorkflowKind, DEFAULT_RETRIEVAL_BUFFER_METERS, + DEFAULT_RINF_ENDPOINT, +}; + +const WGS84: &str = "EPSG:4326"; +const CSV_LAT_COL: &str = "latitude"; +const CSV_LON_COL: &str = "longitude"; +const CSV_TIME_COL: &str = "timestamp"; + +/// Partial mirror of `tp_lib_core::PreparedDetections` used only to recover the +/// `anchors` for path calculation. Remaining fields (records, warnings) are +/// ignored on input. +#[derive(Deserialize)] +struct PreparedDetectionsInput { + #[serde(default)] + anchors: Vec, +} + +unsafe fn load_network( + ptr: *const u8, + len: i32, +) -> Option<( + RailwayNetwork, + Vec, + Vec, +)> { + if ptr.is_null() || len < 0 { + return None; + } + let bytes = std::slice::from_raw_parts(ptr, len as usize); + let text = std::str::from_utf8(bytes).ok()?; + let (netelements, netrelations) = parse_network_geojson_str(text).ok()?; + let net_clone = netelements.clone(); + let network = RailwayNetwork::new(netelements).ok()?; + Some((network, netrelations, net_clone)) +} + +unsafe fn load_gnss(ptr: *const u8, len: i32) -> Option> { + if ptr.is_null() || len < 0 { + return None; + } + let bytes = std::slice::from_raw_parts(ptr, len as usize); + let text = std::str::from_utf8(bytes).ok()?; + if text.trim_start().starts_with('{') { + parse_gnss_geojson_str(text, WGS84).ok() + } else { + parse_gnss_csv_str(text, WGS84, CSV_LAT_COL, CSV_LON_COL, CSV_TIME_COL).ok() + } +} + +/// Project GNSS positions onto the nearest network segments. +/// +/// # Safety +/// All pointers must reference valid UTF-8 byte slices of the indicated length. +#[no_mangle] +pub unsafe extern "C" fn tp_net_project_gnss( + network_ptr: *const u8, + network_len: i32, + gnss_ptr: *const u8, + gnss_len: i32, + config: ProjectionConfigFfi, +) -> ByteBuffer { + let Some((network, _, _)) = load_network(network_ptr, network_len) else { + return ByteBuffer::null_error(); + }; + let Some(gnss) = load_gnss(gnss_ptr, gnss_len) else { + return ByteBuffer::null_error(); + }; + let core_config: tp_lib_core::ProjectionConfig = config.into(); + match project_gnss(&gnss, &network, &core_config) { + Ok(projected) => to_json_bytes(&projected), + Err(_) => ByteBuffer::null_error(), + } +} + +/// Project GNSS positions onto a previously computed train path. +/// +/// # Safety +/// All pointers must reference valid UTF-8 byte slices of the indicated length. +#[no_mangle] +pub unsafe extern "C" fn tp_net_project_onto_path( + network_ptr: *const u8, + network_len: i32, + gnss_ptr: *const u8, + gnss_len: i32, + train_path_ptr: *const u8, + train_path_len: i32, + config: PathConfigFfi, +) -> ByteBuffer { + let Some((_, _, netelements)) = load_network(network_ptr, network_len) else { + return ByteBuffer::null_error(); + }; + let Some(gnss) = load_gnss(gnss_ptr, gnss_len) else { + return ByteBuffer::null_error(); + }; + let Ok(train_path) = from_json_bytes::(train_path_ptr, train_path_len) else { + return ByteBuffer::null_error(); + }; + let core_config: PathConfig = config.into(); + match project_onto_path(&gnss, &train_path, &netelements, &core_config) { + Ok(projected) => to_json_bytes(&projected), + Err(_) => ByteBuffer::null_error(), + } +} + +/// Calculate a train path from GNSS positions and a railway network. +/// +/// `prepared_detections_ptr` may be null (`prepared_detections_len == 0`). +/// When provided, the JSON must include an `anchors` array of [`ResolvedAnchor`]. +/// +/// # Safety +/// All non-null pointers must reference valid UTF-8 byte slices of the +/// indicated length. +#[no_mangle] +pub unsafe extern "C" fn tp_net_calculate_train_path( + network_ptr: *const u8, + network_len: i32, + gnss_ptr: *const u8, + gnss_len: i32, + prepared_detections_ptr: *const u8, + prepared_detections_len: i32, + config: PathConfigFfi, +) -> ByteBuffer { + let Some((_, netrelations, netelements)) = load_network(network_ptr, network_len) else { + return ByteBuffer::null_error(); + }; + let Some(gnss) = load_gnss(gnss_ptr, gnss_len) else { + return ByteBuffer::null_error(); + }; + let mut core_config: PathConfig = config.into(); + if !prepared_detections_ptr.is_null() && prepared_detections_len > 0 { + match from_json_bytes::( + prepared_detections_ptr, + prepared_detections_len, + ) { + Ok(pd) => core_config.anchors = pd.anchors, + Err(_) => return ByteBuffer::null_error(), + } + } + match calculate_train_path(&gnss, &netelements, &netrelations, &core_config) { + Ok(result) => to_json_bytes(&result), + Err(_) => ByteBuffer::null_error(), + } +} + +/// Validate, time-filter and resolve detections into [`ResolvedAnchor`]s for +/// path calculation. +/// +/// `kind_is_linear == 0` β‡’ `Punctual`; non-zero β‡’ `Linear`. +/// +/// # Safety +/// All pointers must reference valid UTF-8 byte slices of the indicated length. +#[no_mangle] +pub unsafe extern "C" fn tp_net_prepare_detections( + network_ptr: *const u8, + network_len: i32, + gnss_ptr: *const u8, + gnss_len: i32, + detections_geojson_ptr: *const u8, + detections_geojson_len: i32, + kind_is_linear: u8, + cutoff_distance_meters: f64, +) -> ByteBuffer { + let Some((_, _, netelements)) = load_network(network_ptr, network_len) else { + return ByteBuffer::null_error(); + }; + let Some(gnss) = load_gnss(gnss_ptr, gnss_len) else { + return ByteBuffer::null_error(); + }; + if detections_geojson_ptr.is_null() || detections_geojson_len < 0 { + return ByteBuffer::null_error(); + } + let det_bytes = + std::slice::from_raw_parts(detections_geojson_ptr, detections_geojson_len as usize); + let Ok(det_text) = std::str::from_utf8(det_bytes) else { + return ByteBuffer::null_error(); + }; + let kind = if kind_is_linear != 0 { + DetectionKind::Linear + } else { + DetectionKind::Punctual + }; + let detections = + match tp_lib_core::io::geojson::detections::load_str(det_text, "", kind) { + Ok(d) => d, + Err(_) => return ByteBuffer::null_error(), + }; + let prepared = match prepare_detections_from_loaded( + detections, + &gnss, + &netelements, + cutoff_distance_meters, + ) { + Ok(p) => p, + Err(_) => return ByteBuffer::null_error(), + }; + #[derive(serde::Serialize)] + struct PreparedDetectionsDto<'a> { + anchors: &'a [ResolvedAnchor], + records: &'a [tp_lib_core::DetectionRecord], + warnings: &'a [String], + } + let dto = PreparedDetectionsDto { + anchors: &prepared.anchors, + records: &prepared.records, + warnings: &prepared.warnings, + }; + to_json_bytes(&dto) +} + +/// Categorise a [`ProjectionError`] into a stable string used by the C# layer +/// to raise the right exception type. +fn classify_projection_error(err: &ProjectionError) -> (&'static str, String) { + match err { + ProjectionError::InvalidGnssInput(m) => ("InvalidGnssInput", m.clone()), + ProjectionError::RinfRetrievalFailed(m) => ("RinfRetrievalFailed", m.clone()), + ProjectionError::RinfMissingCoverage(m) => ("RinfMissingCoverage", m.clone()), + ProjectionError::RinfIncompleteTopology(m) => ("RinfIncompleteTopology", m.clone()), + other => ("Generic", other.to_string()), + } +} + +#[derive(serde::Serialize)] +struct FfiError<'a> { + #[serde(rename = "__error")] + error: &'a str, + message: String, +} + +fn error_buffer(category: &str, message: String) -> ByteBuffer { + to_json_bytes(&FfiError { + error: category, + message, + }) +} + +unsafe fn read_optional_str(ptr: *const u8, len: i32) -> Option { + if ptr.is_null() || len <= 0 { + return None; + } + let bytes = std::slice::from_raw_parts(ptr, len as usize); + std::str::from_utf8(bytes).ok().map(|s| s.to_string()) +} + +fn build_retrieval_config(endpoint: Option, buffer_meters: f64) -> RetrievalConfig { + let ep = endpoint.unwrap_or_else(|| DEFAULT_RINF_ENDPOINT.to_string()); + let buf = if buffer_meters.is_finite() && buffer_meters > 0.0 { + buffer_meters + } else { + DEFAULT_RETRIEVAL_BUFFER_METERS + }; + RetrievalConfig::default() + .with_endpoint(ep) + .with_buffer_meters(buf) +} + +/// Calculate a train path with optional RINF auto-retrieval. +/// +/// When `network_ptr` is null or `network_len <= 0`, the railway topology is +/// retrieved from the configured ERA RINF SPARQL endpoint based on the GNSS +/// positions. Otherwise the supplied network GeoJSON is used as-is (RINF +/// options are ignored). +/// +/// On error, returns a JSON `ByteBuffer` whose payload contains an +/// `{"__error": "...", "message": "..."}` envelope rather than the FFI +/// null-error sentinel, so the C# layer can raise a typed RINF exception. +/// +/// # Safety +/// All non-null pointers must reference valid UTF-8 byte slices of the +/// indicated length. +#[no_mangle] +pub unsafe extern "C" fn tp_net_calculate_train_path_auto( + network_ptr: *const u8, + network_len: i32, + gnss_ptr: *const u8, + gnss_len: i32, + prepared_detections_ptr: *const u8, + prepared_detections_len: i32, + rinf_endpoint_ptr: *const u8, + rinf_endpoint_len: i32, + rinf_buffer_meters: f64, + config: PathConfigFfi, +) -> ByteBuffer { + let Some(gnss) = load_gnss(gnss_ptr, gnss_len) else { + return error_buffer("InvalidGnssInput", "Failed to parse GNSS input".into()); + }; + let mut core_config: PathConfig = config.into(); + if !prepared_detections_ptr.is_null() && prepared_detections_len > 0 { + match from_json_bytes::( + prepared_detections_ptr, + prepared_detections_len, + ) { + Ok(pd) => core_config.anchors = pd.anchors, + Err(_) => { + return error_buffer("Generic", "Failed to parse prepared detections JSON".into()) + } + } + } + + let (netelements, netrelations) = if network_ptr.is_null() || network_len <= 0 { + // Auto-retrieve via RINF. + let endpoint = read_optional_str(rinf_endpoint_ptr, rinf_endpoint_len); + let cfg = build_retrieval_config(endpoint, rinf_buffer_meters); + let client = UreqSparqlClient::default(); + match resolve_topology(WorkflowKind::PathCalculation, &gnss, None, &cfg, &client) { + Ok((topology, _outcome)) => (topology.netelements, topology.netrelations), + Err(e) => { + let (cat, msg) = classify_projection_error(&e); + return error_buffer(cat, msg); + } + } + } else { + let Some((_, netrelations, netelements)) = load_network(network_ptr, network_len) else { + return error_buffer("Generic", "Failed to parse network GeoJSON".into()); + }; + (netelements, netrelations) + }; + + match calculate_train_path(&gnss, &netelements, &netrelations, &core_config) { + Ok(result) => to_json_bytes(&result), + Err(e) => { + let (cat, msg) = classify_projection_error(&e); + error_buffer(cat, msg) + } + } +} + +/// Project GNSS positions with optional RINF auto-retrieval. +/// +/// See [`tp_net_calculate_train_path_auto`] for the auto-retrieval contract. +/// +/// # Safety +/// All non-null pointers must reference valid UTF-8 byte slices of the +/// indicated length. +#[no_mangle] +pub unsafe extern "C" fn tp_net_project_gnss_auto( + network_ptr: *const u8, + network_len: i32, + gnss_ptr: *const u8, + gnss_len: i32, + rinf_endpoint_ptr: *const u8, + rinf_endpoint_len: i32, + rinf_buffer_meters: f64, + config: ProjectionConfigFfi, +) -> ByteBuffer { + let Some(gnss) = load_gnss(gnss_ptr, gnss_len) else { + return error_buffer("InvalidGnssInput", "Failed to parse GNSS input".into()); + }; + let core_config: tp_lib_core::ProjectionConfig = config.into(); + + let network = if network_ptr.is_null() || network_len <= 0 { + let endpoint = read_optional_str(rinf_endpoint_ptr, rinf_endpoint_len); + let cfg = build_retrieval_config(endpoint, rinf_buffer_meters); + let client = UreqSparqlClient::default(); + match resolve_topology(WorkflowKind::Projection, &gnss, None, &cfg, &client) { + Ok((topology, _outcome)) => match RailwayNetwork::new(topology.netelements) { + Ok(n) => n, + Err(e) => { + let (cat, msg) = classify_projection_error(&e); + return error_buffer(cat, msg); + } + }, + Err(e) => { + let (cat, msg) = classify_projection_error(&e); + return error_buffer(cat, msg); + } + } + } else { + let Some((network, _, _)) = load_network(network_ptr, network_len) else { + return error_buffer("Generic", "Failed to parse network GeoJSON".into()); + }; + network + }; + + match project_gnss(&gnss, &network, &core_config) { + Ok(projected) => to_json_bytes(&projected), + Err(e) => { + let (cat, msg) = classify_projection_error(&e); + error_buffer(cat, msg) + } + } +} diff --git a/tp-py/README.md b/tp-py/README.md index 53f3aa0..d1be743 100644 --- a/tp-py/README.md +++ b/tp-py/README.md @@ -10,9 +10,43 @@ pip install tp-lib ## Usage +### Timestamps + +Input timestamps may be RFC3339 with an explicit offset (e.g. +`2025-12-09T14:30:00+01:00` or `2025-12-09T14:30:00Z`) or naive ISO 8601 +(e.g. `2025-12-09T14:30:00`, `2025-12-09 14:30:00`). Naive values are +interpreted in the host's **local** timezone. All timestamps returned by +the library are RFC3339 strings carrying an explicit timezone offset. + ```python -from tp_lib import project_positions, ProjectionConfig +from tp_lib import project_gnss, ProjectionConfig config = ProjectionConfig(max_search_radius_meters=1000.0) -result = project_positions(gnss_positions, network, config) +result = project_gnss(gnss_positions, network, config) +``` + +### Automatic RINF Topology Retrieval + +If you omit `network` (or pass `None`), the library downloads a bounding-box +subset of the ERA RINF topology on demand: + +```python +from tp_lib import project_gnss, RinfRetrievalOptions + +result = project_gnss( + gnss_positions=gnss_positions, + network=None, + rinf_options=RinfRetrievalOptions( + endpoint_url="https://graph.data.era.europa.eu/repositories/rinf-plus", + buffer_meters=1000.0, + ), +) ``` + +Errors are raised as built-in exceptions (`ValueError`, `RuntimeError`) when +retrieval cannot satisfy the request. The same overload exists for +`calculate_train_path`. + +The library may raise: +- `ValueError` for invalid input (e.g., malformed GNSS data or invalid coordinates) +- `RuntimeError` for operational failures (e.g., RINF topology retrieval errors) diff --git a/tp-py/python/tests/test_path_calculation.py b/tp-py/python/tests/test_path_calculation.py index 3f6b287..23b48e2 100644 --- a/tp-py/python/tests/test_path_calculation.py +++ b/tp-py/python/tests/test_path_calculation.py @@ -10,7 +10,6 @@ import json import pytest -from pathlib import Path try: @@ -343,3 +342,50 @@ def test_prepared_detections_repr( r = repr(prepared) assert "PreparedDetections" in r assert "anchors=" in r + + +# === Feature 006: RINF auto-retrieval === + +def test_calculate_train_path_unreachable_rinf_endpoint(): + """T026: Auto-retrieval against an unreachable endpoint surfaces RuntimeError.""" + from tp_lib import RinfRetrievalOptions, calculate_train_path + import tempfile + import os + fd, gnss = tempfile.mkstemp(suffix=".csv") + os.close(fd) + with open(gnss, "w") as f: + f.write("latitude,longitude,timestamp\n") + f.write("50.85,4.35,2025-12-09T14:30:00+01:00\n") + try: + opts = RinfRetrievalOptions(endpoint_url="http://127.0.0.1:1/nope") + with pytest.raises((RuntimeError, OSError)): + calculate_train_path( + gnss_file=gnss, + gnss_crs="EPSG:4326", + rinf_options=opts, + ) + finally: + os.unlink(gnss) + + +def test_calculate_train_path_empty_gnss_invalid_input(tmp_path): + """T026: Empty GNSS without topology must raise ValueError (invalid input).""" + from tp_lib import calculate_train_path + gnss = tmp_path / "empty.csv" + gnss.write_text("latitude,longitude,timestamp\n") + with pytest.raises((ValueError, RuntimeError)): + calculate_train_path( + gnss_file=str(gnss), + gnss_crs="EPSG:4326", + ) + + +def test_calculate_train_path_manual_topology_still_works(gnss_csv, network_geojson): + """T026: Supplying network_file bypasses auto-retrieval (precedence).""" + from tp_lib import calculate_train_path, PathResult + result = calculate_train_path( + gnss_file=gnss_csv, + gnss_crs="EPSG:4326", + network_file=network_geojson, + ) + assert isinstance(result, PathResult) diff --git a/tp-py/python/tests/test_projection.py b/tp-py/python/tests/test_projection.py index 0982fe8..f878320 100644 --- a/tp-py/python/tests/test_projection.py +++ b/tp-py/python/tests/test_projection.py @@ -9,9 +9,6 @@ """ import pytest -import tempfile -import os -from pathlib import Path # Only import if module is built (skip tests if not) @@ -280,7 +277,7 @@ def test_projection_config_defaults(): assert config.max_search_radius_meters == 1000.0 assert config.projection_distance_warning_threshold == 50.0 - assert config.suppress_warnings == False + assert config.suppress_warnings is False def test_projection_config_custom(): @@ -293,7 +290,7 @@ def test_projection_config_custom(): assert config.max_search_radius_meters == 2000.0 assert config.projection_distance_warning_threshold == 100.0 - assert config.suppress_warnings == True + assert config.suppress_warnings is True def test_projection_config_repr(): @@ -343,3 +340,30 @@ def test_single_position(tmp_path, sample_network_geojson): assert len(results) == 1 assert isinstance(results[0], ProjectedPosition) + +def test_project_gnss_manual_topology_precedence(sample_gnss_csv, sample_network_geojson): + """T027: network_file takes precedence over rinf_options (no network call).""" + from tp_lib import project_gnss, RinfRetrievalOptions + bogus = RinfRetrievalOptions(endpoint_url="http://127.0.0.1:1/should-not-be-called") + results = project_gnss( + gnss_file=sample_gnss_csv, + gnss_crs="EPSG:4326", + network_file=sample_network_geojson, + network_crs="EPSG:4326", + target_crs="EPSG:31370", + rinf_options=bogus, + ) + assert len(results) > 0 + + +def test_project_gnss_unreachable_rinf_endpoint(sample_gnss_csv): + """T027: With no network_file, an unreachable RINF endpoint must raise.""" + from tp_lib import project_gnss, RinfRetrievalOptions + opts = RinfRetrievalOptions(endpoint_url="http://127.0.0.1:1/nope") + with pytest.raises((RuntimeError, OSError)): + project_gnss( + gnss_file=sample_gnss_csv, + gnss_crs="EPSG:4326", + target_crs="EPSG:31370", + rinf_options=opts, + ) diff --git a/tp-py/python/tp_lib/__init__.py b/tp-py/python/tp_lib/__init__.py index 61fef42..3ada3dd 100644 --- a/tp-py/python/tp_lib/__init__.py +++ b/tp-py/python/tp_lib/__init__.py @@ -103,6 +103,8 @@ # Spec 004: detections prepare_detections as _prepare_detections, PreparedDetections, + # Spec 006: RINF auto-retrieval + RinfRetrievalOptions, ) # Re-export for cleaner API @@ -120,16 +122,19 @@ # Spec 004 "prepare_detections", "PreparedDetections", + # Spec 006 + "RinfRetrievalOptions", ] def project_gnss( gnss_file: str, gnss_crs: str, - network_file: str, - network_crs: str, - target_crs: str, + network_file: Optional[str] = None, + network_crs: Optional[str] = None, + target_crs: Optional[str] = None, config: Optional[ProjectionConfig] = None, + rinf_options: Optional["RinfRetrievalOptions"] = None, ) -> List[ProjectedPosition]: """ Project GNSS positions onto railway network elements. @@ -177,15 +182,17 @@ def project_gnss( network_crs=network_crs, target_crs=target_crs, config=config, + rinf_options=rinf_options, ) def calculate_train_path( gnss_file: str, gnss_crs: str, - network_file: str, + network_file: Optional[str] = None, config: Optional[PathConfig] = None, detections: Optional[PreparedDetections] = None, + rinf_options: Optional["RinfRetrievalOptions"] = None, ) -> PathResult: """ Calculate the most probable train path through the railway network. @@ -215,6 +222,7 @@ def calculate_train_path( network_file=network_file, config=config, detections=detections, + rinf_options=rinf_options, ) diff --git a/tp-py/src/lib.rs b/tp-py/src/lib.rs index 4384047..05070ca 100644 --- a/tp-py/src/lib.rs +++ b/tp-py/src/lib.rs @@ -17,6 +17,7 @@ use tp_lib_core::{ parse_network_geojson, prepare_detections as core_prepare_detections, project_gnss as core_project_gnss, + resolve_topology, AssociatedNetElement as CoreAssociatedNetElement, // Spec 004: detections DetectionError, @@ -24,6 +25,7 @@ use tp_lib_core::{ DetectionRecord as CoreDetectionRecord, DetectionStatus as CoreDetectionStatus, DiscardReason as CoreDiscardReason, + GnssPosition as CoreGnssPosition, PathCalculationMode as CorePathCalculationMode, PathConfig as CorePathConfig, ProjectedPosition as CoreProjectedPosition, @@ -31,8 +33,13 @@ use tp_lib_core::{ ProjectionError, RailwayNetwork, ResolvedAnchor as CoreResolvedAnchor, + RetrievalConfig, TimestampOrRange as CoreTimestampOrRange, TrainPath as CoreTrainPath, + UreqSparqlClient, + WorkflowKind, + DEFAULT_RETRIEVAL_BUFFER_METERS, + DEFAULT_RINF_ENDPOINT, }; // ============================================================================ @@ -70,6 +77,18 @@ fn convert_error(error: ProjectionError) -> PyErr { ProjectionError::InvalidNetRelation(msg) => { PyValueError::new_err(format!("Invalid netrelation: {}", msg)) } + ProjectionError::InvalidGnssInput(msg) => { + PyValueError::new_err(format!("Invalid GNSS input: {}", msg)) + } + ProjectionError::RinfRetrievalFailed(msg) => { + PyRuntimeError::new_err(format!("RINF retrieval failed: {}", msg)) + } + ProjectionError::RinfMissingCoverage(msg) => { + PyValueError::new_err(format!("RINF missing coverage: {}", msg)) + } + ProjectionError::RinfIncompleteTopology(msg) => { + PyValueError::new_err(format!("RINF incomplete topology: {}", msg)) + } } } @@ -83,6 +102,33 @@ fn convert_detection_error(error: DetectionError) -> PyErr { } } +/// Resolve topology either from a supplied GeoJSON file or via auto-retrieval +/// from the ERA RINF SPARQL endpoint (feature 006). +fn resolve_python_topology( + workflow_kind: WorkflowKind, + network_file: Option<&str>, + gnss_positions: &[CoreGnssPosition], + rinf_options: Option<&RinfRetrievalOptions>, +) -> PyResult<(Vec, Vec)> { + if let Some(path) = network_file { + return parse_network_geojson(path).map_err(convert_error); + } + let endpoint = rinf_options + .map(|o| o.endpoint_url.clone()) + .unwrap_or_else(|| DEFAULT_RINF_ENDPOINT.to_string()); + let buffer = rinf_options + .map(|o| o.buffer_meters) + .unwrap_or(DEFAULT_RETRIEVAL_BUFFER_METERS); + let config = RetrievalConfig::default() + .with_endpoint(endpoint) + .with_buffer_meters(buffer); + let client = UreqSparqlClient::default(); + let (topology, _outcome) = + resolve_topology(workflow_kind, gnss_positions, None, &config, &client) + .map_err(convert_error)?; + Ok((topology.netelements, topology.netrelations)) +} + // ============================================================================ // Helper: DetectionRecord β†’ Python dict // ============================================================================ @@ -242,6 +288,36 @@ impl From for CoreProjectionConfig { } } +/// Options for automatic ERA RINF topology retrieval (feature 006). +#[pyclass] +#[derive(Clone)] +pub struct RinfRetrievalOptions { + #[pyo3(get, set)] + pub endpoint_url: String, + #[pyo3(get, set)] + pub buffer_meters: f64, +} + +#[pymethods] +impl RinfRetrievalOptions { + #[new] + #[pyo3(signature = (endpoint_url=None, buffer_meters=None))] + fn new(endpoint_url: Option, buffer_meters: Option) -> Self { + Self { + endpoint_url: endpoint_url + .unwrap_or_else(|| tp_lib_core::DEFAULT_RINF_ENDPOINT.to_string()), + buffer_meters: buffer_meters.unwrap_or(tp_lib_core::DEFAULT_RETRIEVAL_BUFFER_METERS), + } + } + + fn __repr__(&self) -> String { + format!( + "RinfRetrievalOptions(endpoint_url='{}', buffer_meters={})", + self.endpoint_url, self.buffer_meters + ) + } +} + /// A single GNSS position projected onto a railway network element. #[pyclass] #[derive(Clone)] @@ -254,7 +330,7 @@ pub struct ProjectedPosition { #[pyo3(get)] pub original_longitude: f64, - /// Original timestamp (RFC3339 string). + /// Original timestamp (RFC3339 string with explicit timezone offset). #[pyo3(get)] pub timestamp: String, @@ -714,19 +790,24 @@ impl PreparedDetections { /// /// List of `ProjectedPosition`, one per input GNSS point. #[pyfunction] -#[pyo3(signature = (gnss_file, gnss_crs, network_file, network_crs, target_crs, config=None))] +#[pyo3(signature = (gnss_file, gnss_crs, network_file=None, network_crs=None, target_crs=None, config=None, rinf_options=None))] fn project_gnss( gnss_file: &str, gnss_crs: &str, - network_file: &str, - network_crs: &str, - target_crs: &str, + network_file: Option<&str>, + network_crs: Option<&str>, + target_crs: Option<&str>, config: Option, + rinf_options: Option, ) -> PyResult> { // Validate all CRS strings upfront CrsTransformer::new(gnss_crs.to_string(), gnss_crs.to_string()).map_err(convert_error)?; - CrsTransformer::new(network_crs.to_string(), network_crs.to_string()).map_err(convert_error)?; - CrsTransformer::new(target_crs.to_string(), target_crs.to_string()).map_err(convert_error)?; + if let Some(nc) = network_crs { + CrsTransformer::new(nc.to_string(), nc.to_string()).map_err(convert_error)?; + } + let resolved_target_crs = target_crs.unwrap_or(gnss_crs).to_string(); + CrsTransformer::new(resolved_target_crs.clone(), resolved_target_crs.clone()) + .map_err(convert_error)?; let core_config: CoreProjectionConfig = config .unwrap_or_else(|| ProjectionConfig::new(1000.0, 50.0, false)) @@ -735,8 +816,12 @@ fn project_gnss( let gnss_positions = parse_gnss_csv(gnss_file, gnss_crs, "latitude", "longitude", "timestamp") .map_err(convert_error)?; - let (netelements, _netrelations) = - parse_network_geojson(network_file).map_err(convert_error)?; + let (netelements, _netrelations) = resolve_python_topology( + WorkflowKind::Projection, + network_file, + &gnss_positions, + rinf_options.as_ref(), + )?; let network = RailwayNetwork::new(netelements).map_err(convert_error)?; @@ -746,14 +831,14 @@ fn project_gnss( let mut py_results = Vec::with_capacity(core_results.len()); for core_result in &core_results { let mut pos = ProjectedPosition::from(core_result); - if pos.crs != target_crs { - let transformer = CrsTransformer::new(pos.crs.clone(), target_crs.to_string()) + if pos.crs != resolved_target_crs { + let transformer = CrsTransformer::new(pos.crs.clone(), resolved_target_crs.clone()) .map_err(convert_error)?; let point = geo::Point::new(pos.projected_x, pos.projected_y); let transformed = transformer.transform(point).map_err(convert_error)?; pos.projected_x = transformed.x(); pos.projected_y = transformed.y(); - pos.crs = target_crs.to_string(); + pos.crs = resolved_target_crs.clone(); } py_results.push(pos); } @@ -782,18 +867,24 @@ fn project_gnss( /// `PathResult` with `path`, `mode`, `projected_positions`, `warnings`, /// and `detection_provenance()`. #[pyfunction] -#[pyo3(signature = (gnss_file, gnss_crs, network_file, config=None, detections=None))] +#[pyo3(signature = (gnss_file, gnss_crs, network_file=None, config=None, detections=None, rinf_options=None))] fn calculate_train_path( gnss_file: &str, gnss_crs: &str, - network_file: &str, + network_file: Option<&str>, config: Option, detections: Option<&PreparedDetections>, + rinf_options: Option, ) -> PyResult { let gnss_positions = parse_gnss_csv(gnss_file, gnss_crs, "latitude", "longitude", "timestamp") .map_err(convert_error)?; - let (netelements, netrelations) = parse_network_geojson(network_file).map_err(convert_error)?; + let (netelements, netrelations) = resolve_python_topology( + WorkflowKind::PathCalculation, + network_file, + &gnss_positions, + rinf_options.as_ref(), + )?; let cfg = config.unwrap_or_default(); @@ -928,5 +1019,8 @@ fn tp_lib(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(prepare_detections, m)?)?; m.add_class::()?; + // Spec 006: ERA RINF retrieval options + m.add_class::()?; + Ok(()) }