From cbadfdafd24f2fafc6fa042635feb49a9c0bcf65 Mon Sep 17 00:00:00 2001 From: Nathan Schlechte Date: Wed, 16 Feb 2022 11:37:53 -0600 Subject: [PATCH] Spike polygon validation (#10) --- .../API/Calculator/GeodesicCalculator.swift | 25 ++++++++++ .../API/GeoJson/GeoJsonSimpleViolation.swift | 1 + .../API/GeoJson/Object/Polygon.swift | 18 +++++++ .../GeospatialSwiftTests/Data/MockData.swift | 9 ++++ .../Calculator/GeodesicCalculatorTests.swift | 49 +++++++++++++++++++ .../API/GeoJson/Object/PolygonTests.swift | 18 +++++++ 6 files changed, 120 insertions(+) diff --git a/Sources/GeospatialSwift/API/Calculator/GeodesicCalculator.swift b/Sources/GeospatialSwift/API/Calculator/GeodesicCalculator.swift index 8f4a541..f8d2e66 100644 --- a/Sources/GeospatialSwift/API/Calculator/GeodesicCalculator.swift +++ b/Sources/GeospatialSwift/API/Calculator/GeodesicCalculator.swift @@ -640,6 +640,31 @@ extension GeodesicCalculator { return containedRingIndices } + internal func simpleViolationSpikeIndices(from polygon: GeodesicPolygon, tolerance: Double) -> [LineSegmentPointIndex] { + + var spikePoints = [LineSegmentPointIndex]() + let mainRingSegments = polygon.mainRing.segments + mainRingSegments.enumerated().forEach { currentLineSegmentIndex, currentLineSegment in + var nextLineSegment: GeodesicLineSegment + if (currentLineSegmentIndex == mainRingSegments.endIndex-1) { + nextLineSegment = mainRingSegments[0] + } else { + nextLineSegment = mainRingSegments[currentLineSegmentIndex + 1] + } + let smallerAngle = abs(abs(currentLineSegment.initialBearing.bearing - nextLineSegment.initialBearing.bearing) - 180) + if case 0.1 ... 5.0 = smallerAngle { + let lineSegmentIndex = LineSegmentIndex(lineIndex: 0, segmentIndex: currentLineSegmentIndex) + + if (currentLineSegment.endPoint == nextLineSegment.startPoint || currentLineSegment.endPoint == nextLineSegment.endPoint) { + spikePoints.append(LineSegmentPointIndex(lineSegmentIndex: lineSegmentIndex, pointIndex: .endPoint)) + } else if (currentLineSegment.startPoint == nextLineSegment.startPoint || currentLineSegment.startPoint == nextLineSegment.endPoint) { + spikePoints.append(LineSegmentPointIndex(lineSegmentIndex: lineSegmentIndex, pointIndex: .startPoint)) + } + } + } + return spikePoints + } + internal func simpleViolationIntersectionIndices(from polygons: [GeodesicPolygon], tolerance: Double) -> LineSegmentIndiciesByLineSegmentIndex { var allIntersectionIndices = LineSegmentIndiciesByLineSegmentIndex() polygons.enumerated().forEach { currentPolygonIndex, currentPolygon in diff --git a/Sources/GeospatialSwift/API/GeoJson/GeoJsonSimpleViolation.swift b/Sources/GeospatialSwift/API/GeoJson/GeoJsonSimpleViolation.swift index 8611ae3..8afe0ee 100644 --- a/Sources/GeospatialSwift/API/GeoJson/GeoJsonSimpleViolation.swift +++ b/Sources/GeospatialSwift/API/GeoJson/GeoJsonSimpleViolation.swift @@ -11,6 +11,7 @@ public enum GeoJsonSimpleViolationReason { case polygonNegativeRingContained case polygonSelfIntersection case polygonMultipleVertexIntersection + case polygonSpikeIndices case multiPolygonContained case multiPolygonIntersection } diff --git a/Sources/GeospatialSwift/API/GeoJson/Object/Polygon.swift b/Sources/GeospatialSwift/API/GeoJson/Object/Polygon.swift index 9ed7a71..ef5937e 100644 --- a/Sources/GeospatialSwift/API/GeoJson/Object/Polygon.swift +++ b/Sources/GeospatialSwift/API/GeoJson/Object/Polygon.swift @@ -169,6 +169,24 @@ extension GeoJson.Polygon { return violations } + //Ring has one or more spikes (0.1 <= angle <= 5) + let simpleViolationSpikeIndices = Calculator.simpleViolationSpikeIndices(from: self, tolerance: tolerance) + + guard simpleViolationSpikeIndices.isEmpty else { + return simpleViolationSpikeIndices.map { spikeLineSegmentPointIndex in + let lineSegmentIndex = spikeLineSegmentPointIndex.lineSegmentIndex + let segment = mainRing.segments[lineSegmentIndex.segmentIndex] + + let point1: GeoJson.Point + if spikeLineSegmentPointIndex.pointIndex == .endPoint { + point1 = GeoJson.Point(longitude: segment.endPoint.longitude, latitude: segment.endPoint.latitude, altitude: segment.endPoint.altitude) + } else { + point1 = GeoJson.Point(longitude: segment.startPoint.longitude, latitude: segment.startPoint.latitude, altitude: segment.startPoint.altitude) + } + return GeoJsonSimpleViolation(problems: [point1], reason: .polygonSpikeIndices) + } + } + return [] } } diff --git a/Tests/GeospatialSwiftTests/Data/MockData.swift b/Tests/GeospatialSwiftTests/Data/MockData.swift index 92905ba..6bfb893 100644 --- a/Tests/GeospatialSwiftTests/Data/MockData.swift +++ b/Tests/GeospatialSwiftTests/Data/MockData.swift @@ -33,6 +33,7 @@ final class MockData { static let mShapeMainRingLinearRings: [GeoJson.LineString] = mShapeMainRingRingsList.first! static let doubleMNegativeRingsLinearRings: [GeoJson.LineString] = doubleMNegativeRingsRingsList.first! static let diamondNegativeRingLinearRings: [GeoJson.LineString] = diamondNegativeRingRingsList.first! + static let spikeLinearRings: [GeoJson.LineString] = spikeRingList.first! static let touchingPolygons: [GeoJson.Polygon] = touchingLinearRingsList.map { geoJson.polygon(mainRing: $0.first!, negativeRings: Array($0.dropFirst())).success! } static let sharingEdgePolygons: [GeoJson.Polygon] = sharingEdgeLinearRingsList.map { geoJson.polygon(mainRing: $0.first!, negativeRings: Array($0.dropFirst())).success! } @@ -191,6 +192,14 @@ extension MockData { private static let diamondNegativeRingRingsList: [[GeoJson.LineString]] = diamondNegativeRingPolygonPointsList.map { $0.map { geoJson.lineString(points: $0).success! } } + private static let spikePolygonPointsList: [[[GeoJson.Point]]] = [ + [ + [GeoTestHelper.point(-20, -20), GeoTestHelper.point(0, -20), GeoTestHelper.point(0, 0), GeoTestHelper.point(-5, 150), GeoTestHelper.point(-10, 0), GeoTestHelper.point(-20, 0), GeoTestHelper.point(-20, -20)] + ] + ] + + private static let spikeRingList: [[GeoJson.LineString]] = spikePolygonPointsList.map { $0.map { geoJson.lineString(points: $0).success! } } + private static let touchingPolygonPointsList: [[[GeoJson.Point]]] = [ [ [GeoTestHelper.point(20, 20), GeoTestHelper.point(20, 21), GeoTestHelper.point(21, 21), GeoTestHelper.point(21, 20), GeoTestHelper.point(20, 20)] diff --git a/Tests/GeospatialSwiftTests/Test/API/Calculator/GeodesicCalculatorTests.swift b/Tests/GeospatialSwiftTests/Test/API/Calculator/GeodesicCalculatorTests.swift index ead0b67..1292761 100644 --- a/Tests/GeospatialSwiftTests/Test/API/Calculator/GeodesicCalculatorTests.swift +++ b/Tests/GeospatialSwiftTests/Test/API/Calculator/GeodesicCalculatorTests.swift @@ -358,4 +358,53 @@ class GeodesicCalculatorTests: XCTestCase { XCTAssertEqual(Calculator.averageBearing(from: point2, to: bisectingCross[2]), 9.22, accuracy: 0.01) XCTAssertEqual(Calculator.averageBearing(from: point2, to: bisectingCross[3]), 99.22, accuracy: 0.01) } + + func testSpikeIndices_OneOrMoreSpikes() { + let point1 = SimplePoint(longitude: -2, latitude: -2) + let point2 = SimplePoint(longitude: 0, latitude: -2) + let point3 = SimplePoint(longitude: 0, latitude: 0) + let point4 = SimplePoint(longitude: -0.5, latitude: 15) + let point5 = SimplePoint(longitude: -1, latitude: 0) + let point6 = SimplePoint(longitude: -2, latitude: 0) + let point7 = SimplePoint(longitude: -30, latitude: -1) + var points = [point1, point2, point3, point4, point5, point6, point1] + + guard let mainRingOneSpike = SimpleLine(points: points) else { return } + + guard let polygonOneSpike = SimplePolygon(mainRing: mainRingOneSpike) else { return } + var expected = [LineSegmentPointIndex]() + expected.append(LineSegmentPointIndex(lineSegmentIndex: LineSegmentIndex(lineIndex: 0, segmentIndex: 2), pointIndex: .endPoint)) + var result = Calculator.simpleViolationSpikeIndices(from: polygonOneSpike as GeodesicPolygon, tolerance: 0) + + XCTAssertEqual(result, expected) + + points.removeLast() + points.append(point7) + points.append(point1) + + guard let mainRingTwoSpikes = SimpleLine(points: points) else { return } + + guard let polygonTwoSpikes = SimplePolygon(mainRing: mainRingTwoSpikes) else { return } + expected.append(LineSegmentPointIndex(lineSegmentIndex: LineSegmentIndex(lineIndex: 0, segmentIndex: 5), pointIndex: .endPoint)) + result = Calculator.simpleViolationSpikeIndices(from: polygonTwoSpikes as GeodesicPolygon, tolerance: 0) + + XCTAssertEqual(result, expected) + + } + + func testSpikeIndices_NoSpikes() { + let point1 = SimplePoint(longitude: -2, latitude: -2) + let point2 = SimplePoint(longitude: 0, latitude: -2) + let point3 = SimplePoint(longitude: 0, latitude: 0) + let point4 = SimplePoint(longitude: -2, latitude: 0) + let points = [point1, point2, point3, point4, point1] + + guard let mainRing = SimpleLine(points: points) else { return } + + guard let polygon = SimplePolygon(mainRing: mainRing) else { return } + let expected = [LineSegmentPointIndex]() + let result = Calculator.simpleViolationSpikeIndices(from: polygon as GeodesicPolygon, tolerance: 0) + + XCTAssertEqual(result, expected) + } } diff --git a/Tests/GeospatialSwiftTests/Test/API/GeoJson/Object/PolygonTests.swift b/Tests/GeospatialSwiftTests/Test/API/GeoJson/Object/PolygonTests.swift index e122cdb..23d40f5 100644 --- a/Tests/GeospatialSwiftTests/Test/API/GeoJson/Object/PolygonTests.swift +++ b/Tests/GeospatialSwiftTests/Test/API/GeoJson/Object/PolygonTests.swift @@ -25,6 +25,8 @@ class PolygonTests: XCTestCase { var doubleMNegativeRingsPolygon: Polygon! var diamondNegativeRingLinearRings: [LineString]! var diamondNegativeRingPolygon: Polygon! + var spikeLinearRing: [LineString]! + var spikePolygon: Polygon! var distancePoint: SimplePoint! @@ -81,6 +83,9 @@ class PolygonTests: XCTestCase { diamondNegativeRingLinearRings = MockData.diamondNegativeRingLinearRings diamondNegativeRingPolygon = GeoTestHelper.polygon(diamondNegativeRingLinearRings.first!, Array(diamondNegativeRingLinearRings.dropFirst())) + spikeLinearRing = MockData.spikeLinearRings + spikePolygon = GeoTestHelper.polygon(spikeLinearRing.first!, []) + distancePoint = GeoTestHelper.simplePoint(10, 10, 10) point = GeoTestHelper.point(0, 0, 0) @@ -344,6 +349,19 @@ class PolygonTests: XCTestCase { } } + func testPolygon_WithSpike_IsInvalid() { + let simpleViolations = spikePolygon.simpleViolations(tolerance: 0) + XCTAssertEqual(simpleViolations.count, 1) + XCTAssertEqual(simpleViolations[0].reason, GeoJsonSimpleViolationReason.polygonSpikeIndices) + + if let point = simpleViolations[0].problems[0] as? Point { + XCTAssertEqual(point.longitude, -5.0) + XCTAssertEqual(point.latitude, 150.0) + } else { + XCTFail("Geometry not valid") + } + } + func testObjectBoundingBox() { XCTAssertEqual(polygon.objectBoundingBox, polygon.boundingBox)