Skip to content

Commit

Permalink
Merge pull request #73 from 2kai2kai2/GeoJSON-Parsing
Browse files Browse the repository at this point in the history
Improve GeoJSON Parsing
  • Loading branch information
steinbro authored Dec 12, 2023
2 parents 03b47e3 + 792a98d commit 943ff8a
Show file tree
Hide file tree
Showing 18 changed files with 904 additions and 675 deletions.
60 changes: 58 additions & 2 deletions apps/ios/GuideDogs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -666,9 +666,13 @@
914BAAF32AD745E400CB2171 /* DestinationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BAAF22AD745E400CB2171 /* DestinationManagerTest.swift */; };
914BAAFD2AD7483300CB2171 /* AudioEngineTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */; };
915FF9F62ADE4BAF002B3690 /* AuthoredActivityContentTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915FF9F42ADE3F91002B3690 /* AuthoredActivityContentTest.swift */; };
91745DD52AFB0E6C003EC104 /* GeoJsonGeometryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91745DD42AFB0E6C003EC104 /* GeoJsonGeometryTest.swift */; };
91745DD62AFB0F32003EC104 /* GeometryUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */; };
91745DD82AFC48E0003EC104 /* GeoJsonFeatureTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91745DD72AFC48E0003EC104 /* GeoJsonFeatureTest.swift */; };
91745DDA2AFED7FF003EC104 /* GeoJsonFeatureCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91745DD92AFED7FF003EC104 /* GeoJsonFeatureCollection.swift */; };
91B6ADAA2AF19CB600FFE4E9 /* OSMServiceModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B6ADA92AF19CB600FFE4E9 /* OSMServiceModelTest.swift */; };
91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */; };
91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */; };
91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */; };
B90C27D61EAF81D600007368 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90C27D51EAF81D600007368 /* Sound.swift */; };
B918EE9825100FFF00A5354A /* CalloutRangeContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B918EE9725100FFF00A5354A /* CalloutRangeContext.swift */; };
B91D3F6427AB5546004159A8 /* UserAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91D3F6327AB5546004159A8 /* UserAction.swift */; };
Expand Down Expand Up @@ -1584,6 +1588,10 @@
914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
914DEBDC2A3CE901007B161C /* GeometryUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryUtilsTest.swift; sourceTree = "<group>"; };
915FF9F42ADE3F91002B3690 /* AuthoredActivityContentTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthoredActivityContentTest.swift; sourceTree = "<group>"; };
91745DD42AFB0E6C003EC104 /* GeoJsonGeometryTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoJsonGeometryTest.swift; sourceTree = "<group>"; };
91745DD72AFC48E0003EC104 /* GeoJsonFeatureTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJsonFeatureTest.swift; sourceTree = "<group>"; };
91745DD92AFED7FF003EC104 /* GeoJsonFeatureCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJsonFeatureCollection.swift; sourceTree = "<group>"; };
91B6ADA92AF19CB600FFE4E9 /* OSMServiceModelTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSMServiceModelTest.swift; sourceTree = "<group>"; };
91C82AAC2A5DCF040086D126 /* GeolocationManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = " GeolocationManagerTest.swift"; path = "UnitTests/Sensors/Geolocation/Geolocation Manager/ GeolocationManagerTest.swift"; sourceTree = SOURCE_ROOT; };
91C82ABD2A6B08500086D126 /* RouteGuidanceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RouteGuidanceTest.swift; path = "UnitTests/Behaviors/Route Guidance/RouteGuidanceTest.swift"; sourceTree = SOURCE_ROOT; };
B90C27D51EAF81D600007368 /* Sound.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Sound.swift; path = Code/Audio/Protocols/Sound.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4277,6 +4285,22 @@
path = "Interactive View";
sourceTree = "<group>";
};
914BAAED2AD745BC00CB2171 /* Services */ = {
isa = PBXGroup;
children = (
914BAAEE2AD745BC00CB2171 /* OSM */,
);
path = Services;
sourceTree = "<group>";
};
914BAAEE2AD745BC00CB2171 /* OSM */ = {
isa = PBXGroup;
children = (
91B6ADA92AF19CB600FFE4E9 /* OSMServiceModelTest.swift */,
);
path = OSM;
sourceTree = "<group>";
};
914BAAF12AD745E400CB2171 /* Destination Manager */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4329,6 +4353,31 @@
path = "Authored Activities";
sourceTree = "<group>";
};
91745DD12AFB0E6C003EC104 /* Models */ = {
isa = PBXGroup;
children = (
91745DD22AFB0E6C003EC104 /* JSON Parsing */,
);
path = Models;
sourceTree = "<group>";
};
91745DD22AFB0E6C003EC104 /* JSON Parsing */ = {
isa = PBXGroup;
children = (
91745DD32AFB0E6C003EC104 /* OSM */,
);
path = "JSON Parsing";
sourceTree = "<group>";
};
91745DD32AFB0E6C003EC104 /* OSM */ = {
isa = PBXGroup;
children = (
91745DD72AFC48E0003EC104 /* GeoJsonFeatureTest.swift */,
91745DD42AFB0E6C003EC104 /* GeoJsonGeometryTest.swift */,
);
path = OSM;
sourceTree = "<group>";
};
91C82AA62A4F56A70086D126 /* Sensors */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4356,8 +4405,10 @@
91C82AB52A67182E0086D126 /* Data */ = {
isa = PBXGroup;
children = (
91745DD12AFB0E6C003EC104 /* Models */,
915FF9F32ADE3F91002B3690 /* Authored Activities */,
914BAAF12AD745E400CB2171 /* Destination Manager */,
914BAAED2AD745BC00CB2171 /* Services */,
);
path = Data;
sourceTree = "<group>";
Expand Down Expand Up @@ -4467,6 +4518,7 @@
children = (
D298328F1E4BF80600352A5A /* GeoJsonFeature.swift */,
D29832951E4E249700352A5A /* GeoJsonGeometry.swift */,
91745DD92AFED7FF003EC104 /* GeoJsonFeatureCollection.swift */,
);
name = OSM;
sourceTree = "<group>";
Expand Down Expand Up @@ -5542,7 +5594,10 @@
914BAAF32AD745E400CB2171 /* DestinationManagerTest.swift in Sources */,
91C82ABE2A6B08500086D126 /* RouteGuidanceTest.swift in Sources */,
91C82AAD2A5DCF040086D126 /* GeolocationManagerTest.swift in Sources */,
91DC0CF92A46134600244CC8 /* GeometryUtilsTest.swift in Sources */,
91745DD82AFC48E0003EC104 /* GeoJsonFeatureTest.swift in Sources */,
91745DD52AFB0E6C003EC104 /* GeoJsonGeometryTest.swift in Sources */,
91B6ADAA2AF19CB600FFE4E9 /* OSMServiceModelTest.swift in Sources */,
91745DD62AFB0F32003EC104 /* GeometryUtilsTest.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -6052,6 +6107,7 @@
31D0D7301E031A0E0087C847 /* UINavigationItem+Extension.swift in Sources */,
2821F61B220A6D1600D15EFF /* AuthoredActivityLoader.swift in Sources */,
6220D2D027CD7D4B0063BEA6 /* BeaconPickerItemView.swift in Sources */,
91745DDA2AFED7FF003EC104 /* GeoJsonFeatureCollection.swift in Sources */,
287D3E8D22DE50340084B92B /* StatusTableViewController.swift in Sources */,
6258B3872469DAFA0051F60B /* UniversalLinkPathComponents.swift in Sources */,
28A16122283C3AF00081CFA4 /* TourGenerator.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions apps/ios/GuideDogs/Code/App/ExternalNavigationApps.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import CoreLocation
/// then add to the enum and the switches below
/// "deeplinks" are different than URL schemes; deeplinks can be arbitrary domains
/// and will usually fall back to a web version if the corresponding app isn't installed, while URL schemes tend to be less cross-platform and need to be manually added in Info.plist.
enum ExternalNavigationApps: String, CaseIterable{
enum ExternalNavigationApps: String, CaseIterable {
case appleMaps
case googleMaps
case waze

/// Should return a localized title for each supported app
var localizedTitle: String {
switch self {
case .googleMaps: return "Google Maps"
case .googleMaps: return "Google Maps"
case .waze: return "Waze"
case .appleMaps: return "Apple Maps"
}
Expand Down
71 changes: 3 additions & 68 deletions apps/ios/GuideDogs/Code/App/Helpers/GeometryUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,6 @@ class GeometryUtils {

static let earthRadius = Double(6378137)

/// Parses a GeoJSON string and returns the coordinates and type values.
///
/// See:
/// * https://geojson.org
/// * RFC 7946
static func coordinates(geoJson: String) -> (type: GeometryType?, points: [Any]?) {
guard !geoJson.isEmpty, let jsonObject = GDAJSONObject(string: geoJson) else {
return (nil, nil)
}

let geometryType: GeometryType?
if let typeString = jsonObject.string(atPath: "type") {
geometryType = GeometryType(rawValue: typeString)
} else {
geometryType = nil
}

guard let geometry = jsonObject.array(atPath: "coordinates") else {
return (geometryType, nil)
}

return (geometryType, geometry)
}

/// Returns whether a coordinate lies inside of the region contained within the coordinate path.
/// The path is always considered closed, regardless of whether the last point equals the first or not.
static func geometryContainsLocation(location: CLLocationCoordinate2D, coordinates: [CLLocationCoordinate2D]) -> Bool {
Expand Down Expand Up @@ -307,24 +283,9 @@ class GeometryUtils {
return ((cX - projX) * (cX - projX) + (cY - projY) * (cY - projY), lat, lon)
}

/// Finds the closest point on an edge of the polygon (including intermediate points along edges) to the given coordinate.
/// - Returns: `nil` if the polygon is empty (i.e. no edges)
static func closestEdge(from coordinate: CLLocationCoordinate2D, on polygon: GAMultiLine) -> CLLocation? {
var coordinates: [CLLocationCoordinate2D] = []

// Transform to a continuous coordinates path
for line in polygon {
for point in line {
coordinates.append(point.toCoordinate())
}
}

return closestEdge(from: coordinate, on: coordinates)
}

/// Finds the closest point on the path (including intermediate points along edges) to the given coordinate.
/// - Returns: `nil` if there are no edges (less than two points in `path`)
static func closestEdge(from coordinate: CLLocationCoordinate2D, on path: [CLLocationCoordinate2D]) -> CLLocation? {
static func closestEdge(from coordinate: CLLocationCoordinate2D, on path: [CLLocationCoordinate2D]) -> CLLocationCoordinate2D? {
guard !path.isEmpty else {
return nil;
}
Expand All @@ -333,7 +294,7 @@ class GeometryUtils {
let zoomLevel: UInt = 23
let res: Double = VectorTile.groundResolution(latitude: coordinate.latitude, zoom: zoomLevel)

var closestLocation: CLLocation?
var closestLocation: CLLocationCoordinate2D?
var minimumDistance = CLLocationDistanceMax

for i in 0..<path.count - 1 {
Expand All @@ -356,7 +317,7 @@ class GeometryUtils {
let distance = sqrt(distanceSq) * res

if distance < minimumDistance {
closestLocation = CLLocation(latitude: lat, longitude: long)
closestLocation = CLLocationCoordinate2D(latitude: lat, longitude: long)
minimumDistance = distance
}
}
Expand Down Expand Up @@ -457,32 +418,6 @@ class GeometryUtils {

extension GeometryUtils {

/// Returns a generated coordinate representing the mean center of a given array of coordinates.
/// - Note: See `centroid(coordinates: [CLLocationCoordinate2D]) -> CLLocationCoordinate2D?`
static func centroid(geoJson: String) -> CLLocationCoordinate2D? {
guard let points = GeometryUtils.coordinates(geoJson: geoJson).points else {
return nil
}

// Check if `points` contains one point (e.g. point)
if let point = points as? GAPoint {
return point.toCoordinate()
}

// Check if `points` contains an array of points (e.g. line, polygon)
if let points = points as? GALine {
return GeometryUtils.centroid(coordinates: points.toCoordinates())
}

// Check if `points` contains a two dimensional array of points (e.g. lines, polygons)
if let points = points as? GAMultiLine {
let flattened = Array(points.toCoordinates().joined())
return GeometryUtils.centroid(coordinates: flattened)
}

return nil
}

/// Returns a generated coordinate representing the mean center of a given array of `CLLocation` objects.
/// - Note: See `centroid(coordinates: [CLLocationCoordinate2D]) -> CLLocationCoordinate2D?`
static func centroid(locations: [CLLocation]) -> CLLocationCoordinate2D? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,29 +60,22 @@ class GDASpatialDataResultEntity: Object {

// MARK: - Computed & Non-Realm Properties

var geometryType: GeometryType?

private var _coordinates: [Any]?
var coordinates: [Any]? {
if _coordinates != nil {
return _coordinates
}

// If there aren't coordinates, there is nothing to return
guard let coordinatesJson = coordinatesJson, !coordinatesJson.isEmpty else {
private var _geometry: GeoJsonGeometry?
var geometry: GeoJsonGeometry? {
if _geometry != nil {
return _geometry
}
// Otherwise, try to parse
// TODO: we might want to store that we tried before and failed
guard let geoJSON = coordinatesJson else {
return nil
}

// Get the coordinates and the geometry type from the GeoJSON object
let parsedCoordinates = GeometryUtils.coordinates(geoJson: coordinatesJson)

if let geometryType = parsedCoordinates.type {
self.geometryType = geometryType
}

_coordinates = parsedCoordinates.points

return _coordinates
_geometry = GeoJsonGeometry(geoJSON: geoJSON)
return _geometry
}

var coordinates: Any? {
return geometry?.coordinates
}

private var _entrances: [POI]?
Expand All @@ -92,6 +85,9 @@ class GDASpatialDataResultEntity: Object {
}

// Only POIs with non-point geometries can have entrances
if case .point = geometry {
return nil
}
guard coordinates != nil,
let jsonData = entrancesJson?.data(using: .utf8) else {
return nil
Expand Down Expand Up @@ -122,8 +118,8 @@ class GDASpatialDataResultEntity: Object {

/// Returns the names of properties which Realm should ignore
static override func ignoredProperties() -> [String] {
return ["geometryType",
"_coordinates",
return ["_geometry",
"geometry",
"coordinates",
"_entrances",
"entrances"]
Expand Down Expand Up @@ -182,23 +178,18 @@ class GDASpatialDataResultEntity: Object {
}

// Set geolocation information
if let geometry = feature.geometry {
if geometry.type == .point, let point = geometry.point, point.count > 1 {
latitude = point[1]
longitude = point[0]
} else {
coordinatesJson = geometry.coordinateJSON
}

if let centroid = geometry.centroid {
centroidLatitude = centroid[1]
centroidLongitude = centroid[0]
} else {
centroidLatitude = latitude
centroidLongitude = longitude
}
if case .point(let point) = feature.geometry {
latitude = point.latitude
longitude = point.longitude
} else if let json_data = try? JSONEncoder().encode(feature.geometry) {
coordinatesJson = String(data: json_data, encoding: .utf8)
_geometry = feature.geometry;
}

let centroid = feature.geometry.centroid
centroidLatitude = centroid.latitude
centroidLongitude = centroid.longitude

// Road specific metadata

roundabout = feature.isRoundabout
Expand All @@ -225,14 +216,12 @@ class GDASpatialDataResultEntity: Object {
// MARK: - Geometries

/// Returns whether a coordinate lies inside the entity.
/// - note: This is only valid for entities that contain geometries, such as buildings.
/// - note: This is only valid for entities that contain geometries with an area (polygons and multiPolygons), such as buildings.
func contains(location: CLLocationCoordinate2D) -> Bool {
guard let points = self.coordinates as? GAMultiLine else { return false }
let coordinates = points.toCoordinates()

guard !coordinates.isEmpty else { return false }

return GeometryUtils.geometryContainsLocation(location: location, coordinates: coordinates.first!)
guard let geometry = geometry else {
return false
}
return geometry.withinArea(location)
}

// MARK: `NSObject`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,9 @@ class Intersection: Object, Locatable, Localizable {
self.key += String(roadId)
}

if let lat = feature.geometry?.point?[1] {
latitude = lat
}

if let lon = feature.geometry?.point?[0] {
longitude = lon
if case .point(let coordinate) = feature.geometry {
latitude = coordinate.latitude
longitude = coordinate.longitude
}

self.key += String(latitude) + String(longitude)
Expand Down
Loading

0 comments on commit 943ff8a

Please sign in to comment.