diff --git a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift index c8c052f..6b775f7 100644 --- a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift +++ b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/AttributeEstimationPipeline.swift @@ -55,6 +55,10 @@ public enum AttributeEstimationPipelineConstants { /** An attribute estimation pipeline that processes editable accessibility features to estimate their attributes. + + - MARK: + The individual attribute calculation functions have a lot of redundant code to get the relevant properties from the prerequisite cache. + Find a way to streamline this. */ public class AttributeEstimationPipeline: ObservableObject { public struct PrerequisiteCache: Sendable { @@ -67,6 +71,10 @@ public class AttributeEstimationPipeline: ObservableObject { public var meshTriangles: [MeshTriangle]? = nil public var meshAlignedPlane: Plane? = nil public var meshProjectedPlane: ProjectedPlane? = nil + + /// Additional lazy properties for caching can be added here as needed. + public var surfaceNormalsGrid: SurfaceNormalsForPointsGrid? = nil + } public var captureImageData: (any CaptureImageDataProtocol)? @@ -261,6 +269,20 @@ public class AttributeEstimationPipeline: ObservableObject { try accessibilityFeature.setAttributeValue( surfaceIntegrityAttributeValue, for: .surfaceIntegrity, isCalculated: true ) + case .surfaceDisruption: + let surfaceDisruptionAttributeValue = try self.calculateSurfaceDisruption( + accessibilityFeature: accessibilityFeature + ) + try accessibilityFeature.setAttributeValue( + surfaceDisruptionAttributeValue, for: .surfaceDisruption, isCalculated: true + ) + case .heightFromGround: + let heightFromGroundAttributeValue = try self.calculateHeightFromGround( + accessibilityFeature: accessibilityFeature + ) + try accessibilityFeature.setAttributeValue( + heightFromGroundAttributeValue, for: .heightFromGround, isCalculated: true + ) default: continue } diff --git a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift index 0b75af5..9d04773 100644 --- a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift +++ b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/AttributeEstimation/Extensions/OtherAttributes/SurfaceIntegrityExtension.swift @@ -8,6 +8,7 @@ import SwiftUI import CoreLocation import PointNMapShaderTypes +import simd public extension AttributeEstimationPipeline { func calculateSurfaceIntegrity( @@ -43,9 +44,10 @@ public extension AttributeEstimationPipeline { let projectedPlane: ProjectedPlane = try self.prerequisiteCache.pointProjectedPlane ?? self.calculateProjectedPlane( accessibilityFeature: accessibilityFeature, plane: alignedPlane ) - let surfaceNormalsGrid: SurfaceNormalsForPointsGrid = try surfaceNormalsProcessor.getSurfaceNormalsFromWorldPoints( + let surfaceNormalsGrid: SurfaceNormalsForPointsGrid = try self.prerequisiteCache.surfaceNormalsGrid ?? surfaceNormalsProcessor.getSurfaceNormalsFromWorldPoints( worldPointsGrid: worldPointsGrid, plane: alignedPlane, projectedPlane: projectedPlane ) + self.prerequisiteCache.surfaceNormalsGrid = surfaceNormalsGrid let surfaceIntegrityResults = try surfaceIntegrityProcessor.getIntegrityResultsFromImage( worldPointsGrid: worldPointsGrid, plane: alignedPlane, surfaceNormalsForPointsGrid: surfaceNormalsGrid, damageDetectionResults: damageDetectionResults, captureData: captureImageData @@ -145,3 +147,151 @@ public extension AttributeEstimationPipeline { return worstStatus } } + +/** + Additional sub-attributes of surface-integrity + */ +/// Sub-attribute: Surface disruption +public extension AttributeEstimationPipeline { + func calculateSurfaceDisruption( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + let isMeshEnabled: Bool = self.captureMeshData != nil + if isMeshEnabled { + return try calculateSurfaceDisruptionFromMesh(accessibilityFeature: accessibilityFeature) + } + return try calculateSurfaceDisruptionFromImage(accessibilityFeature: accessibilityFeature) + } + + func calculateSurfaceDisruptionFromImage( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + guard let captureImageData = self.captureImageData else { + throw AttributeEstimationPipelineError.missingCaptureData + } + guard let surfaceNormalsProcessor = self.surfaceNormalsProcessor else { + throw AttributeEstimationPipelineError.missingPreprocessors + } + guard let surfaceIntegrityProcessor = self.surfaceIntegrityProcessor else { + throw AttributeEstimationPipelineError.missingPreprocessors + } + let damageDetectionResults = try getDamageDetectionResults(accessibilityFeature: accessibilityFeature) + let worldPointsGrid = try self.prerequisiteCache.worldPointsGrid ?? self.getWorldPointsGrid(accessibilityFeature: accessibilityFeature) + let worldPoints: [WorldPoint] = try self.prerequisiteCache.worldPoints ?? self.getWorldPoints( + accessibilityFeature: accessibilityFeature + ) + let alignedPlane: Plane = try self.prerequisiteCache.pointAlignedPlane ?? self.calculateAlignedPlane( + accessibilityFeature: accessibilityFeature, worldPoints: worldPoints + ) + let projectedPlane: ProjectedPlane = try self.prerequisiteCache.pointProjectedPlane ?? self.calculateProjectedPlane( + accessibilityFeature: accessibilityFeature, plane: alignedPlane + ) + let surfaceNormalsGrid: SurfaceNormalsForPointsGrid = try self.prerequisiteCache.surfaceNormalsGrid ?? surfaceNormalsProcessor.getSurfaceNormalsFromWorldPoints( + worldPointsGrid: worldPointsGrid, plane: alignedPlane, projectedPlane: projectedPlane + ) + self.prerequisiteCache.surfaceNormalsGrid = surfaceNormalsGrid + let (deviantPoints, validPoints) = try surfaceIntegrityProcessor.getSurfaceNormalIntegrityValueFromImage( + worldPointsGrid: worldPointsGrid, plane: alignedPlane, surfaceNormalsForPointsGrid: surfaceNormalsGrid, + damageDetectionResults: damageDetectionResults, captureData: captureImageData + ) + let deviantPointProportion = validPoints > 0 ? deviantPoints / validPoints : 0 + + guard let surfaceDisruptionAttributeValue = AccessibilityFeatureAttribute.surfaceDisruption.value(from: deviantPointProportion) else { + throw AttributeEstimationPipelineError.attributeAssignmentError + } + return surfaceDisruptionAttributeValue + } + + func calculateSurfaceDisruptionFromMesh( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + guard let captureMeshData = self.captureMeshData else { + throw AttributeEstimationPipelineError.missingCaptureData + } + guard let surfaceIntegrityProcessor = self.surfaceIntegrityProcessor else { + throw AttributeEstimationPipelineError.missingPreprocessors + } + let damageDetectionResults = try getDamageDetectionResults(accessibilityFeature: accessibilityFeature) + let meshContents: MeshContents = try self.prerequisiteCache.meshContents ?? self.getMeshContents( + accessibilityFeature: accessibilityFeature + ) + let meshPolygons: [MeshPolygon] = self.prerequisiteCache.meshPolygons ?? meshContents.polygons + let meshTriangles: [MeshTriangle] = self.prerequisiteCache.meshTriangles ?? meshContents.triangles + let alignedPlane: Plane = try self.prerequisiteCache.meshAlignedPlane ?? self.calculateAlignedPlane( + accessibilityFeature: accessibilityFeature, meshPolygons: meshPolygons + ) + let (totalDeviantPolygons, totalValidPolygons) = try surfaceIntegrityProcessor.getSurfaceNormalIntegrityValueFromMesh( + meshTriangles: meshTriangles, plane: alignedPlane, + damageDetectionResults: damageDetectionResults, captureData: captureMeshData + ) + let deviantPolygonProportion = totalValidPolygons > 0 ? totalDeviantPolygons / totalValidPolygons : 0 + + guard let surfaceDisruptionAttributeValue = AccessibilityFeatureAttribute.surfaceDisruption.value(from: deviantPolygonProportion) else { + throw AttributeEstimationPipelineError.attributeAssignmentError + } + return surfaceDisruptionAttributeValue + } +} + +/// Sub-attribute: Height from ground +extension AttributeEstimationPipeline { + func calculateHeightFromGround( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + let isMeshEnabled: Bool = self.captureMeshData != nil + if isMeshEnabled { + return try calculateHeightFromGroundFromMesh(accessibilityFeature: accessibilityFeature) + } + return try calculateHeightFromGroundFromImage(accessibilityFeature: accessibilityFeature) + } + + func calculateHeightFromGroundFromImage( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + guard let captureImageData = self.captureImageData else { + throw AttributeEstimationPipelineError.missingCaptureData + } + let worldPoints: [WorldPoint] = try self.prerequisiteCache.worldPoints ?? self.getWorldPoints( + accessibilityFeature: accessibilityFeature + ) + let alignedPlane: Plane = try self.prerequisiteCache.pointAlignedPlane ?? self.calculateAlignedPlane( + accessibilityFeature: accessibilityFeature, worldPoints: worldPoints + ) + let heightFromGround = getHeightFromGround(plane: alignedPlane, cameraTransform: captureImageData.cameraTransform) + + guard let heightFromGroundAttributeValue = AccessibilityFeatureAttribute.heightFromGround.value(from: heightFromGround) else { + throw AttributeEstimationPipelineError.attributeAssignmentError + } + return heightFromGroundAttributeValue + } + + func calculateHeightFromGroundFromMesh( + accessibilityFeature: any EditableAccessibilityFeatureProtocol + ) throws -> AccessibilityFeatureAttribute.Value { + guard let captureMeshData = self.captureMeshData else { + throw AttributeEstimationPipelineError.missingCaptureData + } + let meshContents: MeshContents = try self.prerequisiteCache.meshContents ?? self.getMeshContents( + accessibilityFeature: accessibilityFeature + ) + let meshPolygons: [MeshPolygon] = self.prerequisiteCache.meshPolygons ?? meshContents.polygons + let alignedPlane: Plane = try self.prerequisiteCache.meshAlignedPlane ?? self.calculateAlignedPlane( + accessibilityFeature: accessibilityFeature, meshPolygons: meshPolygons + ) + let heightFromGround = getHeightFromGround(plane: alignedPlane, cameraTransform: captureMeshData.cameraTransform) + + guard let heightFromGroundAttributeValue = AccessibilityFeatureAttribute.heightFromGround.value(from: heightFromGround) else { + throw AttributeEstimationPipelineError.attributeAssignmentError + } + return heightFromGroundAttributeValue + } + + private func getHeightFromGround(plane: Plane, cameraTransform: simd_float4x4) -> Double { + let planeNormal = plane.normalVector + let planeOrigin = plane.origin + let userPosition4 = cameraTransform.columns.3 + let userPosition = SIMD3(userPosition4.x, userPosition4.y, userPosition4.z) + let vectorFromPlaneToUser = userPosition - planeOrigin + return Double(simd_dot(vectorFromPlaneToUser, planeNormal)) + } +} diff --git a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Attributes/AccessibilityFeatureAttribute.swift b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Attributes/AccessibilityFeatureAttribute.swift index f531d0c..ef44329 100644 --- a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Attributes/AccessibilityFeatureAttribute.swift +++ b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Attributes/AccessibilityFeatureAttribute.swift @@ -17,6 +17,8 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C case runningSlope case crossSlope case surfaceIntegrity + case surfaceDisruption + case heightFromGround /** - NOTE: Experimental attributes @@ -38,6 +40,7 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C public enum ValueType: Sendable, Codable, Equatable { case length case angle + case number case flag case categorical(typeID: String) } @@ -45,6 +48,7 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C public enum Value: Sendable, Codable, Equatable { case length(Measurement) case angle(Measurement) + case number(Double) case flag(Bool) case categorical(AnyCategoricalValue) @@ -54,6 +58,8 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C return l1 == l2 case (.angle(let a1), .angle(let a2)): return a1 == a2 + case (.number(let n1), .number(let n2)): + return n1 == n2 case (.flag(let f1), .flag(let f2)): return f1 == f2 case (.categorical(let c1), .categorical(let c2)): @@ -93,6 +99,16 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C id: 40, name: "Surface Integrity", unit: nil, valueType: .categorical(typeID: SurfaceIntegrityStatus.typeID), ) + case .surfaceDisruption: + return Metadata( + id: 45, name: "Surface Disruption", unit: nil, + valueType: .number + ) + case .heightFromGround: + return Metadata( + id: 48, name: "Height from Ground", unit: UnitLength.meters, + valueType: .length, + ) case .lidarDepth: return Metadata( id: 50, name: "LiDAR Depth", unit: UnitLength.meters, @@ -175,6 +191,7 @@ public extension AccessibilityFeatureAttribute.Value { switch self { case .length: return .length case .angle: return .angle + case .number: return .number case .flag: return .flag case .categorical(let categoricalValue): return .categorical(typeID: categoricalValue.typeID) } @@ -192,6 +209,7 @@ public extension AccessibilityFeatureAttribute { switch (self.valueType, value) { case (.length, .length), (.angle, .angle), + (.number, .number), (.flag, .flag): return true case (.categorical(let expectedID), .categorical(let cat)): @@ -212,6 +230,8 @@ public extension AccessibilityFeatureAttribute.Value { return measurement.converted(to: .meters).value case .angle(let measurement): return measurement.converted(to: .degrees).value + case .number(let value): + return value case .flag: return nil case .categorical: @@ -234,6 +254,8 @@ public extension AccessibilityFeatureAttribute.Value { return String(format: "%.2f", measurement.converted(to: .meters).value) case .angle(let measurement): return String(format: "%.2f", measurement.converted(to: .degrees).value) + case .number(let value): + return String(format: "%.2f", value) case .flag(let value): return value ? "yes" : "no" case .categorical(let value): @@ -249,6 +271,8 @@ public extension AccessibilityFeatureAttribute { return .length(Measurement(value: double, unit: .meters)) case .angle: return .angle(Measurement(value: double, unit: .degrees)) + case .number: + return .number(double) case .flag: return nil // Flags cannot be represented as doubles case .categorical: @@ -327,6 +351,10 @@ public extension AccessibilityFeatureAttribute { return String(format: "%.2f", measurement.converted(to: .degrees).value) case (.surfaceIntegrity, .categorical(let categoricalValue)): return categoricalValue.rawValue + case (.surfaceDisruption, .number(let value)): + return String(format: "%.2f", value) + case (.heightFromGround, .length(let measurement)): + return String(format: "%.2f", measurement.converted(to: .meters).value) case (.lidarDepth, .length(let measurement)): return String(format: "%.2f", measurement.converted(to: .meters).value) case (.latitudeDelta, .length(let measurement)): diff --git a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Config/AccessibilityFeatureKind.swift b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Config/AccessibilityFeatureKind.swift index a5c9173..f2ce58a 100644 --- a/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Config/AccessibilityFeatureKind.swift +++ b/PointNMapShared/Sources/PointNMap/AccessibilityFeature/Config/AccessibilityFeatureKind.swift @@ -36,6 +36,7 @@ public enum AccessibilityFeatureKind: String, Identifiable, Codable, CaseIterabl switch self { case .sidewalk: return [ .width, .runningSlope, .crossSlope, .surfaceIntegrity, + .surfaceDisruption, .heightFromGround, .widthLegacy, .runningSlopeLegacy, .crossSlopeLegacy, .widthFromImage, .runningSlopeFromImage, .crossSlopeFromImage ] diff --git a/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromImageExtension.swift b/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromImageExtension.swift index 33a4d07..2f1181c 100644 --- a/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromImageExtension.swift +++ b/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromImageExtension.swift @@ -15,7 +15,7 @@ public extension SurfaceIntegrityProcessor { /** This function assesses the integrity of the surface based on the angular deviation of surface normals from the plane normal using GPU acceleration. It calculates the proportion of points that deviate beyond a specified angular threshold and determines the integrity status based on whether this proportion exceeds a defined threshold. */ - func getSurfaceNormalIntegrityResultFromImage( + func getSurfaceNormalIntegrityValueFromImage( worldPointsGrid: WorldPointsGrid, plane: Plane, surfaceNormalsForPointsGrid: SurfaceNormalsForPointsGrid, @@ -23,7 +23,7 @@ public extension SurfaceIntegrityProcessor { captureData: (any CaptureImageDataProtocol), angularDeviationThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.imagePlaneAngularDeviationThreshold, deviantPointProportionThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.imageDeviantPointProportionThreshold - ) throws -> IntegrityStatusDetails { + ) throws -> (totalDeviantPoints: Double, totalValidPoints: Double) { guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { throw SurfaceIntegrityProcessorError.metalPipelineCreationError } @@ -90,10 +90,25 @@ public extension SurfaceIntegrityProcessor { let totalValidPoints = totalValidBuffer.contents().bindMemory(to: UInt32.self, capacity: 1).pointee let totalDeviantPoints = totalDeviantBuffer.contents().bindMemory(to: UInt32.self, capacity: 1).pointee - let deviantPointProportion = totalValidPoints > 0 ? Float(totalDeviantPoints) / Float(totalValidPoints) : 0 + return (Double(totalDeviantPoints), Double(totalValidPoints)) + } + + func getSurfaceNormalIntegrityResultFromImage( + worldPointsGrid: WorldPointsGrid, + plane: Plane, + surfaceNormalsForPointsGrid: SurfaceNormalsForPointsGrid, + damageDetectionResults: [DamageDetectionResult], + captureData: (any CaptureImageDataProtocol), + angularDeviationThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.imagePlaneAngularDeviationThreshold, + deviantPointProportionThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.imageDeviantPointProportionThreshold + ) throws -> IntegrityStatusDetails { + let (deviantPoints, validPoints) = try getSurfaceNormalIntegrityValueFromImage( + worldPointsGrid: worldPointsGrid, plane: plane, surfaceNormalsForPointsGrid: surfaceNormalsForPointsGrid, damageDetectionResults: damageDetectionResults, captureData: captureData, angularDeviationThreshold: angularDeviationThreshold, deviantPointProportionThreshold: deviantPointProportionThreshold + ) + let deviantPointProportion = validPoints > 0 ? Float(deviantPoints) / Float(validPoints) : 0 let statusDetails: IntegrityStatusDetails = IntegrityStatusDetails( status: deviantPointProportion > deviantPointProportionThreshold ? .slight : .intact, - details: "Deviant Point Proportion: \(deviantPointProportion * 100)%, Total Deviant Points: \(totalDeviantPoints), Total Points: \(totalValidPoints)" + details: "Deviant Point Proportion: \(deviantPointProportion * 100)%, Total Deviant Points: \(deviantPoints), Total Points: \(validPoints)" ) return statusDetails } diff --git a/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromMeshExtension.swift b/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromMeshExtension.swift index b889745..8eabd8f 100644 --- a/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromMeshExtension.swift +++ b/PointNMapShared/Sources/PointNMap/ComputerVision/Projection/SurfaceIntegrity/Extensions/SurfaceIntegrityFromMeshExtension.swift @@ -12,14 +12,14 @@ import simd import PointNMapShaderTypes public extension SurfaceIntegrityProcessor { - func getSurfaceNormalIntegrityResultFromMesh( + func getSurfaceNormalIntegrityValueFromMesh( meshTriangles: [MeshTriangle], plane: Plane, damageDetectionResults: [DamageDetectionResult], captureData: (any CaptureMeshDataProtocol), angularDeviationThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.meshPlaneAngularDeviationThreshold, deviantPointProportionThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.meshDeviantPolygonProportionThreshold - ) throws -> IntegrityStatusDetails { + ) throws -> (totalDeviantPoints: Double, totalValidPoints: Double) { guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { throw SurfaceIntegrityProcessorError.metalPipelineCreationError } @@ -79,6 +79,20 @@ public extension SurfaceIntegrityProcessor { let totalValidPolygons = totalValidBuffer.contents().bindMemory(to: UInt32.self, capacity: 1).pointee let totalDeviantPolygons = totalDeviantBuffer.contents().bindMemory(to: UInt32.self, capacity: 1).pointee + return (Double(totalDeviantPolygons), Double(totalValidPolygons)) + } + + func getSurfaceNormalIntegrityResultFromMesh( + meshTriangles: [MeshTriangle], + plane: Plane, + damageDetectionResults: [DamageDetectionResult], + captureData: (any CaptureMeshDataProtocol), + angularDeviationThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.meshPlaneAngularDeviationThreshold, + deviantPointProportionThreshold: Float = PointNMapConstants.SurfaceIntegrityConstants.meshDeviantPolygonProportionThreshold + ) throws -> IntegrityStatusDetails { + let (totalDeviantPolygons, totalValidPolygons) = try getSurfaceNormalIntegrityValueFromMesh( + meshTriangles: meshTriangles, plane: plane, damageDetectionResults: damageDetectionResults, captureData: captureData, angularDeviationThreshold: angularDeviationThreshold, deviantPointProportionThreshold: deviantPointProportionThreshold) + let deviantPolygonProportion = totalValidPolygons > 0 ? Float(totalDeviantPolygons) / Float(totalValidPolygons) : 0 let statusDetails: IntegrityStatusDetails = IntegrityStatusDetails( status: deviantPolygonProportion > deviantPointProportionThreshold ? .slight : .intact, diff --git a/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift b/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift index 5131452..feaabe6 100644 --- a/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift +++ b/PointNMapShared/Sources/PointNMap/View/SubView/AnnotationFeatureDetailViewBase.swift @@ -168,6 +168,21 @@ public struct AnnotationFeatureDetailViewBase< } } + if (accessibilityFeature.accessibilityFeatureClass.kind.attributes.contains(.surfaceDisruption)) { + Section(header: Text(AccessibilityFeatureAttribute.surfaceDisruption.displayName)) { + numberTextFieldView(attribute: .surfaceDisruption) + .focused($focusedField, equals: .surfaceDisruption) + .id(refreshTrigger) // Refresh the Picker view when refreshTrigger changes + } + } + + if (accessibilityFeature.accessibilityFeatureClass.kind.attributes.contains(.heightFromGround)) { + Section(header: Text(AccessibilityFeatureAttribute.heightFromGround.displayName)) { + numberTextFieldView(attribute: .heightFromGround) + .focused($focusedField, equals: .heightFromGround) + } + } + /// Experimental Attributes Section if (accessibilityFeature.accessibilityFeatureClass.kind.experimentalAttributes.contains(.lidarDepth)) { Section(header: Text(AccessibilityFeatureAttribute.lidarDepth.displayName)) {