|
| 1 | +// Copyright 2023 Esri |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +import ArcGIS |
| 16 | +import SwiftUI |
| 17 | + |
| 18 | +struct FilterFeaturesInSceneView: View { |
| 19 | + /// The view model for this sample. |
| 20 | + @StateObject private var model = Model() |
| 21 | + |
| 22 | + /// A Boolean value indicating whether to show an error alert. |
| 23 | + @State private var isShowingAlert = false |
| 24 | + |
| 25 | + /// The error shown in the error alert. |
| 26 | + @State private var error: Error? { |
| 27 | + didSet { isShowingAlert = error != nil } |
| 28 | + } |
| 29 | + |
| 30 | + var body: some View { |
| 31 | + SceneView(scene: model.scene, graphicsOverlays: [model.graphicsOverlay]) |
| 32 | + .task { |
| 33 | + do { |
| 34 | + try await model.scene.load() |
| 35 | + } catch { |
| 36 | + self.error = error |
| 37 | + } |
| 38 | + } |
| 39 | + .toolbar { |
| 40 | + ToolbarItemGroup(placement: .bottomBar) { |
| 41 | + Button(model.filterState.label) { |
| 42 | + model.handleFilterState() |
| 43 | + } |
| 44 | + } |
| 45 | + } |
| 46 | + .alert(isPresented: $isShowingAlert, presentingError: error) |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +private extension FilterFeaturesInSceneView { |
| 51 | + /// The model used to store the geo model and other expensive objects |
| 52 | + /// used in this view. |
| 53 | + class Model: ObservableObject { |
| 54 | + /// The scene for this sample. |
| 55 | + let scene: ArcGIS.Scene |
| 56 | + |
| 57 | + /// The open street map layer for the sample. |
| 58 | + private let osmBuildings = ArcGISSceneLayer( |
| 59 | + item: PortalItem( |
| 60 | + portal: .arcGISOnline(connection: .anonymous), |
| 61 | + id: .osmBuildings |
| 62 | + ) |
| 63 | + ) |
| 64 | + |
| 65 | + /// The San Francisco buildings scene layer for the sample. |
| 66 | + private let buildingsSceneLayer = ArcGISSceneLayer( |
| 67 | + url: URL(string: "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/SanFrancisco_Bldgs/SceneServer")! |
| 68 | + ) |
| 69 | + |
| 70 | + /// The graphics overlay for the scene view. |
| 71 | + let graphicsOverlay = GraphicsOverlay() |
| 72 | + |
| 73 | + /// A polygon that shows the extent of the detailed buildings scene layer. |
| 74 | + private let polygon: ArcGIS.Polygon = makeFilteringPolygon() |
| 75 | + |
| 76 | + /// A red extent boundary graphic that represents the full extent of the detailed buildings scene layer. |
| 77 | + private let sanFranciscoExtentGraphic: Graphic |
| 78 | + |
| 79 | + /// The filter state for the scene view. |
| 80 | + @Published private(set) var filterState: FilterState = .addBuildings |
| 81 | + |
| 82 | + init() { |
| 83 | + // Create basemap. |
| 84 | + let vectorTiledLayer = ArcGISVectorTiledLayer( |
| 85 | + item: PortalItem( |
| 86 | + portal: .arcGISOnline(connection: .anonymous), |
| 87 | + id: .osmTopographic |
| 88 | + ) |
| 89 | + ) |
| 90 | + scene = Scene(basemap: Basemap(baseLayers: [vectorTiledLayer, osmBuildings])) |
| 91 | + |
| 92 | + // Create scene topography. |
| 93 | + let elevationServiceURL = URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")! |
| 94 | + let elevationSource = ArcGISTiledElevationSource(url: elevationServiceURL) |
| 95 | + let surface = Surface() |
| 96 | + surface.addElevationSource(elevationSource) |
| 97 | + scene.baseSurface = surface |
| 98 | + |
| 99 | + // Set the initial viewpoint of the scene. |
| 100 | + scene.initialViewpoint = .sanFranciscoBuildings |
| 101 | + |
| 102 | + let simpleFillSymbol = SimpleFillSymbol( |
| 103 | + style: .solid, |
| 104 | + color: .clear, |
| 105 | + outline: SimpleLineSymbol( |
| 106 | + style: .solid, |
| 107 | + color: .red, |
| 108 | + width: 5 |
| 109 | + ) |
| 110 | + ) |
| 111 | + |
| 112 | + sanFranciscoExtentGraphic = Graphic( |
| 113 | + geometry: polygon, |
| 114 | + symbol: simpleFillSymbol |
| 115 | + ) |
| 116 | + } |
| 117 | + |
| 118 | + /// Creates a polygon that represents the detailed buildings scene layer full extent. |
| 119 | + /// - Returns: A polygon. |
| 120 | + private static func makeFilteringPolygon() -> ArcGIS.Polygon { |
| 121 | + // The buildings scene layer fullExtent. |
| 122 | + let extent = Envelope( |
| 123 | + xRange: -122.514 ... -122.357, |
| 124 | + yRange: 37.705...37.831, |
| 125 | + zRange: -148.843...551.801, |
| 126 | + spatialReference: SpatialReference(wkid: .wgs84, verticalWKID: WKID(5773)!) |
| 127 | + ) |
| 128 | + |
| 129 | + let builder = PolygonBuilder(spatialReference: extent.spatialReference) |
| 130 | + builder.add(Point(x: extent.xMin, y: extent.yMin)) |
| 131 | + builder.add(Point(x: extent.xMax, y: extent.yMin)) |
| 132 | + builder.add(Point(x: extent.xMax, y: extent.yMax)) |
| 133 | + builder.add(Point(x: extent.xMin, y: extent.yMax)) |
| 134 | + |
| 135 | + return builder.toGeometry() |
| 136 | + } |
| 137 | + |
| 138 | + /// Handles the filter state of the sample by either loading, filtering, or reseting the scene. |
| 139 | + func handleFilterState() { |
| 140 | + switch filterState { |
| 141 | + case .addBuildings: |
| 142 | + // Show the detailed buildings scene layer and extent graphic. |
| 143 | + addBuildings() |
| 144 | + case .filter: |
| 145 | + // Hide buildings within the detailed building extent so they don't clip. |
| 146 | + filterScene() |
| 147 | + case .reset: |
| 148 | + // Reset the scene to its original state. |
| 149 | + resetScene() |
| 150 | + } |
| 151 | + |
| 152 | + // Set the next filter state to be applied to the scene. |
| 153 | + filterState = filterState.next() |
| 154 | + } |
| 155 | + |
| 156 | + /// Loads the detailed buildings scene layer and adds an extent graphic. |
| 157 | + private func addBuildings() { |
| 158 | + scene.addOperationalLayer(buildingsSceneLayer) |
| 159 | + graphicsOverlay.addGraphic(sanFranciscoExtentGraphic) |
| 160 | + } |
| 161 | + |
| 162 | + /// Applies a polygon filter to the open street map buildings layer. |
| 163 | + private func filterScene() { |
| 164 | + if let polygonFilter = osmBuildings.polygonFilter { |
| 165 | + // After the scene is reset, the layer will have a polygon filter, but that filter |
| 166 | + // will not have polygons set. |
| 167 | + // Add the polygon back to the polygon filter. |
| 168 | + polygonFilter.addPolygon(polygon) |
| 169 | + } else { |
| 170 | + // Initially, the building layer does not have a polygon filter, set it. |
| 171 | + osmBuildings.polygonFilter = SceneLayerPolygonFilter( |
| 172 | + polygons: [polygon], |
| 173 | + spatialRelationship: .disjoint |
| 174 | + ) |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + /// Resets the scene filters and hides the detailed buildings and extent graphic. |
| 179 | + private func resetScene() { |
| 180 | + // Remove the detailed buildings layer from the scene. |
| 181 | + scene.removeAllOperationalLayers() |
| 182 | + // Clear OSM buildings polygon filter. |
| 183 | + osmBuildings.polygonFilter?.removeAllPolygons() |
| 184 | + // Remove red extent boundary graphic from graphics overlay. |
| 185 | + graphicsOverlay.removeAllGraphics() |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + /// The different states for filtering features in a scene. |
| 190 | + enum FilterState: Equatable { |
| 191 | + case addBuildings, filter, reset |
| 192 | + |
| 193 | + /// A human-readable label for the filter state. |
| 194 | + var label: String { |
| 195 | + switch self { |
| 196 | + case .addBuildings: return "Add Buildings" |
| 197 | + case .filter: return "Filter" |
| 198 | + case .reset: return "Reset" |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + /// The next filter state to apply to a scene. |
| 203 | + func next() -> Self { |
| 204 | + switch self { |
| 205 | + case .addBuildings: |
| 206 | + return .filter |
| 207 | + case .filter: |
| 208 | + return .reset |
| 209 | + case .reset: |
| 210 | + return .addBuildings |
| 211 | + } |
| 212 | + } |
| 213 | + } |
| 214 | +} |
| 215 | + |
| 216 | +private extension PortalItem.ID { |
| 217 | + /// The ID used in the "OpenStreetMap 3D Buildings" portal item. |
| 218 | + static var osmBuildings: Self { Self("ca0470dbbddb4db28bad74ed39949e25")! } |
| 219 | + /// The ID used in the "OpenStreetMap Topographic (for 3D)" portal item. |
| 220 | + static var osmTopographic: Self { Self("1e7d1784d1ef4b79ba6764d0bd6c3150")! } |
| 221 | +} |
| 222 | + |
| 223 | +private extension Viewpoint { |
| 224 | + /// The initial viewpoint to be displayed when the sample is first opened. |
| 225 | + static let sanFranciscoBuildings = Viewpoint( |
| 226 | + latitude: .nan, |
| 227 | + longitude: .nan, |
| 228 | + scale: .nan, |
| 229 | + camera: Camera( |
| 230 | + location: Point( |
| 231 | + x: -122.421, |
| 232 | + y: 37.7041, |
| 233 | + z: 207 |
| 234 | + ), |
| 235 | + heading: 60, |
| 236 | + pitch: 70, |
| 237 | + roll: 0 |
| 238 | + ) |
| 239 | + ) |
| 240 | +} |
0 commit comments