Skip to content

Commit

Permalink
feat(geodata): support decoding GeoJSONL input on GeoJSONFeatureClient
Browse files Browse the repository at this point in the history
  • Loading branch information
navispatial committed Apr 16, 2024
1 parent 7957beb commit 2a4d48e
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 43 deletions.
3 changes: 3 additions & 0 deletions dart/geodata/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

NOTE: Version 1.1.0 currently under development (1.1.0-dev.0).

🧩 Features:
* [Support for GeoJSON Text Sequences](https://github.com/navibyte/geospatial/issues/217)

🛠 Maintenance:
* Adding trailing commas to avoid "Missing a required trailing comma" message.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2020-2023 Navibyte (https://navibyte.com). All rights reserved.
// Copyright (c) 2020-2024 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.
//
Expand All @@ -19,7 +19,7 @@ import '/src/utils/feature_future_adapter.dart';
import '/src/utils/feature_http_adapter.dart';

/// A class with static factory methods to create feature sources conforming to
/// the GeoJSON format.
/// [GeoJSON] or [GeoJSONL] formats.
class GeoJSONFeatures {
/// A client for accessing a `GeoJSON` data resource at [location] via http(s)
/// conforming to [format].
Expand All @@ -38,9 +38,9 @@ class GeoJSONFeatures {
/// overridden by the feature source implementation).
///
/// When [format] is not given, then [GeoJSON] with default settings is used
/// as a default. Note that currently only GeoJSON is supported, but it's
/// possible to inject another format implementation (or with custom
/// configuration) to the default one.
/// as a default. Note that currently only [GeoJSON] and [GeoJSONL] format are
/// tested, but it's possible to inject other format implementations too (or
/// adapt formats mentioned with a custom configuration).
///
/// Use [crs] to give hints (like axis order, and whether x and y must
/// be swapped when read in) about coordinate reference system in text input.
Expand All @@ -67,9 +67,9 @@ class GeoJSONFeatures {
/// resource or other sources. Contents must be GeoJSON compliant data.
///
/// When [format] is not given, then [GeoJSON] with default settings is used
/// as a default. Note that currently only GeoJSON is supported, but it's
/// possible to inject another format implementation (or with custom
/// configuration) to the default one.
/// as a default. Note that currently only [GeoJSON] and [GeoJSONL] format are
/// tested, but it's possible to inject other format implementations too (or
/// adapt formats mentioned with a custom configuration).
///
/// Use [crs] to give hints (like axis order, and whether x and y must
/// be swapped when read in) about coordinate reference system in text input.
Expand Down Expand Up @@ -144,18 +144,18 @@ class _GeoJSONFeatureSource implements BasicFeatureSource {
Future<Paged<FeatureItems>> itemsAllPaged({int? limit}) {
final src = source;

// fetch data as JSON Object + parse GeoJSON feature or feature collection
// fetch data as text + parse GeoJSON feature collection
if (src is Uri) {
// read web resource and convert to entity
return adapter!.getEntityFromJsonObject(
return adapter!.getEntityFromText(
src,
toEntity: (data, _) => _parseFeatureItems(limit, data, format, crs),
toEntity: (text, _) => _parseFeatureItems(limit, text, format, crs),
);
} else if (src is Future<String> Function()) {
// read a future returned by a function
return readEntityFromJsonObject(
return readEntityFromText(
src,
toEntity: (data) => _parseFeatureItems(limit, data, format, crs),
toEntity: (text) => _parseFeatureItems(limit, text, format, crs),
);
}

Expand All @@ -166,14 +166,14 @@ class _GeoJSONFeatureSource implements BasicFeatureSource {

_GeoJSONPagedFeaturesItems _parseFeatureItems(
int? limit,
Map<String, dynamic> data,
String text,
TextReaderFormat<FeatureContent> format,
CoordRefSys? crs,
) {
// NOTE: get count without actually parsing the whole feature collection

// get the whole collection to get count
final collection = FeatureCollection.fromData(data, format: format, crs: crs);
final collection = FeatureCollection.parse(text, format: format, crs: crs);
final count = collection.features.length;

// analyze if only a first set or all items should be returned
Expand All @@ -187,7 +187,7 @@ _GeoJSONPagedFeaturesItems _parseFeatureItems(
}

// return as paged collection (paging through already fetched data)
return _GeoJSONPagedFeaturesItems.parse(format, crs, data, count, range);
return _GeoJSONPagedFeaturesItems.parse(format, crs, text, count, range);
}

class _GeoJSONPagedFeaturesItems with Paged<FeatureItems> {
Expand All @@ -196,20 +196,20 @@ class _GeoJSONPagedFeaturesItems with Paged<FeatureItems> {
this.crs,
this.features,
this.count, [
this.data,
this.text,
this.nextRange,
]);

factory _GeoJSONPagedFeaturesItems.parse(
TextReaderFormat<FeatureContent> format,
CoordRefSys? crs,
Map<String, dynamic> data,
String text,
int count,
_Range? range,
) {
// parse feature items for the range and
final collection = FeatureCollection.fromData(
data,
final collection = FeatureCollection.parse(
text,
format: format,
crs: crs,
options: range != null
Expand All @@ -234,7 +234,7 @@ class _GeoJSONPagedFeaturesItems with Paged<FeatureItems> {

// return a paged result either with ref to next range or without
return nextRange != null
? _GeoJSONPagedFeaturesItems(format, crs, items, count, data, nextRange)
? _GeoJSONPagedFeaturesItems(format, crs, items, count, text, nextRange)
: _GeoJSONPagedFeaturesItems(format, crs, items, count);
}

Expand All @@ -244,24 +244,24 @@ class _GeoJSONPagedFeaturesItems with Paged<FeatureItems> {
final FeatureItems features;
final int count;

final Map<String, dynamic>? data;
final String? text;
final _Range? nextRange;

@override
FeatureItems get current => features;

@override
bool get hasNext => !(nextRange == null || data == null);
bool get hasNext => !(nextRange == null || text == null);

@override
Future<Paged<FeatureItems>?> next() async {
if (nextRange == null || data == null) {
if (nextRange == null || text == null) {
return null;
}
return _GeoJSONPagedFeaturesItems.parse(
format,
crs,
data!,
text!,
count,
nextRange,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Copyright (c) 2020-2023 Navibyte (https://navibyte.com). All rights reserved.
// Copyright (c) 2020-2024 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/common.dart';
import 'package:geobase/coordinates.dart';
import 'package:geobase/meta.dart';
Expand Down Expand Up @@ -157,9 +159,11 @@ class _OGCFeatureClientHttp extends OGCClientHttp implements OGCFeatureService {
_cachedConformance.getAsync(() {
// fetch data as JSON Object, and parse conformance classes
final url = resolveSubResource(endpoint, 'conformance');
return adapter.getEntityFromJson(
return adapter.getEntityFromText(
url,
toEntity: (data, _) {
toEntity: (text, _) {
final data = json.decode(text);

if (data is Map<String, dynamic>) {
// standard: root has JSON Object with "conformsTo" containing
// conformance classes
Expand Down
29 changes: 26 additions & 3 deletions dart/geodata/lib/src/utils/feature_future_adapter.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2020-2023 Navibyte (https://navibyte.com). All rights reserved.
// Copyright (c) 2020-2024 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.
//
Expand All @@ -14,14 +14,14 @@ import '/src/core/features/feature_failure.dart';
/// Maps a JSON Object read from [source] to an entity using [toEntity].
///
/// The source function returns a future that fetches data from a file, a web
/// resource or other sources. Contents must be GeoJSON compliant data.
/// resource or other sources. Content must be GeoJSON compliant data.
@internal
Future<T> readEntityFromJsonObject<T>(
Future<String> Function() source, {
required T Function(Map<String, dynamic> data) toEntity,
}) async {
try {
// read contents as text
// read content as text
final text = await source();

// decode JSON and expect a JSON Object as `Map<String, dynamic>`
Expand All @@ -36,3 +36,26 @@ Future<T> readEntityFromJsonObject<T>(
throw ServiceException(FeatureFailure.clientError, cause: e, trace: st);
}
}

/// Maps text data read from [source] to an entity using [toEntity].
///
/// The source function returns a future that fetches data from a file, a web
/// resource or other sources. Content must be GeoJSON compliant data.
@internal
Future<T> readEntityFromText<T>(
Future<String> Function() source, {
required T Function(String text) toEntity,
}) async {
try {
// read content as text
final text = await source();

// map text to an entity
return toEntity(text);
} on ServiceException<FeatureFailure> {
rethrow;
} catch (e, st) {
// other exceptions (including errors)
throw ServiceException(FeatureFailure.clientError, cause: e, trace: st);
}
}
23 changes: 11 additions & 12 deletions dart/geodata/lib/src/utils/feature_http_adapter.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2020-2023 Navibyte (https://navibyte.com). All rights reserved.
// Copyright (c) 2020-2024 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.
//
Expand Down Expand Up @@ -54,21 +54,23 @@ class FeatureHttpAdapter {
Map<String, String>? headers = _acceptJSON,
List<String>? expect = _expectJSON,
}) =>
getEntityFromJson(
getEntityFromText(
url,
toEntity: (data, responseHeaders) =>
toEntity(data as Map<String, dynamic>, responseHeaders),
toEntity: (text, responseHeaders) {
final data = json.decode(text);
return toEntity(data as Map<String, dynamic>, responseHeaders);
},
headers: headers,
expect: expect,
);

/// Makes `GET` request to [url] with optional [headers].
///
/// Returns an entity mapped from JSON element using [toEntity].
Future<T> getEntityFromJson<T>(
/// Returns an entity mapped from body text using [toEntity].
Future<T> getEntityFromText<T>(
Uri url, {
required T Function(
dynamic data,
String text,
Map<String, String> responseHeaders,
) toEntity,
Map<String, String>? headers = _acceptJSON,
Expand Down Expand Up @@ -101,11 +103,8 @@ class FeatureHttpAdapter {
}
}

// decode JSON
final data = json.decode(response.body);

// map JSON data to an entity
return toEntity(data, response.headers);
// map response body text to an entity
return toEntity(response.body, response.headers);
case 302:
throw const ServiceException(FeatureFailure.found);
case 303:
Expand Down
29 changes: 29 additions & 0 deletions dart/geodata/test/data/london.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "ROG",
"geometry": {
"type": "Point",
"coordinates": [-0.0014, 51.4778, 45.0]
},
"properties": {
"title": "Royal Observatory",
"place": "Greenwich"
}
},
{
"type": "Feature",
"id": "TB",
"geometry": {
"type": "Point",
"coordinates": [-0.075406, 51.5055]
},
"properties": {
"title": "Tower Bridge",
"built": 1886
}
}
]
}
2 changes: 2 additions & 0 deletions dart/geodata/test/data/london.geojsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type":"Feature","id":"ROG","geometry":{"type":"Point","coordinates":[-0.0014,51.4778,45]},"properties":{"title":"Royal Observatory","place":"Greenwich"}}
{"type":"Feature","id":"TB","geometry":{"type":"Point","coordinates":[-0.075406,51.5055]},"properties":{"title":"Tower Bridge","built":1886}}
Loading

0 comments on commit 2a4d48e

Please sign in to comment.