Skip to content

Commit

Permalink
Spike polygon validation (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
SchlNate authored Feb 16, 2022
1 parent 8510cc8 commit cbadfda
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 0 deletions.
25 changes: 25 additions & 0 deletions Sources/GeospatialSwift/API/Calculator/GeodesicCalculator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum GeoJsonSimpleViolationReason {
case polygonNegativeRingContained
case polygonSelfIntersection
case polygonMultipleVertexIntersection
case polygonSpikeIndices
case multiPolygonContained
case multiPolygonIntersection
}
18 changes: 18 additions & 0 deletions Sources/GeospatialSwift/API/GeoJson/Object/Polygon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
}
}
Expand Down
9 changes: 9 additions & 0 deletions Tests/GeospatialSwiftTests/Data/MockData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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! }
Expand Down Expand Up @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit cbadfda

Please sign in to comment.