From e0b079d816f5aae2e94176f794eb04497b6f1738 Mon Sep 17 00:00:00 2001 From: Adrian Schoenig Date: Thu, 23 Nov 2023 13:22:27 +1100 Subject: [PATCH] Fix convex hulls that span the antimeridian --- Sources/GeoJSONKitTurf/ConvexHull.swift | 2 +- .../algorithms/MonotoneChain.swift | 28 ++++++++++++++++--- .../Fixtures/convex/in/nzl.geo.json | 1 + .../Fixtures/convex/out/nzl.geo.json | 1 + Tests/GeoJSONKitTurfTests/PositionTests.swift | 5 +--- 5 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 Tests/GeoJSONKitTurfTests/Fixtures/convex/in/nzl.geo.json create mode 100644 Tests/GeoJSONKitTurfTests/Fixtures/convex/out/nzl.geo.json diff --git a/Sources/GeoJSONKitTurf/ConvexHull.swift b/Sources/GeoJSONKitTurf/ConvexHull.swift index 37a4430..c5827c6 100644 --- a/Sources/GeoJSONKitTurf/ConvexHull.swift +++ b/Sources/GeoJSONKitTurf/ConvexHull.swift @@ -16,7 +16,7 @@ extension Collection where Element == GeoJSON.Position { /// /// - Returns: The convex hull of this sequence as a polygon public func convexHull() -> GeoJSON.Polygon { - let positions = AndrewsMonotoneChain.convexHull(self).map { $0.removingAltitude } + let positions = AndrewsMonotoneChain.convexHull(self).map(\.removingAltitude) return .init(exterior: .init(positions: positions)) } } diff --git a/Sources/GeoJSONKitTurf/algorithms/MonotoneChain.swift b/Sources/GeoJSONKitTurf/algorithms/MonotoneChain.swift index 473f491..cf11969 100644 --- a/Sources/GeoJSONKitTurf/algorithms/MonotoneChain.swift +++ b/Sources/GeoJSONKitTurf/algorithms/MonotoneChain.swift @@ -40,13 +40,28 @@ struct AndrewsMonotoneChain { var lower = [GeoJSON.Position]() var upper = [GeoJSON.Position]() + let boundingBox = GeoJSON.BoundingBox(positions: Array(points), allowSpanningAntimeridian: true) + let normalized: [GeoJSON.Position] + let didNormalize: Bool + if boundingBox.spansAntimeridian { + normalized = points.map { + var position = $0 + position.longitude = position.longitude.wrap(min: 0, max: 360) + return position + } + didNormalize = true + } else { + normalized = Array(points) + didNormalize = false + } + // Sort points in lexicographical order. - let points = points.sorted { a, b in + let sorted = normalized.sorted { a, b in a.x < b.x || a.x == b.x && a.y < b.y } // Construct the lower hull. - for point in points { + for point in sorted { while lower.count >= 2 { let a = lower[lower.count - 2] let b = lower[lower.count - 1] @@ -57,7 +72,7 @@ struct AndrewsMonotoneChain { } // Construct the upper hull. - for point in points.lazy.reversed() { + for point in sorted.lazy.reversed() { while upper.count >= 2 { let a = upper[upper.count - 2] let b = upper[upper.count - 1] @@ -73,7 +88,12 @@ struct AndrewsMonotoneChain { upper.removeLast() // Join the arrays to form the convex hull. - return lower + upper + let joined = lower + upper + if didNormalize { + return joined.map(\.normalized) + } else { + return joined + } } } diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/nzl.geo.json b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/nzl.geo.json new file mode 100644 index 0000000..1e17aad --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/in/nzl.geo.json @@ -0,0 +1 @@ +{"features":[{"geometry":{"type":"MultiPolygon","coordinates":[[[[-179.025,-31.458],[-179.057,-31.384],[-179.052,-31.306],[-179.013,-31.235],[-178.945,-31.181],[-178.862,-31.155],[-178.77,-31.158],[-178.687,-31.191],[-178.624,-31.249],[-178.592,-31.323],[-178.595,-31.401],[-178.634,-31.473],[-178.701,-31.527],[-178.788,-31.554],[-178.88,-31.551],[-178.964,-31.517],[-179.025,-31.458]]],[[[-178.801,-30.521],[-178.75,-30.408],[-178.7,-30.368],[-178.635,-30.341],[-178.666,-30.274],[-178.669,-30.202],[-178.642,-30.132],[-178.591,-30.078],[-178.51,-30.036],[-178.41,-30.025],[-178.323,-30.045],[-178.238,-30.1],[-178.188,-30.182],[-178.186,-30.279],[-178.245,-30.373],[-178.358,-30.432],[-178.327,-30.5],[-178.324,-30.571],[-178.351,-30.642],[-178.403,-30.696],[-178.529,-30.744],[-178.664,-30.722],[-178.724,-30.687],[-178.771,-30.637],[-178.797,-30.581],[-178.801,-30.521]]],[[[-178.21,-29.226],[-178.177,-29.138],[-178.1,-29.07],[-178.01,-29.039],[-177.848,-29.031],[-177.754,-29.058],[-177.678,-29.118],[-177.635,-29.191],[-177.625,-29.272],[-177.664,-29.369],[-177.756,-29.445],[-177.864,-29.492],[-177.977,-29.496],[-178.069,-29.472],[-178.143,-29.416],[-178.201,-29.304],[-178.21,-29.226]]],[[[-177.167,-43.765],[-177.097,-43.667],[-177.188,-43.572],[-177.19,-43.457],[-177.12,-43.37],[-176.996,-43.317],[-176.853,-43.318],[-176.674,-43.381],[-176.59,-43.43],[-176.538,-43.498],[-176.483,-43.516],[-176.34,-43.533],[-176.17,-43.527],[-176.063,-43.553],[-175.942,-43.648],[-175.915,-43.77],[-175.796,-43.763],[-175.696,-43.783],[-175.613,-43.829],[-175.557,-43.896],[-175.542,-43.975],[-175.57,-44.051],[-175.637,-44.114],[-175.737,-44.154],[-175.647,-44.21],[-175.595,-44.283],[-175.587,-44.363],[-175.623,-44.442],[-175.693,-44.498],[-175.782,-44.531],[-175.887,-44.539],[-175.983,-44.52],[-176.055,-44.587],[-176.161,-44.627],[-176.277,-44.633],[-176.386,-44.606],[-176.555,-44.49],[-176.668,-44.326],[-176.772,-44.3],[-176.855,-44.249],[-176.946,-44.102],[-177.088,-44.083],[-177.198,-44.015],[-177.236,-43.953],[-177.244,-43.886],[-177.22,-43.82],[-177.167,-43.765]]],[[[165.554,-50.809],[165.578,-50.71],[165.636,-50.643],[165.634,-50.579],[165.661,-50.522],[165.879,-50.363],[166.047,-50.316],[166.315,-50.285],[166.47,-50.314],[166.582,-50.382],[166.668,-50.52],[166.667,-50.617],[166.623,-50.679],[166.532,-50.745],[166.561,-50.837],[166.524,-50.938],[166.435,-51.022],[166.288,-51.092],[166.178,-51.121],[166.051,-51.128],[165.778,-51.071],[165.698,-51.027],[165.604,-50.936],[165.554,-50.809]]],[[[166.144,-45.867],[166.153,-45.709],[166.178,-45.645],[166.316,-45.499],[166.414,-45.439],[166.515,-45.269],[166.673,-45.075],[167.068,-44.73],[167.312,-44.598],[167.395,-44.522],[167.528,-44.45],[167.829,-44.135],[168.031,-44.011],[168.169,-43.863],[168.287,-43.813],[168.422,-43.801],[168.62,-43.754],[168.758,-43.682],[168.923,-43.643],[169.04,-43.56],[169.23,-43.471],[169.487,-43.393],[169.616,-43.27],[169.71,-43.219],[169.801,-43.197],[169.975,-43.067],[170.089,-42.952],[170.203,-42.912],[170.302,-42.855],[170.463,-42.816],[170.647,-42.703],[170.73,-42.602],[170.858,-42.505],[170.912,-42.439],[170.979,-42.279],[171.046,-42.196],[171.065,-42.063],[171.186,-41.812],[171.187,-41.724],[171.239,-41.622],[171.296,-41.57],[171.372,-41.535],[171.459,-41.521],[171.598,-41.527],[171.642,-41.502],[171.758,-41.338],[171.823,-41.281],[171.839,-41.173],[171.835,-40.867],[171.909,-40.74],[172.039,-40.622],[172.134,-40.582],[172.274,-40.476],[172.49,-40.344],[172.595,-40.308],[172.689,-40.298],[173.014,-40.324],[173.144,-40.366],[173.243,-40.426],[173.304,-40.485],[173.341,-40.569],[173.336,-40.636],[173.289,-40.749],[173.456,-40.879],[173.501,-40.863],[173.536,-40.753],[173.629,-40.622],[173.853,-40.495],[173.939,-40.466],[174.062,-40.467],[174.172,-40.51],[174.24,-40.574],[174.315,-40.73],[174.373,-40.785],[174.47,-40.813],[174.603,-40.896],[174.625,-40.798],[174.717,-40.696],[174.787,-40.65],[174.9,-40.613],[174.946,-40.482],[174.966,-40.336],[174.938,-40.211],[174.86,-40.127],[174.771,-40.067],[174.649,-40.06],[174.43,-39.991],[174.282,-39.908],[174.118,-39.785],[173.896,-39.743],[173.754,-39.683],[173.573,-39.52],[173.532,-39.452],[173.498,-39.346],[173.495,-39.256],[173.526,-39.166],[173.577,-39.082],[173.647,-39.022],[173.894,-38.868],[174,-38.845],[174.133,-38.793],[174.283,-38.782],[174.349,-38.732],[174.375,-38.582],[174.38,-38.359],[174.404,-38.299],[174.451,-38.247],[174.452,-38.21],[174.427,-38.138],[174.37,-38.098],[174.332,-38.046],[174.317,-37.938],[174.382,-37.835],[174.451,-37.794],[174.532,-37.773],[174.562,-37.728],[174.48,-37.593],[174.441,-37.394],[174.339,-37.214],[174.249,-37.126],[174.17,-36.867],[174.141,-36.822],[173.967,-36.61],[173.827,-36.495],[173.79,-36.427],[173.775,-36.351],[173.719,-36.272],[173.461,-35.98],[173.239,-35.757],[173.107,-35.583],[172.843,-35.294],[172.815,-35.234],[172.812,-35.159],[172.84,-35.082],[172.891,-35.023],[172.884,-35.012],[172.74,-34.846],[172.581,-34.697],[172.453,-34.606],[172.405,-34.535],[172.393,-34.457],[172.414,-34.383],[172.5,-34.286],[172.564,-34.244],[172.646,-34.222],[172.759,-34.231],[172.984,-34.195],[173.117,-34.209],[173.23,-34.272],[173.289,-34.356],[173.295,-34.446],[173.243,-34.565],[173.377,-34.569],[173.499,-34.592],[173.563,-34.628],[173.695,-34.742],[173.876,-34.772],[173.974,-34.768],[174.059,-34.788],[174.154,-34.845],[174.232,-34.956],[174.392,-34.967],[174.482,-35],[174.566,-35.086],[174.586,-35.21],[174.622,-35.267],[174.718,-35.244],[174.818,-35.255],[174.904,-35.297],[174.966,-35.366],[174.987,-35.427],[174.989,-35.523],[174.95,-35.635],[174.886,-35.702],[174.932,-35.725],[175.045,-35.699],[175.217,-35.716],[175.323,-35.77],[175.379,-35.825],[175.466,-35.83],[175.547,-35.859],[175.7,-35.987],[175.756,-36.071],[175.755,-36.186],[175.779,-36.228],[175.865,-36.243],[175.936,-36.277],[176,-36.335],[176.033,-36.403],[176.103,-36.439],[176.155,-36.492],[176.197,-36.619],[176.166,-36.731],[176.292,-36.808],[176.35,-36.898],[176.352,-36.988],[176.313,-37.067],[176.407,-37.096],[176.486,-37.158],[176.529,-37.248],[176.515,-37.353],[176.716,-37.475],[176.809,-37.623],[176.95,-37.623],[176.881,-37.522],[176.893,-37.407],[176.981,-37.316],[177.12,-37.276],[177.235,-37.293],[177.351,-37.357],[177.409,-37.407],[177.442,-37.474],[177.442,-37.567],[177.396,-37.648],[177.304,-37.713],[177.19,-37.737],[177.224,-37.784],[177.247,-37.79],[177.311,-37.789],[177.349,-37.766],[177.46,-37.622],[177.568,-37.521],[177.702,-37.476],[177.827,-37.375],[177.929,-37.337],[178.231,-37.333],[178.388,-37.363],[178.757,-37.54],[178.823,-37.622],[178.833,-37.716],[178.792,-37.8],[178.704,-37.878],[178.63,-38.03],[178.616,-38.225],[178.63,-38.32],[178.598,-38.465],[178.555,-38.536],[178.54,-38.598],[178.49,-38.674],[178.368,-38.777],[178.174,-38.895],[178.166,-38.946],[178.242,-39.026],[178.262,-39.129],[178.202,-39.24],[178.186,-39.347],[178.101,-39.432],[177.949,-39.499],[177.819,-39.505],[177.721,-39.473],[177.651,-39.421],[177.613,-39.359],[177.59,-39.255],[177.431,-39.266],[177.296,-39.31],[177.354,-39.674],[177.288,-39.789],[177.281,-39.897],[177.163,-40.055],[177.123,-40.2],[176.995,-40.349],[176.918,-40.4],[176.876,-40.549],[176.84,-40.607],[176.777,-40.655],[176.691,-40.685],[176.538,-40.831],[176.47,-40.987],[176.363,-41.096],[176.293,-41.278],[176.11,-41.419],[176.032,-41.51],[175.84,-41.584],[175.753,-41.65],[175.555,-41.751],[175.401,-41.802],[175.269,-41.815],[175.105,-41.788],[175.029,-41.74],[174.982,-41.678],[174.523,-41.521],[174.429,-41.457],[174.395,-41.48],[174.42,-41.543],[174.501,-41.597],[174.557,-41.679],[174.566,-41.763],[174.534,-41.84],[174.208,-42.147],[174.182,-42.251],[173.995,-42.412],[173.985,-42.478],[173.945,-42.538],[173.862,-42.599],[173.77,-42.63],[173.716,-42.697],[173.56,-42.975],[173.461,-43.077],[173.323,-43.154],[173.233,-43.229],[173.045,-43.275],[173,-43.309],[172.989,-43.425],[173.215,-43.491],[173.324,-43.573],[173.373,-43.637],[173.404,-43.715],[173.406,-43.795],[173.33,-43.948],[173.262,-44.004],[173.163,-44.055],[172.993,-44.099],[172.815,-44.094],[172.71,-44.072],[172.623,-44.032],[172.401,-44.057],[172.059,-44.173],[171.799,-44.283],[171.571,-44.405],[171.544,-44.426],[171.519,-44.52],[171.441,-44.627],[171.456,-44.824],[171.427,-44.973],[171.36,-45.086],[171.14,-45.31],[171.139,-45.455],[171.086,-45.567],[171.008,-45.636],[170.988,-45.676],[171.038,-45.808],[171.03,-45.915],[170.981,-45.99],[170.905,-46.042],[170.761,-46.097],[170.482,-46.141],[170.445,-46.227],[170.394,-46.284],[170.116,-46.454],[170.099,-46.52],[170.048,-46.582],[169.736,-46.75],[169.452,-46.828],[169.254,-46.861],[169.019,-46.877],[168.838,-46.869],[168.722,-46.957],[168.544,-47.015],[168.567,-47.1],[168.533,-47.187],[168.472,-47.24],[168.365,-47.293],[168.195,-47.328],[168.197,-47.42],[168.13,-47.509],[168.113,-47.587],[168.071,-47.64],[168.007,-47.682],[167.836,-47.724],[167.747,-47.718],[167.665,-47.694],[167.553,-47.607],[167.529,-47.549],[167.531,-47.489],[167.426,-47.483],[167.236,-47.44],[167.095,-47.356],[167.036,-47.277],[167.04,-47.163],[167.152,-47.034],[167.241,-46.97],[167.365,-46.934],[167.369,-46.91],[167.327,-46.853],[167.301,-46.772],[167.326,-46.669],[167.392,-46.606],[167.541,-46.531],[167.533,-46.513],[167.292,-46.459],[167.155,-46.465],[167.19,-46.532],[167.193,-46.609],[167.161,-46.676],[167.097,-46.731],[167.004,-46.769],[166.896,-46.782],[166.8,-46.772],[166.698,-46.736],[166.607,-46.667],[166.57,-46.581],[166.592,-46.493],[166.67,-46.418],[166.57,-46.398],[166.46,-46.351],[166.275,-46.206],[166.181,-46.079],[166.141,-45.929],[166.144,-45.867]]],[[[166.204,-48.01],[166.274,-47.927],[166.432,-47.844],[166.529,-47.815],[166.634,-47.81],[166.751,-47.835],[166.843,-47.89],[166.915,-47.985],[166.927,-48.067],[166.882,-48.152],[166.787,-48.215],[166.683,-48.244],[166.456,-48.264],[166.334,-48.236],[166.242,-48.176],[166.195,-48.096],[166.204,-48.01]]],[[[168.711,-52.446],[168.906,-52.321],[168.992,-52.286],[169.11,-52.268],[169.325,-52.276],[169.448,-52.317],[169.528,-52.383],[169.593,-52.522],[169.582,-52.608],[169.525,-52.683],[169.381,-52.771],[169.193,-52.817],[169.032,-52.813],[168.816,-52.736],[168.693,-52.65],[168.656,-52.544],[168.711,-52.446]]],[[[172.347,-34.286],[172.282,-34.336],[172.201,-34.364],[172.052,-34.39],[171.946,-34.376],[171.864,-34.332],[171.809,-34.265],[171.789,-34.188],[171.806,-34.11],[171.86,-34.041],[171.936,-33.996],[172.119,-33.933],[172.252,-33.942],[172.351,-34],[172.405,-34.09],[172.401,-34.201],[172.347,-34.286]]],[[[178.417,-49.723],[178.413,-49.637],[178.464,-49.558],[178.547,-49.506],[178.72,-49.447],[178.847,-49.439],[178.965,-49.463],[179.06,-49.513],[179.117,-49.583],[179.13,-49.666],[179.09,-49.775],[179.002,-49.858],[178.873,-49.906],[178.715,-49.917],[178.561,-49.877],[178.46,-49.811],[178.417,-49.723]]],[[[178.828,-47.915],[178.743,-47.85],[178.707,-47.768],[178.726,-47.683],[178.796,-47.611],[178.912,-47.56],[179.031,-47.546],[179.163,-47.569],[179.276,-47.618],[179.342,-47.687],[179.364,-47.764],[179.339,-47.844],[179.269,-47.91],[179.177,-47.952],[179.064,-47.968],[178.946,-47.955],[178.828,-47.915]]]]},"properties":{"A3":"NZL"},"type":"Feature"}],"bbox":[165.554,-52.817,-175.542,-29.031],"type":"FeatureCollection"} diff --git a/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/nzl.geo.json b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/nzl.geo.json new file mode 100644 index 0000000..ff7cc5d --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/Fixtures/convex/out/nzl.geo.json @@ -0,0 +1 @@ +{"features":[{"geometry":{"coordinates":[[[165.554,-50.809],[165.604,-50.936],[165.698,-51.027],[165.778,-51.071],[168.816,-52.736],[169.032,-52.813],[169.193,-52.817],[169.381,-52.771],[178.873,-49.906],[179.002,-49.858],[179.09,-49.775],[-175.623,-44.442],[-175.587,-44.363],[-175.542,-43.975],[-177.635,-29.191],[-177.678,-29.118],[-177.754,-29.058],[-177.848,-29.031],[-178.01,-29.039],[-178.1,-29.07],[171.936,-33.996],[171.86,-34.041],[171.806,-34.11],[166.178,-45.645],[166.153,-45.709],[165.554,-50.809]]],"type":"Polygon"},"type":"Feature"}],"type":"FeatureCollection"} diff --git a/Tests/GeoJSONKitTurfTests/PositionTests.swift b/Tests/GeoJSONKitTurfTests/PositionTests.swift index 702b6e5..a8e2906 100644 --- a/Tests/GeoJSONKitTurfTests/PositionTests.swift +++ b/Tests/GeoJSONKitTurfTests/PositionTests.swift @@ -49,10 +49,7 @@ class PositionTests: XCTestCase { if actual != expectedPolygon { // Give it another chance on the data-level, too do { - var options: JSONSerialization.WritingOptions = [.prettyPrinted] - if #available(iOS 11.0, OSX 10.13, *) { - options.insert(.sortedKeys) - } + let options: JSONSerialization.WritingOptions = [.sortedKeys] let newData = try GeoJSON(geometry: .single(.polygon(actual))).toData(options: options) let oldData = try GeoJSON(geometry: .single(.polygon(expectedPolygon))).toData(options: options) if newData != oldData {