Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve GeoJSON Parsing #73

Merged
merged 6 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -183,20 +179,16 @@ 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 case .point(let point) = geometry {
latitude = point.latitude
longitude = point.longitude
} else if let json_data = try? JSONEncoder().encode(geometry) {
coordinatesJson = String(data: json_data, encoding: .utf8)
}

if let centroid = geometry.centroid {
centroidLatitude = centroid[1]
centroidLongitude = centroid[0]
} else {
centroidLatitude = latitude
centroidLongitude = longitude
}
let centroid = geometry.centroid
centroidLatitude = centroid.latitude
centroidLongitude = centroid.longitude
}

// Road specific metadata
Expand Down Expand Up @@ -225,14 +217,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
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class TileData: Object {
return VectorTile(quadKey: quadkey)
}

convenience init(withParsedData json: [String: Any], quadkey: String, etag: String, superCategories: SuperCategories) {
convenience init(withParsedData json: GeoJsonFeatureCollection, quadkey: String, etag: String, superCategories: SuperCategories) {
self.init()

// Get the vector tile info
Expand All @@ -85,12 +85,7 @@ class TileData: Object {
// Store the etag for checking future updates
self.etag = etag

guard let featuresJson = json["features"] as? [Any] else { return }

for featureJson in featuresJson {
// Try to parse the feature - the GeoJsonFeature initializer is failable
guard let feature = GeoJsonFeature(json: featureJson as! [String: Any], superCategories: superCategories) else { continue }

for feature in json.features {
// Check if it is a road, intersection, etc.
if feature.superCategory == .roads {
roads.append(GDASpatialDataResultEntity(feature: feature)!)
Expand Down
Loading