-
-
Save nixta/8a5637a288ce22501aa086e6b5933adf to your computer and use it in GitHub Desktop.
An extension to AGSGeometryEngine to work with GeoJSON
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2020 Esri. | |
// | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
import ArcGIS | |
typealias GeoJSONCoordinate = Array<Double?> | |
extension AGSGeometryEngine { | |
struct GeoJSON { | |
/// These are the types of object you can expect to find in valid GeoJSON | |
/// See http://geojson.org/geojson-spec.html | |
enum ObjectType : String { | |
case point = "Point" | |
case multiPoint = "MultiPoint" | |
case lineString = "LineString" | |
case multiLineString = "MultiLineString" | |
case polygon = "Polygon" | |
case multiPolygon = "MultiPolygon" | |
case geometryCollection = "GeometryCollection" | |
case feature = "Feature" | |
case featureCollection = "FeatureCollection" | |
} | |
/// An enumeration to return the parsed result | |
enum ParseResult: CustomStringConvertible { | |
var description: String { | |
switch self { | |
case .geometry(_): | |
return "Geometry" | |
case .feature(_): | |
return "Feature" | |
case .geometryCollection(let geometries): | |
return "Collection of \(geometries.count) geometries" | |
case .featureCollection(let features): | |
return "Collection of \(features.count) features" | |
} | |
} | |
case geometry(AGSGeometry) | |
case geometryCollection([AGSGeometry]) | |
case feature(AGSGeoElement) | |
case featureCollection([AGSGeoElement]) | |
var agsGeometry:AGSGeometry? { | |
switch self { | |
case .geometry(let result): | |
return result | |
default: | |
return nil | |
} | |
} | |
var agsGeometries:[AGSGeometry]? { | |
switch self { | |
case .geometryCollection(let result): | |
return result | |
default: | |
return nil | |
} | |
} | |
var agsFeature:AGSGeoElement? { | |
switch self { | |
case .feature(let result): | |
return result | |
default: | |
return nil | |
} | |
} | |
var agsFeatures:[AGSGeoElement]? { | |
switch self { | |
case .featureCollection(let result): | |
return result | |
default: | |
return nil | |
} | |
} | |
} | |
/// If the result could not be parsed, an exception will be thrown | |
enum ParseError : Error { | |
case invalidGeoJSON(message:String) | |
case unexpectedObjectType(type:String) | |
case invalidGeometry(message: String) | |
case invalidGeometryCoordinates(message: String) | |
} | |
/// MARK: Public Parsing Methods | |
/// Accept a GeoJSON String (useful for testing) | |
static func parse(geojson:String) throws -> ParseResult { | |
let data = geojson.data(using: .utf8) | |
return try AGSGeometryEngine.GeoJSON.parse(geojson: data!) | |
} | |
/// Accept GeoJSON as a Data object (useful for network requests) | |
static func parse(geojson:Data) throws -> ParseResult { | |
let json = try JSONSerialization.jsonObject(with: geojson, options: []) | |
guard let jsonDict = json as? Dictionary<String, Any> else { | |
throw ParseError.invalidGeoJSON(message: "Parsed GeoJSON must be a top-level dictionary of <String,Any>!") | |
} | |
return try AGSGeometryEngine.GeoJSON.parse(geojson: jsonDict) | |
} | |
/// Accept GeoJSON as a deserialized JSON Dictionary | |
static func parse(geojson:Dictionary<String, Any>) throws -> ParseResult { | |
guard let t = geojson["type"] as? String else { | |
throw ParseError.invalidGeoJSON(message: "'type' not found in GeoJSON object!") | |
} | |
guard let geoJSONObjectType = ObjectType(rawValue: t) else { | |
throw ParseError.unexpectedObjectType(type: t) | |
} | |
switch geoJSONObjectType { | |
case .point, .lineString, .polygon, .multiPoint, .multiLineString, .multiPolygon: | |
let geometry = try AGSGeometryEngine().geojson_parseGeometry(geojson: geojson) | |
return ParseResult.geometry(geometry) | |
case .geometryCollection: | |
let geometries = try AGSGeometryEngine().geojson_parseGeometryCollection(geojson: geojson) | |
return ParseResult.geometryCollection(geometries) | |
case .feature: | |
let feature = try AGSGeometryEngine().geojson_parseFeature(geojson: geojson) | |
return ParseResult.feature(feature) | |
case .featureCollection: | |
let features = try AGSGeometryEngine().geojson_parseFeatureCollection(geojson: geojson) | |
return ParseResult.featureCollection(features) | |
} | |
} | |
} | |
private func geojson_parseGeometry(geojson: Dictionary<String, Any>) throws -> AGSGeometry { | |
// Test that we have a valid Geometry Object | |
// See http://geojson.org/geojson-spec.html#geometry-objects | |
guard let t = geojson["type"] as? String else { | |
throw GeoJSON.ParseError.invalidGeometry(message: "Missing Geometry Type") | |
} | |
guard let type = GeoJSON.ObjectType(rawValue: t) else { | |
throw GeoJSON.ParseError.invalidGeometry(message: "Unrecognized geometry type \(t)") | |
} | |
guard let coords = geojson["coordinates"] else { | |
throw GeoJSON.ParseError.invalidGeometry(message: "Geometry has no coordinates element") | |
} | |
// Now parse as appropriate | |
switch type { | |
case .point: | |
if let coordinates = coords as? GeoJSONCoordinate { | |
return try geojson_parsePoint(coordinates: coordinates) | |
} else { | |
throw GeoJSON.ParseError.invalidGeometryCoordinates(message: "Point coordinate must be a pair of doubles.") | |
} | |
case .lineString: | |
if let coordinates = coords as? [GeoJSONCoordinate] { | |
return try geojson_parseLineString(coordinates: coordinates) | |
} else { | |
throw GeoJSON.ParseError.invalidGeometryCoordinates(message: "Unexpected coordinates for linestring.") | |
} | |
case .polygon: | |
if let coordinates = coords as? [[GeoJSONCoordinate]] { | |
return try geojson_parsePolygon(coordinates: coordinates) | |
} else { | |
throw GeoJSON.ParseError.invalidGeometryCoordinates(message: "Unexpected coordinates for polygon.") | |
} | |
case .multiPoint: | |
if let coordinates = coords as? [GeoJSONCoordinate] { | |
return try geojson_parseMultiPoint(coordinates: coordinates) | |
} else { | |
throw GeoJSON.ParseError.invalidGeometryCoordinates(message: "Unexpected coordinates for multipoint.") | |
} | |
case .multiLineString: | |
if let coordinates = coords as? [[GeoJSONCoordinate]] { | |
return try geojson_parseMultiLineString(coordinates: coordinates) | |
} else { | |
throw GeoJSON.ParseError.invalidGeometryCoordinates(message: "Unexpected coordinates for multilinestring.") | |
} | |
case .multiPolygon: | |
if let coordinates = coords as? [[[GeoJSONCoordinate]]] { | |
return try geojson_parseMultiPolygon(coordinates: coordinates) | |
} else { | |
throw GeoJSON.ParseError.invalidGeometryCoordinates(message: "Unexpected coordinates for multipolygon.") | |
} | |
default: | |
throw GeoJSON.ParseError.invalidGeometry(message: "Unsupported geometry type \(t)") | |
} | |
} | |
private func geojson_parseGeometryCollection(geojson: Dictionary<String, Any>) throws -> [AGSGeometry] { | |
guard let geojsonGeoms = geojson["geometries"] as? Array<Dictionary<String, Any>> else { | |
throw GeoJSON.ParseError.invalidGeometry(message: "A GeomertyColleciton must have a 'geometries' property!") | |
} | |
var geoms:[AGSGeometry] = [] | |
for geojsonGeom in geojsonGeoms { | |
geoms.append(try geojson_parseGeometry(geojson: geojsonGeom)) | |
} | |
return geoms | |
} | |
private func geojson_parseFeature(geojson: [String: Any]) throws -> AGSGeoElement { | |
guard let geometry = geojson["geometry"] as? [String: Any], let properties = geojson["properties"] as? [String: Any] else { | |
throw GeoJSON.ParseError.invalidGeoJSON(message: "Feature must have 'geometry' and 'properties' properties!") | |
} | |
let agsGeom = try geojson_parseGeometry(geojson: geometry) | |
let graphic = AGSGraphic(geometry: agsGeom, symbol: nil, attributes: properties) | |
// geojson_fixProperties(geoElement: graphic) | |
return graphic | |
} | |
// private func geojson_fixProperties(geoElement:AGSGeoElement) { | |
// for (key,value) in geoElement.attributes /*where value is NSNumber*/ { | |
// if let value = value as? NSNumber, Double(value.intValue) == value.doubleValue { | |
// geoElement.attributes[key] = NSNumber(value: Int32(value.intValue)) | |
// } | |
// } | |
// } | |
private func geojson_parseFeatureCollection(geojson: Dictionary<String, Any>) throws -> [AGSGeoElement] { | |
guard let geojsonFeatures = geojson["features"] as? [[String:Any]] else { | |
throw GeoJSON.ParseError.invalidGeoJSON(message: "FeatureCollection must have 'features' property!") | |
} | |
var features:[AGSGeoElement] = [] | |
for geojsonFeature in geojsonFeatures { | |
features.append(try geojson_parseFeature(geojson: geojsonFeature)) | |
// print("Parsed feature") | |
} | |
return features | |
} | |
func fixIntsInGeoElements(geoElements:[AGSGeoElement], fields:[AGSField]) { | |
for element in geoElements { | |
for field in fields where field.type == .int32 { | |
if let value = element.attributes[field.name] as? NSNumber{ | |
element.attributes[field.name] = NSNumber(value: Int32(value.intValue)) | |
} | |
} | |
} | |
} | |
private func geojson_pointFromPosition(position: GeoJSONCoordinate) throws -> AGSPoint { | |
guard position.count >= 2, let x = position[0], let y = position[1] else { | |
throw GeoJSON.ParseError.invalidGeometryCoordinates(message: "Position must have at least x and y Double elements! \(position)") | |
} | |
if position.count > 2, let z = position[2] { | |
return AGSPoint(x: x, y: y, z: z, spatialReference: AGSSpatialReference.wgs84()) | |
} else { | |
return AGSPoint(x: x, y: y, spatialReference: AGSSpatialReference.wgs84()) | |
} | |
} | |
private func geojson_getPoints(positions:Array<GeoJSONCoordinate>) throws -> [AGSPoint] { | |
do { | |
return try positions.map { (position) -> AGSPoint in | |
return try geojson_pointFromPosition(position: position) | |
} | |
} catch { | |
print("Error parsing array of positions!") | |
throw error | |
} | |
} | |
private func geojson_parsePoint(coordinates: GeoJSONCoordinate) throws -> AGSPoint { | |
return try geojson_pointFromPosition(position: coordinates) | |
} | |
private func geojson_parseLineString(coordinates: Array<GeoJSONCoordinate>) throws -> AGSPolyline { | |
guard coordinates.count > 1 else { | |
throw GeoJSON.ParseError.invalidGeometryCoordinates(message: "Linestring must have at least 2 positions!") | |
} | |
do { | |
let points = try geojson_getPoints(positions: coordinates) | |
return AGSPolyline(points: points) | |
} catch { | |
print("Error parsing points in linestring!") | |
throw error | |
} | |
} | |
private func geojson_parsePolygon(coordinates: [[[Double?]]]) throws -> AGSPolygon { | |
guard coordinates.count > 0 else { | |
let emptyBuilder = AGSPolygonBuilder(spatialReference: .wgs84()) | |
return emptyBuilder.toGeometry() | |
} | |
let builder = AGSPolygonBuilder(spatialReference: .wgs84()) | |
do { | |
for coordinateRing in coordinates { | |
builder.addPart(with: try geojson_getPoints(positions: coordinateRing)) | |
} | |
} catch { | |
print("Error parsing points in polygon!") | |
throw error | |
} | |
return builder.toGeometry() | |
} | |
private func geojson_parseMultiPoint(coordinates: Array<GeoJSONCoordinate>) throws -> AGSMultipoint { | |
do { | |
let points = try coordinates.map { (position) -> AGSPoint in | |
return try geojson_pointFromPosition(position: position) | |
} | |
return AGSMultipoint(points: points) | |
} catch { | |
print("Error parsing points in multipoint!") | |
throw error | |
} | |
} | |
private func geojson_parseMultiLineString(coordinates: Array<Array<GeoJSONCoordinate>>) throws -> AGSPolyline { | |
let builder = AGSPolylineBuilder(spatialReference: AGSSpatialReference.wgs84()) | |
do { | |
for linePart in coordinates { | |
let agsLinePart = try geojson_parseLineString(coordinates: linePart) | |
builder.addPart(with: agsLinePart.parts[0].points.array()) | |
// builder.addPart(with: try geojson_getPoints(positions: linePart)) | |
} | |
} catch { | |
print("Error parsing point in multilinestring!") | |
throw error | |
} | |
return builder.toGeometry() | |
} | |
private func geojson_parseMultiPolygon(coordinates: Array<Array<Array<GeoJSONCoordinate>>>) throws -> AGSPolygon { | |
guard coordinates.count > 0 else { | |
let emptyBuilder = AGSPolygonBuilder(spatialReference: AGSSpatialReference.wgs84()) | |
return emptyBuilder.toGeometry() | |
} | |
var output:AGSPolygon? | |
do { | |
for polygonPart in coordinates { | |
let polygonItem = try geojson_parsePolygon(coordinates: polygonPart) | |
if output == nil { | |
output = polygonItem | |
} else { | |
output = (AGSGeometryEngine.union(ofGeometry1: output!, geometry2: polygonItem) as! AGSPolygon) | |
} | |
} | |
} catch { | |
print("Error parsing multipolygon!") | |
throw error | |
} | |
return output! | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment