Skip to content

Commit 32cf415

Browse files
authored
Merge pull request #254 from Esri/des12437/scene-layer-spatial-filtering
[New] Filter features in scene
2 parents d3182c9 + 7957396 commit 32cf415

File tree

5 files changed

+327
-0
lines changed

5 files changed

+327
-0
lines changed

Samples.xcodeproj/project.pbxproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@
7676
1C19B4F72A578E69001D2506 /* CreateLoadReportView.Model.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C19B4EF2A578E46001D2506 /* CreateLoadReportView.Model.swift */; };
7777
1C19B4F82A578E69001D2506 /* CreateLoadReportView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C19B4ED2A578E46001D2506 /* CreateLoadReportView.swift */; };
7878
1C19B4F92A578E69001D2506 /* CreateLoadReportView.Views.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C19B4EB2A578E46001D2506 /* CreateLoadReportView.Views.swift */; };
79+
1C26ED192A859525009B7721 /* FilterFeaturesInSceneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C26ED152A859525009B7721 /* FilterFeaturesInSceneView.swift */; };
80+
1C26ED202A8BEC63009B7721 /* FilterFeaturesInSceneView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C26ED152A859525009B7721 /* FilterFeaturesInSceneView.swift */; };
7981
1C3B7DC82A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3B7DC32A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift */; };
8082
1C3B7DCB2A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3B7DC62A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.swift */; };
8183
1C3B7DCD2A5F652500907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C3B7DC32A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift */; };
@@ -282,6 +284,7 @@
282284
dstSubfolderSpec = 7;
283285
files = (
284286
79D84D152A81718F00F45262 /* AddCustomDynamicEntityDataSourceView.swift in Copy Source Code Files */,
287+
1C26ED202A8BEC63009B7721 /* FilterFeaturesInSceneView.swift in Copy Source Code Files */,
285288
1C56B5E72A82C057000381DA /* DisplayPointsUsingClusteringFeatureReductionView.swift in Copy Source Code Files */,
286289
D7ABA3002A3288970021822B /* ShowViewshedFromGeoelementInSceneView.swift in Copy Source Code Files */,
287290
1C3B7DCD2A5F652500907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift in Copy Source Code Files */,
@@ -423,6 +426,7 @@
423426
1C19B4EB2A578E46001D2506 /* CreateLoadReportView.Views.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateLoadReportView.Views.swift; sourceTree = "<group>"; };
424427
1C19B4ED2A578E46001D2506 /* CreateLoadReportView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateLoadReportView.swift; sourceTree = "<group>"; };
425428
1C19B4EF2A578E46001D2506 /* CreateLoadReportView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateLoadReportView.Model.swift; sourceTree = "<group>"; };
429+
1C26ED152A859525009B7721 /* FilterFeaturesInSceneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterFeaturesInSceneView.swift; sourceTree = "<group>"; };
426430
1C3B7DC32A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyzeNetworkWithSubnetworkTraceView.Model.swift; sourceTree = "<group>"; };
427431
1C3B7DC62A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyzeNetworkWithSubnetworkTraceView.swift; sourceTree = "<group>"; };
428432
1C42E04329D2396B004FC4BE /* ShowPopupView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShowPopupView.swift; sourceTree = "<group>"; };
@@ -634,6 +638,7 @@
634638
E004A6D528465C70002A1FE6 /* Display scene */,
635639
E070A0A1286F3B3400F2B606 /* Download preplanned map area */,
636640
E004A6EE284E4B7A002A1FE6 /* Download vector tiles to local cache */,
641+
1C26ED122A859525009B7721 /* Filter features in scene */,
637642
D78666A92A21616D00C60110 /* Find nearest vertex */,
638643
E066DD33285CF3A0004D3D5B /* Find route */,
639644
E088E1722863B5E600413100 /* Generate offline map */,
@@ -860,6 +865,14 @@
860865
path = "Create load report";
861866
sourceTree = "<group>";
862867
};
868+
1C26ED122A859525009B7721 /* Filter features in scene */ = {
869+
isa = PBXGroup;
870+
children = (
871+
1C26ED152A859525009B7721 /* FilterFeaturesInSceneView.swift */,
872+
);
873+
path = "Filter features in scene";
874+
sourceTree = "<group>";
875+
};
863876
1C3B7DC22A5F64FC00907443 /* Analyze network with subnetwork trace */ = {
864877
isa = PBXGroup;
865878
children = (
@@ -1597,6 +1610,7 @@
15971610
E000E7602869E33D005D87C5 /* ClipGeometryView.swift in Sources */,
15981611
4D2ADC6729C50BD6003B367F /* AddDynamicEntityLayerView.Model.swift in Sources */,
15991612
E004A6E928493BCE002A1FE6 /* ShowDeviceLocationView.swift in Sources */,
1613+
1C26ED192A859525009B7721 /* FilterFeaturesInSceneView.swift in Sources */,
16001614
F111CCC1288B5D5600205358 /* DisplayMapFromMobileMapPackageView.swift in Sources */,
16011615
1C43BC842A43781200509BF8 /* SetVisibilityOfSubtypeSublayerView.swift in Sources */,
16021616
00A7A1462A2FC58300F035F7 /* DisplayContentOfUtilityNetworkContainerView.swift in Sources */,
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Filter features in scene
2+
3+
Filter 3D scene features out of a given geometry with a polygon filter.
4+
5+
![Image of filter features in scene sample](filter-features-in-scene.png)
6+
7+
## Use case
8+
9+
You can directly control what users see within a specific scene view to give a more focused or cleaner user experience by using a `SceneLayerPolygonFilter` to selectively show or hide scene features within a given area.
10+
11+
## How to use the sample
12+
13+
The sample initializes showing the 3D buildings OpenStreetMap layer. Tap the "Add Buildings" button to load an additional scene layer that contains more detailed buildings. Notice how the two scene layers overlap and clip into each other. Tap the "Filter" button to set a `SceneLayerPolygonFilter` and filter out the OpenStreetMap buildings within the extent of the detailed buildings scene. Notice how the OSM buildings within and intersecting the extent of the detailed buildings layer are hidden. Tap the "Reset" button to hide the detailed buildings scene layer and clear the OSM buildings filter.
14+
15+
## How it works
16+
17+
1. Construct a `Basemap` for the scene using a topographic `ArcGISVectorTileLayer` and the OpenStreetMap 3D Buildings `ArcGISSceneLayer` as baselayers.
18+
2. Create a `Surface` for the scene and set the World Elevation 3D as an elevation source.
19+
3. Add the 3D San Francisco Buildings `ArcGISSceneLayer` to the scene's operational layers.
20+
4. Construct a `SceneLayerPolygonFilter` with the extent of the San Francisco Buildings Scene Layer and the `SceneLayerPolygonFilter.SpatialRelationship.disjoint` enum to hide all features within the extent.
21+
5. Set the `SceneLayerPolygonFilter` on the OSM Buildings layer to hide all OSM buildings within the extent of the San Francisco Buildings layer.
22+
23+
## Relevant API
24+
25+
* ArcGISSceneLayer
26+
* SceneLayerPolygonFilter
27+
* SceneLayerPolygonFilter.SpatialRelationship
28+
29+
## About the data
30+
31+
This sample uses the [OpenStreetMap 3D Buildings](https://www.arcgis.com/home/item.html?id=ca0470dbbddb4db28bad74ed39949e25) which provides generic 3D outlines of buildings throughout the world. It is based on the OSM Daylight map distribution and is hosted by Esri. It uses the [San Francisco 3D Buildings](https://www.arcgis.com/home/item.html?id=d3344ba99c3f4efaa909ccfbcc052ed5) scene layer which provides detailed 3D models of buildings in San Francisco, California, USA.
32+
33+
## Additional information
34+
35+
This sample uses `SceneLayerPolygonFilter.SpatialRelationship.disjoint` to hide all features within the extent of the given geometry. You can alternatively use `SceneLayerPolygonFilter.SpatialRelationship.contains` to only show features within the extent of the geometry.
36+
37+
You can also show or hide features in a scene layer using `ArcGISSceneLayer.setVisible(_:for:)` and pass in a feature or a list of features and a boolean value to set their visibility.
38+
39+
## Tags
40+
41+
3D, buildings, disjoint, exclude, extent, filter, hide, OSM, polygon
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"category": "Scenes",
3+
"description": "Filter 3D scene features out of a given geometry with a polygon filter.",
4+
"ignore": false,
5+
"images": [
6+
"filter-features-in-scene.png"
7+
],
8+
"keywords": [
9+
"3D",
10+
"OSM",
11+
"buildings",
12+
"disjoint",
13+
"exclude",
14+
"extent",
15+
"filter",
16+
"hide",
17+
"polygon",
18+
"ArcGISSceneLayer",
19+
"SceneLayerPolygonFilter",
20+
"SceneLayerPolygonFilter.SpatialRelationship"
21+
],
22+
"redirect_from": [],
23+
"relevant_apis": [
24+
"ArcGISSceneLayer",
25+
"SceneLayerPolygonFilter",
26+
"SceneLayerPolygonFilter.SpatialRelationship"
27+
],
28+
"snippets": [
29+
"FilterFeaturesInSceneView.swift"
30+
],
31+
"title": "Filter features in scene"
32+
}
250 KB
Loading

0 commit comments

Comments
 (0)