diff --git a/Sources/MapConductorCore/polygon/PolygonUnion.swift b/Sources/MapConductorCore/polygon/PolygonUnion.swift index f234252..8326512 100644 --- a/Sources/MapConductorCore/polygon/PolygonUnion.swift +++ b/Sources/MapConductorCore/polygon/PolygonUnion.swift @@ -123,6 +123,8 @@ private struct DirectedEdgeKey: Hashable { private let gridQ = 1e-9 // Tolerance for parallel/collinear/on-segment tests in feature-scale coords. private let unionEps = 1e-12 +// A sub-edge this close to a mid-point passes through it (degrees, far below gridQ). +private let unionOnEdge = 1e-12 private func unionRings(_ rings: [[Vec]]) -> [[Vec]] { // Directed edges of every ring (closed). @@ -140,9 +142,85 @@ private func unionRings(_ rings: [[Vec]]) -> [[Vec]] { return traceRings(boundary) } +// Reach of intersectionPoints beyond a segment's box (degrees). Its collinear +// test accepts an edge up to unionEps / |seg| off the line, and a segment +// shorter than sqrt(unionEps) = 1e-6 is skipped there, so that is at most 2e-6. +private let meetPad = 4e-6 + +// Farthest a side sample is taken from a sub-edge's mid-point, doubled (degrees). +private let clearanceCap = 2e-5 + +/// Edges filed in a uniform grid over their bounding boxes, so a search asks +/// only the edges near a box instead of all of them. A box covering every cell +/// an edge's box touches finds every edge that box can meet. +private struct EdgeGrid { + private let side: Int + private let minX: Double + private let minY: Double + private let cellW: Double + private let cellH: Double + private var cells: [Int: [Int]] = [:] + private var seen: [Int] + private var stamp = 0 + + init(_ edges: [Edge], pad: Double) { + var x0 = Double.infinity, y0 = Double.infinity + var x1 = -Double.infinity, y1 = -Double.infinity + for e in edges { + x0 = min(x0, e.a.x, e.b.x); x1 = max(x1, e.a.x, e.b.x) + y0 = min(y0, e.a.y, e.b.y); y1 = max(y1, e.a.y, e.b.y) + } + side = max(1, min(1024, Int(Double(edges.count).squareRoot().rounded(.up)))) + minX = x0 + minY = y0 + let w = (x1 - x0) / Double(side), h = (y1 - y0) / Double(side) + cellW = w > 0 ? w : 1 + cellH = h > 0 ? h : 1 + seen = [Int](repeating: 0, count: edges.count) + for (i, e) in edges.enumerated() { + let (cx0, cy0, cx1, cy1) = cellRange( + min(e.a.x, e.b.x) - pad, min(e.a.y, e.b.y) - pad, + max(e.a.x, e.b.x) + pad, max(e.a.y, e.b.y) + pad + ) + for cx in cx0...cx1 { + for cy in cy0...cy1 { cells[cx * side + cy, default: []].append(i) } + } + } + } + + /// Indices of the edges filed in a cell the box touches, each once, in no order. + mutating func near(_ minX: Double, _ minY: Double, _ maxX: Double, _ maxY: Double, into out: inout [Int]) { + out.removeAll(keepingCapacity: true) + stamp += 1 + let (cx0, cy0, cx1, cy1) = cellRange(minX, minY, maxX, maxY) + for cx in cx0...cx1 { + for cy in cy0...cy1 { + guard let bucket = cells[cx * side + cy] else { continue } + for i in bucket where seen[i] != stamp { + seen[i] = stamp + out.append(i) + } + } + } + } + + private func cellRange(_ minX: Double, _ minY: Double, _ maxX: Double, _ maxY: Double) -> (Int, Int, Int, Int) { + (cell(minX, self.minX, cellW), cell(minY, self.minY, cellH), + cell(maxX, self.minX, cellW), cell(maxY, self.minY, cellH)) + } + + private func cell(_ v: Double, _ min: Double, _ size: Double) -> Int { + let c = ((v - min) / size).rounded(.down) + return c < 0 ? 0 : (c > Double(side - 1) ? side - 1 : Int(c)) + } +} + /// Split every edge at all points where another edge crosses or touches it. private func splitEdges(_ edges: [Edge]) -> [Edge] { var result: [Edge] = [] + // Only an edge whose box comes within meetPad of this one's can meet it. + var grid = EdgeGrid(edges, pad: meetPad) + var near: [Int] = [] for i in edges.indices { let seg = edges[i] let dir = sub(seg.b, seg.a) @@ -157,7 +235,12 @@ private func splitEdges(_ edges: [Edge]) -> [Edge] { } add(seg.a) add(seg.b) - for j in edges.indices where i != j { + grid.near( + min(seg.a.x, seg.b.x) - meetPad, min(seg.a.y, seg.b.y) - meetPad, + max(seg.a.x, seg.b.x) + meetPad, max(seg.a.y, seg.b.y) + meetPad, + into: &near + ) + for j in near where i != j { for p in intersectionPoints(seg, edges[j]) { add(p) } } @@ -178,6 +261,9 @@ private func splitEdges(_ edges: [Edge]) -> [Edge] { private func classifyBoundary(_ subEdges: [Edge], rings: [[Vec]]) -> [Edge] { var out: [Edge] = [] var seen = Set() + var grid = EdgeGrid(subEdges, pad: 0) + var near: [Int] = [] + let ringsBounds = rings.map(ringBounds) for seg in subEdges { let dir = sub(seg.b, seg.a) let len = (dir.x * dir.x + dir.y * dir.y).squareRoot() @@ -185,13 +271,17 @@ private func classifyBoundary(_ subEdges: [Edge], rings: [[Vec]]) -> [Edge] { let mid = add2(seg.a, scale(dir, 0.5)) // Unit left normal. Sample coverage just off each side of the mid-point; - // after splitting, no other edge crosses this sub-edge's interior, so a - // small offset stays within the two cells this edge separates. + // after splitting, no other edge crosses this sub-edge's interior, so an + // offset shorter than the distance to the nearest other edge stays within + // the two cells this edge separates. A fixed offset does not: where + // another hole runs closer than it (two holes 1e-5 degree apart), the + // sample lands inside that hole, the edge looks interior on both sides + // and is dropped, and the traced ring cuts across a hole. let nx = -dir.y / len let ny = dir.x / len - let off = min(len * 0.25, 1e-5) - let leftInside = coverage(Vec(x: mid.x + nx * off, y: mid.y + ny * off), rings) > 0 - let rightInside = coverage(Vec(x: mid.x - nx * off, y: mid.y - ny * off), rings) > 0 + let off = min(len * 0.25, 0.5 * clearance(mid, cap: clearanceCap, subEdges, grid: &grid, near: &near)) + let leftInside = coverage(Vec(x: mid.x + nx * off, y: mid.y + ny * off), rings, ringsBounds) > 0 + let rightInside = coverage(Vec(x: mid.x - nx * off, y: mid.y - ny * off), rings, ringsBounds) > 0 if leftInside == rightInside { continue } // interior on both/neither side // Orient so the interior is on the left. @@ -204,6 +294,40 @@ private func classifyBoundary(_ subEdges: [Edge], rings: [[Vec]]) -> [Edge] { return out } +/// Distance from `point` (the mid-point of a sub-edge) to the nearest sub-edge +/// that does not pass through it, or `cap` when none is closer. The sub-edge +/// itself and its duplicates from other rings pass through it and are skipped. +/// Only the sub-edges filed near the box `cap` around the point are asked; any +/// closer than `cap` is among them. +private func clearance( + _ point: Vec, + cap: Double, + _ subEdges: [Edge], + grid: inout EdgeGrid, + near: inout [Int] +) -> Double { + var best = cap + grid.near(point.x - cap, point.y - cap, point.x + cap, point.y + cap, into: &near) + for index in near { + let e = subEdges[index] + if min(e.a.x, e.b.x) > point.x + best || max(e.a.x, e.b.x) < point.x - best + || min(e.a.y, e.b.y) > point.y + best || max(e.a.y, e.b.y) < point.y - best { + continue + } + let d = distanceToSegment(point, e) + if d > unionOnEdge && d < best { best = d } + } + return best +} + +private func distanceToSegment(_ p: Vec, _ e: Edge) -> Double { + let d = sub(e.b, e.a) + let lenSq = dot(d, d) + let t = lenSq > 0 ? clamp01(dot(sub(p, e.a), d) / lenSq) : 0 + let q = add2(e.a, scale(d, t)) + return ((p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y)).squareRoot() +} + /// Chain directed boundary edges into closed rings. private func traceRings(_ edges: [Edge]) -> [[Vec]] { var byStart: [VecKey: [Int]] = [:] @@ -290,11 +414,16 @@ private func intersectionPoints(_ seg: Edge, _ other: Edge) -> [Vec] { return [add2(seg.a, scale(r, clamp01(t)))] } -/// How many rings' interiors contain the point. -private func coverage(_ point: Vec, _ rings: [[Vec]]) -> Int { - rings.reduce(0) { count, ring in - pointInRing(point, ring) ? count + 1 : count +/// How many rings' interiors contain the point. A ring whose latitudes do not +/// straddle the point's has no edge pointInRing would count, so it is skipped. +private func coverage(_ point: Vec, _ rings: [[Vec]], _ bounds: [Bounds]) -> Int { + var count = 0 + for i in rings.indices { + let b = bounds[i] + if point.y < b.minY || point.y >= b.maxY { continue } + if pointInRing(point, rings[i]) { count += 1 } } + return count } /// Even-odd ray-casting point-in-polygon (winding-independent). @@ -339,6 +468,24 @@ private func toRing(_ hole: [GeoPointProtocol], origin: Vec) -> [Vec]? { return points.count >= 3 ? points : nil } +private struct Bounds { + var minX: Double + var minY: Double + var maxX: Double + var maxY: Double +} + +private func ringBounds(_ ring: [Vec]) -> Bounds { + var b = Bounds(minX: .infinity, minY: .infinity, maxX: -.infinity, maxY: -.infinity) + for p in ring { + if p.x < b.minX { b.minX = p.x } + if p.x > b.maxX { b.maxX = p.x } + if p.y < b.minY { b.minY = p.y } + if p.y > b.maxY { b.maxY = p.y } + } + return b +} + private func signedArea(_ ring: [Vec]) -> Double { var area = 0.0 for i in ring.indices { diff --git a/Tests/MapConductorCoreTests/PolygonHoleUnionFixtureTests.swift b/Tests/MapConductorCoreTests/PolygonHoleUnionFixtureTests.swift new file mode 100644 index 0000000..40833b0 --- /dev/null +++ b/Tests/MapConductorCoreTests/PolygonHoleUnionFixtureTests.swift @@ -0,0 +1,94 @@ +import XCTest + +@testable import MapConductorCore + +/// unionHoleRings keeps the area of the union of the holes: holes that touch at a +/// point or share (part of) an edge add up, overlaps count once. +/// +/// js-sdk-core (`PolygonHoleUnion.test.mjs`) and android-sdk-core +/// (`PolygonHoleUnionFixtureTest`) read the same `polygon-hole-union-fixture.txt`; +/// the expected areas are shapely's unary_union. +final class PolygonHoleUnionFixtureTests: XCTestCase { + private struct Case { + let name: String + let unit: Double + let originLon: Double + let originLat: Double + let unionArea: Double + var outer: [Double]? + var holes: [[Double]] = [] + + func toGeo(_ flat: [Double]) -> [GeoPointProtocol] { + stride(from: 0, to: flat.count, by: 2).map { i in + GeoPoint(latitude: originLat + flat[i + 1] * unit, longitude: originLon + flat[i] * unit) + } + } + + /// Planar area in grid units². + func areaInUnits(_ ring: [GeoPointProtocol]) -> Double { + var a = 0.0 + for i in ring.indices { + let p = ring[i] + let q = ring[(i + 1) % ring.count] + let px = (p.longitude - originLon) / unit, py = (p.latitude - originLat) / unit + let qx = (q.longitude - originLon) / unit, qy = (q.latitude - originLat) / unit + a += px * qy - qx * py + } + return a / 2 + } + } + + private func readCases() throws -> [Case] { + let url = try XCTUnwrap( + Bundle.module.url(forResource: "polygon-hole-union-fixture", withExtension: "txt"), + "polygon-hole-union-fixture.txt is not in the test bundle" + ) + var cases: [Case] = [] + for line in try String(contentsOf: url, encoding: .utf8).split(separator: "\n") { + if line.hasPrefix("#") || line.trimmingCharacters(in: .whitespaces).isEmpty { continue } + let parts = line.split(separator: "|", omittingEmptySubsequences: false).map(String.init) + let numbers = { parts[1].split(separator: ",").map { Double($0)! } } + switch parts[0] { + case "case": + cases.append(Case( + name: parts[1], + unit: Double(parts[2])!, + originLon: Double(parts[3])!, + originLat: Double(parts[4])!, + unionArea: Double(parts[5])! + )) + case "outer": + cases[cases.count - 1].outer = numbers() + case "hole": + cases[cases.count - 1].holes.append(numbers()) + default: + break + } + } + return cases + } + + func testUnionKeepsTheAreaOfTheUnion() throws { + let cases = try readCases() + XCTAssertGreaterThanOrEqual(cases.count, 20) + for c in cases { + let holes = c.holes.map { c.toGeo($0) } + let result: [[GeoPointProtocol]] + if let outer = c.outer { + let state = PolygonState(points: c.toGeo(outer), holes: holes) + let unioned = state.unionHoles() + XCTAssertEqual(unioned.points.count, state.points.count, "\(c.name): the outline is left alone") + result = unioned.holes + } else { + // nil: the union left the holes as they were. + result = unionHoleRings(holes)?.map { $0.map { $0 as GeoPointProtocol } } ?? holes + } + let area = result.reduce(0.0) { $0 + abs(c.areaInUnits($1)) } + XCTAssertEqual( + area, c.unionArea, accuracy: 1e-6 * c.unionArea, + "\(c.name): \(result.count) rings from \(holes.count)" + ) + XCTAssertTrue(result.allSatisfy { c.areaInUnits($0) < 0 }, "\(c.name): holes come back clockwise") + } + } +} diff --git a/Tests/MapConductorCoreTests/Resources/polygon-hole-union-fixture.txt b/Tests/MapConductorCoreTests/Resources/polygon-hole-union-fixture.txt new file mode 100644 index 0000000..1156d86 --- /dev/null +++ b/Tests/MapConductorCoreTests/Resources/polygon-hole-union-fixture.txt @@ -0,0 +1,231 @@ +# Hole-union fixture. The same file in js-sdk-core, android-sdk-core and ios-sdk-core. +# +# unionHoleRings must keep the area of the union of the holes: holes that touch +# at a point or share an edge (or part of one) add up, an overlap counts once. +# unionArea is shapely's unary_union of the holes. +# +# case|name|unit|originLongitude|originLatitude|unionArea +# outer|x,y,x,y,... (optional: the polygon's outline) +# hole|x,y,x,y,... (one line per hole, not closed) +# +# A vertex is (originLongitude + x * unit, originLatitude + y * unit) degrees, +# and areas are in unit². The Boston street parcels are Analyze Boston's +# Parcels 2025 (ODC PDDL) rounded to 1e-5 degree, before their holes were +# moved apart (examples/basic/tools/boston/pack.py in react-sdk). + +case|corner touch (1e-05 degree grid)|1e-05|-71.06|42.36|200.0 +hole|0,0,10,0,10,10,0,10 +hole|10,10,20,10,20,20,10,20 + +case|shared edge (1e-05 degree grid)|1e-05|-71.06|42.36|200.0 +hole|0,0,10,0,10,10,0,10 +hole|10,0,20,0,20,10,10,10 + +case|part of an edge shared (1e-05 degree grid)|1e-05|-71.06|42.36|200.0 +hole|0,0,10,0,10,10,0,10 +hole|10,5,20,5,20,15,10,15 + +case|chain of four: edge, corner, part of an edge (1e-05 degree grid)|1e-05|-71.06|42.36|400.0 +hole|0,0,10,0,10,10,0,10 +hole|10,0,20,0,20,10,10,10 +hole|20,10,30,10,30,20,20,20 +hole|25,20,35,20,35,30,25,30 + +case|three around one vertex (1e-05 degree grid)|1e-05|-71.06|42.36|300.0 +hole|0,0,10,0,10,10,0,10 +hole|10,10,20,10,20,20,10,20 +hole|0,10,10,10,10,20,0,20 + +case|overlap (1e-05 degree grid)|1e-05|-71.06|42.36|175.0 +hole|0,0,10,0,10,10,0,10 +hole|5,5,15,5,15,15,5,15 + +case|three overlapping (1e-05 degree grid)|1e-05|-71.06|42.36|221.0 +hole|0,0,10,0,10,10,0,10 +hole|5,5,15,5,15,15,5,15 +hole|8,0,18,0,18,7,8,7 + +case|touching pair beside a hole one unit away (1e-05 degree grid)|1e-05|-71.06|42.36|700.0 +hole|0,0,30,0,30,10,0,10 +hole|30,0,40,0,40,10,30,10 +hole|0,11,30,11,30,21,0,21 + +case|slanted holes one unit apart, one touching a third (1e-05 degree grid)|1e-05|-71.06|42.36|274.0 +hole|0,0,29,-10,32,-7,3,3 +hole|3,4,32,-6,35,-3,6,7 +hole|32,-12,40,-12,40,-7,32,-7 + +case|two holes one unit apart, none touching (1e-05 degree grid)|1e-05|-71.06|42.36|600.0 +hole|0,0,30,0,30,10,0,10 +hole|0,11,30,11,30,21,0,21 + +case|one-unit edges: a diamond across a square, and a square at their corner (1e-05 degree grid)|1e-05|-71.06|42.36|3.5 +hole|1,0,2,1,1,2,0,1 +hole|0,0,1,0,1,1,0,1 +hole|1,-1,2,-1,2,0,1,0 + +case|hole touching the outline, sharing an edge with another hole (1e-05 degree grid)|1e-05|-71.06|42.36|200.0 +outer|-10,-10,50,-10,50,50,-10,50 +hole|-10,0,0,0,0,10,-10,10 +hole|0,0,10,0,10,10,0,10 + +case|corner touch (1e-06 degree grid)|1e-06|-71.06|42.36|200.0 +hole|0,0,10,0,10,10,0,10 +hole|10,10,20,10,20,20,10,20 + +case|shared edge (1e-06 degree grid)|1e-06|-71.06|42.36|200.0 +hole|0,0,10,0,10,10,0,10 +hole|10,0,20,0,20,10,10,10 + +case|part of an edge shared (1e-06 degree grid)|1e-06|-71.06|42.36|200.0 +hole|0,0,10,0,10,10,0,10 +hole|10,5,20,5,20,15,10,15 + +case|chain of four: edge, corner, part of an edge (1e-06 degree grid)|1e-06|-71.06|42.36|400.0 +hole|0,0,10,0,10,10,0,10 +hole|10,0,20,0,20,10,10,10 +hole|20,10,30,10,30,20,20,20 +hole|25,20,35,20,35,30,25,30 + +case|three around one vertex (1e-06 degree grid)|1e-06|-71.06|42.36|300.0 +hole|0,0,10,0,10,10,0,10 +hole|10,10,20,10,20,20,10,20 +hole|0,10,10,10,10,20,0,20 + +case|overlap (1e-06 degree grid)|1e-06|-71.06|42.36|175.0 +hole|0,0,10,0,10,10,0,10 +hole|5,5,15,5,15,15,5,15 + +case|three overlapping (1e-06 degree grid)|1e-06|-71.06|42.36|221.0 +hole|0,0,10,0,10,10,0,10 +hole|5,5,15,5,15,15,5,15 +hole|8,0,18,0,18,7,8,7 + +case|touching pair beside a hole one unit away (1e-06 degree grid)|1e-06|-71.06|42.36|700.0 +hole|0,0,30,0,30,10,0,10 +hole|30,0,40,0,40,10,30,10 +hole|0,11,30,11,30,21,0,21 + +case|slanted holes one unit apart, one touching a third (1e-06 degree grid)|1e-06|-71.06|42.36|274.0 +hole|0,0,29,-10,32,-7,3,3 +hole|3,4,32,-6,35,-3,6,7 +hole|32,-12,40,-12,40,-7,32,-7 + +case|two holes one unit apart, none touching (1e-06 degree grid)|1e-06|-71.06|42.36|600.0 +hole|0,0,30,0,30,10,0,10 +hole|0,11,30,11,30,21,0,21 + +case|one-unit edges: a diamond across a square, and a square at their corner (1e-06 degree grid)|1e-06|-71.06|42.36|3.5 +hole|1,0,2,1,1,2,0,1 +hole|0,0,1,0,1,1,0,1 +hole|1,-1,2,-1,2,0,1,0 + +case|hole touching the outline, sharing an edge with another hole (1e-06 degree grid)|1e-06|-71.06|42.36|200.0 +outer|-10,-10,50,-10,50,50,-10,50 +hole|-10,0,0,0,0,10,-10,10 +hole|0,0,10,0,10,10,0,10 + +case|Boston street parcel 97811, rounded to 1e-5 degree|1e-05|-71.03209|42.37859|376417.41510416666 +hole|3,-46,9,-43,24,-65,26,-70,41,-92,21,-99,-8,-110,-16,-114,-49,-126,-82,-138,-103,-147,-120,-122,-140,-129,-153,-135,-173,-142,-190,-117,-124,-93,-110,-88,-103,-85,-66,-71,-11,-51,-4,-48 +hole|53,-108,58,-104,78,-116,83,-120,89,-123,94,-127,76,-142,70,-148,57,-158,-13,-219,-39,-241,-75,-218 +hole|101,-138,106,-134,142,-157,100,-194,93,-199,76,-215,63,-225,40,-245,3,-222,34,-196,64,-170,92,-146 +hole|155,-176,161,-170,206,-199,189,-214,176,-224,146,-251,121,-271,107,-284,73,-313,51,-298,28,-284,46,-269,64,-253,83,-238,101,-222,120,-205,125,-201,147,-216,147,-215,148,-215,125,-201 +hole|38,-309,50,-316,55,-320,61,-323,31,-350,18,-360,-7,-382,-25,-397,-31,-403,-38,-408,-71,-438,-117,-408,-111,-404,-92,-387,-63,-362,-39,-342,-20,-325,-3,-310,4,-305,16,-294 +hole|-100,-437,-95,-441,-83,-448,-114,-474,-119,-479,-132,-489,-144,-500,-198,-546,-216,-562,-265,-604,-309,-575,-262,-534,-226,-503,-219,-498,-208,-487,-203,-484,-184,-467,-164,-450,-129,-420 +hole|-298,-600,-277,-614,-281,-618,-287,-622,-306,-639,-316,-648,-321,-651,-325,-656,-350,-676,-367,-692,-379,-701,-398,-718,-407,-726,-413,-730,-427,-743,-432,-747,-444,-740,-432,-747,-446,-759,-457,-751,-491,-731,-472,-714,-448,-694,-443,-689,-436,-684,-406,-658,-376,-633,-370,-627,-339,-602,-332,-595,-321,-586 +hole|-492,-767,-479,-756,-469,-763,-463,-766,-458,-770,-484,-793,-491,-798,-499,-806,-521,-792,-512,-785 +hole|-523,-792,-500,-806,-536,-837,-548,-848,-565,-862,-585,-880,-637,-857,-578,-806,-558,-788,-552,-784,-543,-775,-537,-771,-523,-758,-508,-746,-503,-741,-481,-755,-487,-759,-501,-772,-521,-789 +hole|-624,-878,-598,-889,-660,-967,-669,-979,-683,-996,-703,-987,-735,-974,-719,-955,-701,-931,-696,-926,-663,-883,-658,-878,-650,-867 +hole|-755,-967,-759,-967,-759,-966,-761,-966,-763,-964,-764,-964,-766,-957,-765,-957,-765,-955,-692,-863,-691,-863,-691,-862,-689,-862,-687,-861,-679,-861,-677,-862,-676,-862,-676,-863,-674,-864,-673,-866,-672,-866,-672,-868,-671,-869,-744,-963,-745,-963,-745,-964,-748,-965,-748,-966,-750,-966,-752,-967 +hole|-765,-932,-782,-953,-833,-931,-808,-898,-757,-921 +hole|-845,-918,-849,-924,-869,-915,-881,-910,-900,-901,-892,-891,-873,-900,-873,-899,-891,-890,-883,-879,-879,-875,-869,-862,-828,-811,-824,-805,-805,-813,-804,-812,-817,-806,-823,-804,-815,-794,-789,-806,-764,-817,-772,-828,-798,-816,-802,-822,-801,-823,-798,-818,-773,-828,-777,-834,-785,-843,-806,-871,-815,-881,-823,-892,-832,-902,-840,-913 +hole|-866,-831,-917,-894,-969,-871,-918,-808 +hole|-971,-850,-984,-865,-1037,-841,-951,-736,-898,-759,-935,-804 +hole|-999,-772,-1050,-836,-1103,-812,-1052,-749 +hole|-1031,-702,-1116,-807,-1168,-784,-1083,-678 +hole|-982,-707,-1007,-695,-997,-683,-985,-689,-984,-688,-972,-694,-971,-692,-995,-681,-985,-667,-982,-664,-930,-686,-941,-699,-944,-704,-956,-718 +hole|-967,-666,-979,-661,-905,-567,-854,-590,-918,-670,-921,-675,-928,-683,-941,-678,-941,-677 +hole|-835,-567,-845,-579,-896,-556,-853,-501,-835,-480,-785,-417,-770,-423,-769,-424,-735,-439,-785,-503,-818,-546 +hole|-687,-432,-687,-418,-658,-417,-656,-416,-659,-416,-657,-413,-661,-416,-687,-416,-687,-409,-682,-401,-673,-391,-668,-384,-592,-431,-598,-437,-604,-441,-652,-483,-668,-473,-668,-475,-665,-476,-653,-484,-677,-504,-686,-500 +hole|-734,-408,-736,-409,-743,-409,-748,-407,-752,-404,-758,-391,-759,-381,-760,-377,-760,-372,-759,-368,-759,-363,-755,-349,-751,-342,-745,-336,-739,-333,-736,-333,-734,-332,-726,-334,-721,-337,-717,-344,-716,-348,-715,-352,-714,-362,-714,-373,-715,-378,-719,-392,-727,-405 +hole|-664,-344,-691,-304,-662,-294,-644,-287,-612,-275,-595,-269,-586,-265,-520,-241,-511,-237,-504,-235,-487,-228,-470,-253,-437,-241,-454,-216,-421,-204,-388,-253,-414,-263,-445,-274,-470,-283,-486,-290,-528,-305,-578,-324,-595,-300,-578,-324,-657,-353,-661,-349 +hole|-464,-249,-470,-251,-486,-228,-468,-221,-461,-219,-454,-216,-439,-240,-451,-244,-457,-247 +hole|-395,-212,-405,-197,-382,-189,-371,-185,-360,-180,-355,-179,-339,-172,-272,-148,-256,-173,-255,-172,-270,-150,-271,-147,-259,-143,-253,-140,-246,-138,-206,-123,-173,-173,-191,-180,-201,-164,-202,-164,-205,-160,-192,-180,-239,-197,-255,-204,-338,-234,-371,-247,-379,-234 +hole|-169,-148,-173,-143,-153,-135,-143,-150,-137,-159,-156,-167 +hole|-129,-156,-136,-159,-151,-135,-126,-126,-120,-123,-104,-147 +hole|-70,-149,-46,-140,2,-122,7,-126,1,-132,-5,-136,-19,-149,-35,-162,-71,-194,-89,-209,-103,-200,-111,-196,-124,-187,-142,-176,-135,-173,-128,-171,-106,-162,-77,-152 +hole|-122,-206,-101,-220,-135,-248,-157,-268,-164,-273,-182,-289,-204,-308,-211,-313,-220,-322,-235,-334,-269,-312,-292,-297,-279,-287,-261,-271,-254,-266,-237,-251,-223,-261,-221,-259,-235,-249,-231,-245,-225,-241,-213,-230,-206,-225,-200,-219,-193,-214,-158,-183 +hole|-94,-234,-88,-228,-51,-252,-99,-293,-117,-309,-184,-366,-220,-343,-142,-275,-124,-259,-111,-249 +hole|-20,-253,-27,-248,-15,-237,-3,-228,3,-222,40,-246,28,-255,23,-260,9,-271,-5,-262,-13,-258 +hole|-23,-269,-3,-282,-28,-303,-58,-330,-81,-349,-87,-355,-94,-360,-111,-376,-118,-381,-136,-397,-151,-387,-159,-383,-172,-374,-130,-337,-123,-332,-106,-316,-99,-311,-40,-259 +hole|-163,-397,-147,-407,-184,-438,-189,-443,-190,-443,-224,-473,-233,-480,-233,-481,-239,-485,-242,-489,-260,-477,-263,-480,-246,-491,-256,-499,-256,-500,-294,-476,-281,-485,-295,-497,-313,-485,-316,-488,-298,-499,-300,-501,-275,-516,-281,-521,-281,-522,-330,-563,-415,-510,-366,-467,-365,-467,-360,-462,-359,-462,-263,-379,-252,-370,-248,-365,-233,-353,-224,-358,-219,-362,-211,-366,-205,-371,-197,-375 +hole|-283,-491,-278,-487,-256,-500,-261,-505,-267,-509,-272,-514,-294,-500 +hole|-386,-562,-353,-584,-390,-615,-412,-601,-407,-596,-401,-592,-401,-591,-388,-580,-393,-577,-417,-598,-432,-588,-435,-590,-393,-617,-404,-627,-414,-636,-446,-616,-457,-610,-464,-605,-468,-608,-438,-626,-418,-639,-433,-651,-438,-656,-445,-661,-457,-672,-481,-692,-505,-714,-511,-718,-526,-709,-542,-698,-553,-692,-595,-665,-487,-571,-481,-567,-462,-550,-438,-530,-424,-538,-395,-557 +hole|-544,-715,-522,-729,-537,-741,-558,-760,-577,-748,-578,-748,-559,-760,-574,-773,-597,-757,-609,-750,-658,-719,-633,-698,-608,-714,-605,-712,-630,-695,-620,-687,-606,-695,-611,-699,-610,-700,-605,-696,-595,-703,-594,-702,-604,-695,-619,-686,-607,-675 +hole|-601,-769,-582,-781,-589,-786,-607,-802,-644,-779,-650,-776,-677,-775,-678,-737,-667,-727,-613,-762 +hole|-632,-799,-627,-795,-611,-805,-620,-813,-637,-803 +hole|-642,-808,-637,-803,-621,-814,-640,-830,-655,-821,-653,-816,-652,-809,-653,-796,-655,-796,-655,-812,-657,-822,-641,-831,-660,-848,-667,-844,-675,-841,-676,-813,-677,-802,-677,-779,-655,-779,-652,-780,-628,-794,-648,-811,-647,-812,-643,-808 +hole|-720,-836,-696,-846,-717,-871,-768,-849,-759,-837,-745,-844,-731,-850,-730,-848,-758,-836,-749,-824 +hole|-774,-856,-722,-878,-725,-882,-726,-882,-733,-891,-732,-891,-736,-897,-737,-896,-746,-907,-745,-907,-747,-909,-798,-887 +hole|-698,-816,-698,-825,-704,-828,-739,-813,-719,-786,-709,-774,-699,-779 +hole|-745,-794,-755,-806,-768,-800,-749,-776,-751,-776,-770,-799,-806,-783,-771,-739,-767,-733,-766,-733,-721,-676,-704,-656,-703,-656,-700,-737,-741,-788 +hole|-832,-788,-859,-821,-911,-798,-884,-765 +hole|-775,-717,-822,-776,-874,-754,-771,-626,-720,-648,-739,-672,-743,-676,-746,-681,-751,-686,-761,-700 +hole|-879,-734,-890,-748,-921,-733,-941,-725,-929,-710,-904,-721,-903,-720,-928,-709,-908,-683,-898,-672,-880,-648,-828,-671,-847,-695,-861,-712,-874,-729 +hole|-965,-730,-988,-759,-1041,-735,-1018,-706 +hole|-849,-652,-854,-658,-878,-647,-847,-607,-843,-603,-839,-597,-788,-619,-807,-643,-834,-631,-841,-639,-839,-639 +hole|-797,-599,-810,-593,-823,-588,-829,-585,-738,-470,-728,-459,-718,-446,-708,-449,-708,-476,-707,-484,-708,-490,-707,-504,-706,-519,-717,-532,-727,-545,-734,-553,-742,-564,-747,-569,-754,-579,-762,-588,-766,-594,-778,-608 +hole|-653,-505,-566,-430,-509,-466,-521,-476,-452,-521,-548,-604,-555,-609,-555,-610,-560,-615,-579,-630,-608,-656,-665,-620,-635,-594,-634,-594,-621,-582,-592,-558,-568,-537,-557,-528,-538,-515,-559,-520,-568,-523,-569,-524,-571,-524,-575,-527,-577,-528,-580,-527,-679,-611,-682,-609,-684,-531,-663,-513,-643,-525,-642,-525,-662,-513 +hole|-486,-445,-499,-456,-554,-420,-529,-399,-511,-383,-476,-352,-453,-367,-451,-365,-460,-359,-454,-355,-455,-354,-460,-358,-474,-350,-452,-331,-445,-326,-418,-343,-416,-342,-428,-334,-421,-327,-422,-327,-429,-333,-444,-324,-409,-295,-397,-284,-388,-276,-382,-272,-378,-267,-373,-263,-359,-272,-378,-289,-377,-290,-382,-294,-380,-295,-371,-287,-365,-283,-355,-274,-345,-281,-357,-291,-334,-306,-333,-305,-339,-302,-355,-291,-344,-282,-333,-288,-322,-296,-316,-299,-353,-331,-389,-362,-402,-372,-420,-388,-461,-423,-489,-405,-491,-408,-463,-425 +hole|-557,-401,-568,-410,-579,-404,-589,-397,-595,-394,-583,-384,-568,-394,-562,-397 +hole|-583,-381,-596,-393,-617,-379,-643,-363,-618,-353,-589,-343,-569,-335,-554,-357,-568,-335,-544,-326,-535,-323,-518,-334,-518,-335,-496,-349,-555,-399 +hole|-510,-332,-515,-335,-534,-323,-518,-317,-506,-312,-491,-307,-482,-303,-472,-300,-464,-296,-445,-290,-437,-286,-428,-283,-424,-286,-435,-296,-454,-312,-477,-333,-492,-324,-485,-319,-487,-319 +hole|-496,-326,-492,-324,-478,-333,-496,-349,-513,-338,-509,-335,-510,-334,-505,-331,-504,-332 +hole|-345,-264,-358,-256,-338,-249,-329,-245,-321,-243,-299,-235,-287,-230,-280,-228,-270,-242,-289,-248,-302,-253,-304,-253,-280,-268,-305,-290,-321,-279,-327,-276,-333,-271 +hole|-266,-237,-271,-239,-279,-227,-254,-218,-249,-216,-241,-228 +hole|-232,-227,-237,-231,-248,-216,-213,-203,-209,-207 +hole|-255,-234,-241,-229,-237,-231,-256,-247,-268,-239 +hole|-275,-244,-268,-241,-257,-248,-270,-260,-279,-268,-301,-254 +hole|-315,-319,-303,-308,-290,-316,-327,-348,-326,-349,-289,-317,-277,-324,-314,-356,-311,-358,-295,-343,-289,-339,-275,-326,-253,-339,-246,-344,-324,-411,-325,-411,-331,-417,-332,-417,-355,-438,-368,-448,-379,-459,-392,-469,-404,-480,-429,-501,-483,-465,-437,-424,-429,-418,-411,-402,-369,-366,-363,-360,-362,-360,-343,-343 +hole|-649,-648,-621,-666,-653,-694,-660,-688,-659,-682,-660,-682,-663,-689,-654,-695,-675,-713,-679,-711,-680,-660,-662,-660,-662,-659,-681,-659,-681,-634,-677,-630 +hole|-705,-597,-704,-629,-710,-637,-762,-614,-744,-592,-740,-586,-727,-571,-718,-559,-705,-565 +hole|-827,-639,-809,-646,-820,-661,-828,-670,-852,-659,-833,-636 + +case|Boston street parcel 98014, rounded to 1e-5 degree|1e-05|-71.11716|42.30025|378410.0 +hole|-554,-698,-554,-696,-553,-694,-553,-684,-552,-684,-552,-683,-550,-683,-515,-704,-489,-719,-463,-735,-438,-749,-481,-789,-517,-767,-530,-760,-542,-752,-580,-730,-557,-705 +hole|-401,-741,-371,-712,-341,-729,-332,-721,-362,-704,-322,-666,-266,-698,-254,-706,-235,-716,-235,-717,-234,-717,-295,-810,-296,-810,-296,-811,-302,-811,-302,-810,-407,-749,-408,-749,-408,-747,-407,-747 +hole|-288,-672,-312,-658,-272,-619,-234,-642,-207,-657,-202,-659,-202,-660,-201,-660,-211,-680,-225,-703,-225,-704,-226,-704,-226,-705,-232,-705,-232,-704 +hole|-239,-625,-263,-610,-225,-576,-225,-575,-219,-575,-185,-595,-174,-600,-174,-601,-173,-601,-173,-602,-172,-602,-172,-606,-181,-625,-188,-624,-194,-646,-195,-646,-195,-647,-198,-648,-200,-648,-200,-647,-212,-641 +hole|-189,-578,-211,-566,-211,-565,-212,-565,-212,-562,-211,-562,-211,-561,-173,-526,-151,-504,-149,-531,-152,-553,-165,-586,-166,-586,-166,-587,-167,-587,-167,-588,-172,-588 +hole|-333,-810,-318,-818,-316,-819,-316,-820,-315,-820,-315,-821,-314,-821,-314,-822,-313,-822,-313,-828,-314,-828,-330,-853,-336,-866,-339,-875,-325,-879,-326,-889,-326,-900,-321,-917,-366,-903,-524,-856,-420,-759,-371,-787,-416,-829,-435,-847,-421,-851,-360,-794 +hole|-361,-915,-349,-919,-315,-928,-306,-941,-303,-943,-301,-946,-298,-948,-319,-964,-330,-974,-336,-984,-411,-962,-424,-959,-439,-954,-469,-946,-495,-938,-553,-922,-579,-907,-533,-865,-483,-880,-470,-883,-455,-888,-439,-892,-408,-902 +hole|-614,-914,-726,-1020,-756,-1048,-811,-1100,-861,-1146,-861,-1147,-863,-1147,-863,-1148,-868,-1148,-868,-1147,-870,-1147,-870,-1146,-871,-1146,-869,-1135,-864,-1121,-861,-1108,-859,-1089,-859,-1072,-856,-1054,-854,-1048,-847,-1036,-841,-1029,-841,-1028,-798,-988,-788,-980,-779,-971,-769,-963,-760,-954,-729,-927,-715,-913,-703,-899,-692,-881,-687,-873,-674,-842,-669,-835,-667,-830,-663,-824,-662,-824,-635,-791,-595,-814,-595,-815,-542,-846 +hole|-652,-976,-641,-966,-640,-966,-640,-965,-639,-965,-639,-966,-638,-966,-611,-982,-602,-985,-543,-1000,-527,-1005,-512,-1008,-495,-1013,-478,-1017,-421,-1033,-372,-1045,-372,-1046,-370,-1046,-370,-1047,-369,-1047,-369,-1048,-368,-1048,-368,-1053,-377,-1086,-378,-1086,-378,-1087,-381,-1088,-384,-1088,-426,-1078,-523,-1055,-538,-1052,-580,-1041,-610,-1035,-623,-1031,-651,-1025,-659,-1021,-683,-1006 +hole|-445,-1086,-433,-1088,-403,-1095,-392,-1098,-391,-1098,-391,-1099,-390,-1099,-390,-1102,-391,-1102,-391,-1103,-428,-1133,-429,-1133,-429,-1134,-434,-1134,-451,-1130,-478,-1125,-576,-1104,-689,-1080,-699,-1077,-711,-1071,-736,-1056,-693,-1015,-659,-1035,-628,-1042,-614,-1046,-586,-1052,-572,-1056,-514,-1069,-500,-1073,-459,-1082 +hole|-606,-793,-626,-782,-588,-740,-526,-776,-515,-783,-490,-797,-532,-837,-557,-823 +hole|-869,-1181,-860,-1173,-840,-1184,-805,-1206,-793,-1212,-747,-1239,-721,-1255,-709,-1261,-675,-1282,-663,-1288,-630,-1308,-649,-1323,-671,-1342,-672,-1342,-672,-1343,-675,-1343,-677,-1344,-678,-1344,-678,-1343,-681,-1343,-681,-1342,-763,-1294,-776,-1287,-789,-1278,-801,-1272,-813,-1264,-859,-1237,-902,-1211 +hole|-595,-1298,-570,-1278,-568,-1277,-564,-1275,-559,-1273,-554,-1272,-552,-1272,-551,-1271,-490,-1273,-488,-1274,-483,-1275,-481,-1276,-477,-1278,-475,-1280,-464,-1286,-484,-1304,-505,-1322,-516,-1330,-546,-1356,-559,-1366,-620,-1419,-627,-1424,-656,-1451,-729,-1512,-740,-1522,-752,-1531,-789,-1563,-789,-1564,-791,-1564,-791,-1565,-797,-1565,-797,-1564,-799,-1564,-799,-1563,-800,-1563,-845,-1529,-874,-1511,-874,-1510,-875,-1510,-875,-1508,-874,-1506,-824,-1482,-722,-1403,-648,-1341,-638,-1334,-629,-1326,-619,-1319,-597,-1301 +hole|-476,-1313,-455,-1295,-451,-1295,-403,-1328,-456,-1371,-480,-1392,-494,-1403,-549,-1379,-549,-1378,-550,-1378,-550,-1376,-506,-1338 +hole|-398,-1314,-413,-1303,-388,-1283,-376,-1272,-326,-1231,-269,-1266,-321,-1309,-332,-1319,-359,-1341,-373,-1332,-386,-1322 +hole|-421,-1357,-394,-1334,-358,-1359,-336,-1383,-398,-1416,-418,-1437,-483,-1408,-465,-1394 +hole|-346,-1401,-329,-1391,-314,-1412,-310,-1419,-310,-1422,-308,-1424,-308,-1426,-306,-1430,-306,-1432,-304,-1437,-304,-1455,-309,-1469,-312,-1476,-312,-1477,-313,-1477,-313,-1478,-315,-1478,-315,-1479,-317,-1479,-319,-1480,-322,-1480,-322,-1479,-324,-1479,-369,-1459,-386,-1451,-408,-1442,-388,-1422,-380,-1419 +hole|-353,-1477,-321,-1492,-357,-1546,-412,-1522,-427,-1514,-464,-1498,-415,-1450,-402,-1456,-386,-1462 +hole|-336,-1536,-328,-1524,-261,-1544,-270,-1589,-328,-1585,-336,-1551,-339,-1540 +hole|-350,-1560,-348,-1560,-330,-1634,-340,-1762,-343,-1786,-346,-1830,-428,-1787,-450,-1773,-440,-1758,-432,-1742,-424,-1714,-422,-1701,-421,-1700,-412,-1668,-402,-1645,-352,-1561,-351,-1561,-351,-1560 +hole|-381,-1821,-362,-1832,-348,-1839,-348,-1841,-347,-1841,-352,-1886,-356,-1935,-357,-1935,-357,-1937,-358,-1937,-358,-1938,-360,-1938,-362,-1939,-365,-1939,-365,-1938,-367,-1938,-380,-1928,-512,-1842,-512,-1841,-513,-1841,-513,-1837,-512,-1837,-512,-1836,-493,-1820,-482,-1808,-456,-1783,-455,-1783,-455,-1782,-451,-1782,-451,-1783 +hole|-515,-1825,-521,-1830,-522,-1830,-522,-1831,-528,-1831,-542,-1822,-540,-1820,-584,-1791,-571,-1780,-557,-1789,-541,-1763,-533,-1752,-504,-1762,-506,-1765,-469,-1781,-486,-1799 +hole|-585,-1771,-571,-1780,-584,-1791,-599,-1782,-625,-1764,-640,-1755,-642,-1757,-664,-1738,-671,-1730,-673,-1727,-665,-1716,-619,-1733,-626,-1744 +hole|-660,-1688,-679,-1718,-689,-1699,-691,-1691,-699,-1672,-707,-1656,-720,-1636,-720,-1632,-712,-1624,-695,-1604,-680,-1589,-664,-1570,-622,-1524,-622,-1523,-621,-1523,-621,-1522,-619,-1522,-617,-1521,-613,-1521,-613,-1522,-611,-1522,-611,-1523,-572,-1549,-640,-1655 +hole|-685,-1579,-723,-1620,-723,-1621,-724,-1621,-724,-1622,-731,-1622,-731,-1621,-732,-1621,-747,-1605,-767,-1587,-776,-1581,-779,-1578,-780,-1578,-781,-1573,-780,-1573,-780,-1572,-779,-1572,-755,-1551,-692,-1498,-672,-1481,-625,-1513,-664,-1555,-671,-1564,-672,-1564 +hole|-821,-1570,-806,-1581,-891,-1654,-935,-1692,-936,-1692,-936,-1693,-938,-1693,-938,-1692,-940,-1692,-940,-1691,-941,-1691,-966,-1611,-973,-1591,-975,-1582,-989,-1541,-962,-1543,-947,-1543,-899,-1519,-894,-1519,-874,-1533,-856,-1544 +hole|-649,-1481,-662,-1473,-614,-1431,-586,-1450,-582,-1453,-580,-1454,-576,-1456,-575,-1456,-575,-1457,-570,-1458,-567,-1459,-563,-1459,-561,-1460,-556,-1460,-554,-1459,-550,-1459,-548,-1458,-546,-1458,-535,-1452,-535,-1451,-514,-1436,-492,-1416,-490,-1416,-426,-1445,-474,-1491,-505,-1499,-520,-1502,-536,-1507,-545,-1509,-566,-1539,-585,-1525,-613,-1507,-626,-1497,-639,-1489 +hole|-581,-1440,-605,-1423,-561,-1385,-502,-1411,-544,-1446,-549,-1448,-552,-1449,-555,-1449,-557,-1450,-560,-1450,-562,-1449,-566,-1449,-568,-1448,-574,-1445 +hole|-537,-1516,-482,-1501,-403,-1537,-362,-1555,-398,-1615,-448,-1594,-480,-1579,-495,-1573,-541,-1552,-556,-1546,-556,-1545,-557,-1545,-557,-1543,-556,-1543 +hole|-430,-1614,-405,-1625,-419,-1653,-423,-1662,-425,-1670,-428,-1676,-432,-1689,-441,-1730,-451,-1755,-453,-1757,-455,-1762,-465,-1775,-502,-1759,-504,-1761,-533,-1751,-525,-1740,-519,-1729,-518,-1729,-506,-1707,-494,-1677,-491,-1664,-490,-1663,-490,-1652,-493,-1646,-493,-1644,-499,-1637,-501,-1636,-501,-1635,-503,-1634,-505,-1632,-521,-1625,-522,-1625,-522,-1624,-523,-1624,-523,-1621,-502,-1610,-479,-1596,-477,-1596,-477,-1595,-472,-1595,-472,-1596 +hole|-546,-1747,-560,-1768,-561,-1768,-561,-1769,-566,-1769,-566,-1768,-606,-1742,-607,-1742,-607,-1739,-606,-1739,-590,-1713,-575,-1686,-560,-1650,-557,-1644,-556,-1644,-556,-1643,-555,-1643,-555,-1642,-554,-1642,-554,-1641,-553,-1641,-553,-1640,-551,-1639,-547,-1636,-542,-1634,-540,-1634,-538,-1633,-533,-1633,-531,-1634,-529,-1634,-520,-1638,-518,-1638,-518,-1639,-516,-1639,-510,-1644,-509,-1644,-507,-1648,-506,-1648,-506,-1650,-505,-1650,-505,-1653,-504,-1653,-504,-1660,-510,-1680,-527,-1717 +hole|-608,-1716,-618,-1730,-640,-1723,-664,-1714,-635,-1668,-624,-1649,-606,-1623,-599,-1610,-588,-1594,-564,-1555,-492,-1586,-491,-1586,-491,-1590,-492,-1590,-551,-1625,-554,-1626,-564,-1633,-565,-1635,-567,-1636,-573,-1644,-590,-1685,-599,-1701 +hole|-858,-1483,-889,-1498,-889,-1499,-894,-1499,-894,-1498,-896,-1498,-937,-1471,-924,-1459,-902,-1440,-881,-1421,-813,-1362,-775,-1385,-745,-1402,-795,-1441,-804,-1447,-813,-1455,-836,-1472 +hole|-136,-497,-139,-502,-140,-494,-131,-486 +hole|-166,-491,-208,-468,-170,-430,-157,-484,-164,-491 +hole|-134,-461,-150,-477,-163,-424,-136,-398,-103,-368,-62,-393 +hole|-106,-353,-165,-409,-167,-397,-164,-387,-159,-377,-151,-368,-146,-364,-118,-346,-77,-317,2,-266,61,-229,104,-201,108,-203,119,-196,78,-257,32,-302,6,-329,-30,-363,-57,-389,-61,-379