Skip to content

Commit cc01b1a

Browse files
authored
Merge pull request #431 from Esri/chris-webb/New-BrowseOGCAPIFeatureService
[New] Browse OGC API feature service
2 parents 7f0d557 + 16c0f9e commit cc01b1a

File tree

5 files changed

+296
-0
lines changed

5 files changed

+296
-0
lines changed

Samples.xcodeproj/project.pbxproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@
196196
95A5721B2C0FDD34006E8B48 /* ShowScaleBarView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 95A572182C0FDCC9006E8B48 /* ShowScaleBarView.swift */; };
197197
95DEB9B62C127A92009BEC35 /* ShowViewshedFromPointOnMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95DEB9B52C127A92009BEC35 /* ShowViewshedFromPointOnMapView.swift */; };
198198
95DEB9B82C127B5E009BEC35 /* ShowViewshedFromPointOnMapView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 95DEB9B52C127A92009BEC35 /* ShowViewshedFromPointOnMapView.swift */; };
199+
95E980712C26183000CB8912 /* BrowseOGCAPIFeatureServiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95E980702C26183000CB8912 /* BrowseOGCAPIFeatureServiceView.swift */; };
200+
95E980742C26189E00CB8912 /* BrowseOGCAPIFeatureServiceView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 95E980702C26183000CB8912 /* BrowseOGCAPIFeatureServiceView.swift */; };
199201
95F3A52B2C07F09C00885DED /* SetSurfaceNavigationConstraintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F3A52A2C07F09C00885DED /* SetSurfaceNavigationConstraintView.swift */; };
200202
95F3A52D2C07F28700885DED /* SetSurfaceNavigationConstraintView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 95F3A52A2C07F09C00885DED /* SetSurfaceNavigationConstraintView.swift */; };
201203
D70082EB2ACF900100E0C3C2 /* IdentifyKMLFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70082EA2ACF900100E0C3C2 /* IdentifyKMLFeaturesView.swift */; };
@@ -514,6 +516,7 @@
514516
dstPath = "";
515517
dstSubfolderSpec = 7;
516518
files = (
519+
95E980742C26189E00CB8912 /* BrowseOGCAPIFeatureServiceView.swift in Copy Source Code Files */,
517520
955AFAC62C110B8A009C8FE5 /* ApplyMosaicRuleToRastersView.swift in Copy Source Code Files */,
518521
95DEB9B82C127B5E009BEC35 /* ShowViewshedFromPointOnMapView.swift in Copy Source Code Files */,
519522
95A5721B2C0FDD34006E8B48 /* ShowScaleBarView.swift in Copy Source Code Files */,
@@ -823,6 +826,7 @@
823826
955AFAC32C10FD6F009C8FE5 /* ApplyMosaicRuleToRastersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplyMosaicRuleToRastersView.swift; sourceTree = "<group>"; };
824827
95A572182C0FDCC9006E8B48 /* ShowScaleBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowScaleBarView.swift; sourceTree = "<group>"; };
825828
95DEB9B52C127A92009BEC35 /* ShowViewshedFromPointOnMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowViewshedFromPointOnMapView.swift; sourceTree = "<group>"; };
829+
95E980702C26183000CB8912 /* BrowseOGCAPIFeatureServiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseOGCAPIFeatureServiceView.swift; sourceTree = "<group>"; };
826830
95F3A52A2C07F09C00885DED /* SetSurfaceNavigationConstraintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetSurfaceNavigationConstraintView.swift; sourceTree = "<group>"; };
827831
D70082EA2ACF900100E0C3C2 /* IdentifyKMLFeaturesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentifyKMLFeaturesView.swift; sourceTree = "<group>"; };
828832
D7010EBC2B05616900D43F55 /* DisplaySceneFromMobileScenePackageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplaySceneFromMobileScenePackageView.swift; sourceTree = "<group>"; };
@@ -1122,6 +1126,7 @@
11221126
D72F27292ADA1E4400F906DA /* Augment reality to show tabletop scene */,
11231127
218F35B229C28F4A00502022 /* Authenticate with OAuth */,
11241128
E0FE32E528747762002C6ACA /* Browse building floors */,
1129+
95E980732C26185000CB8912 /* Browse OGC API feature service */,
11251130
1C9B74D229DB54560038B06F /* Change camera controller */,
11261131
4D2ADC5329C4F612003B367F /* Change map view background */,
11271132
1C0C1C3229D34DAE005C8B24 /* Change viewpoint */,
@@ -1761,6 +1766,14 @@
17611766
path = "Show viewshed from point on map";
17621767
sourceTree = "<group>";
17631768
};
1769+
95E980732C26185000CB8912 /* Browse OGC API feature service */ = {
1770+
isa = PBXGroup;
1771+
children = (
1772+
95E980702C26183000CB8912 /* BrowseOGCAPIFeatureServiceView.swift */,
1773+
);
1774+
path = "Browse OGC API feature service";
1775+
sourceTree = "<group>";
1776+
};
17641777
95F3A52C2C07F0A600885DED /* Set surface navigation constraint */ = {
17651778
isa = PBXGroup;
17661779
children = (
@@ -3142,6 +3155,7 @@
31423155
00E1D90D2BC0B125001AEB6A /* SnapGeometryEditsView.GeometryEditorModel.swift in Sources */,
31433156
E088E1742863B5F800413100 /* GenerateOfflineMapView.swift in Sources */,
31443157
0074ABC428174F430037244A /* Sample.swift in Sources */,
3158+
95E980712C26183000CB8912 /* BrowseOGCAPIFeatureServiceView.swift in Sources */,
31453159
00A7A14A2A2FC5B700F035F7 /* DisplayContentOfUtilityNetworkContainerView.Model.swift in Sources */,
31463160
E004A6F0284E4B9B002A1FE6 /* DownloadVectorTilesToLocalCacheView.swift in Sources */,
31473161
00ABA94E2BF6721700C0488C /* ShowGridView.swift in Sources */,
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright 2024 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 BrowseOGCAPIFeatureServiceView: View {
19+
/// The error shown in the error alert.
20+
@State private var error: Error?
21+
22+
/// A Boolean value indicating whether the textfield alert should be presented.
23+
@State private var textfieldAlertIsPresented = true
24+
25+
/// The data model for the sample.
26+
@StateObject private var model = Model()
27+
28+
/// The user input for the OGC service resource.
29+
@State private var userInput = URL.daraaService.absoluteString
30+
31+
/// The selected feature collection's title.
32+
@State private var selectedTitle = ""
33+
34+
var body: some View {
35+
MapViewReader { mapProxy in
36+
MapView(map: model.map)
37+
.toolbar {
38+
ToolbarItemGroup(placement: .bottomBar) {
39+
Button("Open Service") {
40+
textfieldAlertIsPresented = true
41+
}
42+
43+
Spacer()
44+
45+
if !model.featureCollectionTitles.isEmpty {
46+
Picker("Layers", selection: $selectedTitle) {
47+
ForEach(model.featureCollectionTitles, id: \.self) { title in
48+
Text(title)
49+
}
50+
}
51+
.task(id: selectedTitle) {
52+
guard !selectedTitle.isEmpty else { return }
53+
let featureCollectionInfo = model.featureCollectionInfos[selectedTitle]!
54+
do {
55+
try await model.displayLayer(with: featureCollectionInfo)
56+
if let extent = featureCollectionInfo.extent {
57+
await mapProxy.setViewpointGeometry(extent, padding: 100)
58+
}
59+
} catch {
60+
self.error = error
61+
}
62+
}
63+
}
64+
}
65+
}
66+
.alert("Load OGC API feature service", isPresented: $textfieldAlertIsPresented) {
67+
// Textfield has a default OGC API URL.
68+
TextField("URL", text: $userInput)
69+
.keyboardType(.URL)
70+
.textContentType(.URL)
71+
Button("Load") {
72+
guard let url = URL(string: userInput) else { return }
73+
Task {
74+
do {
75+
try await model.loadOGCFeatureData(url: url)
76+
// Set the picker selection to the first title in the title list.
77+
if let title = model.featureCollectionTitles.first,
78+
let extent = model.featureCollectionInfos[title]?.extent {
79+
selectedTitle = title
80+
await mapProxy.setViewpointGeometry(extent, padding: 100)
81+
}
82+
} catch {
83+
self.error = error
84+
}
85+
}
86+
}
87+
.disabled(userInput.isEmpty)
88+
Button("Cancel", role: .cancel) {
89+
// Reset the default value of the textfield.
90+
userInput = URL.daraaService.absoluteString
91+
}
92+
} message: {
93+
Text("Please provide a URL to an OGC API feature service.")
94+
}
95+
.errorAlert(presentingError: $error)
96+
}
97+
}
98+
}
99+
100+
private extension BrowseOGCAPIFeatureServiceView {
101+
@MainActor
102+
class Model: ObservableObject {
103+
/// A map with a topographic basemap of the Daraa, Syria.
104+
let map: Map = {
105+
let map = Map(basemapStyle: .arcGISTopographic)
106+
map.initialViewpoint = Viewpoint(
107+
center: Point(latitude: 32.62, longitude: 36.10),
108+
scale: 200_000
109+
)
110+
return map
111+
}()
112+
113+
/// The titles of the feature collection infos in the OGC API.
114+
@Published private(set) var featureCollectionTitles: [String] = []
115+
116+
/// The OGC feature collection info from the OCG API.
117+
private(set) var featureCollectionInfos: [String: OGCFeatureCollectionInfo] = [:]
118+
119+
/// The OGC API feature service.
120+
private var service: OGCFeatureService!
121+
122+
/// The query parameters to populate features from the OGC API service.
123+
private let queryParameters: QueryParameters = {
124+
let queryParameters = QueryParameters()
125+
// Set a limit of 1000 on the number of returned features per request,
126+
// because the default on some services could be as low as 10.
127+
queryParameters.maxFeatures = 1_000
128+
return queryParameters
129+
}()
130+
131+
/// Returns a renderer with the appropriate symbol type for a geometry type.
132+
/// - Parameter geometryType: The geometry type.
133+
/// - Returns: A `SimpleRenderer` with the correct symbol for the given geometry.
134+
private func makeRenderer(withType geometryType: Geometry.Type) -> SimpleRenderer? {
135+
let symbol: Symbol
136+
switch geometryType {
137+
case is Point.Type, is Multipoint.Type:
138+
symbol = SimpleMarkerSymbol(style: .circle, color: .blue, size: 5)
139+
case is Polyline.Type:
140+
symbol = SimpleLineSymbol(style: .solid, color: .blue, width: 1)
141+
case is Polygon.Type, is Envelope.Type:
142+
symbol = SimpleFillSymbol(style: .solid, color: .blue)
143+
default:
144+
return nil
145+
}
146+
return SimpleRenderer(symbol: symbol)
147+
}
148+
149+
/// Creates and loads the OGC API features service from a URL.
150+
/// - Parameter url: The URL of the OGC service.
151+
/// - Returns: Returns a loaded `OCGFeatureService`.
152+
private func makeService(url: URL) async throws -> OGCFeatureService {
153+
let service = OGCFeatureService(url: url)
154+
try await service.load()
155+
if let serviceInfo = service.serviceInfo {
156+
let infos = serviceInfo.featureCollectionInfos
157+
featureCollectionTitles = infos.map(\.title)
158+
// The sample assumes there is no duplicate titles in the service.
159+
// Collections with duplicate titles will be discarded.
160+
featureCollectionInfos = Dictionary(
161+
infos.map { ($0.title, $0) },
162+
uniquingKeysWith: { (title, _) in title }
163+
)
164+
}
165+
return service
166+
}
167+
168+
/// Loads OGC service for a URL so that it can be rendered on the map.
169+
/// - Parameter url: The URL of the OGC service.
170+
func loadOGCFeatureData(url: URL) async throws {
171+
service = try await makeService(url: url)
172+
if let firstFeatureCollectionTitle = featureCollectionTitles.first,
173+
let info = featureCollectionInfos[firstFeatureCollectionTitle] {
174+
try await displayLayer(with: info)
175+
}
176+
}
177+
178+
/// Populates and displays a feature layer from an OGC feature collection table.
179+
/// - Parameter info: The `OGCFeatureCollectionInfo` selected by user.
180+
func displayLayer(with info: OGCFeatureCollectionInfo) async throws {
181+
map.removeAllOperationalLayers()
182+
let table = OGCFeatureCollectionTable(featureCollectionInfo: info)
183+
// Set the feature request mode to manual (only manual is currently
184+
// supported). In this mode, you must manually populate the table -
185+
// panning and zooming won't request features automatically.
186+
table.featureRequestMode = .manualCache
187+
_ = try await table.populateFromService(
188+
using: queryParameters,
189+
clearCache: false
190+
)
191+
let featureLayer = FeatureLayer(featureTable: table)
192+
if let geometryType = table.geometryType {
193+
featureLayer.renderer = makeRenderer(withType: geometryType)
194+
map.addOperationalLayer(featureLayer)
195+
}
196+
}
197+
}
198+
}
199+
200+
private extension URL {
201+
/// The Daraa, Syria OGC API feature service URL.
202+
static var daraaService: URL { URL(string: "https://demo.ldproxy.net/daraa")! }
203+
}
204+
205+
#Preview {
206+
BrowseOGCAPIFeatureServiceView()
207+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Browse OGC API feature service
2+
3+
Browse an OGC API feature service for layers and add them to the map.
4+
5+
![Image of browse OGC API feature service](browse-ogc-api-feature-service.png)
6+
7+
## Use case
8+
9+
OGC API standards are used for sharing geospatial data on the web. As an open standard, the OGC API aims to improve access to geospatial or location information and could be a good choice for sharing data internationally or between organizations. That data could be of any type, including, for example, transportation layers shared between government organizations and private businesses.
10+
11+
## How to use the sample
12+
13+
Select a layer to display from the list of layers shown in an OGC API service. The Daraa data is used as the default feature service, however, alternative feature services can be used.
14+
15+
## How it works
16+
17+
1. Create an `OGCFeatureService` object with a URL to an OGC API feature service.
18+
2. Obtain the `OGCFeatureServiceInfo` from `OGCFeatureService.serviceInfo`.
19+
3. Create a list of feature collections from the `OGCFeatureServiceInfo.featureCollectionInfos`
20+
4. When a feature collection is selected, create an `OGCFeatureCollectionTable` from the `OGCFeatureCollectionInfo`.
21+
5. Populate the `OGCFeatureCollectionTable` using `PopulateFromService` with `QueryParameters` that contain a `MaxFeatures` property.
22+
6. Create a feature layer from the feature table.
23+
7. Add the feature layer to the map.
24+
25+
## Relevant API
26+
27+
* OGCFeatureCollectionInfo
28+
* OGCFeatureCollectionTable
29+
* OGCFeatureService
30+
* OGCFeatureServiceInfo
31+
32+
## About the data
33+
34+
The [Daraa, Syria test data](https://demo.ldproxy.net/daraa) is OpenStreetMap data converted to the Topographic Data Store schema of NGA.
35+
36+
## Additional information
37+
38+
See the [OGC API website](https://ogcapi.ogc.org/) for more information on the OGC API family of standards.
39+
40+
## Tags
41+
42+
browse, catalog, feature, layers, OGC, OGC API, service, web
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"category": "Layers",
3+
"description": "Browse an OGC API feature service for layers and add them to the map.",
4+
"ignore": false,
5+
"images": [
6+
"browse-ogc-api-feature-service.png"
7+
],
8+
"keywords": [
9+
"OGC",
10+
"OGC API",
11+
"browse",
12+
"catalog",
13+
"feature",
14+
"layers",
15+
"service",
16+
"web",
17+
"OGCFeatureCollectionInfo",
18+
"OGCFeatureCollectionTable",
19+
"OGCFeatureService",
20+
"OGCFeatureServiceInfo"
21+
],
22+
"redirect_from": [],
23+
"relevant_apis": [
24+
"OGCFeatureCollectionInfo",
25+
"OGCFeatureCollectionTable",
26+
"OGCFeatureService",
27+
"OGCFeatureServiceInfo"
28+
],
29+
"snippets": [
30+
"BrowseOGCAPIFeatureServiceView.swift"
31+
],
32+
"title": "Browse OGC API feature service"
33+
}
48.4 KB
Loading

0 commit comments

Comments
 (0)