Skip to content

Instantly share code, notes, and snippets.

@nixta
Created April 15, 2021 21:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nixta/8a5637a288ce22501aa086e6b5933adf to your computer and use it in GitHub Desktop.
Save nixta/8a5637a288ce22501aa086e6b5933adf to your computer and use it in GitHub Desktop.
An extension to AGSGeometryEngine to work with GeoJSON
// 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