diff --git a/IOSAccessAssessment/View/ARCameraView.swift b/IOSAccessAssessment/View/ARCameraView.swift index ea592ad..af5ed3a 100644 --- a/IOSAccessAssessment/View/ARCameraView.swift +++ b/IOSAccessAssessment/View/ARCameraView.swift @@ -142,6 +142,7 @@ class MappingDataStatusViewModel: ObservableObject { struct ARCameraView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] @EnvironmentObject var sharedAppData: SharedAppData @EnvironmentObject var sharedAppContext: SharedAppContext @@ -298,7 +299,8 @@ struct ARCameraView: View { .fullScreenCover(isPresented: $showAnnotationView) { if let captureLocation = locationManager.currentLocation?.coordinate { AnnotationView( - selectedClasses: selectedClasses, captureLocation: captureLocation, + selectedClasses: selectedClasses, selectedAttributesByClass: selectedAttributesByClass, + captureLocation: captureLocation, apiChangesetUploadController: apiChangesetUploadController ) } else { diff --git a/IOSAccessAssessment/View/AnnotationView.swift b/IOSAccessAssessment/View/AnnotationView.swift index 46e4db3..abb8e5b 100644 --- a/IOSAccessAssessment/View/AnnotationView.swift +++ b/IOSAccessAssessment/View/AnnotationView.swift @@ -225,6 +225,7 @@ class APIChangesetUploadStatusViewModel: ObservableObject { struct AnnotationView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] let captureLocation: CLLocationCoordinate2D let apiChangesetUploadController: APIChangesetUploadController @@ -521,7 +522,7 @@ struct AnnotationView: View { var lastEstimationError: Error? = nil accessibilityFeatures.forEach { accessibilityFeature in do { - try attributeEstimationPipeline.setPrerequisites(accessibilityFeature: accessibilityFeature) +// try attributeEstimationPipeline.setPrerequisites(accessibilityFeature: accessibilityFeature) try attributeEstimationPipeline.processLocationRequest( deviceLocation: captureLocation, accessibilityFeature: accessibilityFeature @@ -531,7 +532,8 @@ struct AnnotationView: View { mappingData: sharedAppData.currentMappingData, accessibilityFeature: accessibilityFeature ) try attributeEstimationPipeline.processAttributeRequest( - accessibilityFeature: accessibilityFeature + accessibilityFeature: accessibilityFeature, + attributes: selectedAttributesByClass[currentClass] ?? [] ) attributeEstimationPipeline.clearPrerequisites() } catch { @@ -575,11 +577,13 @@ struct AnnotationView: View { featureSelectedStatus[oldFeature.id] = false /// Selected, but not highlighted } /// MARK: Temporary code for visualization. Incurs significant performance overhead. + /// TODO: Check what happens when we use the cache-based methods instead if currentClass.kind.attributes.contains(where: { $0 == .width || $0 == .runningSlope || $0 == .crossSlope || $0 == .surfaceIntegrity }) { + let worldPoints = try attributeEstimationPipeline.getWorldPoints(accessibilityFeature: currentFeature) let plane = try attributeEstimationPipeline.calculateAlignedPlane( - accessibilityFeature: currentFeature, worldPoints: nil + accessibilityFeature: currentFeature, worldPoints: worldPoints ) let projectedPlane = try attributeEstimationPipeline.calculateProjectedPlane( accessibilityFeature: currentFeature, plane: plane diff --git a/IOSAccessAssessment/View/SetupView.swift b/IOSAccessAssessment/View/SetupView.swift index cee2e43..31ff02d 100644 --- a/IOSAccessAssessment/View/SetupView.swift +++ b/IOSAccessAssessment/View/SetupView.swift @@ -71,9 +71,17 @@ enum SetupViewConstants { static let profileIcon = "person.crop.circle" static let logoutIcon = "rectangle.portrait.and.arrow.right" static let uploadIcon = "arrow.up" + + /// Class Selection static let classSelectionColorHintIcon = "circle.fill" static let classSelectionColorHintBorderIcon = "circle" + /// Attribute Selection + static let attributeSelectedStatusIcon = "checkmark.circle.fill" + static let attributeUnselectedStatusIcon = "circle" + static let attributeSectionExpandedIcon = "chevron.up.circle" + static let attributeSectionCollapsedIcon = "chevron.down.circle" + /// InfoTip static let infoIcon = "info.circle" } @@ -218,9 +226,11 @@ class CurrentDatasetStatusViewModel: ObservableObject { struct SetupView: View { @State private var selectedClasses = Set() + @State private var selectedAttributesByClass = [AccessibilityFeatureClass: Set]() private var isSelectionEmpty: Bool { return (self.selectedClasses.count == 0) } + @State private var expandedAttributeSections: Set = [] @EnvironmentObject var workspaceViewModel: WorkspaceViewModel @EnvironmentObject var userStateViewModel: UserStateViewModel @@ -318,37 +328,7 @@ struct SetupView: View { List { ForEach(SharedAppConstants.SelectedAccessibilityFeatureConfig.classes, id: \.self) { accessibilityFeatureClass in - Button(action: { - if self.selectedClasses.contains(accessibilityFeatureClass) { - self.selectedClasses.remove(accessibilityFeatureClass) - } else { - self.selectedClasses.insert(accessibilityFeatureClass) - } - }) { - HStack { - Text(accessibilityFeatureClass.name) - .foregroundStyle( - self.selectedClasses.contains(accessibilityFeatureClass) - ? SetupViewConstants.Colors.selectedClass - : SetupViewConstants.Colors.unselectedClass - ) - Spacer() - Image(systemName: SetupViewConstants.Images.classSelectionColorHintIcon) - .resizable() - .frame(width: 20, height: 20) - .foregroundStyle(Color(UIColor(ciColor: accessibilityFeatureClass.color))) - .overlay( - Image(systemName: SetupViewConstants.Images.classSelectionColorHintBorderIcon) - .resizable() - .frame(width: 20, height: 20) - .foregroundStyle( - self.selectedClasses.contains(accessibilityFeatureClass) - ? SetupViewConstants.Colors.selectedClass - : SetupViewConstants.Colors.unselectedClass - ) - ) - } - } + listElementView(for: accessibilityFeatureClass) } } } @@ -445,12 +425,171 @@ struct SetupView: View { .environmentObject(self.segmentationPipeline) } + @ViewBuilder + private func listElementView(for accessibilityFeatureClass: AccessibilityFeatureClass) -> some View { + HStack { + let isClassSelected = self.selectedClasses.contains(accessibilityFeatureClass) + let attributes = Array(accessibilityFeatureClass.kind.attributes).sorted(by: { $0.name < $1.name }) + let hasAttributes = !attributes.isEmpty + let isExpanded = isAttributeSectionExpanded(for: accessibilityFeatureClass) + + VStack(alignment: .leading, spacing: 8) { + HStack { + Button(action: { + toggleClass(accessibilityFeatureClass) + }) { + HStack { + Text(accessibilityFeatureClass.name) + .foregroundStyle( + isClassSelected + ? SetupViewConstants.Colors.selectedClass + : SetupViewConstants.Colors.unselectedClass + ) + Spacer() + + Image(systemName: SetupViewConstants.Images.classSelectionColorHintIcon) + .resizable() + .frame(width: 20, height: 20) + .foregroundStyle(Color(UIColor(ciColor: accessibilityFeatureClass.color))) + .overlay( + Image(systemName: SetupViewConstants.Images.classSelectionColorHintBorderIcon) + .resizable() + .frame(width: 20, height: 20) + .foregroundStyle( + isClassSelected + ? SetupViewConstants.Colors.selectedClass + : SetupViewConstants.Colors.unselectedClass + ) + ) + } + } + .buttonStyle(.plain) + + if hasAttributes && isClassSelected { + Button(action: { + toggleAttributeSectionExpansion(for: accessibilityFeatureClass) + }) { + HStack(spacing: 4) { + Text(attributeSelectionSummary(for: accessibilityFeatureClass)) + .font(.caption) + .foregroundStyle(.secondary) + + Image(systemName: isExpanded ? SetupViewConstants.Images.attributeSectionExpandedIcon : SetupViewConstants.Images.attributeSectionCollapsedIcon) + .imageScale(.medium) + } + } + } + } + + if hasAttributes && isExpanded && isClassSelected { + VStack(alignment: .leading, spacing: 4) { + ForEach(attributes, id: \.self) { attribute in + Button(action: { + toggleAttribute(attribute, for: accessibilityFeatureClass) + }) { + HStack { + Text(attribute.name) + .font(.subheadline) + Spacer() + Image(systemName: isAttributeSelected(attribute, for: accessibilityFeatureClass) ? SetupViewConstants.Images.attributeSelectedStatusIcon : SetupViewConstants.Images.attributeUnselectedStatusIcon + ) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + .padding(.leading, 12) + .padding(.top, 4) +// .transition(.opacity.combined(with: .move(edge: .top))) + } + } + .padding(.vertical, 4) + } + } + @ViewBuilder private var mappingDestination: some View { if userStateViewModel.appMode == .standard { - ARCameraView(selectedClasses: Array(self.selectedClasses).sorted()) + ARCameraView( + selectedClasses: Array(self.selectedClasses).sorted(), + selectedAttributesByClass: self.selectedAttributesByClass + ) + } else { + TestEnvironmentListView( + selectedClasses: Array(self.selectedClasses).sorted(), + selectedAttributesByClass: self.selectedAttributesByClass + ) + } + } + + private func toggleClass(_ accessibilityFeatureClass: AccessibilityFeatureClass) { + if self.selectedClasses.contains(accessibilityFeatureClass) { + self.selectedClasses.remove(accessibilityFeatureClass) + + /// Clear the selected attributes for the class when it is deselected +// self.selectedAttributesByClass[accessibilityFeatureClass] = nil + self.expandedAttributeSections.remove(accessibilityFeatureClass) + } else { + self.selectedClasses.insert(accessibilityFeatureClass) + + if !self.selectedAttributesByClass.contains(where: { $0.key == accessibilityFeatureClass }) { + /// Add all the attributes for the class when it is selected + self.selectedAttributesByClass[accessibilityFeatureClass] = Set(accessibilityFeatureClass.kind.attributes) + } + } + } + + private func toggleAttribute( + _ attribute: AccessibilityFeatureAttribute, for accessibilityFeatureClass: AccessibilityFeatureClass + ) { + var selectedAttributes = selectedAttributesByClass[accessibilityFeatureClass, default: []] + + if selectedAttributes.contains(attribute) { + selectedAttributes.remove(attribute) + } else { + selectedAttributes.insert(attribute) + } + + selectedAttributesByClass[accessibilityFeatureClass] = selectedAttributes + } + + private func isAttributeSelected( + _ attribute: AccessibilityFeatureAttribute, + for accessibilityFeatureClass: AccessibilityFeatureClass + ) -> Bool { + selectedAttributesByClass[accessibilityFeatureClass, default: []].contains(attribute) + } + + private func toggleAttributeSectionExpansion(for accessibilityFeatureClass: AccessibilityFeatureClass) { +// withAnimation { + if expandedAttributeSections.contains(accessibilityFeatureClass) { + expandedAttributeSections.remove(accessibilityFeatureClass) + } else { + expandedAttributeSections.insert(accessibilityFeatureClass) + } +// } + } + + private func isAttributeSectionExpanded(for accessibilityFeatureClass: AccessibilityFeatureClass) -> Bool { + expandedAttributeSections.contains(accessibilityFeatureClass) + } + + private func attributeSelectionSummary( + for accessibilityFeatureClass: AccessibilityFeatureClass + ) -> String { + let attributes = accessibilityFeatureClass.kind.attributes + guard !attributes.isEmpty else { + return "" + } + let selectedCount = selectedAttributesByClass[accessibilityFeatureClass, default: []].count + + if selectedCount == attributes.count { + return "All" + } else if selectedCount == 0 { + return "None" } else { - TestEnvironmentListView(selectedClasses: Array(self.selectedClasses).sorted()) + return "\(selectedCount)/\(attributes.count)" } } diff --git a/IOSAccessAssessment/View/TestMode/TestCameraView.swift b/IOSAccessAssessment/View/TestMode/TestCameraView.swift index 1e94565..1f12748 100644 --- a/IOSAccessAssessment/View/TestMode/TestCameraView.swift +++ b/IOSAccessAssessment/View/TestMode/TestCameraView.swift @@ -103,6 +103,7 @@ class LocationManagerPlaceholder: NSObject, ObservableObject { */ struct TestCameraView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] let selectedEnvironment: APIEnvironment let workspaceId: String let changesetId: String @@ -268,7 +269,8 @@ struct TestCameraView: View { .fullScreenCover(isPresented: $showAnnotationView) { if let captureLocation = locationManager.currentLocation?.coordinate { AnnotationView( - selectedClasses: selectedClasses, captureLocation: captureLocation, + selectedClasses: selectedClasses, selectedAttributesByClass: selectedAttributesByClass, + captureLocation: captureLocation, apiChangesetUploadController: apiChangesetUploadController ) } else { diff --git a/IOSAccessAssessment/View/TestMode/TestListView.swift b/IOSAccessAssessment/View/TestMode/TestListView.swift index a439310..d380ac8 100644 --- a/IOSAccessAssessment/View/TestMode/TestListView.swift +++ b/IOSAccessAssessment/View/TestMode/TestListView.swift @@ -25,6 +25,7 @@ enum TestListViewError: Error, LocalizedError { struct TestEnvironmentListView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] @Environment(\.dismiss) var dismiss @@ -65,7 +66,10 @@ struct TestEnvironmentListView: View { } } .navigationDestination(item: $selectedEnvironment) { environmentDir in - TestWorkspaceListView(selectedClasses: selectedClasses, datasetLister: datasetLister) + TestWorkspaceListView( + selectedClasses: selectedClasses, selectedAttributesByClass: selectedAttributesByClass, + datasetLister: datasetLister + ) } } @@ -84,6 +88,7 @@ struct TestEnvironmentListView: View { */ struct TestWorkspaceListView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] @ObservedObject var datasetLister: DatasetLister @Environment(\.dismiss) var dismiss @@ -118,7 +123,10 @@ struct TestWorkspaceListView: View { } .navigationBarTitle("Test: Workspace Selection", displayMode: .inline) .navigationDestination(item: $selectedWorkspace) { workspaceDir in - TestChangesetListView(selectedClasses: selectedClasses, datasetLister: datasetLister) + TestChangesetListView( + selectedClasses: selectedClasses, selectedAttributesByClass: selectedAttributesByClass, + datasetLister: datasetLister + ) } } @@ -137,6 +145,7 @@ struct TestWorkspaceListView: View { */ struct TestChangesetListView: View { let selectedClasses: [AccessibilityFeatureClass] + let selectedAttributesByClass: [AccessibilityFeatureClass: Set] @ObservedObject var datasetLister: DatasetLister @Environment(\.dismiss) var dismiss @@ -184,7 +193,8 @@ struct TestChangesetListView: View { if let selectedEnvironment = datasetLister.selectedEnvironment, let selectedWorkspace = datasetLister.selectedWorkspace { TestCameraView( - selectedClasses: selectedClasses, selectedEnvironment: selectedEnvironment.apiEnvironment, + selectedClasses: selectedClasses, selectedAttributesByClass: selectedAttributesByClass, + selectedEnvironment: selectedEnvironment.apiEnvironment, workspaceId: selectedWorkspace.workspaceId, changesetId: changesetDir.changesetId ) } else {