Skip to content

Commit

Permalink
feat(geodata): CQL query and features filter initial support #180
Browse files Browse the repository at this point in the history
  • Loading branch information
navispatial committed Aug 1, 2023
1 parent 3aad613 commit ba7a980
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 18 deletions.
28 changes: 22 additions & 6 deletions dart/geodata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Standard part | Support in this package
------------- | -----------------------
[OGC API - Features - Part 1: Core](https://docs.ogc.org/is/17-069r4/17-069r4.html) | Supported for accessing metadata and GeoJSON feature collections.
[OGC API - Features - Part 2: Coordinate Reference Systems by Reference](https://docs.ogc.org/is/18-058r1/18-058r1.html) | Supported.
OGC API - Features - Part 3: Filtering (draft) | Partially supported (conformance classes, queryables).
OGC API - Features - Part 3: Filtering (draft) | Partially supported (conformance classes, queryables, features filter).

## Introduction

Expand Down Expand Up @@ -437,17 +437,33 @@ The feature source returned by `collection()` provides following methods:
/// Fetches a single feature by id (set in [query]) from this source.
Future<OGCFeatureItem> item(ItemQuery query);
/// Fetches features matching [query] from this source.
/// Fetches features matching [query] (and an optional [cql] query) from this
/// source.
///
/// If both [query] and [cql] are provided, then a service returns only
/// features that match both [query] AND the [cql] query.
///
/// This call accesses only one set of feature items (number of returned items
/// can be limited).
Future<OGCFeatureItems> items(BoundedItemsQuery query);
/// Fetches features as paged sets matching [query] from this source.
@override
Future<OGCFeatureItems> items(
BoundedItemsQuery query, {
CQLQuery? cql,
});
/// Fetches features as paged sets matching [query] (and an optional [cql]
/// query) from this source.
///
/// If both [query] and [cql] are provided, then a service returns only
/// features that match both [query] AND the [cql] query.
///
/// This call returns a first set of feature items (number of returned items
/// can be limited), with a link to an optional next set of feature items.
Future<Paged<OGCFeatureItems>> itemsPaged(BoundedItemsQuery query);
@override
Future<Paged<OGCFeatureItems>> itemsPaged(
BoundedItemsQuery query, {
CQLQuery? cql,
});
```

Methods accessing multiple feature items return a future of `OGCFeatureItems``
Expand Down
36 changes: 33 additions & 3 deletions dart/geodata/example/geodata_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

// ignore_for_file: avoid_print

import 'dart:convert';

import 'package:equatable/equatable.dart';
import 'package:geobase/coordinates.dart';
import 'package:geobase/vector_data.dart';
Expand Down Expand Up @@ -38,6 +40,9 @@ dart example/geodata_example.dart ogcfeat https://demo.pygeoapi.io/master/ lakes
dart example/geodata_example.dart ogcfeat https://weather.obs.fmibeta.com/ fmi_aws_observations 2 items bbox 23,62,24,63
dart example/geodata_example.dart ogcfeat https://demo.ldproxy.net/zoomstack airports 2 items
OGC API Features feature items from collections using CQL2:
dart example/geodata_example.dart ogcfeat https://demo.ldproxy.net/zoomstack airports 2 items cql cql2-text - "name='London Oxford Airport'"
More OGC API Features implementations:
https://github.com/opengeospatial/ogcapi-features/tree/master/implementations
*/
Expand Down Expand Up @@ -71,6 +76,7 @@ Future<void> main(List<String> args) async {

// parse query
GeospatialQuery query = BoundedItemsQuery(limit: limit);
CQLQuery? cql;

if (args.length >= 7) {
switch (args[5]) {
Expand Down Expand Up @@ -106,6 +112,27 @@ Future<void> main(List<String> args) async {
}
}
break;
case 'cql':
if (args.length >= 9) {
final filterLang = args[6];
final filterCrs =
args[7] == '-' ? null : CoordRefSys.normalized(args[7]);
switch (filterLang) {
case CQLQuery.filterLangCQL2Text:
cql = CQLQuery.fromText(
args[8],
filterCrs: filterCrs,
);
break;
case CQLQuery.filterLangCQL2Json:
cql = CQLQuery.fromJson(
json.decode(args[8]) as Map<String, dynamic>,
filterCrs: filterCrs,
);
break;
}
}
break;
}
}

Expand All @@ -127,7 +154,7 @@ Future<void> main(List<String> args) async {
case 'items':
// get actual data, a single feature or features
if (query is BoundedItemsQuery) {
await _callItemsPaged(source, query, maxPagedResults);
await _callItemsPaged(source, query, null, maxPagedResults);
} else if (query is ItemQuery) {
await _callItemById(source, query);
}
Expand Down Expand Up @@ -193,7 +220,7 @@ Future<void> main(List<String> args) async {

// get actual data, a single feature or features
if (query is BoundedItemsQuery) {
await _callItemsPaged(source, query, maxPagedResults);
await _callItemsPaged(source, query, cql, maxPagedResults);
} else if (query is ItemQuery) {
await _callItemById(source, query);
}
Expand Down Expand Up @@ -235,12 +262,15 @@ Future<bool> _callItemById(
Future<bool> _callItemsPaged(
BasicFeatureSource source,
BoundedItemsQuery query,
CQLQuery? cql,
int maxPagedResults,
) async {
// fetch feature items as paged results, max rounds by maxPagedResults
var round = 0;
Paged<FeatureItems>? page;
if (source is FeatureSource) {
if (source is OGCFeatureSource) {
page = await source.itemsPaged(query, cql: cql);
} else if (source is FeatureSource) {
page = await source.itemsPaged(query);
} else {
page = await source.itemsAllPaged(limit: query.limit);
Expand Down
1 change: 1 addition & 0 deletions dart/geodata/lib/ogcapi_features_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ library ogcapi_features_client;
export 'common.dart';
export 'core.dart';

export 'src/cql2/model/cql_query.dart';
export 'src/ogcapi_common/model/ogc_collection_meta.dart';
export 'src/ogcapi_common/model/ogc_conformance.dart';
export 'src/ogcapi_common/model/ogc_queryable_object.dart';
Expand Down
111 changes: 111 additions & 0 deletions dart/geodata/lib/src/cql2/model/cql_query.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) 2020-2023 Navibyte (https://navibyte.com). All rights reserved.
// Use of this source code is governed by a “BSD-3-Clause”-style license that is
// specified in the LICENSE file.
//
// Docs: https://github.com/navibyte/geospatial

import 'dart:convert';

import 'package:geobase/coordinates.dart';
import 'package:meta/meta.dart';

/// A query based on the `Common Query Language` (CQL2).
///
/// References:
/// * [OGC API - Features](https://github.com/opengeospatial/ogcapi-features) -
/// Part 3: Filtering (draft).
/// * Common Query Language,
/// [CQL2 draft](https://docs.ogc.org/DRAFTS/21-065.html).
@immutable
class CQLQuery {
final String _filter;
final String _filterLang;
final CoordRefSys? _filterCrs;

const CQLQuery._(
String filter, {
String filterLang = filterLangCQL2Text,
CoordRefSys? filterCrs,
}) : _filter = filter,
_filterLang = filterLang,
_filterCrs = filterCrs;

/// Creates a query based on the the `cql2-text` encoding of the
/// `Common Query Language` (CQL2).
///
/// Parameters:
/// * [filter]: A filter expression conforming to `cql2-text` to be applied
/// when retrieving resources.
/// * [filterCrs]: An optional coordinate reference system used by the
/// [filter] expression. When not specified, WGS84 longitude / latitude is
/// assumed.
///
/// NOTE: text data in [filter] is not validated by this client-side
/// implementation (that is, it's sent to a server as-is).
const CQLQuery.fromText(
String filter, {
CoordRefSys? filterCrs,
}) : this._(
filter,
filterLang: filterLangCQL2Text,
filterCrs: filterCrs,
);

/// Creates a query based on the the `cql2-json` encoding of the
/// `Common Query Language` (CQL2).
///
/// Parameters:
/// * [filter]: A filter expression as JSON Object conforming to `cql2-json`
/// to be applied when retrieving resources.
/// * [filterCrs]: An optional coordinate reference system used by the
/// [filter] expression. When not specified, WGS84 longitude / latitude is
/// assumed.
///
/// NOTE: JSON data in [filter] is not validated by this client-side
/// implementation (that is, it's sent to a server as-is).
CQLQuery.fromJson(
Map<String, dynamic> filter, {
CoordRefSys? filterCrs,
}) : this._(
json.encode(filter),
filterLang: filterLangCQL2Json,
filterCrs: filterCrs,
);

/// The text encoding of CQL2 (Common Query Language).
static const filterLangCQL2Text = 'cql2-text';

/// The JSON encoding of CQL2 (Common Query Language).
static const filterLangCQL2Json = 'cql2-json';

/// The filter expression of [filterLang] to be applied when retrieving
/// resources.
String get filter => _filter;

/// The predicate language that [filter] conforms to.
///
/// For example "cql2-text" or "cql2-json".
///
/// See [filterLangCQL2Text] and [filterLangCQL2Json].
String get filterLang => _filterLang;

/// An optional coordinate reference system used by the [filter] expression.
///
/// When not specified, WGS84 longitude / latitude is assumed
/// (CoordRefSys.CRS84] or [CoordRefSys.CRS84h]).
CoordRefSys? get filterCrs => _filterCrs;

@override
String toString() =>
'{filter: $filter, filterLang: $filterLang, filterCrs: $filterCrs}';

@override
bool operator ==(Object other) =>
other is CQLQuery &&
filter == other.filter &&
filterLang == other.filterLang &&
filterCrs == other.filterCrs;

@override
int get hashCode => Object.hash(filter, filterLang, filterCrs);
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class OGCQueryableProperty {
/// * `number` / `integer` (numeric properties)
/// * `boolean` (boolean properties)
/// * `array` (array properties)
///
///
/// In practise different OGC API Features implementations seem also to use
/// different specifiers for types.
final String type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
//
// Docs: https://github.com/navibyte/geospatial

import '/src/common/paged/paged.dart';
import '/src/core/data/bounded_items_query.dart';
import '/src/core/features/feature_source.dart';
import '/src/cql2/model/cql_query.dart';
import '/src/ogcapi_common/model/ogc_collection_meta.dart';
import '/src/ogcapi_common/model/ogc_queryable_object.dart';

Expand All @@ -22,7 +25,7 @@ abstract class OGCFeatureSource
///
/// Returns null if no "queryables" metadata is available for this feature
/// source.
///
///
/// An instance of `OGCQueryableObject` contains metadata about supported
/// queryable parameters (in `properties`) instantiated as
/// `OGCQueryableProperty` objects. You may use this information when
Expand All @@ -33,4 +36,54 @@ abstract class OGCFeatureSource
/// support for the `Queryables` conformance class specified in the
/// `OGC API - Features - Part 3: Filtering` standard.
Future<OGCQueryableObject?> queryables();

/// Fetches features matching [query] (and an optional [cql] query) from this
/// source.
///
/// If both [query] and [cql] are provided, then a service returns only
/// features that match both [query] AND the [cql] query.
///
/// This call accesses only one set of feature items (number of returned items
/// can be limited).
///
/// [query] defines a filter or query parameters based on standards:
/// * `OGC API - Features - Part 1: Core`
/// * `OGC API - Features - Part 2: Coordinate Reference Systems by Reference`
/// * `OGC API - Features - Part 3: Filtering`: Queryables as Query Parameters
///
/// [cql] defines a filter or query parameters based on standards:
/// * `OGC API - Features - Part 3: Filtering`: Filter / Features Filter
/// * `Common Query Language (CQL2)`
///
/// Throws `ServiceException<FeatureFailure>` in a case of a failure.
@override
Future<OGCFeatureItems> items(
BoundedItemsQuery query, {
CQLQuery? cql,
});

/// Fetches features as paged sets matching [query] (and an optional [cql]
/// query) from this source.
///
/// If both [query] and [cql] are provided, then a service returns only
/// features that match both [query] AND the [cql] query.
///
/// This call returns a first set of feature items (number of returned items
/// can be limited), with a link to an optional next set of feature items.
///
/// [query] defines a filter or query parameters based on standards:
/// * `OGC API - Features - Part 1: Core`
/// * `OGC API - Features - Part 2: Coordinate Reference Systems by Reference`
/// * `OGC API - Features - Part 3: Filtering`: Queryables as Query Parameters
///
/// [cql] defines a filter or query parameters based on standards:
/// * `OGC API - Features - Part 3: Filtering`: Filter / Features Filter
/// * `Common Query Language (CQL2)`
///
/// Throws `ServiceException<FeatureFailure>` in a case of a failure.
@override
Future<Paged<OGCFeatureItems>> itemsPaged(
BoundedItemsQuery query, {
CQLQuery? cql,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import '/src/common/paged/paged.dart';
import '/src/common/service/service_exception.dart';
import '/src/core/data/bounded_items_query.dart';
import '/src/core/data/item_query.dart';
import '/src/cql2/model/cql_query.dart';
import '/src/ogcapi_common/model/ogc_collection_meta.dart';
import '/src/ogcapi_common/model/ogc_queryable_object.dart';
import '/src/ogcapi_common/service/client/ogc_client.dart';
Expand Down Expand Up @@ -230,7 +231,7 @@ class _OGCFeatureSourceHttp implements OGCFeatureSource {
final crs = query.crs;
var params = <String, String>{
//'f': 'json',
if (crs != null) 'crs': crs.toString(),
if (crs != null) 'crs': crs.id,
};
if (query.queryablesAsParameters != null) {
params = Map.of(query.queryablesAsParameters!)..addAll(params);
Expand Down Expand Up @@ -281,12 +282,18 @@ class _OGCFeatureSourceHttp implements OGCFeatureSource {
}

@override
Future<OGCFeatureItems> items(BoundedItemsQuery query) async =>
Future<OGCFeatureItems> items(
BoundedItemsQuery query, {
CQLQuery? cql,
}) async =>
// read only first set of feature items
(await itemsPaged(query)).current;
(await itemsPaged(query, cql: cql)).current;

@override
Future<Paged<OGCFeatureItems>> itemsPaged(BoundedItemsQuery query) async {
Future<Paged<OGCFeatureItems>> itemsPaged(
BoundedItemsQuery query, {
CQLQuery? cql,
}) async {
// read "collections/{collectionId}/items" and return as paged response

// form a query url
Expand All @@ -298,10 +305,13 @@ class _OGCFeatureSourceHttp implements OGCFeatureSource {
var params = <String, String>{
//'f': 'json',
if (limit != null) 'limit': limit.toString(),
if (crs != null) 'crs': crs.toString(),
if (bboxCrs != null) 'bbox-crs': bboxCrs.toString(),
if (crs != null) 'crs': crs.id,
if (bboxCrs != null) 'bbox-crs': bboxCrs.id,
if (bbox != null) 'bbox': bbox,
if (datetime != null) 'datetime': datetime,
if (cql != null) 'filter': cql.filter,
if (cql != null) 'filter-lang': cql.filterLang,
if (cql != null && cql.filterCrs != null) 'filter-crs': cql.filterCrs!.id,
};
if (query.queryablesAsParameters != null) {
params = Map.of(query.queryablesAsParameters!)..addAll(params);
Expand Down

0 comments on commit ba7a980

Please sign in to comment.