diff --git a/src/WayfarerMobile.Core/Helpers/TripSegmentGeometryParser.cs b/src/WayfarerMobile.Core/Helpers/TripSegmentGeometryParser.cs
new file mode 100644
index 00000000..1487e364
--- /dev/null
+++ b/src/WayfarerMobile.Core/Helpers/TripSegmentGeometryParser.cs
@@ -0,0 +1,197 @@
+using System.Text.Json;
+
+namespace WayfarerMobile.Core.Helpers;
+
+public enum SegmentGeometryFailure
+{
+ Empty,
+ MalformedGeoJson,
+ UnsupportedGeoJsonType,
+ MalformedEncodedPolyline,
+ InvalidCoordinate,
+ InsufficientPoints
+}
+
+public sealed record SegmentGeometryParseResult(
+ IReadOnlyList<(double Latitude, double Longitude)> Coordinates,
+ SegmentGeometryFailure? Failure)
+{
+ public bool IsSuccess => Failure is null;
+}
+
+///
+/// Parses Segment transport geometry into validated geographic coordinates.
+///
+public static class TripSegmentGeometryParser
+{
+ private static readonly IReadOnlyList<(double Latitude, double Longitude)> NoCoordinates =
+ Array.Empty<(double Latitude, double Longitude)>();
+
+ public static SegmentGeometryParseResult Parse(string? geometry)
+ {
+ if (string.IsNullOrWhiteSpace(geometry))
+ return Failure(SegmentGeometryFailure.Empty);
+
+ var detected = geometry.AsSpan().TrimStart();
+ return detected[0] == '{'
+ ? ParseGeoJson(detected)
+ : ParseEncodedPolyline(geometry);
+ }
+
+ private static SegmentGeometryParseResult ParseGeoJson(ReadOnlySpan geometry)
+ {
+ JsonDocument document;
+ try
+ {
+ document = JsonDocument.Parse(geometry.ToString());
+ }
+ catch (JsonException)
+ {
+ return Failure(SegmentGeometryFailure.MalformedGeoJson);
+ }
+
+ using (document)
+ {
+ var root = document.RootElement;
+ if (root.ValueKind != JsonValueKind.Object ||
+ !root.TryGetProperty("type", out var type) ||
+ type.ValueKind != JsonValueKind.String)
+ {
+ return Failure(SegmentGeometryFailure.MalformedGeoJson);
+ }
+
+ if (type.GetString() != "LineString")
+ return Failure(SegmentGeometryFailure.UnsupportedGeoJsonType);
+
+ if (!root.TryGetProperty("coordinates", out var coordinates) ||
+ coordinates.ValueKind != JsonValueKind.Array)
+ {
+ return Failure(SegmentGeometryFailure.MalformedGeoJson);
+ }
+
+ var result = new List<(double Latitude, double Longitude)>();
+ foreach (var position in coordinates.EnumerateArray())
+ {
+ if (position.ValueKind != JsonValueKind.Array || position.GetArrayLength() < 2)
+ return Failure(SegmentGeometryFailure.InvalidCoordinate);
+
+ var ordinates = position.EnumerateArray();
+ ordinates.MoveNext();
+ var longitudeValue = ordinates.Current;
+ ordinates.MoveNext();
+ var latitudeValue = ordinates.Current;
+ if (longitudeValue.ValueKind != JsonValueKind.Number ||
+ latitudeValue.ValueKind != JsonValueKind.Number ||
+ !longitudeValue.TryGetDouble(out var longitude) ||
+ !latitudeValue.TryGetDouble(out var latitude) ||
+ !IsValidCoordinate(latitude, longitude))
+ {
+ return Failure(SegmentGeometryFailure.InvalidCoordinate);
+ }
+
+ result.Add((latitude, longitude));
+ }
+
+ return result.Count < 2
+ ? Failure(SegmentGeometryFailure.InsufficientPoints)
+ : Success(result);
+ }
+ }
+
+ private static SegmentGeometryParseResult ParseEncodedPolyline(string geometry)
+ {
+ if (!IsStructurallyValidEncodedPolyline(geometry))
+ return Failure(SegmentGeometryFailure.MalformedEncodedPolyline);
+
+ var coordinates = PolylineDecoder.DecodeToTuples(geometry);
+ if (coordinates.Any(point => !IsValidCoordinate(point.Latitude, point.Longitude)))
+ return Failure(SegmentGeometryFailure.InvalidCoordinate);
+
+ return coordinates.Count < 2
+ ? Failure(SegmentGeometryFailure.InsufficientPoints)
+ : Success(coordinates);
+ }
+
+ private static bool IsStructurallyValidEncodedPolyline(string encoded)
+ {
+ var index = 0;
+ long latitude = 0;
+ long longitude = 0;
+
+ while (index < encoded.Length)
+ {
+ if (!TryReadComponent(encoded, ref index, out var latitudeDelta) ||
+ !TryReadComponent(encoded, ref index, out var longitudeDelta))
+ {
+ return false;
+ }
+
+ try
+ {
+ latitude = checked(latitude + latitudeDelta);
+ longitude = checked(longitude + longitudeDelta);
+ }
+ catch (OverflowException)
+ {
+ return false;
+ }
+
+ if (latitude is < -9_000_000 or > 9_000_000 ||
+ longitude is < -18_000_000 or > 18_000_000)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool TryReadComponent(string encoded, ref int index, out long delta)
+ {
+ ulong result = 0;
+ var shift = 0;
+
+ while (index < encoded.Length)
+ {
+ var character = encoded[index++];
+ if (character is < '?' or > '~')
+ {
+ delta = 0;
+ return false;
+ }
+
+ var value = character - 63;
+ if (shift > 30 || (shift == 30 && (value & 0x1f) > 1))
+ {
+ delta = 0;
+ return false;
+ }
+
+ result |= (ulong)(value & 0x1f) << shift;
+ if (value < 0x20)
+ {
+ delta = (result & 1) == 0
+ ? (long)(result >> 1)
+ : -(long)(result >> 1) - 1;
+ return true;
+ }
+
+ shift += 5;
+ }
+
+ delta = 0;
+ return false;
+ }
+
+ private static bool IsValidCoordinate(double latitude, double longitude) =>
+ double.IsFinite(latitude) &&
+ double.IsFinite(longitude) &&
+ latitude is >= -90 and <= 90 &&
+ longitude is >= -180 and <= 180;
+
+ private static SegmentGeometryParseResult Success(
+ IReadOnlyList<(double Latitude, double Longitude)> coordinates) => new(coordinates, null);
+
+ private static SegmentGeometryParseResult Failure(SegmentGeometryFailure failure) =>
+ new(NoCoordinates, failure);
+}
diff --git a/src/WayfarerMobile.Core/Navigation/TripNavigationGraphBuilder.cs b/src/WayfarerMobile.Core/Navigation/TripNavigationGraphBuilder.cs
new file mode 100644
index 00000000..95cab577
--- /dev/null
+++ b/src/WayfarerMobile.Core/Navigation/TripNavigationGraphBuilder.cs
@@ -0,0 +1,68 @@
+using WayfarerMobile.Core.Helpers;
+using WayfarerMobile.Core.Models;
+
+namespace WayfarerMobile.Core.Navigation;
+
+///
+/// Builds the local navigation graph from transport trip data.
+///
+public static class TripNavigationGraphBuilder
+{
+ public static TripNavigationGraph Build(
+ TripDetails trip,
+ Action? geometryFailure = null)
+ {
+ var graph = new TripNavigationGraph { TripId = trip.Id };
+
+ foreach (var region in trip.Regions)
+ {
+ foreach (var place in region.Places)
+ {
+ graph.AddNode(new NavigationNode
+ {
+ Id = place.Id.ToString(),
+ Name = place.Name,
+ Latitude = place.Latitude,
+ Longitude = place.Longitude,
+ Type = NavigationNodeType.Place,
+ SortOrder = place.SortOrder,
+ Notes = place.Notes,
+ IconName = place.Icon
+ });
+ }
+ }
+
+ foreach (var segment in trip.Segments)
+ {
+ var edge = new NavigationEdge
+ {
+ FromNodeId = (segment.OriginId ?? Guid.Empty).ToString(),
+ ToNodeId = (segment.DestinationId ?? Guid.Empty).ToString(),
+ TransportMode = segment.TransportMode ?? "walking",
+ DistanceKm = segment.DistanceKm ?? 0,
+ DurationMinutes = (int)(segment.DurationMinutes ?? 0),
+ EdgeType = NavigationEdgeType.UserSegment
+ };
+
+ var parseResult = TripSegmentGeometryParser.Parse(segment.Geometry);
+ if (parseResult.IsSuccess)
+ {
+ edge.RouteGeometry = parseResult.Coordinates
+ .Select(point => new RoutePoint
+ {
+ Latitude = point.Latitude,
+ Longitude = point.Longitude
+ })
+ .ToList();
+ }
+ else if (parseResult.Failure != SegmentGeometryFailure.Empty)
+ {
+ geometryFailure?.Invoke(segment.Id, parseResult.Failure!.Value);
+ }
+
+ graph.AddEdge(edge);
+ }
+
+ return graph;
+ }
+}
diff --git a/src/WayfarerMobile/Services/TripLayerService.cs b/src/WayfarerMobile/Services/TripLayerService.cs
index 43544718..d9c61004 100644
--- a/src/WayfarerMobile/Services/TripLayerService.cs
+++ b/src/WayfarerMobile/Services/TripLayerService.cs
@@ -210,44 +210,24 @@ public void UpdateTripSegments(WritableLayer layer, IEnumerable seg
var segmentCount = 0;
foreach (var segment in segmentList)
{
- _logger.LogDebug("Processing segment {Id}: Mode={Mode}, Geometry length={Length}",
- segment.Id, segment.TransportMode ?? "null",
- segment.Geometry?.Length ?? 0);
-
if (string.IsNullOrEmpty(segment.Geometry))
{
_logger.LogWarning("Skipping segment {Id}: no geometry", segment.Id);
continue;
}
- try
+ var parseResult = TripSegmentGeometryParser.Parse(segment.Geometry);
+ if (!parseResult.IsSuccess)
{
- // Parse geometry - could be GeoJSON LineString or encoded polyline
- List<(double Latitude, double Longitude)> coordinates;
-
- if (segment.Geometry.TrimStart().StartsWith("{"))
- {
- // GeoJSON format: {"type":"LineString","coordinates":[[lon,lat],...]}
- coordinates = ParseGeoJsonLineString(segment.Geometry);
- _logger.LogDebug("Parsed GeoJSON segment {Id}: {PointCount} points", segment.Id, coordinates.Count);
- }
- else
- {
- // Encoded polyline format
- var points = PolylineDecoder.Decode(segment.Geometry);
- coordinates = points.Select(p => (p.Latitude, p.Longitude)).ToList();
- _logger.LogDebug("Decoded polyline segment {Id}: {PointCount} points", segment.Id, coordinates.Count);
- }
-
- if (coordinates.Count < 2)
- {
- _logger.LogWarning("Skipping segment {Id}: only {Count} points after parsing",
- segment.Id, coordinates.Count);
- continue;
- }
+ if (parseResult.Failure != SegmentGeometryFailure.Empty)
+ _logger.LogWarning("Skipping segment {SegmentId}: geometry failure {Failure}", segment.Id, parseResult.Failure);
+ continue;
+ }
+ try
+ {
// Convert to map coordinates
- var mapCoordinates = coordinates
+ var mapCoordinates = parseResult.Coordinates
.Select(p =>
{
var (x, y) = SphericalMercator.FromLonLat(p.Longitude, p.Latitude);
@@ -559,43 +539,4 @@ public void ClearPlaceSelection(WritableLayer layer)
#endregion
- #region GeoJSON Parsing
-
- ///
- /// Parses a GeoJSON LineString into coordinate pairs.
- ///
- /// GeoJSON string with type "LineString".
- /// List of (Latitude, Longitude) tuples.
- private static List<(double Latitude, double Longitude)> ParseGeoJsonLineString(string geoJson)
- {
- var result = new List<(double Latitude, double Longitude)>();
-
- try
- {
- using var doc = System.Text.Json.JsonDocument.Parse(geoJson);
- var root = doc.RootElement;
-
- // GeoJSON LineString format: { "type": "LineString", "coordinates": [[lon,lat], [lon,lat], ...] }
- if (root.TryGetProperty("coordinates", out var coordinates))
- {
- foreach (var point in coordinates.EnumerateArray())
- {
- if (point.GetArrayLength() >= 2)
- {
- var lon = point[0].GetDouble();
- var lat = point[1].GetDouble();
- result.Add((lat, lon));
- }
- }
- }
- }
- catch
- {
- // Invalid GeoJSON, return empty list
- }
-
- return result;
- }
-
- #endregion
}
diff --git a/src/WayfarerMobile/Services/TripNavigationService.cs b/src/WayfarerMobile/Services/TripNavigationService.cs
index fe555ddf..d4c9c6f9 100644
--- a/src/WayfarerMobile/Services/TripNavigationService.cs
+++ b/src/WayfarerMobile/Services/TripNavigationService.cs
@@ -634,53 +634,12 @@ public IEnumerable GetTripPlaces()
///
private TripNavigationGraph BuildNavigationGraph(TripDetails trip)
{
- var graph = new TripNavigationGraph { TripId = trip.Id };
-
- // Add all places as nodes
- foreach (var region in trip.Regions)
- {
- foreach (var place in region.Places)
- {
- graph.AddNode(new NavigationNode
- {
- Id = place.Id.ToString(),
- Name = place.Name,
- Latitude = place.Latitude,
- Longitude = place.Longitude,
- Type = NavigationNodeType.Place,
- SortOrder = place.SortOrder,
- Notes = place.Notes,
- IconName = place.Icon
- });
- }
- }
-
- // Add segments as edges
- foreach (var segment in trip.Segments)
- {
- var edge = new NavigationEdge
- {
- FromNodeId = (segment.OriginId ?? Guid.Empty).ToString(),
- ToNodeId = (segment.DestinationId ?? Guid.Empty).ToString(),
- TransportMode = segment.TransportMode ?? "walking",
- DistanceKm = segment.DistanceKm ?? 0,
- DurationMinutes = (int)(segment.DurationMinutes ?? 0),
- EdgeType = NavigationEdgeType.UserSegment
- };
-
- // Decode route geometry if available
- if (!string.IsNullOrEmpty(segment.Geometry))
- {
- edge.RouteGeometry = PolylineDecoder.Decode(segment.Geometry);
- }
-
- graph.AddEdge(edge);
- }
-
- // No fallback connections - if no segment exists, use direct route
- // This matches old app behavior: honest navigation with bearing + distance
-
- return graph;
+ return TripNavigationGraphBuilder.Build(
+ trip,
+ (segmentId, failure) => _logger.LogWarning(
+ "Segment {SegmentId} geometry failure {Failure}",
+ segmentId,
+ failure));
}
///
diff --git a/tests/WayfarerMobile.Tests/Unit/Helpers/TripSegmentGeometryParserTests.cs b/tests/WayfarerMobile.Tests/Unit/Helpers/TripSegmentGeometryParserTests.cs
new file mode 100644
index 00000000..972d39ea
--- /dev/null
+++ b/tests/WayfarerMobile.Tests/Unit/Helpers/TripSegmentGeometryParserTests.cs
@@ -0,0 +1,82 @@
+using WayfarerMobile.Core.Helpers;
+
+namespace WayfarerMobile.Tests.Unit.Helpers;
+
+public class TripSegmentGeometryParserTests
+{
+ [Fact]
+ public void Parse_ApiGeoJsonLineString_PreservesOrderedLongitudeLatitudeCoordinates()
+ {
+ const string geometry = """
+ {"type":"LineString","coordinates":[[23.7275,37.9838],[23.7281,37.9844,42]]}
+ """;
+
+ var result = TripSegmentGeometryParser.Parse(geometry);
+
+ result.IsSuccess.Should().BeTrue();
+ result.Failure.Should().BeNull();
+ result.Coordinates.Should().BeEquivalentTo(
+ [(37.9838, 23.7275), (37.9844, 23.7281)],
+ options => options.WithStrictOrdering());
+ }
+
+ [Fact]
+ public void Parse_EncodedPolyline_PreservesExistingPrecisionAndOrder()
+ {
+ var result = TripSegmentGeometryParser.Parse("_p~iF~ps|U_ulLnnqC_mqNvxq`@");
+
+ result.IsSuccess.Should().BeTrue();
+ result.Coordinates.Should().BeEquivalentTo(
+ [(38.5, -120.2), (40.7, -120.95), (43.252, -126.453)],
+ options => options.WithStrictOrdering());
+ }
+
+ [Theory]
+ [InlineData(null, SegmentGeometryFailure.Empty)]
+ [InlineData(" ", SegmentGeometryFailure.Empty)]
+ [InlineData("{not json", SegmentGeometryFailure.MalformedGeoJson)]
+ [InlineData("{\"Type\":\"LineString\",\"coordinates\":[[1,2],[3,4]]}", SegmentGeometryFailure.MalformedGeoJson)]
+ [InlineData("{\"type\":\"linestring\",\"coordinates\":[[1,2],[3,4]]}", SegmentGeometryFailure.UnsupportedGeoJsonType)]
+ [InlineData("{\"type\":\"Point\",\"coordinates\":[1,2]}", SegmentGeometryFailure.UnsupportedGeoJsonType)]
+ [InlineData("{\"type\":\"LineString\",\"coordinates\":null}", SegmentGeometryFailure.MalformedGeoJson)]
+ [InlineData("{\"type\":\"LineString\",\"coordinates\":[[1],[3,4]]}", SegmentGeometryFailure.InvalidCoordinate)]
+ [InlineData("{\"type\":\"LineString\",\"coordinates\":[[1,\"2\"],[3,4]]}", SegmentGeometryFailure.InvalidCoordinate)]
+ [InlineData("{\"type\":\"LineString\",\"coordinates\":[[1,2]]}", SegmentGeometryFailure.InsufficientPoints)]
+ [InlineData("{\"type\":\"LineString\",\"coordinates\":[[181,2],[3,4]]}", SegmentGeometryFailure.InvalidCoordinate)]
+ [InlineData("{\"type\":\"LineString\",\"coordinates\":[[1,91],[3,4]]}", SegmentGeometryFailure.InvalidCoordinate)]
+ [InlineData("{\"type\":\"LineString\",\"coordinates\":[[1e400,2],[3,4]]}", SegmentGeometryFailure.InvalidCoordinate)]
+ [InlineData("_p~iF", SegmentGeometryFailure.MalformedEncodedPolyline)]
+ [InlineData("_p~iF~ps|U\n", SegmentGeometryFailure.MalformedEncodedPolyline)]
+ [InlineData("~~~~~~~?", SegmentGeometryFailure.MalformedEncodedPolyline)]
+ [InlineData("_p~iF~ps|U", SegmentGeometryFailure.InsufficientPoints)]
+ public void Parse_InvalidGeometry_ReturnsBoundedFailure(string? geometry, SegmentGeometryFailure expected)
+ {
+ var result = TripSegmentGeometryParser.Parse(geometry);
+
+ result.IsSuccess.Should().BeFalse();
+ result.Failure.Should().Be(expected);
+ result.Coordinates.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void Parse_MalformedGeoJson_DoesNotFallThroughToEncodedPolyline()
+ {
+ var result = TripSegmentGeometryParser.Parse(" {????????????");
+
+ result.Failure.Should().Be(SegmentGeometryFailure.MalformedGeoJson);
+ }
+
+ [Fact]
+ public void Parse_PositiveNegativeAndAntimeridianValues_PreservesMeaningAndOrder()
+ {
+ const string geometry = """
+ {"type":"LineString","coordinates":[[179.9999,-45.25],[-179.9998,45.5]]}
+ """;
+
+ var result = TripSegmentGeometryParser.Parse(geometry);
+
+ result.Coordinates.Should().BeEquivalentTo(
+ [(-45.25, 179.9999), (45.5, -179.9998)],
+ options => options.WithStrictOrdering());
+ }
+}
diff --git a/tests/WayfarerMobile.Tests/Unit/Navigation/TripNavigationGraphBuilderTests.cs b/tests/WayfarerMobile.Tests/Unit/Navigation/TripNavigationGraphBuilderTests.cs
new file mode 100644
index 00000000..7ceee265
--- /dev/null
+++ b/tests/WayfarerMobile.Tests/Unit/Navigation/TripNavigationGraphBuilderTests.cs
@@ -0,0 +1,75 @@
+using WayfarerMobile.Core.Helpers;
+using WayfarerMobile.Core.Models;
+using WayfarerMobile.Core.Navigation;
+
+namespace WayfarerMobile.Tests.Unit.Navigation;
+
+public class TripNavigationGraphBuilderTests
+{
+ [Fact]
+ public void Build_ApiGeoJsonSegment_RetainsEdgeWithExactRouteGeometry()
+ {
+ var (trip, fromId, toId) = CreateTrip(
+ "{\"type\":\"LineString\",\"coordinates\":[[23.7275,37.9838],[23.7281,37.9844]]}");
+
+ var graph = TripNavigationGraphBuilder.Build(trip);
+
+ var edge = graph.GetEdgeBetween(fromId.ToString(), toId.ToString());
+ edge.Should().NotBeNull();
+ edge!.RouteGeometry.Should().BeEquivalentTo(
+ [
+ new RoutePoint { Latitude = 37.9838, Longitude = 23.7275 },
+ new RoutePoint { Latitude = 37.9844, Longitude = 23.7281 }
+ ],
+ options => options.WithStrictOrdering());
+
+ var corruptPoints = PolylineDecoder.Decode(trip.Segments.Single().Geometry!);
+ edge.RouteGeometry.Should().NotBeEquivalentTo(corruptPoints);
+ }
+
+ [Fact]
+ public void Build_InvalidSegmentGeometry_RetainsEdgeWithoutDetailedGeometry()
+ {
+ var (trip, fromId, toId) = CreateTrip("{not json");
+
+ var graph = TripNavigationGraphBuilder.Build(trip);
+
+ var edge = graph.GetEdgeBetween(fromId.ToString(), toId.ToString());
+ edge.Should().NotBeNull();
+ edge!.RouteGeometry.Should().BeNullOrEmpty();
+ }
+
+ private static (TripDetails Trip, Guid FromId, Guid ToId) CreateTrip(string geometry)
+ {
+ var fromId = Guid.NewGuid();
+ var toId = Guid.NewGuid();
+ return (
+ new TripDetails
+ {
+ Id = Guid.NewGuid(),
+ Regions =
+ [
+ new TripRegion
+ {
+ Places =
+ [
+ new TripPlace { Id = fromId, Name = "From", Latitude = 37.9838, Longitude = 23.7275 },
+ new TripPlace { Id = toId, Name = "To", Latitude = 37.9844, Longitude = 23.7281 }
+ ]
+ }
+ ],
+ Segments =
+ [
+ new TripSegment
+ {
+ Id = Guid.NewGuid(),
+ OriginId = fromId,
+ DestinationId = toId,
+ Geometry = geometry
+ }
+ ]
+ },
+ fromId,
+ toId);
+ }
+}
diff --git a/tests/WayfarerMobile.Tests/Unit/Services/TripLayerServiceTests.cs b/tests/WayfarerMobile.Tests/Unit/Services/TripLayerServiceTests.cs
index 56619684..37c1b7aa 100644
--- a/tests/WayfarerMobile.Tests/Unit/Services/TripLayerServiceTests.cs
+++ b/tests/WayfarerMobile.Tests/Unit/Services/TripLayerServiceTests.cs
@@ -1,25 +1,27 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using WayfarerMobile.Core.Helpers;
namespace WayfarerMobile.Tests.Unit.Services;
///
/// Unit tests for TripLayerService.
-/// Tests place marker creation, segment polyline styling, and layer management.
+/// Tests place marker creation, segment styling, and layer management.
///
///
/// TripLayerService manages trip-related map layers including:
/// - Place markers with custom icons or colored dot fallbacks
/// - Segment polylines with transport mode styling
///
-/// This test file includes test-local implementations since the main
+/// This test file includes test-local implementations for non-geometry behavior since the main
/// WayfarerMobile project targets MAUI platforms (android/ios) which cannot be directly
/// referenced from a pure .NET test project. The tests verify the core logic:
/// - Place marker creation with valid coordinates
/// - Empty places list handling
/// - Zero coordinates skipped
/// - Transport mode styling (walk, drive, bike, transit colors)
-/// - Segment polyline creation
+/// Segment geometry parsing is intentionally not mirrored here; a source contract below verifies
+/// that the production service calls the shared Core parser before Mapsui projection.
/// - Layer clearing
///
public class TripLayerServiceTests
@@ -233,6 +235,20 @@ public void UpdateTripSegments_ValidSegment_CreatesPolyline()
_segmentsLayer.Features.Should().HaveCount(1);
}
+ [Fact]
+ public void ProductionUpdateTripSegments_UsesSharedParserAndSkipsFailures()
+ {
+ var sourcePath = Path.Combine(
+ AppContext.BaseDirectory,
+ "..", "..", "..", "..", "..",
+ "src", "WayfarerMobile", "Services", "TripLayerService.cs");
+ var source = File.ReadAllText(Path.GetFullPath(sourcePath));
+
+ source.Should().Contain("TripSegmentGeometryParser.Parse(segment.Geometry)");
+ source.Should().Contain("if (!parseResult.IsSuccess)");
+ source.Should().NotContain("ParseGeoJsonLineString");
+ }
+
[Fact]
public void UpdateTripSegments_NullGeometry_SkipsSegment()
{