Query with CQL filters

View on GitHubSample viewer app

Query data from an OGC API feature service using CQL filters.

Image of Query with CQL Filters

Use case

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.

How to use the sample

The sample loads displaying all features within the OGC API feature service. Select a sample CQL query from the drop down menu. Optionally, adjust the max features value in the text box, and toggle the time extent check box to query the data with a time extent. Press the "Apply query" button to see the query applied to the OGC API features shown on the map.

How it works

  1. Create an OgcFeatureCollectionTable object using a URL to an OGC API feature service and a collection ID.
  2. Create QueryParameters and set a CQL filter string to it using queryParameters.whereClause.
  3. Set the maximum amount of features to be returned with queryParamters.maxFeatures.
  4. Create a new TimeExtent from start and end date values, and set it to the queryParameters.timeExtent method.
  5. Populate the OGC feature collection table using .populateFromServiceAsync() with the custom QueryParameters created in the previous steps.
  6. Use mapView.setViewpointGeometryAsync() with the OGC feature collection table extent to view the newly-queried features.

Relevant API

  • OgcFeatureCollectionTable
  • QueryParameters
  • TimeExtent

About the data

The Daraa, Syria test data is OpenStreetMap data converted to the Topographic Data Store schema of NGA.

Additional information

See the OGC API website for more information on the OGC API family of standards. See the CQL documentation to learn more about the common query language.

Tags

browse, catalog, common query language, CQL, feature table, filter, OGC, OGC API, query, service, web

Sample Code

MainActivity.kt
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
/* Copyright 2021 Esri
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.esri.arcgisruntime.sample.querywithcqlfilters

import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.esri.arcgisruntime.ArcGISRuntimeEnvironment
import com.esri.arcgisruntime.data.OgcFeatureCollectionTable
import com.esri.arcgisruntime.data.QueryParameters
import com.esri.arcgisruntime.data.ServiceFeatureTable
import com.esri.arcgisruntime.geometry.Geometry
import com.esri.arcgisruntime.geometry.GeometryEngine
import com.esri.arcgisruntime.layers.FeatureLayer
import com.esri.arcgisruntime.loadable.LoadStatus
import com.esri.arcgisruntime.mapping.ArcGISMap
import com.esri.arcgisruntime.mapping.BasemapStyle
import com.esri.arcgisruntime.mapping.TimeExtent
import com.esri.arcgisruntime.mapping.view.MapView
import com.esri.arcgisruntime.sample.querywithcqlfilters.databinding.ActivityMainBinding
import com.esri.arcgisruntime.sample.querywithcqlfilters.databinding.CqlFiltersLayoutBinding
import com.esri.arcgisruntime.symbology.SimpleLineSymbol
import com.esri.arcgisruntime.symbology.SimpleRenderer
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.floatingactionbutton.FloatingActionButton
import java.util.*


class MainActivity : AppCompatActivity() {

    // Keep loadable in scope to avoid garbage collection
    private lateinit var ogcFeatureCollectionTable: OgcFeatureCollectionTable

    // List of CQL where clauses
    private val cqlQueryList: List<String> = listOf(
        "F_CODE = 'AP010'",
        "{ \"op\": \"=\", \"args\": [ { \"property\": \"F_CODE\" }, \"AP010\" ] }",
        "F_CODE LIKE 'AQ%'",
        "{\"op\": \"and\", \"args\":[" +
                "{ \"op\": \"=\", \"args\":[{ \"property\" : \"F_CODE\" }, \"AP010\"]}, " +
                "{ \"op\": \"t_before\", \"args\":[{ \"property\" : \"ZI001_SDV\"},\"2013-01-01\"]}]}",
        "No Query"
    )

    // Current selected where query
    private var cqlQueryListPosition = 4

    // Set a limit of 3000 on the number of returned features per request,
    // the default on some services could be as low as 10
    private var maxFeatures = 3000

    // Defines date range in queryParameters.timeExtent
    private var fromDate = Calendar.getInstance()
    private var toDate = Calendar.getInstance()

    private val activityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    private val mapView: MapView by lazy {
        activityMainBinding.mapView
    }

    private val fab: FloatingActionButton by lazy {
        activityMainBinding.fab
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(activityMainBinding.root)

        // Authentication with an API key or named user is required to
        // access basemaps and other location services
        ArcGISRuntimeEnvironment.setApiKey(BuildConfig.API_KEY)

        // Create a map with a topographic basemap
        val map = ArcGISMap(BasemapStyle.ARCGIS_TOPOGRAPHIC)

        // Set the map to be displayed in the layout's MapView
        mapView.map = map

        // Define strings for the service URL and collection id
        // Note that the service defines the collection id which can be
        // accessed via OgcFeatureCollectionInfo.getCollectionId()
        val serviceUrl = "https://demo.ldproxy.net/daraa"
        val collectionId = "TransportationGroundCrv"

        // Create an OGC feature collection table from the service url and collection ID
        ogcFeatureCollectionTable = OgcFeatureCollectionTable(serviceUrl, collectionId)

        // Set the feature request mode to manual
        // In this mode, the table must be manually populated
        // and panning and zooming won't request features automatically
        ogcFeatureCollectionTable.featureRequestMode =
            ServiceFeatureTable.FeatureRequestMode.MANUAL_CACHE

        ogcFeatureCollectionTable.addDoneLoadingListener {
            if (ogcFeatureCollectionTable.loadStatus == LoadStatus.LOADED) {

                // Create a feature layer and set a renderer to it to visualize the OGC API features
                val featureLayer = FeatureLayer(ogcFeatureCollectionTable)
                val simpleRenderer = SimpleRenderer(
                    SimpleLineSymbol(
                        SimpleLineSymbol.Style.SOLID,
                        Color.MAGENTA,
                        3f
                    )
                )
                featureLayer.renderer = simpleRenderer

                // Add the layer to the map
                map.operationalLayers.add(featureLayer)

                // Zoom to the dataset extent
                val datasetExtent = ogcFeatureCollectionTable.extent
                if (datasetExtent != null && !datasetExtent.isEmpty) {
                    mapView.setViewpointGeometryAsync(datasetExtent)
                }

                val visibleExtentQuery = QueryParameters().apply {
                    // Create a query based on the current visible extent
                    geometry = datasetExtent

                    // Set a limit of 3000 on the number of returned features per request,
                    // the default on some services could be as low as 10
                    maxFeatures = this@MainActivity.maxFeatures
                }

                try {
                    // Populate the table with the query, leaving existing table entries intact
                    // Setting the outfields parameter to null requests all fields
                    ogcFeatureCollectionTable.populateFromServiceAsync(
                        visibleExtentQuery,
                        false,
                        null
                    )
                } catch (e: Exception) {
                    val errorMessage =
                        "Error populating OGC Feature Collection Table from service: " + e.message
                    Toast.makeText(
                        this,
                        errorMessage,
                        Toast.LENGTH_LONG
                    ).show()
                    Log.e(
                        "OGC Service error: ",
                        errorMessage
                    )
                }
            } else {
                val errorMessage =
                    "Failed to load OGC Feature Collection Table" + ogcFeatureCollectionTable.loadError.message
                Toast.makeText(
                    this,
                    errorMessage,
                    Toast.LENGTH_LONG
                ).show()
                Log.e(
                    "OGC Load error: ",
                    errorMessage
                )
            }
        }

        // Load the table
        ogcFeatureCollectionTable.loadAsync()

        // Handles CQL Filters in a Bottom Sheet View.
        fab.setOnClickListener {
            openBottomSheetView()
        }
    }

    /**
     * Handles CQL Filters in a Bottom Sheet View.
     */
    private fun openBottomSheetView() {

        // Resets views in bottomSheet to default values.
        resetBottomSheetValues()

        // Creates a new BottomSheetDialog
        val dialog = BottomSheetDialog(this)

        // Inflates layout file
        val bottomSheetBinding = CqlFiltersLayoutBinding.inflate(layoutInflater)

        bottomSheetBinding.apply {
            // Set the current selection of CQL query
            cqlQueryTextView.text = cqlQueryList[cqlQueryListPosition]

            // Sets the Where Clause for CQL filter
            whereClauseLayout.setOnClickListener {

                // Creates a dialog to choose a where clause
                val alertDialog: AlertDialog.Builder = AlertDialog.Builder(this@MainActivity)
                alertDialog.setTitle("Select Query")
                val checkedItem = cqlQueryListPosition

                alertDialog.setSingleChoiceItems(
                    cqlQueryList.toTypedArray(), checkedItem
                ) { dialog, which ->

                    // Updates the selected where clause
                    cqlQueryListPosition = which
                    cqlQueryTextView.text = cqlQueryList[which]

                    // Dismiss dialog
                    dialog.dismiss()
                }

                // Displays the where clause dialog
                val alert: AlertDialog = alertDialog.create()
                alert.show()
            }

            // Sets the view to the default value of max features (3000)
            maxFeaturesEditText.setText(maxFeatures.toString())

            // Sets from date to Jun-13-2011 by default
            fromDatePicker.updateDate(2011, 5, 13)
            // Sets to date to Jan-7-2012 by default
            toDatePicker.updateDate(2012, 0, 7)

            // Sets up filters for the query when Apply is clicked.
            applyTv.setOnClickListener {
                // Retrieves the max features
                val maxFeaturesText =
                    maxFeaturesEditText.text.toString()
                maxFeatures = when {
                    maxFeaturesText == "" -> 3000
                    maxFeaturesText.toInt() <= 0 -> 3000
                    else -> maxFeaturesText.toInt()
                }

                // Retrieves if date filter is selected
                val isDateFilterSelected = dateSwitch.isChecked

                // Retrieves from & to dates from the DatePicker
                val fromDatePicker = fromDatePicker
                val toDatePicker = toDatePicker
                fromDate.set(fromDatePicker.year, fromDatePicker.month, fromDatePicker.dayOfMonth)
                toDate.set(toDatePicker.year, toDatePicker.month, toDatePicker.dayOfMonth)

                // Dismiss bottom sheet view
                dialog.dismiss()

                // Runs the query using the selected filters
                runQuery(isDateFilterSelected)
            }

            // Dismiss bottom sheet view when cancel is clicked
            cancelTv.setOnClickListener { dialog.dismiss() }
        }


        dialog.setCancelable(false)

        // Sets bottom sheet content view to layout
        dialog.setContentView(bottomSheetBinding.root)

        // Displays bottom sheet view
        dialog.show()
    }

    /**
     * Resets views in bottomSheet to default values.
     */
    private fun resetBottomSheetValues() {
        cqlQueryListPosition = 4
        maxFeatures = 3000
        fromDate.set(2011, 5, 13)
        toDate.set(2012, 0, 7)
    }

    /**
     * Populates features from provided query parameters, and displays the result on the map.
     * If [isDateFilterSelected] is true, query searches between fromDate-toDate
     */
    private fun runQuery(isDateFilterSelected: Boolean) {
        val queryParameters = QueryParameters()

        queryParameters.apply {
            // Set the query parameter's where clause with the the selected query
            // If position is 4 ("Empty Query") manually set [whereClause] to empty string ("")
            whereClause = if (cqlQueryListPosition == 4) {
                ""
            } else {
                cqlQueryList[cqlQueryListPosition]
            }

            // Sets the max features to the number entered in the text field
            maxFeatures = this@MainActivity.maxFeatures

            // If date filter is selected, retrieve the date selected from the date picker
            // and set it to the query parameters time extent
            if (isDateFilterSelected) {
                // set the query parameters time extent
                timeExtent = TimeExtent(fromDate, toDate)
            }
        }

        // Populate the table with the query, clear existing table entries
        // and set the outfields parameter to null requests all fields
        val result = ogcFeatureCollectionTable.populateFromServiceAsync(
            queryParameters,
            true,
            null
        )
        result.addDoneListener {

            // Create a new list to store returned geometries in
            val featureGeometryList: MutableList<Geometry> = ArrayList()

            // Iterate through each result to get its geometry and add it to the geometry list
            result.get().iterator().forEach { feature ->
                featureGeometryList.add(feature.geometry)
            }

            if (featureGeometryList.isNotEmpty()) {
                // Zoom to the total extent of the geometries returned by the query
                val totalExtent = GeometryEngine.combineExtents(featureGeometryList)
                mapView.setViewpointGeometryAsync(totalExtent, 20.0)
            }

            // Display number of features returned
            showResultDialog(ogcFeatureCollectionTable.totalFeatureCount)
        }
    }

    /**
     * Function to show the number of features returned
     */
    private fun showResultDialog(totalFeatureCount: Long) {

        // Build an alert dialog
        val dialogBuilder = AlertDialog.Builder(this)

        // Display message using OGC Feature Collection Table
        dialogBuilder.setMessage("Query returned $totalFeatureCount features")
            .setPositiveButton("Ok") { dialog, _ -> dialog.dismiss() }

        // Create dialog box
        val alert = dialogBuilder.create()
        alert.setTitle("Query Result")

        // Shows alert dialog
        alert.show()
    }

    override fun onPause() {
        mapView.pause()
        super.onPause()
    }

    override fun onResume() {
        super.onResume()
        mapView.resume()
    }

    override fun onDestroy() {
        mapView.dispose()
        super.onDestroy()
    }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.