Skip to content

Commit e90c33e

Browse files
authored
Merge pull request #551 from Esri/Caleb/New-QueryWithCQLFilters
[New] Query with CQL filters
2 parents cd79199 + aec4b05 commit e90c33e

File tree

5 files changed

+350
-0
lines changed

5 files changed

+350
-0
lines changed

Samples.xcodeproj/project.pbxproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,8 @@
424424
D79EE76F2A4CEA7F005A52AE /* SetUpLocationDrivenGeotriggersView.Model.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D79EE76D2A4CEA5D005A52AE /* SetUpLocationDrivenGeotriggersView.Model.swift */; };
425425
D7A737E02BABB9FE00B7C7FC /* AugmentRealityToShowHiddenInfrastructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A737DC2BABB9FE00B7C7FC /* AugmentRealityToShowHiddenInfrastructureView.swift */; };
426426
D7A737E32BABBA2200B7C7FC /* AugmentRealityToShowHiddenInfrastructureView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D7A737DC2BABB9FE00B7C7FC /* AugmentRealityToShowHiddenInfrastructureView.swift */; };
427+
D7A85A082CD5ABF5009DC68A /* QueryWithCQLFiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A85A022CD5ABF5009DC68A /* QueryWithCQLFiltersView.swift */; };
428+
D7A85A092CD5AC0B009DC68A /* QueryWithCQLFiltersView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D7A85A022CD5ABF5009DC68A /* QueryWithCQLFiltersView.swift */; };
427429
D7ABA2F92A32579C0021822B /* MeasureDistanceInSceneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ABA2F82A32579C0021822B /* MeasureDistanceInSceneView.swift */; };
428430
D7ABA2FA2A32760D0021822B /* MeasureDistanceInSceneView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D7ABA2F82A32579C0021822B /* MeasureDistanceInSceneView.swift */; };
429431
D7ABA2FF2A32881C0021822B /* ShowViewshedFromGeoelementInSceneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ABA2FE2A32881C0021822B /* ShowViewshedFromGeoelementInSceneView.swift */; };
@@ -583,6 +585,7 @@
583585
dstPath = "";
584586
dstSubfolderSpec = 7;
585587
files = (
588+
D7A85A092CD5AC0B009DC68A /* QueryWithCQLFiltersView.swift in Copy Source Code Files */,
586589
D771D0C92CD5522A004C13CB /* ApplyRasterRenderingRuleView.swift in Copy Source Code Files */,
587590
D751B4CB2CD3E598005CE750 /* AddKMLLayerWithNetworkLinksView.swift in Copy Source Code Files */,
588591
D70789952CD1611E000DF215 /* ApplyDictionaryRendererToGraphicsOverlayView.swift in Copy Source Code Files */,
@@ -1046,6 +1049,7 @@
10461049
D79482D02C35D872006521CD /* CreateDynamicBasemapGalleryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateDynamicBasemapGalleryView.swift; sourceTree = "<group>"; };
10471050
D79EE76D2A4CEA5D005A52AE /* SetUpLocationDrivenGeotriggersView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetUpLocationDrivenGeotriggersView.Model.swift; sourceTree = "<group>"; };
10481051
D7A737DC2BABB9FE00B7C7FC /* AugmentRealityToShowHiddenInfrastructureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AugmentRealityToShowHiddenInfrastructureView.swift; sourceTree = "<group>"; };
1052+
D7A85A022CD5ABF5009DC68A /* QueryWithCQLFiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryWithCQLFiltersView.swift; sourceTree = "<group>"; };
10491053
D7ABA2F82A32579C0021822B /* MeasureDistanceInSceneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeasureDistanceInSceneView.swift; sourceTree = "<group>"; };
10501054
D7ABA2FE2A32881C0021822B /* ShowViewshedFromGeoelementInSceneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShowViewshedFromGeoelementInSceneView.swift; sourceTree = "<group>"; };
10511055
D7AE861D2AC39DC50049B626 /* DisplayAnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayAnnotationView.swift; sourceTree = "<group>"; };
@@ -1351,6 +1355,7 @@
13511355
108EC03F29D25AE1000F35D0 /* Query feature table */,
13521356
D73F06652B5EE73D000B574F /* Query features with Arcade expression */,
13531357
D7BA38962BFBFC0F009954F5 /* Query related features */,
1358+
D7A85A052CD5ABF5009DC68A /* Query with CQL filters */,
13541359
D7ECF5942AB8BDCA003FB2BE /* Render multilayer symbols */,
13551360
1CAB8D402A3CEAB0002AA649 /* Run valve isolation trace */,
13561361
D75F66322B48EABC00434974 /* Search for web map */,
@@ -2681,6 +2686,14 @@
26812686
path = "Augment reality to show hidden infrastructure";
26822687
sourceTree = "<group>";
26832688
};
2689+
D7A85A052CD5ABF5009DC68A /* Query with CQL filters */ = {
2690+
isa = PBXGroup;
2691+
children = (
2692+
D7A85A022CD5ABF5009DC68A /* QueryWithCQLFiltersView.swift */,
2693+
);
2694+
path = "Query with CQL filters";
2695+
sourceTree = "<group>";
2696+
};
26842697
D7ABA2F52A3256610021822B /* Measure distance in scene */ = {
26852698
isa = PBXGroup;
26862699
children = (
@@ -3450,6 +3463,7 @@
34503463
E000E7602869E33D005D87C5 /* ClipGeometryView.swift in Sources */,
34513464
9503056E2C46ECB70091B32D /* ShowDeviceLocationUsingIndoorPositioningView.Model.swift in Sources */,
34523465
4D2ADC6729C50BD6003B367F /* AddDynamicEntityLayerView.Model.swift in Sources */,
3466+
D7A85A082CD5ABF5009DC68A /* QueryWithCQLFiltersView.swift in Sources */,
34533467
E004A6E928493BCE002A1FE6 /* ShowDeviceLocationView.swift in Sources */,
34543468
1C26ED192A859525009B7721 /* FilterFeaturesInSceneView.swift in Sources */,
34553469
D751B4C82CD3E572005CE750 /* AddKMLLayerWithNetworkLinksView.swift in Sources */,
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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 QueryWithCQLFiltersView: View {
19+
/// The view model for the sample.
20+
@StateObject private var model = Model()
21+
22+
/// The count of feature from the last feature query result.
23+
@State private var resultFeatureCount = 0
24+
25+
/// A Boolean value indicating whether the OCG feature collection table is currently being populated.
26+
@State private var isPopulatingFeatureTable = true
27+
28+
/// A Boolean value indicating whether the CQL Filters form is presented.
29+
@State private var isShowingCQLFiltersForm = false
30+
31+
/// The error shown in the error alert.
32+
@State private var error: Error?
33+
34+
var body: some View {
35+
MapViewReader { mapViewProxy in
36+
MapView(map: model.map)
37+
.overlay(alignment: .top) {
38+
Text(
39+
isPopulatingFeatureTable
40+
? "Populating the feature table..."
41+
: "Populated \(resultFeatureCount) features(s)."
42+
)
43+
.multilineTextAlignment(.center)
44+
.frame(maxWidth: .infinity, alignment: .center)
45+
.padding(8)
46+
.background(.regularMaterial, ignoresSafeAreaEdges: .horizontal)
47+
}
48+
.toolbar {
49+
ToolbarItem(placement: .bottomBar) {
50+
Button("CQL Filters") {
51+
isShowingCQLFiltersForm = true
52+
}
53+
.popover(isPresented: $isShowingCQLFiltersForm) {
54+
CQLQueryFiltersForm(model: model) {
55+
isPopulatingFeatureTable = true
56+
}
57+
.presentationDetents([.fraction(0.5)])
58+
.frame(idealWidth: 320, idealHeight: 380)
59+
}
60+
}
61+
}
62+
.task(id: isPopulatingFeatureTable) {
63+
guard isPopulatingFeatureTable else {
64+
return
65+
}
66+
defer { isPopulatingFeatureTable = false }
67+
68+
do {
69+
// Queries the feature table using the query parameters.
70+
let featureQueryResult = try await model.ogcFeatureCollectionTable
71+
.populateFromService(using: model.queryParameters, clearCache: true)
72+
73+
let queryResultFeatures = Array(featureQueryResult.features())
74+
resultFeatureCount = queryResultFeatures.count
75+
76+
// Sets the viewpoint to the extent of the query result.
77+
let geometries = queryResultFeatures.compactMap(\.geometry)
78+
if let combinedExtent = GeometryEngine.combineExtents(of: geometries) {
79+
await mapViewProxy.setViewpointGeometry(combinedExtent, padding: 20)
80+
}
81+
} catch {
82+
self.error = error
83+
}
84+
}
85+
.errorAlert(presentingError: $error)
86+
}
87+
}
88+
}
89+
90+
// MARK: - CQLQueryFiltersForm
91+
92+
private extension QueryWithCQLFiltersView {
93+
/// A form with filter controls for a CQL query.
94+
struct CQLQueryFiltersForm: View {
95+
/// The view model for the sample.
96+
@ObservedObject var model: Model
97+
98+
/// The action to perform when the "Apply" button is pressed.
99+
let onApply: () -> Void
100+
101+
/// The action to dismiss the view.
102+
@Environment(\.dismiss) private var dismiss
103+
104+
/// The attribute expression that defines features to be included in the query.
105+
@State private var selectedWhereClause = ""
106+
107+
/// The maximum number of features the query should return.
108+
@State private var maxFeatures = 1000
109+
110+
/// A Boolean value indicating whether the query includes a time extent.
111+
@State private var includesDateFilter = false
112+
113+
/// The start date of the query's time extent.
114+
@State private var selectedStartDate: Date = {
115+
let components = DateComponents(year: 2011, month: 6, day: 13)
116+
return Calendar.current.date(from: components)!
117+
}()
118+
119+
/// The end date of the query's time extent.
120+
@State private var selectedEndDate: Date = {
121+
let components = DateComponents(year: 2012, month: 1, day: 7)
122+
return Calendar.current.date(from: components)!
123+
}()
124+
125+
var body: some View {
126+
NavigationStack {
127+
Form {
128+
Picker("Where Clause", selection: $selectedWhereClause) {
129+
ForEach(model.sampleWhereClauses, id: \.self) { whereClause in
130+
Text(whereClause)
131+
}
132+
}
133+
.pickerStyle(.navigationLink)
134+
135+
LabeledContent("Max Features") {
136+
TextField("1000", value: $maxFeatures, format: .number)
137+
.multilineTextAlignment(.trailing)
138+
.onChange(of: maxFeatures) { newValue in
139+
maxFeatures = newValue == 0 ? 1 : abs(newValue)
140+
}
141+
}
142+
143+
Section {
144+
Toggle("Date Filter", isOn: $includesDateFilter)
145+
DatePicker(
146+
"Start Date",
147+
selection: $selectedStartDate,
148+
in: ...selectedEndDate,
149+
displayedComponents: [.date]
150+
)
151+
.disabled(!includesDateFilter)
152+
DatePicker(
153+
"End Date",
154+
selection: $selectedEndDate,
155+
in: selectedStartDate...,
156+
displayedComponents: [.date]
157+
)
158+
.disabled(!includesDateFilter)
159+
}
160+
}
161+
.navigationTitle("CQL Filters")
162+
.navigationBarTitleDisplayMode(.inline)
163+
.toolbar {
164+
ToolbarItem(placement: .cancellationAction) {
165+
Button("Cancel", role: .cancel) {
166+
dismiss()
167+
}
168+
}
169+
ToolbarItem(placement: .confirmationAction) {
170+
Button("Apply") {
171+
updateQueryParameters()
172+
onApply()
173+
dismiss()
174+
}
175+
}
176+
}
177+
}
178+
.onAppear {
179+
// Sets the control's initial state using the values from the last query.
180+
selectedWhereClause = model.queryParameters.whereClause
181+
maxFeatures = model.queryParameters.maxFeatures
182+
183+
if let timeExtent = model.queryParameters.timeExtent {
184+
includesDateFilter = true
185+
selectedStartDate = timeExtent.startDate!
186+
selectedEndDate = timeExtent.endDate!
187+
}
188+
}
189+
}
190+
191+
/// Updates the model's query parameters using the values from the form.
192+
private func updateQueryParameters() {
193+
model.queryParameters.whereClause = selectedWhereClause
194+
model.queryParameters.maxFeatures = maxFeatures
195+
196+
model.queryParameters.timeExtent = includesDateFilter
197+
? TimeExtent(startDate: selectedStartDate, endDate: selectedEndDate)
198+
: nil
199+
}
200+
}
201+
}
202+
203+
// MARK: - Model
204+
205+
private extension QueryWithCQLFiltersView {
206+
/// The view model for the sample.
207+
final class Model: ObservableObject {
208+
/// A map with a topographic basemap.
209+
let map: Map = {
210+
let map = Map(basemapStyle: .arcGISTopographic)
211+
map.initialViewpoint = Viewpoint(latitude: 32.62, longitude: 36.10, scale: 20_000)
212+
return map
213+
}()
214+
215+
/// An OGC API - Features feature collection table for the "Daraa" test dataset.
216+
let ogcFeatureCollectionTable: OGCFeatureCollectionTable = {
217+
let table = OGCFeatureCollectionTable(
218+
url: URL(string: "https://demo.ldproxy.net/daraa")!,
219+
collectionID: "TransportationGroundCrv"
220+
)
221+
// Sets the feature request mode to manual. In this mode, the table must be populated
222+
// manually. Panning and zooming won't request features automatically.
223+
table.featureRequestMode = .manualCache
224+
return table
225+
}()
226+
227+
/// The sample where clause expressions to use with the query parameters.
228+
let sampleWhereClauses: [String] = [
229+
// An empty query.
230+
"",
231+
// A CQL2 TEXT query for features with an F_CODE property of "AP010".
232+
"F_CODE = 'AP010'",
233+
// A CQL2 JSON query for features with an F_CODE property of "AP010".
234+
#"{ "op": "=", "args": [ { "property": "F_CODE" }, "AP010" ] }"#,
235+
// A CQL2 TEXT query for features with an F_CODE attribute property similar to "AQ".
236+
"F_CODE LIKE 'AQ%'",
237+
// A CQL2 JSON query that combines the "before" and "eq" operators
238+
// with the logical "and" operator.
239+
#"{"op": "and", "args":[{ "op": "=", "args":[{ "property" : "F_CODE" }, "AP010"]}, { "op": "t_before", "args":[{ "property" : "ZI001_SDV"},"2013-01-01"]}]}"#
240+
]
241+
242+
/// The parameters for filtering the features returned form a query.
243+
let queryParameters: QueryParameters = {
244+
let queryParameters = QueryParameters()
245+
queryParameters.maxFeatures = 1000
246+
return queryParameters
247+
}()
248+
249+
init() {
250+
// Creates a feature layer to visualize the OGC API features and adds it to the map.
251+
let ogcFeatureLayer = FeatureLayer(featureTable: ogcFeatureCollectionTable)
252+
let redLineSymbol = SimpleLineSymbol(style: .solid, color: .red, width: 3)
253+
ogcFeatureLayer.renderer = SimpleRenderer(symbol: redLineSymbol)
254+
255+
map.addOperationalLayer(ogcFeatureLayer)
256+
}
257+
}
258+
}
259+
260+
#Preview {
261+
QueryWithCQLFiltersView()
262+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Query with CQL filters
2+
3+
Query data from an OGC API feature service using CQL filters.
4+
5+
![Screenshot of Query with CQL filters sample](query-with-cql-filters.png)
6+
7+
## Use case
8+
9+
CQL (Common Query Language) is an OGC-created query language used to query for subsets of features. Use CQL filters to narrow geometry results from an OGC feature table.
10+
11+
## How to use the sample
12+
13+
Configure a CQL query by setting the where clause, max features count, and time extent. Tap the "Apply" button to see the query applied to the OGC API features shown on the map.
14+
15+
## How it works
16+
17+
1. Create an `OGCFeatureCollectionTable` object using a URL to an OGC API feature service and a collection ID.
18+
2. Create a `QueryParameters` object.
19+
3. Set the parameters' `whereClause` and `maxFeatures` properties.
20+
4. Create a `TimeExtent` object using `Date` objects for the start time and end time being queried. Set the `timeExtent` property on the parameters.
21+
5. Populate the feature table using `populateFromService(using:clearCache:outFields:)` with the custom query parameters created in the previous steps.
22+
6. Set the map view's viewpoint to view the newly queried features
23+
24+
## Relevant API
25+
26+
* OGCFeatureCollectionTable
27+
* QueryParameters
28+
* TimeExtent
29+
30+
## About the data
31+
32+
The [Daraa, Syria test data](https://demo.ldproxy.net/daraa) is OpenStreetMap data converted to the Topographic Data Store schema of NGA.
33+
34+
## Additional information
35+
36+
See the [OGC API website](https://ogcapi.ogc.org) for more information on the OGC API family of standards. See the [CQL documentation](https://portal.ogc.org/files/96288#cql-core) to learn more about the common query language.
37+
38+
## Tags
39+
40+
browse, catalog, common query language, CQL, feature table, filter, OGC, OGC API, query, service, web
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"category": "Search and Query",
3+
"description": "Query data from an OGC API feature service using CQL filters.",
4+
"ignore": false,
5+
"images": [
6+
"query-with-cql-filters.png"
7+
],
8+
"keywords": [
9+
"CQL",
10+
"OGC",
11+
"OGC API",
12+
"browse",
13+
"catalog",
14+
"common query language",
15+
"feature table",
16+
"filter",
17+
"query",
18+
"service",
19+
"web",
20+
"OGCFeatureCollectionTable",
21+
"QueryParameters",
22+
"TimeExtent"
23+
],
24+
"redirect_from": [],
25+
"relevant_apis": [
26+
"OGCFeatureCollectionTable",
27+
"QueryParameters",
28+
"TimeExtent"
29+
],
30+
"snippets": [
31+
"QueryWithCQLFiltersView.swift"
32+
],
33+
"title": "Query with CQL filters"
34+
}
132 KB
Loading

0 commit comments

Comments
 (0)