Skip to content

Commit

Permalink
feat(geodata): queryables resource for feature collections as additio…
Browse files Browse the repository at this point in the history
…nal metadata #180
  • Loading branch information
navispatial committed Aug 1, 2023
1 parent f4e9e63 commit 9716aeb
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 11 deletions.
17 changes: 17 additions & 0 deletions dart/geodata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +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).

## Introduction

Expand Down Expand Up @@ -307,6 +308,15 @@ Future<void> main(List<String> args) async {
print(' $crs');
}
// optional metadata about queryable properties
final queryables = await source.queryables();
if (queryables != null) {
print('Queryables for ${queryables.title}:');
for (final prop in queryables.properties.values) {
print(' ${prop.name} (${prop.title}): ${prop.type}');
}
}
// next read actual data (wind mills) from this collection
// `items` is used for filtered queries, here bounding box, WGS 84 coordinates
Expand Down Expand Up @@ -383,6 +393,13 @@ The feature source returned by `collection()` provides following methods:
/// Get metadata about the feature collection represented by this source.
Future<OGCCollectionMeta> meta();
/// Get optional metadata about queryable properties for the feature
/// collection represented by this source.
///
/// Returns null if no "queryables" metadata is available for this feature
/// source.
Future<OGCQueryableObject?> queryables();
/// Fetches a single feature by [id] from this source.
///
/// An identifier should be an integer number (int or BigInt) or a string.
Expand Down
42 changes: 35 additions & 7 deletions dart/geodata/example/geodata_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,23 @@ GeoJSON (web / http) resource as a data source:
dart example/geodata_example.dart geojson https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/ 2.5_day.geojson,significant_week.geojson 3 items
dart example/geodata_example.dart geojson https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/ significant_month.geojson 3 items id ok2022cedc
OGC API Features data sources:
OGC API Features service meta queries:
dart example/geodata_example.dart ogcfeat https://demo.pygeoapi.io/master/ - 1 meta
dart example/geodata_example.dart ogcfeat https://weather.obs.fmibeta.com/ - 1 meta
dart example/geodata_example.dart ogcfeat https://demo.ldproxy.net/zoomstack - 1 meta
OGC API Features meta and queryables from collections:
dart example/geodata_example.dart ogcfeat https://demo.pygeoapi.io/master/ lakes,obs,dutch_windmills 1 collection
dart example/geodata_example.dart ogcfeat https://weather.obs.fmibeta.com/ fmi_aws_observations 1 collection
dart example/geodata_example.dart ogcfeat https://demo.ldproxy.net/zoomstack airports 1 collection
OGC API Features feature items from collections:
dart example/geodata_example.dart ogcfeat https://demo.pygeoapi.io/master/ lakes 2 items
dart example/geodata_example.dart ogcfeat https://demo.pygeoapi.io/master/ lakes 2 items id 3
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 meta queries:
dart example/geodata_example.dart ogcfeat https://demo.pygeoapi.io/master/ - 1 meta
dart example/geodata_example.dart ogcfeat https://weather.obs.fmibeta.com/ - 1 meta
More demo APIs (however this page seems to be somewhat outdated, be careful!):
More OGC API Features implementations:
https://github.com/opengeospatial/ogcapi-features/tree/master/implementations
*/

Expand Down Expand Up @@ -159,7 +166,7 @@ Future<void> main(List<String> args) async {
_printCollection(coll);
}
break;
case 'items':
case 'collection':
// Loop over all collections
for (final collectionId in collectionIds) {
// get feature source for a collection
Expand All @@ -170,6 +177,20 @@ Future<void> main(List<String> args) async {
print('Collection meta:');
_printCollection(meta);

// optional metadata about queryable properties
final queryables = await source.queryables();
if (queryables != null) {
// got queryables
_printQueryables(queryables);
}
}
break;
case 'items':
// Loop over all collections
for (final collectionId in collectionIds) {
// get feature source for a collection
final source = await service.collection(collectionId);

// get actual data, a single feature or features
if (query is BoundedItemsQuery) {
await _callItemsPaged(source, query, maxPagedResults);
Expand Down Expand Up @@ -264,6 +285,13 @@ void _printMeta(ResourceMeta meta) {
}
}

void _printQueryables(OGCQueryableObject queryables) {
print('Queryables for ${queryables.title}:');
for (final prop in queryables.properties.values) {
print(' ${prop.name} (${prop.title}): ${prop.type}');
}
}

void _printOpenAPI(OpenAPIDocument document) {
print('OpenAPI ${document.openapi}');
final servers = document.content['servers'] as Iterable<dynamic>;
Expand Down
10 changes: 9 additions & 1 deletion dart/geodata/example/ogcapi_features_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ Future<void> main(List<String> args) async {
print(' $crs');
}

// optional metadata about queryable properties
final queryables = await source.queryables();
if (queryables != null) {
print('Queryables for ${queryables.title}:');
for (final prop in queryables.properties.values) {
print(' ${prop.name} (${prop.title}): ${prop.type}');
}
}

// next read actual data (wind mills) from this collection

// `itemsAll` lets access all features on source (optionally limited by limit)
Expand Down Expand Up @@ -132,7 +141,6 @@ Future<void> main(List<String> args) async {
//
// In this case check the following queryables resource from the service:
// https://demo.pygeoapi.io/master/collections/dutch_windmills/queryables
// (currently the geodata client does not decode queryables yet)
//
// Try to get result geometries projected to WGS 84 / Web Mercator instead of
// using geographic coordinates of WGS84.
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 @@ -17,6 +17,7 @@ export 'core.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';
export 'src/ogcapi_common/model/ogc_service.dart';
export 'src/ogcapi_common/model/ogc_service_meta.dart';
export 'src/ogcapi_features/model/ogc_feature_conformance.dart';
Expand Down
28 changes: 28 additions & 0 deletions dart/geodata/lib/src/common/links/links.dart
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,32 @@ class Links with EquatableMixin {
type: type,
hreflang: hreflang,
);

/// All links with `rel` matching
/// `http://www.opengis.net/def/rel/ogc/1.0/queryables` or
/// `[ogc-rel:queryables]` or `queryables` (the last one not standard but
/// was found somewhere..).
///
/// OGC API - Features - Part 3: Filtering: "The Queryables resource SHALL be
/// referenced from any filterable resource with a link with the link relation
/// type http://www.opengis.net/def/rel/ogc/1.0/queryables (or, alternatively,
/// [ogc-rel:queryables])".
///
/// Optional [type] and [hreflang] params can specify links more precisely.
Iterable<Link> queryables({String? type, String? hreflang}) =>
_byRelInternal('queryables', type: type, hreflang: hreflang)
.followedBy(
_byRelInternal(
'[ogc-rel:queryables]',
type: type,
hreflang: hreflang,
).followedBy(
_byRelInternal(
'http://www.opengis.net/def/rel/ogc/1.0/queryables',
type: type,
hreflang: hreflang,
),
),
)
.toList(growable: false);
}
177 changes: 177 additions & 0 deletions dart/geodata/lib/src/ogcapi_common/model/ogc_queryable_object.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// 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 'package:meta/meta.dart';

/// Represents `Queryables` document for an OGC API service parsed from JSON
/// Schema data.
///
/// Note: this class wraps decoded JSON Object from a JSON document containing
/// JSON Schema data. To utilize such data JSON Object tree in [content] can
/// be traversed as needed.
///
/// This class provides also decoded attributes like [title], [description],
/// [additionalProperties] and [properties] originating from JSON Schema data
/// and referenced by the `OGC API - Features - Part 3: Filtering` standard.
/// These attributes do not cover all use cases described in the standard
/// however, so for certain special use cases original JSON Schema tree in
/// [content] might be needed.
///
/// See also:
/// * https://github.com/opengeospatial/ogcapi-features (see Part 3 / Filtering)
/// * https://json-schema.org/
@immutable
class OGCQueryableObject {
const OGCQueryableObject._({
required this.content,
required this.id,
required this.schemaId,
required this.title,
this.description,
this.additionalProperties = true,
required this.properties,
});

/// Parses `Queryables` document for an OGC API service from JSON Schema based
/// data in [content].
factory OGCQueryableObject.fromJson(Map<String, dynamic> content) {
if ((content['type'] as String).toLowerCase() != 'object') {
throw const FormatException('Not valid JSON Schema type.');
}

final properties = content['properties'];
return OGCQueryableObject._(
content: content,
id: content[r'$id'] as String, // required
schemaId: content[r'$schema'] as String, // required
title: content['title'] as String? ?? 'Queryables',
description: content['description'] as String?,
additionalProperties: content['additionalProperties'] as bool? ?? true,
properties: properties != null && properties is Map<String, dynamic>
? OGCQueryableProperty._parseMap(properties)
: <String, OGCQueryableProperty>{},
);
}

/// JSON Schema based data representing `Queryables` document for an OGC API
/// service.
///
/// This is data that is directly parsed from JSON Schema data an OGC API
/// Service has published. Use this for more detailed inspection of
/// Queryables metadata when other class members are not enough.
final Map<String, dynamic> content;

/// The URI of the resource without query parameters.
final String id;

/// The schema id of JSON Schema data in content.
///
/// Should be either "https://json-schema.org/draft/2019-09/schema" or
/// "https://json-schema.org/draft/2020-12/schema" according to the
/// `OGC API - Features - Part 3: Filtering` standard.
final String schemaId;

/// The human readable title for this queryable object.
final String title;

/// An optional human readable description.
final String? description;

/// If true, any properties are valid in filter expressions even when not
/// declared in a queryable schema.
final bool additionalProperties;

/// A map of queryable properties for this queryable object.
///
/// The map key represents a property name (that is accessible also from
/// the `name` property of `OGCQueryableProperty` object).
///
/// NOTE: currently this contains only non-geospatial properties that SHOULD
/// have at least "type" and "title" attributes.
final Map<String, OGCQueryableProperty> properties;

@override
String toString() => content.toString();

@override
bool operator ==(Object other) =>
other is OGCQueryableObject && content == other.content;

@override
int get hashCode => content.hashCode;
}

/// A queryable non-geospatial property.
@immutable
class OGCQueryableProperty {
const OGCQueryableProperty._({
required this.name,
required this.title,
this.description,
required this.type,
});

static Map<String, OGCQueryableProperty> _parseMap(
Map<String, dynamic> properties,
) {
final map = <String, OGCQueryableProperty>{};

for (final entry in properties.entries) {
final name = entry.key;
final def = entry.value;
if (def is Map<String, dynamic>) {
final type = def['type'];
if (type != null && type is String) {
map[name] = OGCQueryableProperty._(
name: name,
title: def['title'] as String? ?? name,
description: def['description'] as String?,
type: type,
);
}
}
}

return map;
}

/// The property name.
final String name;

/// The human readable title for this property.
final String title;

/// An optional human readable description.
final String? description;

/// The type for this property.
///
/// According to the `OGC API - Features - Part 3: Filtering` standard a type
/// SHOULD be one of the following:
/// * `string` (string or temporal properties)
/// * `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;

@override
String toString() =>
'{name: $name, title: $title, description: $description, type: $type}';

@override
bool operator ==(Object other) =>
other is OGCQueryableProperty &&
name == other.name &&
title == other.title &&
description == other.description &&
type == other.type;

@override
int get hashCode => Object.hash(name, title, description, type);
}
12 changes: 12 additions & 0 deletions dart/geodata/lib/src/ogcapi_features/model/ogc_feature_source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import '/src/core/features/feature_source.dart';
import '/src/ogcapi_common/model/ogc_collection_meta.dart';
import '/src/ogcapi_common/model/ogc_queryable_object.dart';

import 'ogc_feature_item.dart';
import 'ogc_feature_items.dart';
Expand All @@ -15,4 +16,15 @@ abstract class OGCFeatureSource
extends FeatureSource<OGCFeatureItem, OGCFeatureItems> {
/// Get metadata about the feature collection represented by this source.
Future<OGCCollectionMeta> meta();

/// Get optional metadata about queryable properties for the feature
/// collection represented by this source.
///
/// Returns null if no "queryables" metadata is available for this feature
/// source.
///
/// An OGC API Features service providing "queryables" metadata must publish
/// support for the `Queryables` conformance class specified in the
/// `OGC API - Features - Part 3: Filtering` standard.
Future<OGCQueryableObject?> queryables();
}

0 comments on commit 9716aeb

Please sign in to comment.