From d8348603fababbf1bcebd3872419281fcc62abe0 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Sat, 28 Mar 2026 01:48:16 -0700 Subject: [PATCH 1/9] Add location helpers and additional corrections --- .../ARCamera/ARCameraManager.swift | 12 +-- .../Localization/LocationManager.swift | 91 ++++++++++++++++++- IOSAccessAssessment/Shared/Constants.swift | 3 + .../TDEI/Config/APIEnvironment.swift | 2 +- .../TDEI/Services/WorkspaceService.swift | 6 ++ .../APITransmissionController.swift | 15 ++- .../Transmission/APITransmissionHelpers.swift | 1 - IOSAccessAssessment/View/ARCameraView.swift | 15 ++- IOSAccessAssessment/View/AnnotationView.swift | 2 +- 9 files changed, 126 insertions(+), 21 deletions(-) diff --git a/IOSAccessAssessment/ARCamera/ARCameraManager.swift b/IOSAccessAssessment/ARCamera/ARCameraManager.swift index d015d4da..3d9a1c72 100644 --- a/IOSAccessAssessment/ARCamera/ARCameraManager.swift +++ b/IOSAccessAssessment/ARCamera/ARCameraManager.swift @@ -251,10 +251,8 @@ final class ARCameraManager: NSObject, ObservableObject, ARSessionCameraProcessi self.cameraOutputImageCallback = cameraOutputImageCallback self.isConfigured = true - Task { - await MainActor.run { - self.capturedMeshSnapshotGenerator = CapturedMeshSnapshotGenerator() - } + Task { @MainActor in + self.capturedMeshSnapshotGenerator = CapturedMeshSnapshotGenerator() } } @@ -268,10 +266,8 @@ final class ARCameraManager: NSObject, ObservableObject, ARSessionCameraProcessi } func setOrientation(_ orientation: UIInterfaceOrientation) { - Task { - await MainActor.run { - self.interfaceOrientation = orientation - } + Task { @MainActor in + self.interfaceOrientation = orientation } } diff --git a/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift b/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift index c181b4ef..a2574ce5 100644 --- a/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift +++ b/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift @@ -6,6 +6,30 @@ // import CoreLocation +import UIKit +import MapKit + +struct BBox { + let minLat: Double + let maxLat: Double + let minLon: Double + let maxLon: Double +} + +class LocationHelpers { + static func boundingBoxAroundLocation(location: CLLocationCoordinate2D, radius: CLLocationDistance) -> BBox { + let region = MKCoordinateRegion(center: location, latitudinalMeters: radius, longitudinalMeters: radius) + let center = region.center + let span = region.span + let minLat = center.latitude - span.latitudeDelta + let maxLat = center.latitude + span.latitudeDelta + let minLon = center.longitude - span.longitudeDelta + let maxLon = center.longitude + span.longitudeDelta + + return BBox(minLat: minLat, maxLat: maxLat, minLon: minLon, maxLon: maxLon) + } +} + enum LocationManagerError: Error, LocalizedError { case locationUnavailable @@ -24,26 +48,79 @@ enum LocationManagerError: Error, LocalizedError { /** A wrapper around CLLocationManager to manage location and heading updates in a more controlled and safe manner. */ -class LocationManager { - let locationManager: CLLocationManager +@MainActor +class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { + private let locationManager: CLLocationManager = CLLocationManager() - init() { - self.locationManager = CLLocationManager() - self.setupLocationManager() + @Published var currentLocation: CLLocation? + @Published var currentHeading: CLHeading? + + override init() { + super.init() + } + + func startLocationUpdates() { + setupLocationManager() } private func setupLocationManager() { + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation locationManager.distanceFilter = kCLDistanceFilterNone + // TODO: Sync heading with the device orientation locationManager.headingOrientation = .portrait locationManager.headingFilter = kCLHeadingFilterNone + locationManager.pausesLocationUpdatesAutomatically = false // Prevent auto-pausing + locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation() locationManager.startUpdatingHeading() } + /** + Updates the heading orientation of the location manager based on the current device orientation. This ensures that heading data is accurate and consistent with the user's perspective. + */ + public func updateOrientation(_ orientation: UIInterfaceOrientation) { + switch orientation { + case .portrait: + locationManager.headingOrientation = .portrait + case .portraitUpsideDown: + locationManager.headingOrientation = .portraitUpsideDown + /// Flipped because the heading is relative to the device's top, which is opposite in landscape orientations + case .landscapeLeft: + locationManager.headingOrientation = .landscapeRight + case .landscapeRight: + locationManager.headingOrientation = .landscapeLeft + default: + locationManager.headingOrientation = .portrait + } + } + + nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let latestLocation = locations.last else { return } + guard let horizontalAccuracy = latestLocation.horizontalAccuracy as CLLocationAccuracy?, + let verticalAccuracy = latestLocation.verticalAccuracy as CLLocationAccuracy?, + horizontalAccuracy > 0, verticalAccuracy > 0 else { + return + } + Task { @MainActor in + self.currentLocation = latestLocation + } + } + + nonisolated func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { + guard let headingAccuracy = newHeading.headingAccuracy as CLLocationDirection?, + headingAccuracy > 0 else { + return + } + Task { @MainActor in + self.currentHeading = newHeading + } + } + func getLocation() throws -> CLLocation { guard let location = locationManager.location, location.horizontalAccuracy > 0, location.verticalAccuracy > 0 else { @@ -69,4 +146,8 @@ class LocationManager { let heading = try getHeading() return heading.trueHeading } + + func stopLocationUpdates() { + locationManager.stopUpdatingLocation() + } } diff --git a/IOSAccessAssessment/Shared/Constants.swift b/IOSAccessAssessment/Shared/Constants.swift index c9df153e..4e290d6b 100644 --- a/IOSAccessAssessment/Shared/Constants.swift +++ b/IOSAccessAssessment/Shared/Constants.swift @@ -28,6 +28,9 @@ struct Constants { // ["1463"] // ["288", "349", "1411"] // "252", "322", "368", "374", "378", "381", "384", "323", "369", "156", "375", "379"] + + static let fetchRadiusInMeters: Double = 100.0 + static let fetchUpdateRadiusThresholdInMeters: Double = 50.0 } struct OtherConstants { diff --git a/IOSAccessAssessment/TDEI/Config/APIEnvironment.swift b/IOSAccessAssessment/TDEI/Config/APIEnvironment.swift index cf92fbf3..e9f8c0ba 100644 --- a/IOSAccessAssessment/TDEI/Config/APIEnvironment.swift +++ b/IOSAccessAssessment/TDEI/Config/APIEnvironment.swift @@ -10,7 +10,7 @@ import Foundation /** TODO: Use this to replace the environment-agnostic APIConstants */ -enum APIEnvironment: String, CaseIterable, Hashable { +enum APIEnvironment: String, CaseIterable, Sendable, Hashable { // case development = "Development" case staging = "Staging" // case production = "Production" diff --git a/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift b/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift index 2fd6f3e0..517589e8 100644 --- a/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift +++ b/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift @@ -105,4 +105,10 @@ class WorkspaceService { throw APIError.decoding(error) } } + + func fetchOSMElements( + + ) async throws { + + } } diff --git a/IOSAccessAssessment/TDEI/Transmission/APITransmissionController.swift b/IOSAccessAssessment/TDEI/Transmission/APITransmissionController.swift index 9ab09ef4..343d2ad3 100644 --- a/IOSAccessAssessment/TDEI/Transmission/APITransmissionController.swift +++ b/IOSAccessAssessment/TDEI/Transmission/APITransmissionController.swift @@ -28,6 +28,7 @@ class APITransmissionController: ObservableObject { func uploadFeatures( accessibilityFeatures: [any AccessibilityFeatureProtocol], + mappingData: MappingData, inputs: APITransmissionInputs ) async throws -> APITransmissionResults { idGenerator = IntIdGenerator() @@ -45,16 +46,19 @@ class APITransmissionController: ObservableObject { case .point: apiTransmissionResults = try await uploadPoints( accessibilityFeatures: accessibilityFeatures, + mappingData: mappingData, inputs: inputs ) case .linestring: apiTransmissionResults = try await uploadLineStrings( accessibilityFeatures: accessibilityFeatures, + mappingData: mappingData, inputs: inputs ) case .polygon: apiTransmissionResults = try await uploadPolygons( accessibilityFeatures: accessibilityFeatures, + mappingData: mappingData, inputs: inputs ) } @@ -112,6 +116,7 @@ class APITransmissionController: ObservableObject { extension APITransmissionController { func uploadPoints( accessibilityFeatures: [any AccessibilityFeatureProtocol], + mappingData: MappingData, inputs: APITransmissionInputs ) async throws -> APITransmissionResults { let accessibilityFeatures = accessibilityFeatures @@ -120,7 +125,7 @@ extension APITransmissionController { let featureCache: APIFeatureCache = APIFeatureCache() let additionalTags: [String: String] = getAdditionalTags( accessibilityFeatureClass: inputs.accessibilityFeatureClass, - captureData: inputs.captureData, mappingData: inputs.mappingData + captureData: inputs.captureData, mappingData: mappingData ) for feature in accessibilityFeatures { let oswElement = featureToPoint(feature, additionalTags: additionalTags) @@ -230,6 +235,7 @@ extension APITransmissionController { extension APITransmissionController { func uploadLineStrings( accessibilityFeatures: [any AccessibilityFeatureProtocol], + mappingData: MappingData, inputs: APITransmissionInputs ) async throws -> APITransmissionResults { var accessibilityFeatures = accessibilityFeatures @@ -246,7 +252,7 @@ extension APITransmissionController { let featureCache: APIFeatureCache = APIFeatureCache() let additionalTags: [String: String] = getAdditionalTags( accessibilityFeatureClass: inputs.accessibilityFeatureClass, - captureData: inputs.captureData, mappingData: inputs.mappingData + captureData: inputs.captureData, mappingData: mappingData ) for feature in accessibilityFeatures { let oswElement = featureToLineString(feature, additionalTags: additionalTags) @@ -258,7 +264,7 @@ extension APITransmissionController { var uploadOperations: [ChangesetDiffOperation] = featureCache.getOSWElements().map { .create($0) } /// For the sidewalk class, get the previously uploaded linestring, connect it to the new linestring, and add a modify operation if inputs.accessibilityFeatureClass.oswPolicy.oswElementClass == .Sidewalk, - let existingMappedFeature = inputs.mappingData.featuresMap[inputs.accessibilityFeatureClass]?.last { + let existingMappedFeature = mappingData.featuresMap[inputs.accessibilityFeatureClass]?.last { let existingOSWElement = existingMappedFeature.oswElement if var existingOSWLineString = existingOSWElement as? OSWLineString, let newOSWLineString = featureCache.getOSWLineStrings().first, @@ -406,6 +412,7 @@ extension APITransmissionController { extension APITransmissionController { func uploadPolygons( accessibilityFeatures: [any AccessibilityFeatureProtocol], + mappingData: MappingData, inputs: APITransmissionInputs ) async throws -> APITransmissionResults { let accessibilityFeatures = accessibilityFeatures @@ -414,7 +421,7 @@ extension APITransmissionController { let featureCache: APIFeatureCache = APIFeatureCache() let additionalTags: [String: String] = getAdditionalTags( accessibilityFeatureClass: inputs.accessibilityFeatureClass, - captureData: inputs.captureData, mappingData: inputs.mappingData + captureData: inputs.captureData, mappingData: mappingData ) for feature in accessibilityFeatures { let oswElement = featureToPolygon(feature, additonalTags: additionalTags) diff --git a/IOSAccessAssessment/TDEI/Transmission/APITransmissionHelpers.swift b/IOSAccessAssessment/TDEI/Transmission/APITransmissionHelpers.swift index d1ba381e..b44851d9 100644 --- a/IOSAccessAssessment/TDEI/Transmission/APITransmissionHelpers.swift +++ b/IOSAccessAssessment/TDEI/Transmission/APITransmissionHelpers.swift @@ -73,7 +73,6 @@ struct APITransmissionInputs { let accessibilityFeatureClass: AccessibilityFeatureClass let captureData: CaptureData let captureLocation: CLLocationCoordinate2D - let mappingData: MappingData let accessToken: String let environment: APIEnvironment? } diff --git a/IOSAccessAssessment/View/ARCameraView.swift b/IOSAccessAssessment/View/ARCameraView.swift index 152c55e2..373f4843 100644 --- a/IOSAccessAssessment/View/ARCameraView.swift +++ b/IOSAccessAssessment/View/ARCameraView.swift @@ -97,7 +97,7 @@ struct ARCameraView: View { @StateObject private var managerConfigureStatusViewModel = ARCameraManagerStatusViewModel() @State private var cameraHintText: String = ARCameraViewConstants.Texts.cameraHintPlaceholderText - var locationManager: LocationManager = LocationManager() + @StateObject private var locationManager: LocationManager = LocationManager() @State private var captureLocation: CLLocationCoordinate2D? @State private var captureHeading: CLLocationDirection? @@ -155,6 +155,7 @@ struct ARCameraView: View { } .navigationBarTitle(ARCameraViewConstants.Texts.contentViewTitle, displayMode: .inline) .onAppear { + locationManager.startLocationUpdates() showAnnotationView = false segmentationPipeline.setSelectedClasses(selectedClasses) do { @@ -169,6 +170,15 @@ struct ARCameraView: View { } } .onDisappear { + locationManager.stopLocationUpdates() + print("ARCameraView disappeared, stopping location updates.") + Task { + do { + try manager.pause() + } catch { + print("Error pausing ARCameraManager: \(error)") + } + } } .alert(ARCameraViewConstants.Texts.managerStatusAlertTitleKey, isPresented: $managerConfigureStatusViewModel.isFailed, actions: { Button(ARCameraViewConstants.Texts.managerStatusAlertDismissButtonKey) { @@ -206,6 +216,9 @@ struct ARCameraView: View { } } } + .onChange(of: manager.interfaceOrientation) { oldOrientation, newOrientation in + locationManager.updateOrientation(newOrientation) + } .sheet(isPresented: $showARCameraLearnMoreSheet) { ARCameraLearnMoreSheetView() .presentationDetents([.medium, .large]) diff --git a/IOSAccessAssessment/View/AnnotationView.swift b/IOSAccessAssessment/View/AnnotationView.swift index c3885be5..b4dcfe55 100644 --- a/IOSAccessAssessment/View/AnnotationView.swift +++ b/IOSAccessAssessment/View/AnnotationView.swift @@ -664,12 +664,12 @@ struct AnnotationView: View { accessibilityFeatureClass: accessibilityFeatureClass, captureData: currentCaptureDataRecord, captureLocation: captureLocation, - mappingData: sharedAppData.mappingData, accessToken: accessToken, environment: userStateViewModel.selectedEnvironment ) let apiTransmissionResults = try await apiTransmissionController.uploadFeatures( accessibilityFeatures: featuresToUpload, + mappingData: sharedAppData.mappingData, inputs: apiTransmissionInputs ) guard let mappedAccessibilityFeatures = apiTransmissionResults.accessibilityFeatures else { From 9a39dfe2f96a04ce8bff85105cd935a63ed4a872 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Sat, 28 Mar 2026 23:24:14 -0700 Subject: [PATCH 2/9] Start adding get map feature --- IOSAccessAssessment.xcodeproj/project.pbxproj | 4 ++ .../Localization/LocationManager.swift | 4 ++ .../Shared/Utils/Extensions.swift | 14 +++++++ .../TDEI/Config/APIEndpoint.swift | 5 +++ IOSAccessAssessment/TDEI/OSM/OSMElement.swift | 1 + .../TDEI/Services/WorkspaceService.swift | 40 ++++++++++++++++--- IOSAccessAssessment/View/ARCameraView.swift | 2 + 7 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 IOSAccessAssessment/Shared/Utils/Extensions.swift diff --git a/IOSAccessAssessment.xcodeproj/project.pbxproj b/IOSAccessAssessment.xcodeproj/project.pbxproj index 4d1c4ceb..909fbdba 100644 --- a/IOSAccessAssessment.xcodeproj/project.pbxproj +++ b/IOSAccessAssessment.xcodeproj/project.pbxproj @@ -109,6 +109,7 @@ A3B2DDBF2DC99DEF003416FB /* HomographyRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B2DDBE2DC99DE9003416FB /* HomographyRequestProcessor.swift */; }; A3B2DDC12DC99F44003416FB /* SegmentationModelRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B2DDC02DC99F3D003416FB /* SegmentationModelRequestProcessor.swift */; }; A3B61FC52F76480B0052AE2C /* EnvironmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B61FC42F7647FC0052AE2C /* EnvironmentService.swift */; }; + A3B61FC92F78F93B0052AE2C /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B61FC82F78F9390052AE2C /* Extensions.swift */; }; A3BB5AFB2DB210AE008673ED /* BinaryMaskFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BB5AFA2DB210A8008673ED /* BinaryMaskFilter.swift */; }; A3BCBC502EFBB92900D15E15 /* AccessibilityFeatureEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BCBC4F2EFBB92500D15E15 /* AccessibilityFeatureEncoder.swift */; }; A3C22FD32CF194A600533BF7 /* CGImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C22FD22CF194A200533BF7 /* CGImageUtils.swift */; }; @@ -314,6 +315,7 @@ A3B2DDBE2DC99DE9003416FB /* HomographyRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomographyRequestProcessor.swift; sourceTree = ""; }; A3B2DDC02DC99F3D003416FB /* SegmentationModelRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentationModelRequestProcessor.swift; sourceTree = ""; }; A3B61FC42F7647FC0052AE2C /* EnvironmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentService.swift; sourceTree = ""; }; + A3B61FC82F78F9390052AE2C /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; A3BB5AFA2DB210A8008673ED /* BinaryMaskFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryMaskFilter.swift; sourceTree = ""; }; A3BCBC4F2EFBB92500D15E15 /* AccessibilityFeatureEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityFeatureEncoder.swift; sourceTree = ""; }; A3C22FD22CF194A200533BF7 /* CGImageUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGImageUtils.swift; sourceTree = ""; }; @@ -700,6 +702,7 @@ A35547C02EC1AE4600F43AFD /* Utils */ = { isa = PBXGroup; children = ( + A3B61FC82F78F9390052AE2C /* Extensions.swift */, A35547C12EC1AE4C00F43AFD /* SafeDeque.swift */, A3DA4DB02EB99A5A005BB812 /* MetalBufferUtils.swift */, ); @@ -1424,6 +1427,7 @@ A30BED3A2ED162F1004A5B51 /* ConnectedComponents.swift in Sources */, A32943462EE7C07E00C4C1BC /* OSWGeometry.swift in Sources */, A35547CA2EC2045F00F43AFD /* CapturedMeshSnapshot.swift in Sources */, + A3B61FC92F78F93B0052AE2C /* Extensions.swift in Sources */, DAA7F8B52CA38C11003666D8 /* Constants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift b/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift index a2574ce5..678520b3 100644 --- a/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift +++ b/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift @@ -14,6 +14,10 @@ struct BBox { let maxLat: Double let minLon: Double let maxLon: Double + + func toQueryString() -> String { + return "\(minLat.roundedTo7Digits()),\(minLon.roundedTo7Digits()),\(maxLat.roundedTo7Digits()),\(maxLon.roundedTo7Digits())" + } } class LocationHelpers { diff --git a/IOSAccessAssessment/Shared/Utils/Extensions.swift b/IOSAccessAssessment/Shared/Utils/Extensions.swift new file mode 100644 index 00000000..0c8646fb --- /dev/null +++ b/IOSAccessAssessment/Shared/Utils/Extensions.swift @@ -0,0 +1,14 @@ +// +// Extensions.swift +// IOSAccessAssessment +// +// Created by Himanshu on 3/28/26. +// + +import Foundation + +extension Double { + func roundedTo7Digits() -> Double { + (self * 1_000_0000).rounded() / 1_000_0000 + } +} diff --git a/IOSAccessAssessment/TDEI/Config/APIEndpoint.swift b/IOSAccessAssessment/TDEI/Config/APIEndpoint.swift index 1f39f28c..7dba8a50 100644 --- a/IOSAccessAssessment/TDEI/Config/APIEndpoint.swift +++ b/IOSAccessAssessment/TDEI/Config/APIEndpoint.swift @@ -31,6 +31,11 @@ struct APIEndpoint { return baseURL?.appending(path: "workspaces/mine") } + static let getMapData = { (environment: APIEnvironment) in + let baseURL = URL(string: environment.osmBaseURL) + return baseURL?.appending(path: "map.json") + } + static let createChangeset = { (environment: APIEnvironment) in let baseURL = URL(string: environment.osmBaseURL) return baseURL?.appending(path: "changeset/create") diff --git a/IOSAccessAssessment/TDEI/OSM/OSMElement.swift b/IOSAccessAssessment/TDEI/OSM/OSMElement.swift index e83980d6..3a28caf0 100644 --- a/IOSAccessAssessment/TDEI/OSM/OSMElement.swift +++ b/IOSAccessAssessment/TDEI/OSM/OSMElement.swift @@ -8,6 +8,7 @@ protocol OSMElement: Sendable, Equatable { var id: String { get } var version: String { get } + var tags: [String: String] { get } func toOSMCreateXML(changesetId: String) -> String func toOSMModifyXML(changesetId: String) -> String diff --git a/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift b/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift index 517589e8..3b45d7a3 100644 --- a/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift +++ b/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift @@ -60,16 +60,16 @@ class WorkspaceService { private init() {} func fetchWorkspaces( - location: CLLocationCoordinate2D?, radius: Int = 2000, + location: CLLocationCoordinate2D?, radius: Double = 2000, accessToken: String, environment: APIEnvironment? = nil ) async throws -> [Workspace] { let selectedEnvironment = environment ?? EnvironmentService.shared.environment - guard let url = APIEndpoint.getWorkspaces(selectedEnvironment) + guard let urlEndpoint = APIEndpoint.getWorkspaces(selectedEnvironment) else { throw APIError.invalidURL } - var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) + var comps = URLComponents(url: urlEndpoint, resolvingAgainstBaseURL: false) comps?.queryItems = [ URLQueryItem(name: "radius", value: "\(radius)") ] @@ -106,9 +106,39 @@ class WorkspaceService { } } - func fetchOSMElements( - + func fetchMapData( + workspaceId: Int, + location: CLLocationCoordinate2D, radius: Double = 1000, + accessToken: String, + environment: APIEnvironment? = nil ) async throws { + let selectedEnvironment = environment ?? EnvironmentService.shared.environment + guard let urlEndpoint = APIEndpoint.getMapData(selectedEnvironment) else { + throw APIError.invalidURL + } + let bbox: BBox = LocationHelpers.boundingBoxAroundLocation(location: location, radius: radius) + + var comps = URLComponents(url: urlEndpoint, resolvingAgainstBaseURL: false) + comps?.queryItems = [ + URLQueryItem(name: "bbox", value: bbox.toQueryString()) + ] + guard let url = comps?.url else { throw APIError.invalidURL } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("\(workspaceId)", forHTTPHeaderField: "X-Workspace") + request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { + throw APIError.invalidResponse + } + + guard (200...299).contains(httpResponse.statusCode) else { + throw APIError.badStatus(httpResponse.statusCode) + } +// do { +// } } diff --git a/IOSAccessAssessment/View/ARCameraView.swift b/IOSAccessAssessment/View/ARCameraView.swift index 373f4843..ffadd1cf 100644 --- a/IOSAccessAssessment/View/ARCameraView.swift +++ b/IOSAccessAssessment/View/ARCameraView.swift @@ -219,6 +219,8 @@ struct ARCameraView: View { .onChange(of: manager.interfaceOrientation) { oldOrientation, newOrientation in locationManager.updateOrientation(newOrientation) } + .onChange(of: locationManager.currentLocation) { oldLocation, newLocation in + } .sheet(isPresented: $showARCameraLearnMoreSheet) { ARCameraLearnMoreSheetView() .presentationDetents([.medium, .large]) From b727c769e1634d914d6f130f6771fc7f78f2d8a8 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Sun, 29 Mar 2026 00:07:09 -0700 Subject: [PATCH 3/9] Naming convention changes and addition of OSMElementType --- IOSAccessAssessment.xcodeproj/project.pbxproj | 20 +++-- ...> OSMChangesetUploadResponseElement.swift} | 30 +++++-- IOSAccessAssessment/TDEI/OSM/OSMElement.swift | 8 +- IOSAccessAssessment/TDEI/OSM/OSMMapData.swift | 69 +++++++++++++++ .../TDEI/OSM/OSMRelation.swift | 14 +-- IOSAccessAssessment/TDEI/OSW/OSWElement.swift | 2 +- .../TDEI/OSW/OSWLineString.swift | 2 +- IOSAccessAssessment/TDEI/OSW/OSWPoint.swift | 2 +- IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift | 4 +- .../TDEI/Services/ChangesetService.swift | 26 +----- ...ift => APIChangesetUploadController.swift} | 86 +++++++++---------- .../Transmission/APITransmissionHelpers.swift | 18 ++-- IOSAccessAssessment/View/ARCameraView.swift | 4 +- IOSAccessAssessment/View/AnnotationView.swift | 60 ++++++------- .../View/TestMode/TestCameraView.swift | 4 +- 15 files changed, 208 insertions(+), 141 deletions(-) rename IOSAccessAssessment/TDEI/OSM/{OSMResponseElement.swift => OSMChangesetUploadResponseElement.swift} (52%) create mode 100644 IOSAccessAssessment/TDEI/OSM/OSMMapData.swift rename IOSAccessAssessment/TDEI/Transmission/{APITransmissionController.swift => APIChangesetUploadController.swift} (90%) diff --git a/IOSAccessAssessment.xcodeproj/project.pbxproj b/IOSAccessAssessment.xcodeproj/project.pbxproj index 909fbdba..f4d91577 100644 --- a/IOSAccessAssessment.xcodeproj/project.pbxproj +++ b/IOSAccessAssessment.xcodeproj/project.pbxproj @@ -65,7 +65,7 @@ A35E050A2EDE299F003C26CF /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35E05092EDE2999003C26CF /* LocationManager.swift */; }; A35E050D2EDE35E1003C26CF /* LocalizationProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35E050C2EDE35DE003C26CF /* LocalizationProcessor.swift */; }; A35E05102EDE60C0003C26CF /* InvalidContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35E050F2EDE60BC003C26CF /* InvalidContentView.swift */; }; - A35E05162EDEA050003C26CF /* APITransmissionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35E05152EDEA04B003C26CF /* APITransmissionController.swift */; }; + A35E05162EDEA050003C26CF /* APIChangesetUploadController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35E05152EDEA04B003C26CF /* APIChangesetUploadController.swift */; }; A35E05182EDEA476003C26CF /* AttributeEstimationPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35E05172EDEA470003C26CF /* AttributeEstimationPipeline.swift */; }; A35E051A2EDFB017003C26CF /* OSMPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35E05192EDFB015003C26CF /* OSMPayload.swift */; }; A35E051C2EDFB094003C26CF /* OSMNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35E051B2EDFB093003C26CF /* OSMNode.swift */; }; @@ -76,7 +76,7 @@ A364B5DD2F259AFE00325E5C /* WorldPoints.metal in Sources */ = {isa = PBXBuildFile; fileRef = A364B5DC2F259AF900325E5C /* WorldPoints.metal */; }; A364B5DF2F26DB5700325E5C /* WorldPointsProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A364B5DE2F26DB5300325E5C /* WorldPointsProcessor.swift */; }; A36C6E022E134CE600A86004 /* bisenetv2_35_640_640.mlpackage in Sources */ = {isa = PBXBuildFile; fileRef = A36C6E012E134CE600A86004 /* bisenetv2_35_640_640.mlpackage */; }; - A374FAB72EE0173600055268 /* OSMResponseElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A374FAB62EE0173200055268 /* OSMResponseElement.swift */; }; + A374FAB72EE0173600055268 /* OSMChangesetUploadResponseElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A374FAB62EE0173200055268 /* OSMChangesetUploadResponseElement.swift */; }; A37C3C182F3141FF001F4248 /* Plane.metal in Sources */ = {isa = PBXBuildFile; fileRef = A37C3C172F3141F9001F4248 /* Plane.metal */; }; A37C3C1A2F3144F7001F4248 /* PlaneAttributeProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A37C3C192F3144F4001F4248 /* PlaneAttributeProcessor.swift */; }; A37C3C1C2F356254001F4248 /* IntersectionFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A37C3C1B2F356254001F4248 /* IntersectionFilter.swift */; }; @@ -110,6 +110,7 @@ A3B2DDC12DC99F44003416FB /* SegmentationModelRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B2DDC02DC99F3D003416FB /* SegmentationModelRequestProcessor.swift */; }; A3B61FC52F76480B0052AE2C /* EnvironmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B61FC42F7647FC0052AE2C /* EnvironmentService.swift */; }; A3B61FC92F78F93B0052AE2C /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B61FC82F78F9390052AE2C /* Extensions.swift */; }; + A3B61FCB2F79036A0052AE2C /* OSMMapData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B61FCA2F7903660052AE2C /* OSMMapData.swift */; }; A3BB5AFB2DB210AE008673ED /* BinaryMaskFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BB5AFA2DB210A8008673ED /* BinaryMaskFilter.swift */; }; A3BCBC502EFBB92900D15E15 /* AccessibilityFeatureEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BCBC4F2EFBB92500D15E15 /* AccessibilityFeatureEncoder.swift */; }; A3C22FD32CF194A600533BF7 /* CGImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C22FD22CF194A200533BF7 /* CGImageUtils.swift */; }; @@ -269,7 +270,7 @@ A35E05092EDE2999003C26CF /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; A35E050C2EDE35DE003C26CF /* LocalizationProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationProcessor.swift; sourceTree = ""; }; A35E050F2EDE60BC003C26CF /* InvalidContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvalidContentView.swift; sourceTree = ""; }; - A35E05152EDEA04B003C26CF /* APITransmissionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITransmissionController.swift; sourceTree = ""; }; + A35E05152EDEA04B003C26CF /* APIChangesetUploadController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIChangesetUploadController.swift; sourceTree = ""; }; A35E05172EDEA470003C26CF /* AttributeEstimationPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributeEstimationPipeline.swift; sourceTree = ""; }; A35E05192EDFB015003C26CF /* OSMPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMPayload.swift; sourceTree = ""; }; A35E051B2EDFB093003C26CF /* OSMNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMNode.swift; sourceTree = ""; }; @@ -280,7 +281,7 @@ A364B5DC2F259AF900325E5C /* WorldPoints.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = WorldPoints.metal; sourceTree = ""; }; A364B5DE2F26DB5300325E5C /* WorldPointsProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldPointsProcessor.swift; sourceTree = ""; }; A36C6E012E134CE600A86004 /* bisenetv2_35_640_640.mlpackage */ = {isa = PBXFileReference; lastKnownFileType = folder.mlpackage; path = bisenetv2_35_640_640.mlpackage; sourceTree = ""; }; - A374FAB62EE0173200055268 /* OSMResponseElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMResponseElement.swift; sourceTree = ""; }; + A374FAB62EE0173200055268 /* OSMChangesetUploadResponseElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMChangesetUploadResponseElement.swift; sourceTree = ""; }; A37C3C172F3141F9001F4248 /* Plane.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Plane.metal; sourceTree = ""; }; A37C3C192F3144F4001F4248 /* PlaneAttributeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaneAttributeProcessor.swift; sourceTree = ""; }; A37C3C1B2F356254001F4248 /* IntersectionFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntersectionFilter.swift; sourceTree = ""; }; @@ -316,6 +317,7 @@ A3B2DDC02DC99F3D003416FB /* SegmentationModelRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentationModelRequestProcessor.swift; sourceTree = ""; }; A3B61FC42F7647FC0052AE2C /* EnvironmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentService.swift; sourceTree = ""; }; A3B61FC82F78F9390052AE2C /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + A3B61FCA2F7903660052AE2C /* OSMMapData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMMapData.swift; sourceTree = ""; }; A3BB5AFA2DB210A8008673ED /* BinaryMaskFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryMaskFilter.swift; sourceTree = ""; }; A3BCBC4F2EFBB92500D15E15 /* AccessibilityFeatureEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityFeatureEncoder.swift; sourceTree = ""; }; A3C22FD22CF194A200533BF7 /* CGImageUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGImageUtils.swift; sourceTree = ""; }; @@ -751,7 +753,7 @@ A35E05142EDE7494003C26CF /* Transmission */ = { isa = PBXGroup; children = ( - A35E05152EDEA04B003C26CF /* APITransmissionController.swift */, + A35E05152EDEA04B003C26CF /* APIChangesetUploadController.swift */, A3EE6E512F5F9F1100F515E6 /* APITransmissionHelpers.swift */, ); path = Transmission; @@ -761,12 +763,13 @@ isa = PBXGroup; children = ( A39C9F3A2DD9B03000455E45 /* OSMElement.swift */, - A374FAB62EE0173200055268 /* OSMResponseElement.swift */, A35E05192EDFB015003C26CF /* OSMPayload.swift */, A35E051B2EDFB093003C26CF /* OSMNode.swift */, A35E051D2EDFB099003C26CF /* OSMWay.swift */, A329434F2EE80EC200C4C1BC /* OSMRelation.swift */, A3EE6EFF2F6A29F300F515E6 /* OSMLocation.swift */, + A374FAB62EE0173200055268 /* OSMChangesetUploadResponseElement.swift */, + A3B61FCA2F7903660052AE2C /* OSMMapData.swift */, ); path = OSM; sourceTree = ""; @@ -1304,13 +1307,14 @@ A35E051A2EDFB017003C26CF /* OSMPayload.swift in Sources */, A30801612EC09BB700B1BA3A /* CocoCustom53ClassConfig.swift in Sources */, A30BED382ED162E7004A5B51 /* MeshDefinitions.swift in Sources */, - A374FAB72EE0173600055268 /* OSMResponseElement.swift in Sources */, + A374FAB72EE0173600055268 /* OSMChangesetUploadResponseElement.swift in Sources */, A35547C22EC1AE4E00F43AFD /* SafeDeque.swift in Sources */, A3C55A492EAFFABF00F6CFDC /* CenterCropTransformUtilsExtension.swift in Sources */, A3EE6E4A2F580D6200F515E6 /* TestCameraView.swift in Sources */, A329433C2EE7BEE100C4C1BC /* OSWPolicy.swift in Sources */, A38338BF2EDA889C00F1A402 /* CustomPicker.swift in Sources */, A32943592EE8204400C4C1BC /* OSWPolygon.swift in Sources */, + A3B61FCB2F79036A0052AE2C /* OSMMapData.swift in Sources */, CAF812BC2CF78F8100D44B84 /* NetworkError.swift in Sources */, A305B06C2E18A85F00ECCF9B /* DepthCoder.swift in Sources */, A3DA4DBC2EBCB881005BB812 /* SegmentationMeshRecord.swift in Sources */, @@ -1318,7 +1322,7 @@ A3FFAA7E2DE3E41D002B99BD /* SegmentationARPipeline.swift in Sources */, A30BED3C2ED2F48B004A5B51 /* MeshClusteringUtils.swift in Sources */, A308016C2EC15CC400B1BA3A /* AccessibilityFeatureAttribute.swift in Sources */, - A35E05162EDEA050003C26CF /* APITransmissionController.swift in Sources */, + A35E05162EDEA050003C26CF /* APIChangesetUploadController.swift in Sources */, A36C6E022E134CE600A86004 /* bisenetv2_35_640_640.mlpackage in Sources */, A35BB2862DC30386009A3FE0 /* CameraOrientation.swift in Sources */, A35547CC2EC3018E00F43AFD /* AnnotationView.swift in Sources */, diff --git a/IOSAccessAssessment/TDEI/OSM/OSMResponseElement.swift b/IOSAccessAssessment/TDEI/OSM/OSMChangesetUploadResponseElement.swift similarity index 52% rename from IOSAccessAssessment/TDEI/OSM/OSMResponseElement.swift rename to IOSAccessAssessment/TDEI/OSM/OSMChangesetUploadResponseElement.swift index d85d0b7b..5907118d 100644 --- a/IOSAccessAssessment/TDEI/OSM/OSMResponseElement.swift +++ b/IOSAccessAssessment/TDEI/OSM/OSMChangesetUploadResponseElement.swift @@ -1,5 +1,5 @@ // -// OSMResponseElement.swift +// OSMChangesetUploadResponseElement.swift // IOSAccessAssessment // // Created by Himanshu on 12/2/25. @@ -7,13 +7,13 @@ import Foundation -protocol OSMResponseElement: Sendable, Equatable, Hashable { +protocol OSMChangesetUploadResponseElement: Sendable, Equatable, Hashable { var oldId: String { get } var newId: String { get } var newVersion: String { get } } -struct OSMResponseNode: OSMResponseElement { +struct OSMChangesetUploadResponseNode: OSMChangesetUploadResponseElement { let oldId: String let newId: String let newVersion: String @@ -28,7 +28,7 @@ struct OSMResponseNode: OSMResponseElement { } } -struct OSMResponseWay: OSMResponseElement { +struct OSMChangesetUploadResponseWay: OSMChangesetUploadResponseElement { let oldId: String let newId: String let newVersion: String @@ -43,7 +43,7 @@ struct OSMResponseWay: OSMResponseElement { } } -struct OSMResponseRelation: OSMResponseElement { +struct OSMChangesetUploadResponseRelation: OSMChangesetUploadResponseElement { let oldId: String let newId: String let newVersion: String @@ -58,4 +58,22 @@ struct OSMResponseRelation: OSMResponseElement { } } - +struct OSMChangesetUploadResponseElements: Sendable { + let nodes: [String: OSMChangesetUploadResponseNode] + let ways: [String: OSMChangesetUploadResponseWay] + let relations: [String: OSMChangesetUploadResponseRelation] + + var oldToNewIdMap: [String: String] { + let nodeMap = nodes.reduce(into: [String: String]()) { dict, pair in + dict[pair.value.oldId] = pair.value.newId + } + let wayMap = ways.reduce(into: [String: String]()) { dict, pair in + dict[pair.value.oldId] = pair.value.newId + } + let relationMap = relations.reduce(into: [String: String]()) { dict, pair in + dict[pair.value.oldId] = pair.value.newId + } + return nodeMap.merging(wayMap) { _, new in new } + .merging(relationMap) { _, new in new } + } +} diff --git a/IOSAccessAssessment/TDEI/OSM/OSMElement.swift b/IOSAccessAssessment/TDEI/OSM/OSMElement.swift index 3a28caf0..f8dfec22 100644 --- a/IOSAccessAssessment/TDEI/OSM/OSMElement.swift +++ b/IOSAccessAssessment/TDEI/OSM/OSMElement.swift @@ -5,7 +5,7 @@ // Created by Himanshu on 5/17/25. // -protocol OSMElement: Sendable, Equatable { +protocol OSMElement: Sendable, Equatable, Codable { var id: String { get } var version: String { get } var tags: [String: String] { get } @@ -14,3 +14,9 @@ protocol OSMElement: Sendable, Equatable { func toOSMModifyXML(changesetId: String) -> String func toOSMDeleteXML(changesetId: String) -> String } + +enum OSMElementType: String, Codable { + case node = "node" + case way = "way" + case relation = "relation" +} diff --git a/IOSAccessAssessment/TDEI/OSM/OSMMapData.swift b/IOSAccessAssessment/TDEI/OSM/OSMMapData.swift new file mode 100644 index 00000000..1a7d14a0 --- /dev/null +++ b/IOSAccessAssessment/TDEI/OSM/OSMMapData.swift @@ -0,0 +1,69 @@ +// +// OSMMapDataResponse.swift +// IOSAccessAssessment +// +// Created by Himanshu on 3/28/26. +// + +import Foundation + +struct OSMMapDataResponse: Codable { + public let version, generator, copyright: String + public let attribution, license: String + public let bounds: Bounds +} + +struct Bounds: Codable { // use this for bounds in the map + public let minlat, minlon, maxlat, maxlon: Double +} + +struct OSMMapDataResponseElement: Codable { + public var isInteresting: Bool? = false + public var isSkippable: Bool? = false + public let type: OSMElementType + public let id: Int + public let lat, lon: Double? + public let timestamp: Date + public var version, changeset: Int + public let user: String + public let uid: Int + public let tags: [String: String] + public let nodes: [Int]? + public let members: [OSMRelationMember]? + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + isInteresting = try values.decodeIfPresent(Bool.self, forKey: .isInteresting) ?? false + isSkippable = try values.decodeIfPresent(Bool.self, forKey: .isSkippable) ?? false + id = try values.decodeIfPresent(Int.self, forKey: .id) ?? 0 + lat = try values.decodeIfPresent(Double.self, forKey: .lat) ?? 0.0 + lon = try values.decodeIfPresent(Double.self, forKey: .lon) ?? 0.0 + timestamp = try values.decodeIfPresent(Date.self, forKey: .timestamp) ?? Date() + version = try values.decodeIfPresent(Int.self, forKey: .version) ?? 0 + changeset = try values.decodeIfPresent(Int.self, forKey: .changeset) ?? 0 + user = try values.decodeIfPresent(String.self, forKey: .user) ?? "" + uid = try values.decodeIfPresent(Int.self, forKey: .uid) ?? 0 + tags = try values.decodeIfPresent([String: String].self, forKey: .tags) ?? [:] + nodes = try values.decodeIfPresent([Int].self, forKey: .nodes) ?? [] + type = try values.decodeIfPresent(OSMElementType.self, forKey: .type) ?? TypeEnum.node + members = try values.decodeIfPresent([OSMRelationMember].self, forKey: .members) ?? [] + } + +// func toOSMElement() -> OSMElement? { +// switch type { +// case .node: +// return toOSMNode() +// case .way: +// return toOSMWay() +// case .relation: +// return nil +// } +// } +// +// private func toOSMNode() -> OSMNode { +// OSMNode(type: "node", id: id, lat: lat!, lon: lon!, timestamp: timestamp, version: version, changeset: changeset, user: user, uid: uid,tags: tags) +// } +// private func toOSMWay() -> OSMWay { +// OSMWay(type: "way", id: id, timestamp: timestamp, version: version, changeset: changeset, user: user, uid: uid, nodes: nodes ?? [], tags: tags) +// } +} diff --git a/IOSAccessAssessment/TDEI/OSM/OSMRelation.swift b/IOSAccessAssessment/TDEI/OSM/OSMRelation.swift index 059b7a9a..7c2d7462 100644 --- a/IOSAccessAssessment/TDEI/OSM/OSMRelation.swift +++ b/IOSAccessAssessment/TDEI/OSM/OSMRelation.swift @@ -7,18 +7,8 @@ import Foundation -enum OSMRelationMemberType: String, Sendable { - case node - case way - case relation - - var description: String { - return self.rawValue - } -} - -struct OSMRelationMember: Sendable, Equatable, Hashable { - let type: OSMRelationMemberType +struct OSMRelationMember: Sendable, Equatable, Hashable, Codable { + let type: OSMElementType let ref: String let role: String diff --git a/IOSAccessAssessment/TDEI/OSW/OSWElement.swift b/IOSAccessAssessment/TDEI/OSW/OSWElement.swift index d11b4c92..7e9dabe9 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWElement.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWElement.swift @@ -8,7 +8,7 @@ import Foundation import CoreLocation protocol OSWElement: Sendable, CustomStringConvertible { - var elementOSMString: String { get } + var osmElementType: OSMElementType { get } var id: String { get } var version: String { get } diff --git a/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift b/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift index 5d59d7cc..b636cd4c 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift @@ -9,7 +9,7 @@ import Foundation import CoreLocation struct OSWLineString: OSWElement { - let elementOSMString: String = "way" + let osmElementType: OSMElementType = .way let id: String let version: String diff --git a/IOSAccessAssessment/TDEI/OSW/OSWPoint.swift b/IOSAccessAssessment/TDEI/OSW/OSWPoint.swift index cd80e458..35eb77b3 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWPoint.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWPoint.swift @@ -9,7 +9,7 @@ import Foundation import CoreLocation struct OSWPoint: OSWElement { - let elementOSMString: String = "node" + let osmElementType: OSMElementType = .node let id: String let version: String diff --git a/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift b/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift index 0ea8a947..e85fb2ff 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift @@ -23,12 +23,12 @@ struct OSWRelationMember: Sendable { } var toXML: String { - return "" + return "" } } struct OSWPolygon: OSWElement { - let elementOSMString: String = "relation" + let osmElementType: OSMElementType = .relation let id: String let version: String diff --git a/IOSAccessAssessment/TDEI/Services/ChangesetService.swift b/IOSAccessAssessment/TDEI/Services/ChangesetService.swift index 15104727..10f58031 100644 --- a/IOSAccessAssessment/TDEI/Services/ChangesetService.swift +++ b/IOSAccessAssessment/TDEI/Services/ChangesetService.swift @@ -7,26 +7,6 @@ import Foundation -struct UploadedOSMResponseElements: Sendable { - let nodes: [String: OSMResponseNode] - let ways: [String: OSMResponseWay] - let relations: [String: OSMResponseRelation] - - var oldToNewIdMap: [String: String] { - let nodeMap = nodes.reduce(into: [String: String]()) { dict, pair in - dict[pair.value.oldId] = pair.value.newId - } - let wayMap = ways.reduce(into: [String: String]()) { dict, pair in - dict[pair.value.oldId] = pair.value.newId - } - let relationMap = relations.reduce(into: [String: String]()) { dict, pair in - dict[pair.value.oldId] = pair.value.newId - } - return nodeMap.merging(wayMap) { _, new in new } - .merging(relationMap) { _, new in new } - } -} - enum ChangesetDiffOperation { case create(any OSWElement) case modify(any OSWElement) @@ -108,7 +88,7 @@ class ChangesetService { operations: [ChangesetDiffOperation], accessToken: String, environment: APIEnvironment? = nil, - completion: @escaping (Result) -> Void + completion: @escaping (Result) -> Void ) { let selectedEnvironment = environment ?? EnvironmentService.shared.environment guard let url = APIEndpoint.uploadChanges(selectedEnvironment, changesetId) else { @@ -165,7 +145,7 @@ class ChangesetService { // print("Parsed Ways: ", parser.waysWithAttributes) // print("Parsed Relations: ", parser.relationsWithAttributes) - completion(.success(UploadedOSMResponseElements( + completion(.success(OSMChangesetUploadResponseElements( nodes: parser.nodesWithAttributes, ways: parser.waysWithAttributes, relations: parser.relationsWithAttributes @@ -244,7 +224,7 @@ extension ChangesetService { operations: [ChangesetDiffOperation], accessToken: String, environment: APIEnvironment? = nil - ) async throws -> UploadedOSMResponseElements { + ) async throws -> OSMChangesetUploadResponseElements { return try await withCheckedThrowingContinuation { continuation in performUpload( workspaceId: workspaceId, changesetId: changesetId, operations: operations, diff --git a/IOSAccessAssessment/TDEI/Transmission/APITransmissionController.swift b/IOSAccessAssessment/TDEI/Transmission/APIChangesetUploadController.swift similarity index 90% rename from IOSAccessAssessment/TDEI/Transmission/APITransmissionController.swift rename to IOSAccessAssessment/TDEI/Transmission/APIChangesetUploadController.swift index 343d2ad3..2809f7c2 100644 --- a/IOSAccessAssessment/TDEI/Transmission/APITransmissionController.swift +++ b/IOSAccessAssessment/TDEI/Transmission/APIChangesetUploadController.swift @@ -1,5 +1,5 @@ // -// APITransmissionController.swift +// APIChangesetUploadController.swift // IOSAccessAssessment // // Created by Himanshu on 12/1/25. @@ -8,7 +8,7 @@ import SwiftUI import CoreLocation -enum APITransmissionError: Error, LocalizedError { +enum APIChangesetUploadError: Error, LocalizedError { case featureClassNotLineString(AccessibilityFeatureClass) case featureClassNotPolygon(AccessibilityFeatureClass) @@ -22,15 +22,15 @@ enum APITransmissionError: Error, LocalizedError { } } -class APITransmissionController: ObservableObject { +class APIChangesetUploadController: ObservableObject { private var idGenerator: IntIdGenerator = IntIdGenerator() public var capturedFrameIds: Set = [] func uploadFeatures( accessibilityFeatures: [any AccessibilityFeatureProtocol], mappingData: MappingData, - inputs: APITransmissionInputs - ) async throws -> APITransmissionResults { + inputs: APIChangesetUploadInputs + ) async throws -> APIChangesetUploadResults { idGenerator = IntIdGenerator() var isFailedCaptureUpload = false if !capturedFrameIds.contains(inputs.captureData.id) { @@ -41,34 +41,34 @@ class APITransmissionController: ObservableObject { isFailedCaptureUpload = true } } - var apiTransmissionResults: APITransmissionResults + var apiChangesetUploadResults: APIChangesetUploadResults switch inputs.accessibilityFeatureClass.oswPolicy.oswElementClass.geometry { case .point: - apiTransmissionResults = try await uploadPoints( + apiChangesetUploadResults = try await uploadPoints( accessibilityFeatures: accessibilityFeatures, mappingData: mappingData, inputs: inputs ) case .linestring: - apiTransmissionResults = try await uploadLineStrings( + apiChangesetUploadResults = try await uploadLineStrings( accessibilityFeatures: accessibilityFeatures, mappingData: mappingData, inputs: inputs ) case .polygon: - apiTransmissionResults = try await uploadPolygons( + apiChangesetUploadResults = try await uploadPolygons( accessibilityFeatures: accessibilityFeatures, mappingData: mappingData, inputs: inputs ) } - return APITransmissionResults( - from: apiTransmissionResults, + return APIChangesetUploadResults( + from: apiChangesetUploadResults, isFailedCaptureUpload: isFailedCaptureUpload ) } - func uploadCapturePoint(inputs: APITransmissionInputs) async throws { + func uploadCapturePoint(inputs: APIChangesetUploadInputs) async throws { let additionalTags: [String: String] = [ APIConstants.TagKeys.captureIdKey: inputs.captureData.id.uuidString, APIConstants.TagKeys.captureLatitudeKey: String(inputs.captureLocation.latitude), @@ -113,16 +113,16 @@ class APITransmissionController: ObservableObject { /** Extension for methods to handle points transmission */ -extension APITransmissionController { +extension APIChangesetUploadController { func uploadPoints( accessibilityFeatures: [any AccessibilityFeatureProtocol], mappingData: MappingData, - inputs: APITransmissionInputs - ) async throws -> APITransmissionResults { + inputs: APIChangesetUploadInputs + ) async throws -> APIChangesetUploadResults { let accessibilityFeatures = accessibilityFeatures let totalFeatures = accessibilityFeatures.count /// Map Accessibility Features to OSW Elements - let featureCache: APIFeatureCache = APIFeatureCache() + let featureCache: APIChangesetUploadCache = APIChangesetUploadCache() let additionalTags: [String: String] = getAdditionalTags( accessibilityFeatureClass: inputs.accessibilityFeatureClass, captureData: inputs.captureData, mappingData: mappingData @@ -141,12 +141,12 @@ extension APITransmissionController { accessToken: inputs.accessToken ) guard featureCache.getOSWPoints().count > 0 else { - return APITransmissionResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) + return APIChangesetUploadResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) } /// Get the new ids and other details for the OSW Elements, from the uploaded elements response let uploadedOSWElements = getUploadedOSWPoints(from: uploadedElements, featureCache: featureCache) guard !uploadedOSWElements.isEmpty else { - return APITransmissionResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) + return APIChangesetUploadResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) } /// Created Mapped Accessibility Features from the uploaded OSW Elements /// Make sure you are using the old ids of the uploaded elements to map back to the features @@ -162,7 +162,7 @@ extension APITransmissionController { ) } let failedUploads = totalFeatures - mappedAccessibilityFeatures.count - return APITransmissionResults( + return APIChangesetUploadResults( accessibilityFeatures: mappedAccessibilityFeatures, failedFeatureUploads: failedUploads, totalFeatureUploads: totalFeatures ) @@ -199,8 +199,8 @@ extension APITransmissionController { } private func getUploadedOSWPoints( - from uploadedElements: UploadedOSMResponseElements, - featureCache: APIFeatureCache + from uploadedElements: OSMChangesetUploadResponseElements, + featureCache: APIChangesetUploadCache ) -> [OSWPoint] { let cachedOSWPoints = featureCache.getOSWPoints() var uploadedOSWPoints: [OSWPoint?] = Array(repeating: nil, count: cachedOSWPoints.count) @@ -232,16 +232,16 @@ extension APITransmissionController { /** Extension to handle line string transmission */ -extension APITransmissionController { +extension APIChangesetUploadController { func uploadLineStrings( accessibilityFeatures: [any AccessibilityFeatureProtocol], mappingData: MappingData, - inputs: APITransmissionInputs - ) async throws -> APITransmissionResults { + inputs: APIChangesetUploadInputs + ) async throws -> APIChangesetUploadResults { var accessibilityFeatures = accessibilityFeatures var totalFeatures = accessibilityFeatures.count guard totalFeatures > 0, let firstFeature = accessibilityFeatures.first else { - return APITransmissionResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) + return APIChangesetUploadResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) } /// For the sidewalk feature class, only upload one linestring representing the entire sidewalk if inputs.accessibilityFeatureClass.oswPolicy.oswElementClass == .Sidewalk { @@ -249,7 +249,7 @@ extension APITransmissionController { totalFeatures = 1 } /// Map Accessibility Features to OSW Elements - let featureCache: APIFeatureCache = APIFeatureCache() + let featureCache: APIChangesetUploadCache = APIChangesetUploadCache() let additionalTags: [String: String] = getAdditionalTags( accessibilityFeatureClass: inputs.accessibilityFeatureClass, captureData: inputs.captureData, mappingData: mappingData @@ -281,7 +281,7 @@ extension APITransmissionController { accessToken: inputs.accessToken ) guard featureCache.getOSWLineStrings().count > 0 else { - return APITransmissionResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) + return APIChangesetUploadResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) } /// Get the new ids and other details for the OSW Elements, from the uploaded elements response let uploadedOSWElements = getUploadedOSWLineStrings( @@ -289,7 +289,7 @@ extension APITransmissionController { featureCache: featureCache ) guard !uploadedOSWElements.isEmpty else { - return APITransmissionResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) + return APIChangesetUploadResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) } /// Created Mapped Accessibility Features from the uploaded OSW Elements /// Make sure you are using the old ids of the uploaded elements to map back to the features @@ -305,7 +305,7 @@ extension APITransmissionController { ) } let failedUploads = totalFeatures - mappedAccessibilityFeatures.count - return APITransmissionResults( + return APIChangesetUploadResults( accessibilityFeatures: mappedAccessibilityFeatures, failedFeatureUploads: failedUploads, totalFeatureUploads: totalFeatures ) @@ -365,8 +365,8 @@ extension APITransmissionController { } private func getUploadedOSWLineStrings( - from uploadedElements: UploadedOSMResponseElements, - featureCache: APIFeatureCache + from uploadedElements: OSMChangesetUploadResponseElements, + featureCache: APIChangesetUploadCache ) -> [OSWLineString] { let cachedOSWLineStrings = featureCache.getOSWLineStrings() var uploadedOSWLineStrings: [OSWLineString?] = Array(repeating: nil, count: cachedOSWLineStrings.count) @@ -381,7 +381,7 @@ extension APITransmissionController { return } /// First, get a new feature cache for the nodes that belong to this linestring - let pointsCache: APIFeatureCache = APIFeatureCache() + let pointsCache: APIChangesetUploadCache = APIChangesetUploadCache() matchedOriginalOSWLineString.points.forEach { point in pointsCache.addEntry(osmOldId: point.id, feature: nil, oswElement: point) } @@ -409,16 +409,16 @@ extension APITransmissionController { /** Extension to handle polygon transmission */ -extension APITransmissionController { +extension APIChangesetUploadController { func uploadPolygons( accessibilityFeatures: [any AccessibilityFeatureProtocol], mappingData: MappingData, - inputs: APITransmissionInputs - ) async throws -> APITransmissionResults { + inputs: APIChangesetUploadInputs + ) async throws -> APIChangesetUploadResults { let accessibilityFeatures = accessibilityFeatures let totalFeatures = accessibilityFeatures.count /// Map Accessibility Features to OSW Elements - let featureCache: APIFeatureCache = APIFeatureCache() + let featureCache: APIChangesetUploadCache = APIChangesetUploadCache() let additionalTags: [String: String] = getAdditionalTags( accessibilityFeatureClass: inputs.accessibilityFeatureClass, captureData: inputs.captureData, mappingData: mappingData @@ -437,7 +437,7 @@ extension APITransmissionController { accessToken: inputs.accessToken ) guard featureCache.getOSWPolygons().count > 0 else { - return APITransmissionResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) + return APIChangesetUploadResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) } /// Get the new ids and other details for the OSW Elements, from the uploaded elements response let uploadedOSWElements = getUploadedPolygons( @@ -445,7 +445,7 @@ extension APITransmissionController { featureCache: featureCache ) guard !uploadedOSWElements.isEmpty else { - return APITransmissionResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) + return APIChangesetUploadResults(failedFeatureUploads: totalFeatures, totalFeatureUploads: totalFeatures) } /// Created Mapped Accessibility Features from the uploaded OSW Elements /// Make sure you are using the old ids of the uploaded elements to map back to the features @@ -461,7 +461,7 @@ extension APITransmissionController { ) } let failedUploads = totalFeatures - mappedAccessibilityFeatures.count - return APITransmissionResults( + return APIChangesetUploadResults( accessibilityFeatures: mappedAccessibilityFeatures, failedFeatureUploads: failedUploads, totalFeatureUploads: totalFeatures ) @@ -543,8 +543,8 @@ extension APITransmissionController { } private func getUploadedPolygons( - from uploadedElements: UploadedOSMResponseElements, - featureCache: APIFeatureCache + from uploadedElements: OSMChangesetUploadResponseElements, + featureCache: APIChangesetUploadCache ) -> [OSWPolygon] { let cachedOSWPolygons = featureCache.getOSWPolygons() var uploadedOSWPolygons: [OSWPolygon?] = Array(repeating: nil, count: cachedOSWPolygons.count) @@ -559,7 +559,7 @@ extension APITransmissionController { return } /// First, create a new feature cache for the point members of the polygon - let pointsCache: APIFeatureCache = APIFeatureCache() + let pointsCache: APIChangesetUploadCache = APIChangesetUploadCache() matchedOriginalOSWPolygon.members.forEach { member in let element = member.element guard let point = element as? OSWPoint else { return } @@ -571,7 +571,7 @@ extension APITransmissionController { featureCache: pointsCache ) /// Second, create a new feature cache for the linestring members of the polygon - let lineStringsCache: APIFeatureCache = APIFeatureCache() + let lineStringsCache: APIChangesetUploadCache = APIChangesetUploadCache() matchedOriginalOSWPolygon.members.forEach { member in let element = member.element guard let lineString = element as? OSWLineString else { return } diff --git a/IOSAccessAssessment/TDEI/Transmission/APITransmissionHelpers.swift b/IOSAccessAssessment/TDEI/Transmission/APITransmissionHelpers.swift index b44851d9..be7c34b4 100644 --- a/IOSAccessAssessment/TDEI/Transmission/APITransmissionHelpers.swift +++ b/IOSAccessAssessment/TDEI/Transmission/APITransmissionHelpers.swift @@ -8,15 +8,15 @@ import SwiftUI import CoreLocation -struct APIFeatureCacheEntry: @unchecked Sendable { +struct APIChangesetUploadCacheEntry: @unchecked Sendable { let osmOldId: String let feature: (any AccessibilityFeatureProtocol)? let oswElement: any OSWElement } -class APIFeatureCache { - /// OSM old ID to APIFeatureCacheEntry - var cacheEntry: [APIFeatureCacheEntry] +class APIChangesetUploadCache { + /// OSM old ID to APIChangesetUploadCacheEntry + var cacheEntry: [APIChangesetUploadCacheEntry] init() { self.cacheEntry = [] @@ -26,12 +26,12 @@ class APIFeatureCache { return cacheEntry.firstIndex { $0.osmOldId == osmOldId } } - func getEntry(osmOldId: String) -> APIFeatureCacheEntry? { + func getEntry(osmOldId: String) -> APIChangesetUploadCacheEntry? { return cacheEntry.first { $0.osmOldId == osmOldId } } func addEntry(osmOldId: String, feature: (any AccessibilityFeatureProtocol)?, oswElement: any OSWElement) { - let entry = APIFeatureCacheEntry(osmOldId: osmOldId, feature: feature, oswElement: oswElement) + let entry = APIChangesetUploadCacheEntry(osmOldId: osmOldId, feature: feature, oswElement: oswElement) cacheEntry.append(entry) } @@ -67,7 +67,7 @@ class APIFeatureCache { } } -struct APITransmissionInputs { +struct APIChangesetUploadInputs { let workspaceId: String let changesetId: String let accessibilityFeatureClass: AccessibilityFeatureClass @@ -77,7 +77,7 @@ struct APITransmissionInputs { let environment: APIEnvironment? } -struct APITransmissionResults: @unchecked Sendable { +struct APIChangesetUploadResults: @unchecked Sendable { let accessibilityFeatures: [MappedAccessibilityFeature]? let failedFeatureUploads: Int @@ -105,7 +105,7 @@ struct APITransmissionResults: @unchecked Sendable { } /// Clone method for overwriting isFailedCaptureUpload - init(from other: APITransmissionResults, isFailedCaptureUpload: Bool) { + init(from other: APIChangesetUploadResults, isFailedCaptureUpload: Bool) { self.accessibilityFeatures = other.accessibilityFeatures self.failedFeatureUploads = other.failedFeatureUploads self.totalFeatureUploads = other.totalFeatureUploads diff --git a/IOSAccessAssessment/View/ARCameraView.swift b/IOSAccessAssessment/View/ARCameraView.swift index ffadd1cf..d57e41b9 100644 --- a/IOSAccessAssessment/View/ARCameraView.swift +++ b/IOSAccessAssessment/View/ARCameraView.swift @@ -104,7 +104,7 @@ struct ARCameraView: View { @State private var showARCameraLearnMoreSheet = false @State private var showAnnotationView = false - @StateObject private var apiTransmissionController: APITransmissionController = APITransmissionController() + @StateObject private var apiChangesetUploadController: APIChangesetUploadController = APIChangesetUploadController() var body: some View { Group { @@ -192,7 +192,7 @@ struct ARCameraView: View { if let captureLocation { AnnotationView( selectedClasses: selectedClasses, captureLocation: captureLocation, - apiTransmissionController: apiTransmissionController + apiChangesetUploadController: apiChangesetUploadController ) } else { InvalidContentView( diff --git a/IOSAccessAssessment/View/AnnotationView.swift b/IOSAccessAssessment/View/AnnotationView.swift index b4dcfe55..9b569e17 100644 --- a/IOSAccessAssessment/View/AnnotationView.swift +++ b/IOSAccessAssessment/View/AnnotationView.swift @@ -33,10 +33,10 @@ enum AnnotationViewConstants { static let managerStatusAlertDismissButtonKey = "OK" static let managerStatusAlertMessageDismissScreenSuffixKey = "Press OK to close this screen." static let managerStatusAlertMessageDismissAlertSuffixKey = "Press OK to dismiss this alert." - static let apiTransmissionStatusAlertTitleKey = "Upload Error" - static let apiTransmissionStatusAlertDismissButtonKey = "OK" - static let apiTransmissionStatusAlertGenericMessageKey = "Failed to upload features. Press OK to dismiss this alert." - static let apiTransmissionStatusAlertMessageSuffixKey = " feature(s) failed to upload. Press OK to dismiss this alert." + static let apiChangesetUploadStatusAlertTitleKey = "Upload Error" + static let apiChangesetUploadStatusAlertDismissButtonKey = "OK" + static let apiChangesetUploadStatusAlertGenericMessageKey = "Failed to upload features. Press OK to dismiss this alert." + static let apiChangesetUploadStatusAlertMessageSuffixKey = " feature(s) failed to upload. Press OK to dismiss this alert." /// SelectObjectInfoTip static let selectFeatureInfoTipTitle = "Select a Feature" @@ -74,7 +74,7 @@ enum AnnotationViewError: Error, LocalizedError { case workspaceConfigurationFailed case attributeEstimationFailed(Error) case uploadFailed - case apiTransmissionFailed(APITransmissionResults) + case apiChangesetUploadFailed(APIChangesetUploadResults) var errorDescription: String? { switch self { @@ -94,7 +94,7 @@ enum AnnotationViewError: Error, LocalizedError { return "Some Attribute Estimation calculations failed. They may be ignored. \nError: \(error.localizedDescription)" case .uploadFailed: return "Failed to upload annotations." - case .apiTransmissionFailed(let results): + case .apiChangesetUploadFailed(let results): return "API Transmission failed with \(results.failedFeatureUploads) failed uploads." } } @@ -201,7 +201,7 @@ class AnnotationViewStatusViewModel: ObservableObject { } } -class APITransmissionStatusViewModel: ObservableObject { +class APIChangesetUploadStatusViewModel: ObservableObject { @Published var isFailed: Bool = false @Published var errorMessage: String = "" @@ -210,14 +210,14 @@ class APITransmissionStatusViewModel: ObservableObject { self.errorMessage = errorMessage } - func update(apiTransmissionResults: APITransmissionResults) { - let failedFeatureUploads = apiTransmissionResults.failedFeatureUploads + func update(apiChangesetUploadResults: APIChangesetUploadResults) { + let failedFeatureUploads = apiChangesetUploadResults.failedFeatureUploads if failedFeatureUploads == 0 { self.isFailed = true self.errorMessage = "Unknown Error Occurred." } else { self.isFailed = true - self.errorMessage = "\(failedFeatureUploads) \(AnnotationViewConstants.Texts.apiTransmissionStatusAlertMessageSuffixKey)" + self.errorMessage = "\(failedFeatureUploads) \(AnnotationViewConstants.Texts.apiChangesetUploadStatusAlertMessageSuffixKey)" } } } @@ -225,7 +225,7 @@ class APITransmissionStatusViewModel: ObservableObject { struct AnnotationView: View { let selectedClasses: [AccessibilityFeatureClass] let captureLocation: CLLocationCoordinate2D - let apiTransmissionController: APITransmissionController + let apiChangesetUploadController: APIChangesetUploadController @EnvironmentObject var userStateViewModel: UserStateViewModel @EnvironmentObject var workspaceViewModel: WorkspaceViewModel @@ -238,7 +238,7 @@ struct AnnotationView: View { @StateObject var attributeEstimationPipeline: AttributeEstimationPipeline = AttributeEstimationPipeline() @StateObject private var managerStatusViewModel = AnnotationViewStatusViewModel() - @StateObject private var apiTransmissionStatusViewModel = APITransmissionStatusViewModel() + @StateObject private var apiChangesetUploadStatusViewModel = APIChangesetUploadStatusViewModel() @State private var interfaceOrientation: UIInterfaceOrientation = .portrait // To bind one-way with manager's orientation @StateObject var featureClassSelectionViewModel = AnnotationFeatureClassSelectionViewModel() @@ -318,10 +318,10 @@ struct AnnotationView: View { }, message: { Text(managerStatusViewModel.errorMessage) }) - .alert(AnnotationViewConstants.Texts.apiTransmissionStatusAlertTitleKey, - isPresented: $apiTransmissionStatusViewModel.isFailed, actions: { + .alert(AnnotationViewConstants.Texts.apiChangesetUploadStatusAlertTitleKey, + isPresented: $apiChangesetUploadStatusViewModel.isFailed, actions: { Button(AnnotationViewConstants.Texts.managerStatusAlertDismissButtonKey) { - apiTransmissionStatusViewModel.update(isFailed: false, errorMessage: "") + apiChangesetUploadStatusViewModel.update(isFailed: false, errorMessage: "") do { try moveToNextClass() } catch { @@ -329,7 +329,7 @@ struct AnnotationView: View { } } }, message: { - Text(apiTransmissionStatusViewModel.errorMessage) + Text(apiChangesetUploadStatusViewModel.errorMessage) }) } @@ -602,19 +602,19 @@ struct AnnotationView: View { private func confirmAnnotation() { Task { do { - let apiTransmissionResults = try await uploadFeatures() - if let apiTransmissionResults, apiTransmissionResults.failedFeatureUploads > 0 { - throw AnnotationViewError.apiTransmissionFailed(apiTransmissionResults) + let apiChangesetUploadResults = try await uploadFeatures() + if let apiChangesetUploadResults, apiChangesetUploadResults.failedFeatureUploads > 0 { + throw AnnotationViewError.apiChangesetUploadFailed(apiChangesetUploadResults) } try moveToNextClass() } catch AnnotationViewError.classIndexOutofBounds { managerStatusViewModel.update(isFailed: true, error: AnnotationViewError.classIndexOutofBounds) - } catch AnnotationViewError.apiTransmissionFailed(let results) { - apiTransmissionStatusViewModel.update(apiTransmissionResults: results) + } catch AnnotationViewError.apiChangesetUploadFailed(let results) { + apiChangesetUploadStatusViewModel.update(apiChangesetUploadResults: results) } catch { - apiTransmissionStatusViewModel.update( + apiChangesetUploadStatusViewModel.update( isFailed: true, - errorMessage: AnnotationViewConstants.Texts.apiTransmissionStatusAlertGenericMessageKey + errorMessage: AnnotationViewConstants.Texts.apiChangesetUploadStatusAlertGenericMessageKey ) } } @@ -634,7 +634,7 @@ struct AnnotationView: View { try featureClassSelectionViewModel.setCurrent(index: currentClassIndex + 1, classes: segmentedClasses) } - private func uploadFeatures() async throws -> APITransmissionResults? { + private func uploadFeatures() async throws -> APIChangesetUploadResults? { guard let currentCaptureDataRecord = sharedAppData.currentCaptureDataRecord else { throw AnnotationViewError.invalidCaptureDataRecord } @@ -658,7 +658,7 @@ struct AnnotationView: View { guard !featuresToUpload.isEmpty else { return nil } - let apiTransmissionInputs = APITransmissionInputs( + let apiChangesetUploadInputs = APIChangesetUploadInputs( workspaceId: workspaceId, changesetId: changesetId, accessibilityFeatureClass: accessibilityFeatureClass, @@ -667,13 +667,13 @@ struct AnnotationView: View { accessToken: accessToken, environment: userStateViewModel.selectedEnvironment ) - let apiTransmissionResults = try await apiTransmissionController.uploadFeatures( + let apiChangesetUploadResults = try await apiChangesetUploadController.uploadFeatures( accessibilityFeatures: featuresToUpload, mappingData: sharedAppData.mappingData, - inputs: apiTransmissionInputs + inputs: apiChangesetUploadInputs ) - guard let mappedAccessibilityFeatures = apiTransmissionResults.accessibilityFeatures else { - throw AnnotationViewError.apiTransmissionFailed(apiTransmissionResults) + guard let mappedAccessibilityFeatures = apiChangesetUploadResults.accessibilityFeatures else { + throw AnnotationViewError.apiChangesetUploadFailed(apiChangesetUploadResults) } sharedAppData.mappingData.updateFeatures(mappedAccessibilityFeatures, for: accessibilityFeatureClass) print("Mapping Data: \(sharedAppData.mappingData)") @@ -684,7 +684,7 @@ struct AnnotationView: View { ) sharedAppData.isUploadReady = true - return apiTransmissionResults + return apiChangesetUploadResults } private func addFeaturesToCurrentDataset( diff --git a/IOSAccessAssessment/View/TestMode/TestCameraView.swift b/IOSAccessAssessment/View/TestMode/TestCameraView.swift index aa334d71..6402b3ac 100644 --- a/IOSAccessAssessment/View/TestMode/TestCameraView.swift +++ b/IOSAccessAssessment/View/TestMode/TestCameraView.swift @@ -79,7 +79,7 @@ struct TestCameraView: View { @State private var showARCameraLearnMoreSheet = false @State private var showAnnotationView = false - @StateObject private var apiTransmissionController: APITransmissionController = APITransmissionController() + @StateObject private var apiChangesetUploadController: APIChangesetUploadController = APIChangesetUploadController() // Latest dataset capture data @State private var datasetCaptureData: DatasetCaptureData? @@ -201,7 +201,7 @@ struct TestCameraView: View { if let captureLocation { AnnotationView( selectedClasses: selectedClasses, captureLocation: captureLocation, - apiTransmissionController: apiTransmissionController + apiChangesetUploadController: apiChangesetUploadController ) } else { InvalidContentView( From b3814b72230adbfb70ca031e5eb2d8c5af033552 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Sun, 29 Mar 2026 00:20:34 -0700 Subject: [PATCH 4/9] Complete implementation of OSMMapData --- .../TDEI/Helpers/CustomXMLParser.swift | 12 ++-- IOSAccessAssessment/TDEI/OSM/OSMMapData.swift | 71 +++++++++++++------ 2 files changed, 57 insertions(+), 26 deletions(-) diff --git a/IOSAccessAssessment/TDEI/Helpers/CustomXMLParser.swift b/IOSAccessAssessment/TDEI/Helpers/CustomXMLParser.swift index 8d73badc..8818cd6f 100644 --- a/IOSAccessAssessment/TDEI/Helpers/CustomXMLParser.swift +++ b/IOSAccessAssessment/TDEI/Helpers/CustomXMLParser.swift @@ -11,9 +11,9 @@ class ChangesetXMLParser: NSObject, XMLParserDelegate { var currentElement = "" var parsedItems: [String] = [] - var nodesWithAttributes: [String: OSMResponseNode] = [:] - var waysWithAttributes: [String: OSMResponseWay] = [:] - var relationsWithAttributes: [String: OSMResponseRelation] = [:] + var nodesWithAttributes: [String: OSMChangesetUploadResponseNode] = [:] + var waysWithAttributes: [String: OSMChangesetUploadResponseWay] = [:] + var relationsWithAttributes: [String: OSMChangesetUploadResponseRelation] = [:] func parse(data: Data) { let parser = XMLParser(data: data) @@ -33,7 +33,7 @@ class ChangesetXMLParser: NSObject, XMLParserDelegate { let newVersion = attributeDict[APIConstants.AttributeKeys.newVersion] else { return } - nodesWithAttributes[oldId] = OSMResponseNode( + nodesWithAttributes[oldId] = OSMChangesetUploadResponseNode( oldId: oldId, newId: newId, newVersion: newVersion, attributeDict: attributeDict ) } @@ -43,7 +43,7 @@ class ChangesetXMLParser: NSObject, XMLParserDelegate { let newVersion = attributeDict[APIConstants.AttributeKeys.newVersion] else { return } - waysWithAttributes[oldId] = OSMResponseWay( + waysWithAttributes[oldId] = OSMChangesetUploadResponseWay( oldId: oldId, newId: newId, newVersion: newVersion, attributeDict: attributeDict ) } @@ -53,7 +53,7 @@ class ChangesetXMLParser: NSObject, XMLParserDelegate { let newVersion = attributeDict[APIConstants.AttributeKeys.newVersion] else { return } - relationsWithAttributes[oldId] = OSMResponseRelation( + relationsWithAttributes[oldId] = OSMChangesetUploadResponseRelation( oldId: oldId, newId: newId, newVersion: newVersion, attributeDict: attributeDict ) } diff --git a/IOSAccessAssessment/TDEI/OSM/OSMMapData.swift b/IOSAccessAssessment/TDEI/OSM/OSMMapData.swift index 1a7d14a0..1931ab09 100644 --- a/IOSAccessAssessment/TDEI/OSM/OSMMapData.swift +++ b/IOSAccessAssessment/TDEI/OSM/OSMMapData.swift @@ -7,10 +7,32 @@ import Foundation +enum OSMMapDataError: Error, LocalizedError { + case invalidNodeCoordinates + + var errorDescription: String? { + switch self { + case .invalidNodeCoordinates: + return "Node has invalid coordinates." + } + } +} + struct OSMMapDataResponse: Codable { - public let version, generator, copyright: String - public let attribution, license: String - public let bounds: Bounds + let version, generator, copyright: String + let attribution, license: String + let bounds: Bounds + let elements: [OSMMapDataResponseElement] + + func getOSMElements() -> [String: any OSMElement] { + var osmElements: [String: any OSMElement] = [:] + for element in elements { + if let osmElement = try? element.toOSMElement() { + osmElements["\(element.id)"] = osmElement + } + } + return osmElements + } } struct Bounds: Codable { // use this for bounds in the map @@ -45,25 +67,34 @@ struct OSMMapDataResponseElement: Codable { uid = try values.decodeIfPresent(Int.self, forKey: .uid) ?? 0 tags = try values.decodeIfPresent([String: String].self, forKey: .tags) ?? [:] nodes = try values.decodeIfPresent([Int].self, forKey: .nodes) ?? [] - type = try values.decodeIfPresent(OSMElementType.self, forKey: .type) ?? TypeEnum.node + type = try values.decodeIfPresent(OSMElementType.self, forKey: .type) ?? OSMElementType.node members = try values.decodeIfPresent([OSMRelationMember].self, forKey: .members) ?? [] } -// func toOSMElement() -> OSMElement? { -// switch type { -// case .node: -// return toOSMNode() -// case .way: -// return toOSMWay() -// case .relation: -// return nil -// } -// } + func toOSMElement() throws -> (any OSMElement)? { + switch type { + case .node: + return try toOSMNode() + case .way: + return toOSMWay() + case .relation: + return toOSMRelation() + } + } // -// private func toOSMNode() -> OSMNode { -// OSMNode(type: "node", id: id, lat: lat!, lon: lon!, timestamp: timestamp, version: version, changeset: changeset, user: user, uid: uid,tags: tags) -// } -// private func toOSMWay() -> OSMWay { -// OSMWay(type: "way", id: id, timestamp: timestamp, version: version, changeset: changeset, user: user, uid: uid, nodes: nodes ?? [], tags: tags) -// } + private func toOSMNode() throws -> OSMNode { + guard let lat = lat, let lon = lon else { + throw OSMMapDataError.invalidNodeCoordinates + } + return OSMNode(id: "\(id)", version: "\(version)", latitude: lat, longitude: lon, tags: tags) + } + + private func toOSMWay() -> OSMWay { + let nodeRefs = nodes?.map { "\($0)" } ?? [] + return OSMWay(id: "\(id)", version: "\(version)", tags: tags, nodeRefs: nodeRefs) + } + + private func toOSMRelation() -> OSMRelation { + return OSMRelation(id: "\(id)", version: "\(version)", tags: tags, members: members ?? []) + } } From 87cde2759d0693fdd852d3740b1c59f82510a9a9 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Sun, 29 Mar 2026 18:35:47 -0700 Subject: [PATCH 5/9] Complete fetch map workflow without inferring the osw features into current mapping --- IOSAccessAssessment.xcodeproj/project.pbxproj | 20 +++--- IOSAccessAssessment/Shared/Constants.swift | 1 + .../Definitions/CurrentMappingData.swift | 30 +++++++++ ...appingData.swift => LiveMappingData.swift} | 8 +-- .../Shared/SharedAppData.swift | 5 +- ...MapData.swift => OSMMapDataResponse.swift} | 4 +- .../TDEI/Services/WorkspaceService.swift | 17 +++-- .../APIChangesetUploadController.swift | 24 +++---- IOSAccessAssessment/View/ARCameraView.swift | 62 +++++++++++++++---- IOSAccessAssessment/View/AnnotationView.swift | 6 +- 10 files changed, 130 insertions(+), 47 deletions(-) create mode 100644 IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift rename IOSAccessAssessment/Shared/Definitions/{MappingData.swift => LiveMappingData.swift} (93%) rename IOSAccessAssessment/TDEI/OSM/{OSMMapData.swift => OSMMapDataResponse.swift} (96%) diff --git a/IOSAccessAssessment.xcodeproj/project.pbxproj b/IOSAccessAssessment.xcodeproj/project.pbxproj index f4d91577..91c5216f 100644 --- a/IOSAccessAssessment.xcodeproj/project.pbxproj +++ b/IOSAccessAssessment.xcodeproj/project.pbxproj @@ -55,7 +55,7 @@ A355471E2EC1A47400F43AFD /* SharedAppData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A355471D2EC1A47200F43AFD /* SharedAppData.swift */; }; A35547C22EC1AE4E00F43AFD /* SafeDeque.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35547C12EC1AE4C00F43AFD /* SafeDeque.swift */; }; A35547C42EC1AF5700F43AFD /* CaptureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35547C32EC1AF5500F43AFD /* CaptureData.swift */; }; - A35547C82EC1B0DB00F43AFD /* MappingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35547C72EC1B0D900F43AFD /* MappingData.swift */; }; + A35547C82EC1B0DB00F43AFD /* LiveMappingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35547C72EC1B0D900F43AFD /* LiveMappingData.swift */; }; A35547CA2EC2045F00F43AFD /* CapturedMeshSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35547C92EC2045F00F43AFD /* CapturedMeshSnapshot.swift */; }; A35547CC2EC3018E00F43AFD /* AnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35547CB2EC3018C00F43AFD /* AnnotationView.swift */; }; A35547CE2EC3048700F43AFD /* AnnotationImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35547CD2EC3048200F43AFD /* AnnotationImageViewController.swift */; }; @@ -80,6 +80,7 @@ A37C3C182F3141FF001F4248 /* Plane.metal in Sources */ = {isa = PBXBuildFile; fileRef = A37C3C172F3141F9001F4248 /* Plane.metal */; }; A37C3C1A2F3144F7001F4248 /* PlaneAttributeProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A37C3C192F3144F4001F4248 /* PlaneAttributeProcessor.swift */; }; A37C3C1C2F356254001F4248 /* IntersectionFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A37C3C1B2F356254001F4248 /* IntersectionFilter.swift */; }; + A37D4A2C2F79F9AC0025A91F /* CurrentMappingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A37D4A2B2F79F9AC0025A91F /* CurrentMappingData.swift */; }; A37E3E3C2EED60F300B07B77 /* PngEncoder.mm in Sources */ = {isa = PBXBuildFile; fileRef = A37E3E3B2EED60F300B07B77 /* PngEncoder.mm */; }; A37E3E3D2EED60F300B07B77 /* lodepng.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A37E3E392EED60F300B07B77 /* lodepng.cpp */; }; A37E3E952EFB66EB00B07B77 /* CameraIntrinsicsCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A37E3E942EFB66E600B07B77 /* CameraIntrinsicsCoder.swift */; }; @@ -110,7 +111,7 @@ A3B2DDC12DC99F44003416FB /* SegmentationModelRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B2DDC02DC99F3D003416FB /* SegmentationModelRequestProcessor.swift */; }; A3B61FC52F76480B0052AE2C /* EnvironmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B61FC42F7647FC0052AE2C /* EnvironmentService.swift */; }; A3B61FC92F78F93B0052AE2C /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B61FC82F78F9390052AE2C /* Extensions.swift */; }; - A3B61FCB2F79036A0052AE2C /* OSMMapData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B61FCA2F7903660052AE2C /* OSMMapData.swift */; }; + A3B61FCB2F79036A0052AE2C /* OSMMapDataResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B61FCA2F7903660052AE2C /* OSMMapDataResponse.swift */; }; A3BB5AFB2DB210AE008673ED /* BinaryMaskFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BB5AFA2DB210A8008673ED /* BinaryMaskFilter.swift */; }; A3BCBC502EFBB92900D15E15 /* AccessibilityFeatureEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BCBC4F2EFBB92500D15E15 /* AccessibilityFeatureEncoder.swift */; }; A3C22FD32CF194A600533BF7 /* CGImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C22FD22CF194A200533BF7 /* CGImageUtils.swift */; }; @@ -260,7 +261,7 @@ A355471D2EC1A47200F43AFD /* SharedAppData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedAppData.swift; sourceTree = ""; }; A35547C12EC1AE4C00F43AFD /* SafeDeque.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeDeque.swift; sourceTree = ""; }; A35547C32EC1AF5500F43AFD /* CaptureData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureData.swift; sourceTree = ""; }; - A35547C72EC1B0D900F43AFD /* MappingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MappingData.swift; sourceTree = ""; }; + A35547C72EC1B0D900F43AFD /* LiveMappingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveMappingData.swift; sourceTree = ""; }; A35547C92EC2045F00F43AFD /* CapturedMeshSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturedMeshSnapshot.swift; sourceTree = ""; }; A35547CB2EC3018C00F43AFD /* AnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationView.swift; sourceTree = ""; }; A35547CD2EC3048200F43AFD /* AnnotationImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationImageViewController.swift; sourceTree = ""; }; @@ -285,6 +286,7 @@ A37C3C172F3141F9001F4248 /* Plane.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Plane.metal; sourceTree = ""; }; A37C3C192F3144F4001F4248 /* PlaneAttributeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaneAttributeProcessor.swift; sourceTree = ""; }; A37C3C1B2F356254001F4248 /* IntersectionFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntersectionFilter.swift; sourceTree = ""; }; + A37D4A2B2F79F9AC0025A91F /* CurrentMappingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentMappingData.swift; sourceTree = ""; }; A37E3E382EED60F300B07B77 /* lodepng.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = lodepng.h; sourceTree = ""; }; A37E3E392EED60F300B07B77 /* lodepng.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = lodepng.cpp; sourceTree = ""; }; A37E3E3A2EED60F300B07B77 /* PngEncoder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PngEncoder.h; sourceTree = ""; }; @@ -317,7 +319,7 @@ A3B2DDC02DC99F3D003416FB /* SegmentationModelRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentationModelRequestProcessor.swift; sourceTree = ""; }; A3B61FC42F7647FC0052AE2C /* EnvironmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentService.swift; sourceTree = ""; }; A3B61FC82F78F9390052AE2C /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; - A3B61FCA2F7903660052AE2C /* OSMMapData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMMapData.swift; sourceTree = ""; }; + A3B61FCA2F7903660052AE2C /* OSMMapDataResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMMapDataResponse.swift; sourceTree = ""; }; A3BB5AFA2DB210A8008673ED /* BinaryMaskFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryMaskFilter.swift; sourceTree = ""; }; A3BCBC4F2EFBB92500D15E15 /* AccessibilityFeatureEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityFeatureEncoder.swift; sourceTree = ""; }; A3C22FD22CF194A200533BF7 /* CGImageUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGImageUtils.swift; sourceTree = ""; }; @@ -769,7 +771,7 @@ A329434F2EE80EC200C4C1BC /* OSMRelation.swift */, A3EE6EFF2F6A29F300F515E6 /* OSMLocation.swift */, A374FAB62EE0173200055268 /* OSMChangesetUploadResponseElement.swift */, - A3B61FCA2F7903660052AE2C /* OSMMapData.swift */, + A3B61FCA2F7903660052AE2C /* OSMMapDataResponse.swift */, ); path = OSM; sourceTree = ""; @@ -855,7 +857,8 @@ children = ( A3EE6E452F57FE6200F515E6 /* AppMode.swift */, A35547C32EC1AF5500F43AFD /* CaptureData.swift */, - A35547C72EC1B0D900F43AFD /* MappingData.swift */, + A35547C72EC1B0D900F43AFD /* LiveMappingData.swift */, + A37D4A2B2F79F9AC0025A91F /* CurrentMappingData.swift */, A3DA4DBD2EBCB9F9005BB812 /* MetalContext.swift */, A3A413A32ECD3C7B0039298C /* RasterizeConfig.swift */, ); @@ -1279,6 +1282,7 @@ A3BB5AFB2DB210AE008673ED /* BinaryMaskFilter.swift in Sources */, A355471E2EC1A47400F43AFD /* SharedAppData.swift in Sources */, A3B2DDC12DC99F44003416FB /* SegmentationModelRequestProcessor.swift in Sources */, + A37D4A2C2F79F9AC0025A91F /* CurrentMappingData.swift in Sources */, A3B2DDBF2DC99DEF003416FB /* HomographyRequestProcessor.swift in Sources */, A3FFAA802DE444C6002B99BD /* AnnotationOption.swift in Sources */, CAF812C42CFA108100D44B84 /* UserStateViewModel.swift in Sources */, @@ -1294,7 +1298,7 @@ A308015C2EC09BB700B1BA3A /* CityscapesClassConfig.swift in Sources */, A308015D2EC09BB700B1BA3A /* CityscapesSubsetClassConfig.swift in Sources */, A364B5352F25589B00325E5C /* DepthFiltering.metal in Sources */, - A35547C82EC1B0DB00F43AFD /* MappingData.swift in Sources */, + A35547C82EC1B0DB00F43AFD /* LiveMappingData.swift in Sources */, A37E3EA02EFBAADD00B07B77 /* AccessibilityFeatureClassSnapshot.swift in Sources */, A35547C42EC1AF5700F43AFD /* CaptureData.swift in Sources */, A3EE6E462F57FE6400F515E6 /* AppMode.swift in Sources */, @@ -1314,7 +1318,7 @@ A329433C2EE7BEE100C4C1BC /* OSWPolicy.swift in Sources */, A38338BF2EDA889C00F1A402 /* CustomPicker.swift in Sources */, A32943592EE8204400C4C1BC /* OSWPolygon.swift in Sources */, - A3B61FCB2F79036A0052AE2C /* OSMMapData.swift in Sources */, + A3B61FCB2F79036A0052AE2C /* OSMMapDataResponse.swift in Sources */, CAF812BC2CF78F8100D44B84 /* NetworkError.swift in Sources */, A305B06C2E18A85F00ECCF9B /* DepthCoder.swift in Sources */, A3DA4DBC2EBCB881005BB812 /* SegmentationMeshRecord.swift in Sources */, diff --git a/IOSAccessAssessment/Shared/Constants.swift b/IOSAccessAssessment/Shared/Constants.swift index 4e290d6b..3c844e63 100644 --- a/IOSAccessAssessment/Shared/Constants.swift +++ b/IOSAccessAssessment/Shared/Constants.swift @@ -31,6 +31,7 @@ struct Constants { static let fetchRadiusInMeters: Double = 100.0 static let fetchUpdateRadiusThresholdInMeters: Double = 50.0 + static let updateElementDistanceThresholdInMeters: Double = 20.0 } struct OtherConstants { diff --git a/IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift b/IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift new file mode 100644 index 00000000..39eb3117 --- /dev/null +++ b/IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift @@ -0,0 +1,30 @@ +// +// CurrentMappingData.swift +// IOSAccessAssessment +// +// Created by Himanshu on 11/9/25. +// + +import Foundation + +enum CurrentMappingDataError: Error, LocalizedError { +} + +class CurrentMappingData { + var featuresMap: [AccessibilityFeatureClass: [OSWElement]] = [:] + var otherFeatures: [OSWElement] = [] + + init() { + + } + + init(osmMapDataResponse: OSMMapDataResponse, accessibilityFeatureClasses: [AccessibilityFeatureClass]) { + updateFeatures(with: osmMapDataResponse, accessibilityFeatureClasses: accessibilityFeatureClasses) + } + + func updateFeatures(with osmMapDataResponse: OSMMapDataResponse, accessibilityFeatureClasses: [AccessibilityFeatureClass]) { + for featureClass in accessibilityFeatureClasses { + let oswElementClass = featureClass.oswPolicy.oswElementClass + } + } +} diff --git a/IOSAccessAssessment/Shared/Definitions/MappingData.swift b/IOSAccessAssessment/Shared/Definitions/LiveMappingData.swift similarity index 93% rename from IOSAccessAssessment/Shared/Definitions/MappingData.swift rename to IOSAccessAssessment/Shared/Definitions/LiveMappingData.swift index f364b889..f80d8bf4 100644 --- a/IOSAccessAssessment/Shared/Definitions/MappingData.swift +++ b/IOSAccessAssessment/Shared/Definitions/LiveMappingData.swift @@ -1,5 +1,5 @@ // -// MapData.swift +// LiveMappingData.swift // IOSAccessAssessment // // Created by Himanshu on 11/9/25. @@ -7,7 +7,7 @@ import Foundation -enum MappingDataError: Error, LocalizedError { +enum LiveMappingDataError: Error, LocalizedError { case accessibilityFeatureClassNotWay(AccessibilityFeatureClass) case noActiveWayForFeatureClass(AccessibilityFeatureClass) case accessibilityFeatureNodeNotPresent(AccessibilityFeatureClass) @@ -24,7 +24,7 @@ enum MappingDataError: Error, LocalizedError { } } -class MappingData: CustomStringConvertible { +class LiveMappingData: CustomStringConvertible { var featuresMap: [AccessibilityFeatureClass: [MappedAccessibilityFeature]] = [:] var featureIdToIndexDictMap: [AccessibilityFeatureClass: [UUID: Int]] = [:] @@ -51,7 +51,7 @@ class MappingData: CustomStringConvertible { } var description: String { - var desc = "MappingData:\n" + var desc = "LiveMappingData:\n" desc += "Feature Nodes:\n" featuresMap.forEach { (featureClass, featureData) in return featureData.forEach { feature in diff --git a/IOSAccessAssessment/Shared/SharedAppData.swift b/IOSAccessAssessment/Shared/SharedAppData.swift index ab059a50..e82195f6 100644 --- a/IOSAccessAssessment/Shared/SharedAppData.swift +++ b/IOSAccessAssessment/Shared/SharedAppData.swift @@ -21,7 +21,8 @@ final class SharedAppData: ObservableObject { var captureDataQueue: SafeDeque var captureDataCapacity: Int - var mappingData: MappingData = MappingData() + var currentMappingData: CurrentMappingData = CurrentMappingData() + var liveMappingData: LiveMappingData = LiveMappingData() init(captureDataCapacity: Int = 5) { self.captureDataCapacity = captureDataCapacity @@ -37,6 +38,8 @@ final class SharedAppData: ObservableObject { self.currentDatasetEncoder = nil self.currentDatasetDecoder = nil self.currentCaptureDataRecord = nil + self.currentMappingData = CurrentMappingData() + self.liveMappingData = LiveMappingData() } func saveCaptureData(_ data: CaptureData) { diff --git a/IOSAccessAssessment/TDEI/OSM/OSMMapData.swift b/IOSAccessAssessment/TDEI/OSM/OSMMapDataResponse.swift similarity index 96% rename from IOSAccessAssessment/TDEI/OSM/OSMMapData.swift rename to IOSAccessAssessment/TDEI/OSM/OSMMapDataResponse.swift index 1931ab09..7b9c5d87 100644 --- a/IOSAccessAssessment/TDEI/OSM/OSMMapData.swift +++ b/IOSAccessAssessment/TDEI/OSM/OSMMapDataResponse.swift @@ -7,7 +7,7 @@ import Foundation -enum OSMMapDataError: Error, LocalizedError { +enum OSMMapDataResponseError: Error, LocalizedError { case invalidNodeCoordinates var errorDescription: String? { @@ -84,7 +84,7 @@ struct OSMMapDataResponseElement: Codable { // private func toOSMNode() throws -> OSMNode { guard let lat = lat, let lon = lon else { - throw OSMMapDataError.invalidNodeCoordinates + throw OSMMapDataResponseError.invalidNodeCoordinates } return OSMNode(id: "\(id)", version: "\(version)", latitude: lat, longitude: lon, tags: tags) } diff --git a/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift b/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift index 3b45d7a3..900075c4 100644 --- a/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift +++ b/IOSAccessAssessment/TDEI/Services/WorkspaceService.swift @@ -107,11 +107,11 @@ class WorkspaceService { } func fetchMapData( - workspaceId: Int, + workspaceId: String, location: CLLocationCoordinate2D, radius: Double = 1000, accessToken: String, environment: APIEnvironment? = nil - ) async throws { + ) async throws -> OSMMapDataResponse { let selectedEnvironment = environment ?? EnvironmentService.shared.environment guard let urlEndpoint = APIEndpoint.getMapData(selectedEnvironment) else { throw APIError.invalidURL @@ -126,7 +126,7 @@ class WorkspaceService { var request = URLRequest(url: url) request.httpMethod = "GET" - request.setValue("\(workspaceId)", forHTTPHeaderField: "X-Workspace") + request.setValue(workspaceId, forHTTPHeaderField: "X-Workspace") request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") let (data, response) = try await URLSession.shared.data(for: request) @@ -138,7 +138,14 @@ class WorkspaceService { throw APIError.badStatus(httpResponse.statusCode) } -// do { -// + do { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let mapData: OSMMapDataResponse = try decoder.decode(OSMMapDataResponse.self, from: data) + + return mapData + } catch { + throw APIError.decoding(error) + } } } diff --git a/IOSAccessAssessment/TDEI/Transmission/APIChangesetUploadController.swift b/IOSAccessAssessment/TDEI/Transmission/APIChangesetUploadController.swift index 2809f7c2..bab6cf12 100644 --- a/IOSAccessAssessment/TDEI/Transmission/APIChangesetUploadController.swift +++ b/IOSAccessAssessment/TDEI/Transmission/APIChangesetUploadController.swift @@ -28,7 +28,7 @@ class APIChangesetUploadController: ObservableObject { func uploadFeatures( accessibilityFeatures: [any AccessibilityFeatureProtocol], - mappingData: MappingData, + liveMappingData: LiveMappingData, inputs: APIChangesetUploadInputs ) async throws -> APIChangesetUploadResults { idGenerator = IntIdGenerator() @@ -46,19 +46,19 @@ class APIChangesetUploadController: ObservableObject { case .point: apiChangesetUploadResults = try await uploadPoints( accessibilityFeatures: accessibilityFeatures, - mappingData: mappingData, + liveMappingData: liveMappingData, inputs: inputs ) case .linestring: apiChangesetUploadResults = try await uploadLineStrings( accessibilityFeatures: accessibilityFeatures, - mappingData: mappingData, + liveMappingData: liveMappingData, inputs: inputs ) case .polygon: apiChangesetUploadResults = try await uploadPolygons( accessibilityFeatures: accessibilityFeatures, - mappingData: mappingData, + liveMappingData: liveMappingData, inputs: inputs ) } @@ -94,7 +94,7 @@ class APIChangesetUploadController: ObservableObject { private func getAdditionalTags( accessibilityFeatureClass: AccessibilityFeatureClass, captureData: CaptureData, - mappingData: MappingData, + liveMappingData: LiveMappingData, ) -> [String: String] { var enhancedAnalysisMode: Bool = false switch captureData { @@ -116,7 +116,7 @@ class APIChangesetUploadController: ObservableObject { extension APIChangesetUploadController { func uploadPoints( accessibilityFeatures: [any AccessibilityFeatureProtocol], - mappingData: MappingData, + liveMappingData: LiveMappingData, inputs: APIChangesetUploadInputs ) async throws -> APIChangesetUploadResults { let accessibilityFeatures = accessibilityFeatures @@ -125,7 +125,7 @@ extension APIChangesetUploadController { let featureCache: APIChangesetUploadCache = APIChangesetUploadCache() let additionalTags: [String: String] = getAdditionalTags( accessibilityFeatureClass: inputs.accessibilityFeatureClass, - captureData: inputs.captureData, mappingData: mappingData + captureData: inputs.captureData, liveMappingData: liveMappingData ) for feature in accessibilityFeatures { let oswElement = featureToPoint(feature, additionalTags: additionalTags) @@ -235,7 +235,7 @@ extension APIChangesetUploadController { extension APIChangesetUploadController { func uploadLineStrings( accessibilityFeatures: [any AccessibilityFeatureProtocol], - mappingData: MappingData, + liveMappingData: LiveMappingData, inputs: APIChangesetUploadInputs ) async throws -> APIChangesetUploadResults { var accessibilityFeatures = accessibilityFeatures @@ -252,7 +252,7 @@ extension APIChangesetUploadController { let featureCache: APIChangesetUploadCache = APIChangesetUploadCache() let additionalTags: [String: String] = getAdditionalTags( accessibilityFeatureClass: inputs.accessibilityFeatureClass, - captureData: inputs.captureData, mappingData: mappingData + captureData: inputs.captureData, liveMappingData: liveMappingData ) for feature in accessibilityFeatures { let oswElement = featureToLineString(feature, additionalTags: additionalTags) @@ -264,7 +264,7 @@ extension APIChangesetUploadController { var uploadOperations: [ChangesetDiffOperation] = featureCache.getOSWElements().map { .create($0) } /// For the sidewalk class, get the previously uploaded linestring, connect it to the new linestring, and add a modify operation if inputs.accessibilityFeatureClass.oswPolicy.oswElementClass == .Sidewalk, - let existingMappedFeature = mappingData.featuresMap[inputs.accessibilityFeatureClass]?.last { + let existingMappedFeature = liveMappingData.featuresMap[inputs.accessibilityFeatureClass]?.last { let existingOSWElement = existingMappedFeature.oswElement if var existingOSWLineString = existingOSWElement as? OSWLineString, let newOSWLineString = featureCache.getOSWLineStrings().first, @@ -412,7 +412,7 @@ extension APIChangesetUploadController { extension APIChangesetUploadController { func uploadPolygons( accessibilityFeatures: [any AccessibilityFeatureProtocol], - mappingData: MappingData, + liveMappingData: LiveMappingData, inputs: APIChangesetUploadInputs ) async throws -> APIChangesetUploadResults { let accessibilityFeatures = accessibilityFeatures @@ -421,7 +421,7 @@ extension APIChangesetUploadController { let featureCache: APIChangesetUploadCache = APIChangesetUploadCache() let additionalTags: [String: String] = getAdditionalTags( accessibilityFeatureClass: inputs.accessibilityFeatureClass, - captureData: inputs.captureData, mappingData: mappingData + captureData: inputs.captureData, liveMappingData: liveMappingData ) for feature in accessibilityFeatures { let oswElement = featureToPolygon(feature, additonalTags: additionalTags) diff --git a/IOSAccessAssessment/View/ARCameraView.swift b/IOSAccessAssessment/View/ARCameraView.swift index d57e41b9..5d6a6c03 100644 --- a/IOSAccessAssessment/View/ARCameraView.swift +++ b/IOSAccessAssessment/View/ARCameraView.swift @@ -65,11 +65,17 @@ enum ARCameraViewConstants { enum ARCameraViewError: Error, LocalizedError { case captureNoSegmentationAccessibilityFeatures + case workspaceConfigurationFailed + case authenticationError var errorDescription: String? { switch self { case .captureNoSegmentationAccessibilityFeatures: return "No accessibility features were captured. Please try again." + case .workspaceConfigurationFailed: + return "Workspace configuration failed. Please check your workspace settings." + case .authenticationError: + return "Authentication error. Please log in again." } } } @@ -91,6 +97,7 @@ struct ARCameraView: View { @EnvironmentObject var sharedAppContext: SharedAppContext @EnvironmentObject var segmentationPipeline: SegmentationARPipeline @EnvironmentObject var userStateViewModel: UserStateViewModel + @EnvironmentObject var workspaceViewModel: WorkspaceViewModel @Environment(\.dismiss) var dismiss @StateObject private var manager: ARCameraManager = ARCameraManager() @@ -98,8 +105,6 @@ struct ARCameraView: View { @State private var cameraHintText: String = ARCameraViewConstants.Texts.cameraHintPlaceholderText @StateObject private var locationManager: LocationManager = LocationManager() - @State private var captureLocation: CLLocationCoordinate2D? - @State private var captureHeading: CLLocationDirection? @State private var showARCameraLearnMoreSheet = false @@ -170,11 +175,10 @@ struct ARCameraView: View { } } .onDisappear { - locationManager.stopLocationUpdates() - print("ARCameraView disappeared, stopping location updates.") Task { do { try manager.pause() + locationManager.stopLocationUpdates() } catch { print("Error pausing ARCameraManager: \(error)") } @@ -189,7 +193,7 @@ struct ARCameraView: View { Text(managerConfigureStatusViewModel.errorMessage) }) .fullScreenCover(isPresented: $showAnnotationView) { - if let captureLocation { + if let captureLocation = locationManager.currentLocation?.coordinate { AnnotationView( selectedClasses: selectedClasses, captureLocation: captureLocation, apiChangesetUploadController: apiChangesetUploadController @@ -206,9 +210,8 @@ struct ARCameraView: View { Task { if (oldValue == true && newValue == false) { do { + locationManager.startLocationUpdates() await sharedAppData.refreshQueue() - captureLocation = nil - captureHeading = nil try manager.resume() } catch { managerConfigureStatusViewModel.update(isFailed: true, errorMessage: error.localizedDescription) @@ -220,6 +223,7 @@ struct ARCameraView: View { locationManager.updateOrientation(newOrientation) } .onChange(of: locationManager.currentLocation) { oldLocation, newLocation in + handleLocationUpdate(oldLocation: oldLocation, newLocation: newLocation) } .sheet(isPresented: $showARCameraLearnMoreSheet) { ARCameraLearnMoreSheetView() @@ -264,14 +268,13 @@ struct ARCameraView: View { throw ARCameraViewError.captureNoSegmentationAccessibilityFeatures } } - captureLocation = try locationManager.getLocationCoordinate() - captureHeading = try locationManager.getHeadingDegrees() try manager.pause() + locationManager.stopLocationUpdates() /// Get location. Done after pausing the manager to avoid delays, despite being less accurate. sharedAppData.saveCaptureData(captureData) addCaptureDataToCurrentDataset( captureImageData: captureData.imageData, captureMeshData: captureData.meshData, - location: captureLocation, heading: captureHeading + location: locationManager.currentLocation?.coordinate, heading: locationManager.currentHeading?.trueHeading ) showAnnotationView = true } catch ARCameraManagerError.finalSessionMeshUnavailable { @@ -300,8 +303,8 @@ struct ARCameraView: View { try sharedAppData.currentDatasetEncoder?.addCaptureData( captureImageData: captureImageData, captureMeshData: captureMeshData, - location: captureLocation, - heading: captureHeading + location: locationManager.currentLocation?.coordinate, + heading: locationManager.currentHeading?.trueHeading ) } catch { print("Error adding capture data to dataset encoder: \(error)") @@ -309,6 +312,41 @@ struct ARCameraView: View { } } + private func handleLocationUpdate(oldLocation: CLLocation?, newLocation: CLLocation?) { + var shouldUpdateMap = oldLocation == nil && newLocation != nil + if let oldLocation, let newLocation { + let distance = oldLocation.distance(from: newLocation) + shouldUpdateMap = distance > Constants.WorkspaceConstants.fetchUpdateRadiusThresholdInMeters + } + if !shouldUpdateMap { + return + } + Task { + do { + guard let workspaceId = workspaceViewModel.workspaceId, + let location = newLocation?.coordinate else { + throw ARCameraViewError.workspaceConfigurationFailed + } + guard let accessToken = userStateViewModel.getAccessToken() else { + throw ARCameraViewError.authenticationError + } + let mapData = try await WorkspaceService.shared.fetchMapData( + workspaceId: workspaceId, + location: location, + radius: Constants.WorkspaceConstants.fetchRadiusInMeters, + accessToken: accessToken, + environment: userStateViewModel.selectedEnvironment + ) + sharedAppData.currentMappingData.updateFeatures( + with: mapData, + accessibilityFeatureClasses: selectedClasses + ) + } catch { + setHintText(error.localizedDescription) + } + } + } + /// Set text for 2 seconds, and then fall back to placeholder private func setHintText(_ text: String) { cameraHintText = text diff --git a/IOSAccessAssessment/View/AnnotationView.swift b/IOSAccessAssessment/View/AnnotationView.swift index 9b569e17..ce96a90d 100644 --- a/IOSAccessAssessment/View/AnnotationView.swift +++ b/IOSAccessAssessment/View/AnnotationView.swift @@ -669,14 +669,14 @@ struct AnnotationView: View { ) let apiChangesetUploadResults = try await apiChangesetUploadController.uploadFeatures( accessibilityFeatures: featuresToUpload, - mappingData: sharedAppData.mappingData, + liveMappingData: sharedAppData.liveMappingData, inputs: apiChangesetUploadInputs ) guard let mappedAccessibilityFeatures = apiChangesetUploadResults.accessibilityFeatures else { throw AnnotationViewError.apiChangesetUploadFailed(apiChangesetUploadResults) } - sharedAppData.mappingData.updateFeatures(mappedAccessibilityFeatures, for: accessibilityFeatureClass) - print("Mapping Data: \(sharedAppData.mappingData)") + sharedAppData.liveMappingData.updateFeatures(mappedAccessibilityFeatures, for: accessibilityFeatureClass) + print("Live Mapping Data: \(sharedAppData.liveMappingData)") addFeaturesToCurrentDataset( captureImageData: currentCaptureDataRecord.imageData, From ad6abd97cf73c99232f775fd58e49993d5f1cc54 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Sun, 29 Mar 2026 19:30:06 -0700 Subject: [PATCH 6/9] Add alert in case map data retrieval fails and incorporate similar logic in test camera view --- .../Localization/LocationManager.swift | 28 +--- IOSAccessAssessment/View/ARCameraView.swift | 31 ++++ .../View/TestMode/TestCameraView.swift | 133 ++++++++++++++++-- 3 files changed, 157 insertions(+), 35 deletions(-) diff --git a/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift b/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift index 678520b3..2f967ec6 100644 --- a/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift +++ b/IOSAccessAssessment/AccessibilityFeature/AttributeEstimation/Localization/LocationManager.swift @@ -16,7 +16,7 @@ struct BBox { let maxLon: Double func toQueryString() -> String { - return "\(minLat.roundedTo7Digits()),\(minLon.roundedTo7Digits()),\(maxLat.roundedTo7Digits()),\(maxLon.roundedTo7Digits())" + return "\(minLon.roundedTo7Digits()),\(minLat.roundedTo7Digits()),\(maxLon.roundedTo7Digits()),\(maxLat.roundedTo7Digits())" } } @@ -125,32 +125,6 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { } } - func getLocation() throws -> CLLocation { - guard let location = locationManager.location, - location.horizontalAccuracy > 0, location.verticalAccuracy > 0 else { - throw LocationManagerError.locationUnavailable - } - return location - } - - private func getHeading() throws -> CLHeading { - guard let heading = locationManager.heading, - heading.headingAccuracy > 0 else { - throw LocationManagerError.headingUnavailable - } - return heading - } - - func getLocationCoordinate() throws -> CLLocationCoordinate2D { - let location = try getLocation() - return location.coordinate - } - - func getHeadingDegrees() throws -> CLLocationDirection { - let heading = try getHeading() - return heading.trueHeading - } - func stopLocationUpdates() { locationManager.stopUpdatingLocation() } diff --git a/IOSAccessAssessment/View/ARCameraView.swift b/IOSAccessAssessment/View/ARCameraView.swift index 5d6a6c03..eb43c9f2 100644 --- a/IOSAccessAssessment/View/ARCameraView.swift +++ b/IOSAccessAssessment/View/ARCameraView.swift @@ -31,6 +31,11 @@ enum ARCameraViewConstants { static let managerStatusAlertTitleKey = "Error" static let managerStatusAlertDismissButtonKey = "OK" + /// Mapping Data Status Alert + static let mappingDataStatusAlertTitleKey = "Error" + static let mappingDataStatusAlertRetryButtonKey = "Retry" + static let mappingDataStatusAlertDismissButtonKey = "OK" + /// Invalid Content View static let invalidContentViewTitle = "Invalid Capture" static let invalidContentViewMessage = "The captured data is invalid. Please try again." @@ -90,6 +95,16 @@ class ARCameraManagerStatusViewModel: ObservableObject { } } +class MappingDataStatusViewModel: ObservableObject { + @Published var isFailed: Bool = false + @Published var errorMessage: String = "" + + func update(isFailed: Bool, errorMessage: String) { + self.isFailed = isFailed + self.errorMessage = errorMessage + } +} + struct ARCameraView: View { let selectedClasses: [AccessibilityFeatureClass] @@ -105,6 +120,9 @@ struct ARCameraView: View { @State private var cameraHintText: String = ARCameraViewConstants.Texts.cameraHintPlaceholderText @StateObject private var locationManager: LocationManager = LocationManager() +// @State private var captureLocation: CLLocationCoordinate2D? +// @State private var captureHeading: CLLocationDirection? + @StateObject private var mappingDataStatusViewModel = MappingDataStatusViewModel() @State private var showARCameraLearnMoreSheet = false @@ -192,6 +210,18 @@ struct ARCameraView: View { }, message: { Text(managerConfigureStatusViewModel.errorMessage) }) + .alert(ARCameraViewConstants.Texts.mappingDataStatusAlertTitleKey, isPresented: $mappingDataStatusViewModel.isFailed, actions: { + Button(ARCameraViewConstants.Texts.mappingDataStatusAlertRetryButtonKey) { + mappingDataStatusViewModel.update(isFailed: false, errorMessage: "") + handleLocationUpdate(oldLocation: nil, newLocation: locationManager.currentLocation) + } + Button(ARCameraViewConstants.Texts.mappingDataStatusAlertDismissButtonKey) { + mappingDataStatusViewModel.update(isFailed: false, errorMessage: "") + dismiss() + } + }, message: { + Text(mappingDataStatusViewModel.errorMessage) + }) .fullScreenCover(isPresented: $showAnnotationView) { if let captureLocation = locationManager.currentLocation?.coordinate { AnnotationView( @@ -342,6 +372,7 @@ struct ARCameraView: View { accessibilityFeatureClasses: selectedClasses ) } catch { + /// TODO: Replace with an alert that either retries the fetch or dismissed the view. setHintText(error.localizedDescription) } } diff --git a/IOSAccessAssessment/View/TestMode/TestCameraView.swift b/IOSAccessAssessment/View/TestMode/TestCameraView.swift index 6402b3ac..5c8192a9 100644 --- a/IOSAccessAssessment/View/TestMode/TestCameraView.swift +++ b/IOSAccessAssessment/View/TestMode/TestCameraView.swift @@ -53,6 +53,50 @@ enum TestCameraViewError: Error, LocalizedError { } } +@MainActor +class LocationManagerPlaceholder: NSObject, ObservableObject { + @Published var currentLocation: CLLocation? + @Published var currentHeading: CLHeading? + + override init() { + super.init() + } + + func startLocationUpdates() {} + + private func setupLocationManager() {} + + /** + Updates the heading orientation of the location manager based on the current device orientation. This ensures that heading data is accurate and consistent with the user's perspective. + */ + public func updateOrientation(_ orientation: UIInterfaceOrientation) {} + + func locationManager(didUpdateLocations locations: [CLLocation]) { + guard let latestLocation = locations.last else { return } + guard let horizontalAccuracy = latestLocation.horizontalAccuracy as CLLocationAccuracy?, + let verticalAccuracy = latestLocation.verticalAccuracy as CLLocationAccuracy?, + horizontalAccuracy > 0, verticalAccuracy > 0 else { + return + } + Task { @MainActor in + self.currentLocation = latestLocation + } + } + + func locationManager(didUpdateHeading newHeading: CLHeading) { + guard let headingAccuracy = newHeading.headingAccuracy as CLLocationDirection?, + headingAccuracy > 0 else { + return + } + Task { @MainActor in + self.currentHeading = newHeading + } + } + + func stopLocationUpdates() {} + +} + /** TestCameraView uses the data saved in the changeset directory, to simulate mapping */ @@ -65,6 +109,7 @@ struct TestCameraView: View { @EnvironmentObject var sharedAppContext: SharedAppContext @EnvironmentObject var segmentationPipeline: SegmentationARPipeline @EnvironmentObject var userStateViewModel: UserStateViewModel + @EnvironmentObject var workspaceViewModel: WorkspaceViewModel @Environment(\.dismiss) var dismiss @StateObject private var manager: TestCameraManager = TestCameraManager() @@ -72,9 +117,10 @@ struct TestCameraView: View { @State private var cameraHintDefaultText: String = ARCameraViewConstants.Texts.cameraHintPlaceholderText @State private var cameraHintText: String = ARCameraViewConstants.Texts.cameraHintPlaceholderText -// var locationManager: LocationManager = LocationManager() - @State private var captureLocation: CLLocationCoordinate2D? - @State private var captureHeading: CLLocationDirection? + var locationManager: LocationManagerPlaceholder = LocationManagerPlaceholder() +// @State private var captureLocation: CLLocationCoordinate2D? +// @State private var captureHeading: CLLocationDirection? + @StateObject private var mappingDataStatusViewModel = MappingDataStatusViewModel() @State private var showARCameraLearnMoreSheet = false @@ -170,8 +216,16 @@ struct TestCameraView: View { ) sharedAppData.currentDatasetDecoder = datasetDecoder self.datasetCaptureData = datasetCaptureData - self.captureLocation = datasetCaptureData.location - self.captureHeading = datasetCaptureData.heading + if let captureLocation = datasetCaptureData.location { + self.locationManager.locationManager(didUpdateLocations: [ + CLLocation(latitude: captureLocation.latitude, longitude: captureLocation.longitude) + ]) + } + if let captureHeading = datasetCaptureData.heading { + let heading = CLHeading() + heading.setValue(captureHeading, forKey: "trueHeading") + self.locationManager.locationManager(didUpdateHeading: heading) + } try manager.configure( selectedClasses: selectedClasses, segmentationPipeline: segmentationPipeline, metalContext: sharedAppContext.metalContext, @@ -197,8 +251,20 @@ struct TestCameraView: View { }, message: { Text(managerConfigureStatusViewModel.errorMessage) }) + .alert(ARCameraViewConstants.Texts.mappingDataStatusAlertTitleKey, isPresented: $mappingDataStatusViewModel.isFailed, actions: { + Button(ARCameraViewConstants.Texts.mappingDataStatusAlertRetryButtonKey) { + mappingDataStatusViewModel.update(isFailed: false, errorMessage: "") + handleLocationUpdate(oldLocation: nil, newLocation: locationManager.currentLocation) + } + Button(ARCameraViewConstants.Texts.mappingDataStatusAlertDismissButtonKey) { + mappingDataStatusViewModel.update(isFailed: false, errorMessage: "") + dismiss() + } + }, message: { + Text(mappingDataStatusViewModel.errorMessage) + }) .fullScreenCover(isPresented: $showAnnotationView) { - if let captureLocation { + if let captureLocation = locationManager.currentLocation?.coordinate { AnnotationView( selectedClasses: selectedClasses, captureLocation: captureLocation, apiChangesetUploadController: apiChangesetUploadController @@ -219,6 +285,12 @@ struct TestCameraView: View { } } } +// .onChange(of: manager.interfaceOrientation) { oldOrientation, newOrientation in +// locationManager.updateOrientation(newOrientation) +// } + .onChange(of: locationManager.currentLocation) { oldLocation, newLocation in + handleLocationUpdate(oldLocation: oldLocation, newLocation: newLocation) + } .onChange(of: currentIndex) { oldValue, newValue in do { guard let datasetDecoder = sharedAppData.currentDatasetDecoder else { @@ -228,8 +300,17 @@ struct TestCameraView: View { datasetDecoder: datasetDecoder, enhancedAnalysisMode: userStateViewModel.isEnhancedAnalysisEnabled ) self.datasetCaptureData = datasetCaptureData - self.captureLocation = datasetCaptureData.location - self.captureHeading = datasetCaptureData.heading + if let captureLocation = datasetCaptureData.location { + self.locationManager.locationManager(didUpdateLocations: [ + CLLocation(latitude: captureLocation.latitude, longitude: captureLocation.longitude) + ]) + } + if let captureHeading = datasetCaptureData.heading { + let heading = CLHeading() + heading.setValue(captureHeading, forKey: "trueHeading") + self.locationManager.locationManager(didUpdateHeading: heading) + } + manager.handleSessionUpdate(datasetCaptureData: datasetCaptureData) /// For easier testing @@ -340,6 +421,42 @@ struct TestCameraView: View { } } + private func handleLocationUpdate(oldLocation: CLLocation?, newLocation: CLLocation?) { + var shouldUpdateMap = oldLocation == nil && newLocation != nil + if let oldLocation, let newLocation { + let distance = oldLocation.distance(from: newLocation) + shouldUpdateMap = distance > Constants.WorkspaceConstants.fetchUpdateRadiusThresholdInMeters + } + if !shouldUpdateMap { + return + } + Task { + do { + guard let workspaceId = workspaceViewModel.workspaceId, + let location = newLocation?.coordinate else { + throw ARCameraViewError.workspaceConfigurationFailed + } + guard let accessToken = userStateViewModel.getAccessToken() else { + throw ARCameraViewError.authenticationError + } + let mapData = try await WorkspaceService.shared.fetchMapData( + workspaceId: workspaceId, + location: location, + radius: Constants.WorkspaceConstants.fetchRadiusInMeters, + accessToken: accessToken, + environment: userStateViewModel.selectedEnvironment + ) + sharedAppData.currentMappingData.updateFeatures( + with: mapData, + accessibilityFeatureClasses: selectedClasses + ) + } catch { + /// TODO: Replace with an alert that either retries the fetch or dismissed the view. + setHintText(error.localizedDescription) + } + } + } + /// Set text for 2 seconds, and then fall back to placeholder private func setHintText(_ text: String) { cameraHintText = text From cb9ad35e6d14db061f20ee764c6c96475f9b4460 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Sun, 29 Mar 2026 22:38:45 -0700 Subject: [PATCH 7/9] Add initializers to OSW element structs to initialize from OSM elements --- .../TDEI/OSW/OSWLineString.swift | 52 +++++++++-- IOSAccessAssessment/TDEI/OSW/OSWPoint.swift | 41 +++++++-- IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift | 88 +++++++++++++++++-- .../APIChangesetUploadController.swift | 17 ++-- 4 files changed, 169 insertions(+), 29 deletions(-) diff --git a/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift b/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift index b636cd4c..83574e60 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift @@ -41,6 +41,34 @@ struct OSWLineString: OSWElement { self.additionalTags = additionalTags } + /** + Initializes an OSWLineString from an OSMWay and its associated OSMNodes. + + - Parameters: + - osmWay: The OSMWay object representing the way element from OpenStreetMap. + - osmElementClass: The OSWElementClass that defines the classification of the way element. + - osmNodes: An array of OSMNode objects that are associated with the OSMWay. These nodes generally represent the points that make up the way. But they may contain additional nodes that are not part of the way, so we filter them based on the node references in the OSMWay. + */ + init( + osmWay: OSMWay, + oswElementClass: OSWElementClass, + osmNodes: [OSMNode] + ) { + self.id = osmWay.id + self.version = osmWay.version + self.oswElementClass = oswElementClass + self.attributeValues = [:] + self.calculatedAttributeValues = [:] + self.experimentalAttributeValues = [:] + let nodeRefs = osmWay.nodeRefs + self.points = osmNodes.compactMap { osmNode in + if !nodeRefs.contains(osmNode.id) { + return nil + } + return OSWPoint(osmNode: osmNode, oswElementClass: oswElementClass) + } + } + var tags: [String: String] { var identifyingFieldTags: [String: String] = [:] if oswElementClass.geometry == .linestring { @@ -52,14 +80,22 @@ struct OSWLineString: OSWElement { if let calculatedAttributeValues { calculatedAttributeTags = getTagsFromAttributeValues(attributeValues: calculatedAttributeValues, isCalculated: true) } - let tags = identifyingFieldTags.merging(attributeTags) { _, new in - return new - }.merging(experimentalAttributeTags) { _, new in - return new - }.merging(calculatedAttributeTags) { _, new in - return new - }.merging(additionalTags) { _, new in - return new + /** + The merging strategy for tags is to prioritize as follows (high to low): + 1. Identifying Field Tags: These are derived from the OSWElementClass and are essential for defining the type of element. + 2. Attribute Tags: These are derived from the attribute values of the element. + 3. Experimental Attribute Tags: These are derived from the experimental attribute values and may represent new or in-testing features. + 4. Calculated Attribute Tags: These are derived from calculated attribute values and may represent attributes that are not directly set but inferred from other data. + 5. Additional Tags: These are any extra tags that may be added for specific use cases or to provide additional context. + */ + let tags = identifyingFieldTags.merging(attributeTags) { old, new in + return old + }.merging(experimentalAttributeTags) { old, new in + return old + }.merging(calculatedAttributeTags) { old, new in + return old + }.merging(additionalTags) { old, new in + return old } return tags } diff --git a/IOSAccessAssessment/TDEI/OSW/OSWPoint.swift b/IOSAccessAssessment/TDEI/OSW/OSWPoint.swift index 35eb77b3..87ad093d 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWPoint.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWPoint.swift @@ -43,6 +43,23 @@ struct OSWPoint: OSWElement { self.additionalTags = additionalTags } + init( + osmNode: OSMNode, + oswElementClass: OSWElementClass + ) { + self.id = osmNode.id + self.version = osmNode.version + self.oswElementClass = oswElementClass + self.latitude = osmNode.latitude + self.longitude = osmNode.longitude + self.attributeValues = [:] + self.calculatedAttributeValues = [:] + self.experimentalAttributeValues = [:] + /// NOTE: Some tags might actually correspond to attribute values, but these will be overwritten when the attribute values are set. + /// The OSM xml functions are designed such that attribute value tags take precedence over additional tags, so this should not cause any issues. + self.additionalTags = osmNode.tags + } + var tags: [String: String] { var identifyingFieldTags: [String: String] = [:] if oswElementClass.geometry == .point { @@ -54,14 +71,22 @@ struct OSWPoint: OSWElement { if let calculatedAttributeValues { calculatedAttributeTags = getTagsFromAttributeValues(attributeValues: calculatedAttributeValues, isCalculated: true) } - let tags = identifyingFieldTags.merging(attributeTags) { _, new in - return new - }.merging(experimentalAttributeTags) { _, new in - return new - }.merging(calculatedAttributeTags) { _, new in - return new - }.merging(additionalTags) { _, new in - return new + /** + The merging strategy for tags is to prioritize as follows (high to low): + 1. Identifying Field Tags: These are derived from the OSWElementClass and are essential for defining the type of element. + 2. Attribute Tags: These are derived from the attribute values of the element. + 3. Experimental Attribute Tags: These are derived from the experimental attribute values and may represent new or in-testing features. + 4. Calculated Attribute Tags: These are derived from calculated attribute values and may represent attributes that are not directly set but inferred from other data. + 5. Additional Tags: These are any extra tags that may be added for specific use cases or to provide additional context. + */ + let tags = identifyingFieldTags.merging(attributeTags) { old, new in + return old + }.merging(experimentalAttributeTags) { old, new in + return old + }.merging(calculatedAttributeTags) { old, new in + return old + }.merging(additionalTags) { old, new in + return old } return tags } diff --git a/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift b/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift index e85fb2ff..79785f43 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift @@ -61,6 +61,78 @@ struct OSWPolygon: OSWElement { self.additionalTags = additionalTags } + /** + Initializes an OSWPolygon from an OSMRelation and its member elements. + + - Parameters: + - osmRelation: The OSMRelation object representing the relation element in OSM. + - oswElementClass: The OSWElementClass corresponding to the relation's tags. + - osmMemberElements: An array of OSWElement objects representing the members of the relation. This array can actually represent nested relations as discreet elements. This initializer is supposed to identify the members of the relation and assign them the correct roles. + */ + init( + osmRelation: OSMRelation, + oswElementClass: OSWElementClass, + osmMemberElements: [any OSMElement] + ) { + self.id = osmRelation.id + self.version = osmRelation.version + self.oswElementClass = oswElementClass + self.attributeValues = [:] + self.calculatedAttributeValues = [:] + self.experimentalAttributeValues = [:] + + let osmMemberRefs: [OSMRelationMember] = osmRelation.members + let osmNodeMemberRefs = osmMemberRefs.filter { $0.type == .node } + let osmWayMemberRefs = osmMemberRefs.filter { $0.type == .way } + let osmRelationMemberRefs = osmMemberRefs.filter { $0.type == .relation } + + let osmNodeElements: [OSMNode] = osmMemberElements.filter { element in + return osmNodeMemberRefs.contains { $0.ref == element.id } + }.compactMap { element in + return element as? OSMNode + } + let osmWayElements: [OSMWay] = osmMemberElements.filter { element in + return osmWayMemberRefs.contains { $0.ref == element.id } + }.compactMap { element in + return element as? OSMWay + } + let osmRelationElements: [OSMRelation] = osmMemberElements.filter { element in + return osmRelationMemberRefs.contains { $0.ref == element.id } + }.compactMap { element in + return element as? OSMRelation + } + + var oswRelationMembers: [OSWRelationMember] = [] + osmNodeMemberRefs.forEach { osmNodeMemberRef in + if let matchingOSMNodeElement = osmNodeElements.first(where: { $0.id == osmNodeMemberRef.ref }) { + let oswPoint: OSWPoint = OSWPoint(osmNode: matchingOSMNodeElement, oswElementClass: oswElementClass) + let oswRelationMember = OSWRelationMember(element: oswPoint, role: osmNodeMemberRef.role) + oswRelationMembers.append(oswRelationMember) + } + } + osmWayMemberRefs.forEach { osmWayMemberRef in + if let matchingOSMWayElement = osmWayElements.first(where: { $0.id == osmWayMemberRef.ref }) { + let oswLineString: OSWLineString = OSWLineString( + osmWay: matchingOSMWayElement, oswElementClass: oswElementClass, + osmNodes: osmNodeElements + ) + let oswRelationMember = OSWRelationMember(element: oswLineString, role: osmWayMemberRef.role) + oswRelationMembers.append(oswRelationMember) + } + } + osmRelationMemberRefs.forEach { osmRelationMemberRef in + if let matchingOSMRelationElement = osmRelationElements.first(where: { $0.id == osmRelationMemberRef.ref }) { + let oswPolygon: OSWPolygon = OSWPolygon( + osmRelation: matchingOSMRelationElement, oswElementClass: oswElementClass, + osmMemberElements: osmMemberElements + ) + let oswRelationMember = OSWRelationMember(element: oswPolygon, role: osmRelationMemberRef.role) + oswRelationMembers.append(oswRelationMember) + } + } + self.members = oswRelationMembers + } + var tags: [String: String] { var identifyingFieldTags: [String: String] = [:] if oswElementClass.geometry == .polygon { @@ -72,14 +144,14 @@ struct OSWPolygon: OSWElement { if let calculatedAttributeValues { calculatedAttributeTags = getTagsFromAttributeValues(attributeValues: calculatedAttributeValues, isCalculated: true) } - let tags = identifyingFieldTags.merging(attributeTags) { _, new in - return new - }.merging(experimentalAttributeTags) { _, new in - return new - }.merging(calculatedAttributeTags) { _, new in - return new - }.merging(additionalTags) { _, new in - return new + let tags = identifyingFieldTags.merging(attributeTags) { old, new in + return old + }.merging(experimentalAttributeTags) { old, new in + return old + }.merging(calculatedAttributeTags) { old, new in + return old + }.merging(additionalTags) { old, new in + return old } return tags } diff --git a/IOSAccessAssessment/TDEI/Transmission/APIChangesetUploadController.swift b/IOSAccessAssessment/TDEI/Transmission/APIChangesetUploadController.swift index bab6cf12..384de626 100644 --- a/IOSAccessAssessment/TDEI/Transmission/APIChangesetUploadController.swift +++ b/IOSAccessAssessment/TDEI/Transmission/APIChangesetUploadController.swift @@ -22,6 +22,18 @@ enum APIChangesetUploadError: Error, LocalizedError { } } +/** + This controller is responsible for handling the upload of accessibility features as OSW elements to the OSW API, and mapping the response back to the accessibility features with their new ids and other details from OSM. + + The general workflow for the upload is as follows: + 1. Map the accessibility features to OSW elements based on their geometry type (point, linestring, polygon). This involves creating the corresponding OSW elements (OSWPoint, OSWLineString, OSWPolygon) and preparing any additional tags or properties that need to be uploaded along with the elements. + 2. Prepare the upload operations (create, modify) for the OSW elements and perform the upload using the ChangesetService. + 3. Handle the response from the upload to get the new ids and details for the uploaded OSW elements. This involves mapping the response back to the original accessibility features using a cache that keeps track of the mapping between the original features and the OSW elements. + 4. Return the results of the upload, including the mapped accessibility features with their new ids and any failed uploads. + + - TODO: Eventually, the methods here such as featureToPoint, featureToLineString, featureToPolygon can be moved to the AccessibilityFeatureProtocol as extension methods (or a dedicated helper class) since they are responsible for mapping an accessibility feature to an OSW element, which is a core responsibility of the accessibility feature model. + + */ class APIChangesetUploadController: ObservableObject { private var idGenerator: IntIdGenerator = IntIdGenerator() public var capturedFrameIds: Set = [] @@ -477,16 +489,11 @@ extension APIChangesetUploadController { } var oswElements: [any OSWElement] = [] -// guard let featureLocationArrays: [[CLLocationCoordinate2D]] = feature.locationDetails?.coordinates, -// let firstLocationArray = featureLocationArrays.first, !firstLocationArray.isEmpty else { -// return nil -// } guard let featureLocationDetails: OSMLocationDetails = feature.locationDetails, !featureLocationDetails.locations.isEmpty else { return nil } featureLocationDetails.locations.forEach { locationElement in -// featureLocationArrays.forEach { locationArray in guard !locationElement.coordinates.isEmpty else { return } var oswPoints: [OSWPoint] = [] locationElement.coordinates.forEach { location in From a4160bad1aa82fdd1cd8cab1da61d1e5f595c463 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Sun, 29 Mar 2026 23:33:31 -0700 Subject: [PATCH 8/9] Complete initial version of getting map data from map.json response and other fixes --- .../Definitions/CurrentMappingData.swift | 78 ++++++++++++++++++- .../TDEI/OSM/OSMMapDataResponse.swift | 8 +- .../TDEI/OSW/OSWGeometry.swift | 11 +++ .../TDEI/OSW/OSWLineString.swift | 3 +- IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift | 24 ++++-- 5 files changed, 110 insertions(+), 14 deletions(-) diff --git a/IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift b/IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift index 39eb3117..df13690a 100644 --- a/IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift +++ b/IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift @@ -6,25 +6,95 @@ // import Foundation +import CoreLocation enum CurrentMappingDataError: Error, LocalizedError { } class CurrentMappingData { - var featuresMap: [AccessibilityFeatureClass: [OSWElement]] = [:] - var otherFeatures: [OSWElement] = [] + var featuresMap: [AccessibilityFeatureClass: [any OSWElement]] = [:] +// var otherFeatures: [OSWElement] = [] init() { } init(osmMapDataResponse: OSMMapDataResponse, accessibilityFeatureClasses: [AccessibilityFeatureClass]) { - updateFeatures(with: osmMapDataResponse, accessibilityFeatureClasses: accessibilityFeatureClasses) + self.featuresMap = getFeatures(with: osmMapDataResponse, accessibilityFeatureClasses: accessibilityFeatureClasses) } - func updateFeatures(with osmMapDataResponse: OSMMapDataResponse, accessibilityFeatureClasses: [AccessibilityFeatureClass]) { + func getFeatures( + with osmMapDataResponse: OSMMapDataResponse, accessibilityFeatureClasses: [AccessibilityFeatureClass] + ) -> [AccessibilityFeatureClass: [any OSWElement]] { + var featuresMap: [AccessibilityFeatureClass: [any OSWElement]] = [:] + let osmMapDataResponseElements: [OSMMapDataResponseElement] = osmMapDataResponse.elements + let osmElements: [any OSMElement] = osmMapDataResponseElements.compactMap { element in + return element.toOSMElement() + } + var featureNodes: [String: OSMNode] = [:] + var featureWays: [String: OSMWay] = [:] + var featureRelations: [String: OSMRelation] = [:] + osmElements.forEach { osmElement in + if let osmNode = osmElement as? OSMNode { + featureNodes[osmElement.id] = osmNode + } else if let osmWay = osmElement as? OSMWay { + featureWays[osmElement.id] = osmWay + } else if let osmRelation = osmElement as? OSMRelation { + featureRelations[osmElement.id] = osmRelation + } + } + for featureClass in accessibilityFeatureClasses { + if featuresMap[featureClass] == nil { + featuresMap[featureClass] = [] + } let oswElementClass = featureClass.oswPolicy.oswElementClass + let geometry = oswElementClass.geometry + let identifyingFieldTags: [String: String] = oswElementClass.identifyingFieldTags + + switch geometry.osmElementType { + case .node: + let matchingOSWPoints: [OSWPoint] = featureNodes.values.filter { node in + return identifyingFieldTags.allSatisfy { tagKey, tagValue in + return node.tags[tagKey] == tagValue + } + }.compactMap { node in + return OSWPoint(osmNode: node, oswElementClass: oswElementClass) + } + featuresMap[featureClass]?.append(contentsOf: matchingOSWPoints) + case .way: + let matchingOSWLineStrings: [OSWLineString] = featureWays.values.filter { way in + return identifyingFieldTags.allSatisfy { tagKey, tagValue in + return way.tags[tagKey] == tagValue + } + }.compactMap { way in + return OSWLineString( + osmWay: way, oswElementClass: oswElementClass, + osmNodes: Array(featureNodes.values) + ) + } + featuresMap[featureClass]?.append(contentsOf: matchingOSWLineStrings) + case .relation: + let matchingOSWPolygons: [OSWPolygon] = featureRelations.values.filter { relation in + return identifyingFieldTags.allSatisfy { tagKey, tagValue in + return relation.tags[tagKey] == tagValue + } + }.compactMap { relation in + return OSWPolygon( + osmRelation: relation, oswElementClass: oswElementClass, + osmMemberElements: osmElements + ) + } + featuresMap[featureClass]?.append(contentsOf: matchingOSWPolygons) + } } + return featuresMap + } + + func getNearestFeature( + to location: CLLocationCoordinate2D, featureClass: AccessibilityFeatureClass, + distanceThreshold: CLLocationDistance = 50.0 + ) -> (any OSWElement)? { + return nil } } diff --git a/IOSAccessAssessment/TDEI/OSM/OSMMapDataResponse.swift b/IOSAccessAssessment/TDEI/OSM/OSMMapDataResponse.swift index 7b9c5d87..d2379dbd 100644 --- a/IOSAccessAssessment/TDEI/OSM/OSMMapDataResponse.swift +++ b/IOSAccessAssessment/TDEI/OSM/OSMMapDataResponse.swift @@ -71,10 +71,10 @@ struct OSMMapDataResponseElement: Codable { members = try values.decodeIfPresent([OSMRelationMember].self, forKey: .members) ?? [] } - func toOSMElement() throws -> (any OSMElement)? { + func toOSMElement() -> (any OSMElement)? { switch type { case .node: - return try toOSMNode() + return toOSMNode() case .way: return toOSMWay() case .relation: @@ -82,9 +82,9 @@ struct OSMMapDataResponseElement: Codable { } } // - private func toOSMNode() throws -> OSMNode { + private func toOSMNode() -> OSMNode? { guard let lat = lat, let lon = lon else { - throw OSMMapDataResponseError.invalidNodeCoordinates + return nil } return OSMNode(id: "\(id)", version: "\(version)", latitude: lat, longitude: lon, tags: tags) } diff --git a/IOSAccessAssessment/TDEI/OSW/OSWGeometry.swift b/IOSAccessAssessment/TDEI/OSW/OSWGeometry.swift index 68c25ecf..e2e8b33d 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWGeometry.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWGeometry.swift @@ -20,4 +20,15 @@ enum OSWGeometry: String, CaseIterable, Hashable, Codable { return "Polygon" } } + + var osmElementType: OSMElementType { + switch self { + case .point: + return .node + case .linestring: + return .way + case .polygon: + return .way + } + } } diff --git a/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift b/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift index 83574e60..e80415ad 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift @@ -61,8 +61,9 @@ struct OSWLineString: OSWElement { self.calculatedAttributeValues = [:] self.experimentalAttributeValues = [:] let nodeRefs = osmWay.nodeRefs + let nodeRefSet = Set(nodeRefs) self.points = osmNodes.compactMap { osmNode in - if !nodeRefs.contains(osmNode.id) { + if !nodeRefSet.contains(osmNode.id) { return nil } return OSWPoint(osmNode: osmNode, oswElementClass: oswElementClass) diff --git a/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift b/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift index 79785f43..f1d049ee 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift @@ -86,45 +86,59 @@ struct OSWPolygon: OSWElement { let osmWayMemberRefs = osmMemberRefs.filter { $0.type == .way } let osmRelationMemberRefs = osmMemberRefs.filter { $0.type == .relation } + let osmMemberElementsDict: [String: any OSMElement] = Dictionary( + uniqueKeysWithValues: osmMemberElements.map { ($0.id, $0) } + ) let osmNodeElements: [OSMNode] = osmMemberElements.filter { element in return osmNodeMemberRefs.contains { $0.ref == element.id } }.compactMap { element in return element as? OSMNode } + let osmNodeElementsDict: [String: OSMNode] = Dictionary(uniqueKeysWithValues: osmNodeElements.map { ($0.id, $0) }) let osmWayElements: [OSMWay] = osmMemberElements.filter { element in return osmWayMemberRefs.contains { $0.ref == element.id } }.compactMap { element in return element as? OSMWay } + let osmWayElementsDict: [String: OSMWay] = Dictionary(uniqueKeysWithValues: osmWayElements.map { ($0.id, $0) }) let osmRelationElements: [OSMRelation] = osmMemberElements.filter { element in return osmRelationMemberRefs.contains { $0.ref == element.id } }.compactMap { element in return element as? OSMRelation } + let osmRelationElementsDict: [String: OSMRelation] = Dictionary( + uniqueKeysWithValues: osmRelationElements.map { ($0.id, $0) } + ) var oswRelationMembers: [OSWRelationMember] = [] osmNodeMemberRefs.forEach { osmNodeMemberRef in - if let matchingOSMNodeElement = osmNodeElements.first(where: { $0.id == osmNodeMemberRef.ref }) { + if let matchingOSMNodeElement = osmNodeElementsDict[osmNodeMemberRef.ref] { let oswPoint: OSWPoint = OSWPoint(osmNode: matchingOSMNodeElement, oswElementClass: oswElementClass) let oswRelationMember = OSWRelationMember(element: oswPoint, role: osmNodeMemberRef.role) oswRelationMembers.append(oswRelationMember) } } osmWayMemberRefs.forEach { osmWayMemberRef in - if let matchingOSMWayElement = osmWayElements.first(where: { $0.id == osmWayMemberRef.ref }) { + if let matchingOSMWayElement = osmWayElementsDict[osmWayMemberRef.ref] { + let matchingOSWWayNodes = matchingOSMWayElement.nodeRefs.compactMap { nodeRef in + return osmNodeElementsDict[nodeRef] + } let oswLineString: OSWLineString = OSWLineString( osmWay: matchingOSMWayElement, oswElementClass: oswElementClass, - osmNodes: osmNodeElements + osmNodes: matchingOSWWayNodes ) let oswRelationMember = OSWRelationMember(element: oswLineString, role: osmWayMemberRef.role) oswRelationMembers.append(oswRelationMember) } } osmRelationMemberRefs.forEach { osmRelationMemberRef in - if let matchingOSMRelationElement = osmRelationElements.first(where: { $0.id == osmRelationMemberRef.ref }) { + if let matchingOSMRelationElement = osmRelationElementsDict[osmRelationMemberRef.ref] { + let matchingOSMRelationMemberElements = matchingOSMRelationElement.members.compactMap { memberRef in + return osmMemberElementsDict[memberRef.ref] + } let oswPolygon: OSWPolygon = OSWPolygon( osmRelation: matchingOSMRelationElement, oswElementClass: oswElementClass, - osmMemberElements: osmMemberElements + osmMemberElements: matchingOSMRelationMemberElements ) let oswRelationMember = OSWRelationMember(element: oswPolygon, role: osmRelationMemberRef.role) oswRelationMembers.append(oswRelationMember) From 8e8ebd0b57e0ad6d76b3ab5d4d9a15799e15f918 Mon Sep 17 00:00:00 2001 From: himanshunaidu Date: Mon, 30 Mar 2026 13:16:06 -0700 Subject: [PATCH 9/9] Final corrections to retrieving existing map and converting to a feature dictionary --- .../Definitions/CurrentMappingData.swift | 18 +++++++++++++++++- .../TDEI/OSW/OSWLineString.swift | 1 + IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift | 1 + IOSAccessAssessment/View/ARCameraView.swift | 7 +++---- .../View/TestMode/TestCameraView.swift | 7 +++---- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift b/IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift index df13690a..504e068f 100644 --- a/IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift +++ b/IOSAccessAssessment/Shared/Definitions/CurrentMappingData.swift @@ -11,7 +11,8 @@ import CoreLocation enum CurrentMappingDataError: Error, LocalizedError { } -class CurrentMappingData { +class CurrentMappingData: CustomStringConvertible { + var featuresMap: [AccessibilityFeatureClass: [any OSWElement]] = [:] // var otherFeatures: [OSWElement] = [] @@ -19,8 +20,23 @@ class CurrentMappingData { } + var description: String { + var description = "CurrentMappingData:\n" + for (featureClass, features) in featuresMap { + description += "- \(featureClass.name): \(features.count) features\n" + } + return description + } + init(osmMapDataResponse: OSMMapDataResponse, accessibilityFeatureClasses: [AccessibilityFeatureClass]) { self.featuresMap = getFeatures(with: osmMapDataResponse, accessibilityFeatureClasses: accessibilityFeatureClasses) + print("Initialized features map with OSM data. \n\(description)") + } + + /// Note: Replaces the feature map instead of incrementally updating it. + func update(osmMapDataResponse: OSMMapDataResponse, accessibilityFeatureClasses: [AccessibilityFeatureClass]) { + self.featuresMap = getFeatures(with: osmMapDataResponse, accessibilityFeatureClasses: accessibilityFeatureClasses) + print("Updated features map with new OSM data. \n\(description)") } func getFeatures( diff --git a/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift b/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift index e80415ad..9e520acf 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWLineString.swift @@ -68,6 +68,7 @@ struct OSWLineString: OSWElement { } return OSWPoint(osmNode: osmNode, oswElementClass: oswElementClass) } + self.additionalTags = osmWay.tags } var tags: [String: String] { diff --git a/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift b/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift index f1d049ee..0aae8fac 100644 --- a/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift +++ b/IOSAccessAssessment/TDEI/OSW/OSWPolygon.swift @@ -145,6 +145,7 @@ struct OSWPolygon: OSWElement { } } self.members = oswRelationMembers + self.additionalTags = osmRelation.tags } var tags: [String: String] { diff --git a/IOSAccessAssessment/View/ARCameraView.swift b/IOSAccessAssessment/View/ARCameraView.swift index eb43c9f2..6c055883 100644 --- a/IOSAccessAssessment/View/ARCameraView.swift +++ b/IOSAccessAssessment/View/ARCameraView.swift @@ -367,13 +367,12 @@ struct ARCameraView: View { accessToken: accessToken, environment: userStateViewModel.selectedEnvironment ) - sharedAppData.currentMappingData.updateFeatures( - with: mapData, + sharedAppData.currentMappingData.update( + osmMapDataResponse: mapData, accessibilityFeatureClasses: selectedClasses ) } catch { - /// TODO: Replace with an alert that either retries the fetch or dismissed the view. - setHintText(error.localizedDescription) + mappingDataStatusViewModel.update(isFailed: true, errorMessage: error.localizedDescription) } } } diff --git a/IOSAccessAssessment/View/TestMode/TestCameraView.swift b/IOSAccessAssessment/View/TestMode/TestCameraView.swift index 5c8192a9..4c2485f0 100644 --- a/IOSAccessAssessment/View/TestMode/TestCameraView.swift +++ b/IOSAccessAssessment/View/TestMode/TestCameraView.swift @@ -446,13 +446,12 @@ struct TestCameraView: View { accessToken: accessToken, environment: userStateViewModel.selectedEnvironment ) - sharedAppData.currentMappingData.updateFeatures( - with: mapData, + sharedAppData.currentMappingData.update( + osmMapDataResponse: mapData, accessibilityFeatureClasses: selectedClasses ) } catch { - /// TODO: Replace with an alert that either retries the fetch or dismissed the view. - setHintText(error.localizedDescription) + mappingDataStatusViewModel.update(isFailed: true, errorMessage: error.localizedDescription) } } }