Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/GISTools/Algorithms/Simplify.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ public enum Simplify {

switch firstCoordinate.projection.kind {
case .geographic:
let oneDegreeLongitudeDistanceInMeters: CLLocationDistance = (cos(firstCoordinate.longitude * .pi / 180.0) * 111.0) * 1000.0
let oneDegreeLongitudeDistanceInMeters: CLLocationDistance = (cos(firstCoordinate.latitude * .pi / 180.0) * 111.0) * 1000.0
let toleranceInDegrees: CLLocationDegrees = toleranceInMeters / oneDegreeLongitudeDistanceInMeters
Comment on lines +358 to 359

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, optional: this now duplicates the conversion that already exists as Coordinate3D.degrees(fromMeters:) (Sources/GISTools/Algorithms/Conversions.swift). Suggested change below reuses it.

Note that this also swaps the hardcoded 111.0 km/degree for GISTool.earthCircumference / 360.0 (~111.32 km/degree), i.e. it is slightly more accurate but does change results marginally. Entirely reasonable to defer this refactor to a follow-up if you want this PR strictly minimal.

Suggested change
let oneDegreeLongitudeDistanceInMeters: CLLocationDistance = (cos(firstCoordinate.latitude * .pi / 180.0) * 111.0) * 1000.0
let toleranceInDegrees: CLLocationDegrees = toleranceInMeters / oneDegreeLongitudeDistanceInMeters
let toleranceInDegrees = firstCoordinate.degrees(fromMeters: toleranceInMeters).longitudeDegrees


return simplify(
Expand Down
31 changes: 31 additions & 0 deletions Tests/GISToolsTests/Algorithms/SimplifyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -345,4 +345,35 @@ struct SimplifyTests {
#expect(result.projection == .epsg4326)
}

// MARK: - Tolerance conversion

// Validates that the metre-to-degree tolerance conversion depends on the
// latitude, not on the longitude, of the geometry (regression test).
@Test
func toleranceConversionIsIndependentOfLongitude() throws {
// A zigzag line with 100 points spread over a few kilometres.
func zigzag(longitude: Double) throws -> LineString {
let coordinates = (0 ..< 100).map { i in
Coordinate3D(
latitude: 40.0 + (Double(i) * 0.0005),
longitude: longitude + (i.isMultiple(of: 2) ? 0.0 : 0.001))
}
return try #require(LineString(coordinates))
}

let atGreenwich = try zigzag(longitude: 0.0)
let atMinus90 = try zigzag(longitude: -90.0)
let atMinus111 = try zigzag(longitude: -111.0)

let simplifiedAtGreenwich = atGreenwich.simplified(tolerance: 10.0)
let simplifiedAtMinus90 = atMinus90.simplified(tolerance: 10.0)
let simplifiedAtMinus111 = atMinus111.simplified(tolerance: 10.0)

// The zigzag amplitude (~85m) is far above the tolerance, so most
// vertices must survive regardless of the meridian.
#expect(simplifiedAtGreenwich.coordinates.count > 50)
#expect(simplifiedAtMinus90.coordinates.count == simplifiedAtGreenwich.coordinates.count)
#expect(simplifiedAtMinus111.coordinates.count == simplifiedAtGreenwich.coordinates.count)

@trasch trasch Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: bug fixes should include tests for all affected projections. The degree conversion only runs on the geographic branch, so one cheap planar assertion in the new test is enough (keeps the regression test self-contained; broader coverage already exists in simplify3857). Note Coordinate3D(x:y:) defaults to .epsg3857.

Suggested change
#expect(simplifiedAtMinus111.coordinates.count == simplifiedAtGreenwich.coordinates.count)
#expect(simplifiedAtMinus111.coordinates.count == simplifiedAtGreenwich.coordinates.count)
// The degree conversion only applies to geographic projections;
// planar projections (e.g. EPSG:3857) pass meters through directly.
let planarLine = try #require(LineString([
Coordinate3D(x: 0.0, y: 0.0),
Coordinate3D(x: 250.0, y: 250.0),
Coordinate3D(x: 500.0, y: 0.0),
]))
#expect(planarLine.simplified(tolerance: 100.0).projection == .epsg3857)

}

}