From b22b5d6c64e69d4a86c64c381185019c70e05d76 Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Fri, 6 Nov 2026 18:15:27 -0500 Subject: [PATCH 1/2] Use the faster json render in JSONFG --- pygeoapi_plugins/formatter/json_.py | 26 +++++++++------ pygeoapi_plugins/formatter/jsonfg.py | 31 ++++++++--------- tests/test_jsonfg_formatter.py | 50 ++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 27 deletions(-) diff --git a/pygeoapi_plugins/formatter/json_.py b/pygeoapi_plugins/formatter/json_.py index a55c299..9dd7187 100644 --- a/pygeoapi_plugins/formatter/json_.py +++ b/pygeoapi_plugins/formatter/json_.py @@ -216,20 +216,26 @@ def _rebuild_geometry(raw: str | bytes, data: dict) -> GeoJSONGeometry: return geometry +# Feature members that can hold a geometry: GeoJSON geometry, JSON-FG place +_GEOMETRY_KEYS = ('geometry', 'place') + + def _embed_geometry(feature: dict) -> dict: """ - Embed a pre-serialized geometry (e.g. from ST_AsGeoJSON) verbatim, - rather than re-encoding it + Embed pre-serialized geometries (e.g. from ST_AsGeoJSON) verbatim, + rather than re-encoding them - :param feature: `dict` of GeoJSON feature + :param feature: `dict` of GeoJSON or JSON-FG feature - :returns: `dict` of GeoJSON feature + :returns: `dict` of feature """ - geometry = feature.get('geometry') - if isinstance(geometry, GeoJSONGeometry): - geometry = geometry.raw - if isinstance(geometry, (str, bytes)): - return {**feature, 'geometry': orjson.Fragment(geometry)} + embedded = {} + for key in _GEOMETRY_KEYS: + geometry = feature.get(key) + if isinstance(geometry, GeoJSONGeometry): + geometry = geometry.raw + if isinstance(geometry, (str, bytes)): + embedded[key] = orjson.Fragment(geometry) - return feature + return {**feature, **embedded} if embedded else feature diff --git a/pygeoapi_plugins/formatter/jsonfg.py b/pygeoapi_plugins/formatter/jsonfg.py index d6e932b..f680aa6 100644 --- a/pygeoapi_plugins/formatter/jsonfg.py +++ b/pygeoapi_plugins/formatter/jsonfg.py @@ -36,22 +36,21 @@ # import osgeo if using geojson2jsonfg # from osgeo import gdal - from pygeoapi.crs import ( DEFAULT_CRS, crs_transform_feature, - get_transform_from_crs, get_crs, + get_transform_from_crs, ) -from pygeoapi.formatter.base import BaseFormatter -from pygeoapi.util import to_json, url_join +from pygeoapi.util import url_join +from pygeoapi_plugins.formatter.json_ import JSONFormatter LOGGER = logging.getLogger(__name__) -class JSONFGFormatter(BaseFormatter): - """JSON-FG formatter""" +class JSONFGFormatter(JSONFormatter): + """JSON-FG formatter, written as `JSONFormatter` writes JSON""" def __init__(self, formatter_def: dict): """ @@ -62,32 +61,30 @@ def __init__(self, formatter_def: dict): :returns: `pygeoapi_plugins.formatter.jsonfg.JSONFGFormatter` """ - geom = False - if 'geom' in formatter_def: - geom = formatter_def['geom'] - - super().__init__({'name': 'jsonfg', 'geom': geom}) - self.mimetype = 'application/geo+json' + super().__init__(formatter_def) + self.name = 'jsonfg' self.f = 'jsonfg' - self.extension = 'json' - def write(self, options: dict = {}, data: dict | None = {}) -> str: + def write( + self, options: dict | None = None, data: dict | None = None + ) -> bytes: """ Generate data in JSON-FG format :param options: JSON-FG formatting options :param data: dict of GeoJSON data - :returns: string representation of format + :returns: bytes representation of format """ + options = options or {} format_conditions = [ data is None, isinstance(data, dict) and not data.get('features'), ] if any(format_conditions): LOGGER.warning('No features to write to JSON-FG') - return str() + return b'' provider_def = options.get('provider_def', {}) time_field = provider_def.get('time_field') @@ -149,7 +146,7 @@ def write(self, options: dict = {}, data: dict | None = {}) -> str: # LOGGER.error(err) # raise FormatterSerializationError('Error writing JSONFG output') - return to_json(jsonfg) + return super().write(options, jsonfg) def __repr__(self): return f' {self.name}' diff --git a/tests/test_jsonfg_formatter.py b/tests/test_jsonfg_formatter.py index f4ec29e..9ff625e 100644 --- a/tests/test_jsonfg_formatter.py +++ b/tests/test_jsonfg_formatter.py @@ -30,8 +30,10 @@ # ================================================================= import json + import pytest +from pygeoapi_plugins.formatter.json_ import GeoJSONGeometry from pygeoapi_plugins.formatter.jsonfg import JSONFGFormatter @@ -147,3 +149,51 @@ def test_jsonfg_spatiotemporal_formatter(spatiotemporal_fixture): assert f_jsonfg['features'][0]['time'] == '2020-01-01T00:00:00Z' assert len(f_jsonfg['links']) == 1 + + +def test_jsonfg_formatter_identity(): + f = JSONFGFormatter({}) + + assert f.name == 'jsonfg' + assert f.f == 'jsonfg' + assert f.extension == 'json' + assert f.mimetype == 'application/geo+json' + + +def test_jsonfg_formatter_json_render(fixture): + fixture['features'][0]['properties']['name'] = 'Dinagat' + result = JSONFGFormatter({}).write(data=fixture) + + # Written as JSONFormatter writes JSON: bytes, < and > not escaped + assert isinstance(result, bytes) + assert b'Dinagat' in result + assert b'<' not in result + + +def test_jsonfg_formatter_geojson_geometry(fixture): + geometry = GeoJSONGeometry('{"type":"Point","coordinates":[125.6,10.1]}') + fixture['features'][0]['geometry'] = geometry + + f_jsonfg = json.loads(JSONFGFormatter({}).write(data=fixture)) + + assert f_jsonfg['features'][0]['geometry'] == { + 'type': 'Point', + 'coordinates': [125.6, 10.1], + } + # Written from the database's JSON, without parsing it + assert geometry._loaded is False + + +def test_jsonfg_formatter_geojson_place(spatiotemporal_fixture): + raw = '{"type":"Point","coordinates":[13981728.0436351,1130195.397638891]}' + spatiotemporal_fixture['features'][0]['geometry'] = GeoJSONGeometry(raw) + options = {'content_crs': 'http://www.opengis.net/def/crs/EPSG/0/3857'} + + result = JSONFGFormatter({}).write( + data=spatiotemporal_fixture, options=options + ) + feature = json.loads(result)['features'][0] + + # place keeps the stored coordinates, geometry is in CRS84 + assert feature['place'] == json.loads(raw) + assert feature['geometry']['coordinates'] == pytest.approx([125.6, 10.1]) From 25d070b4da14cc0dd801f15fd462c8aa2eb8ee4c Mon Sep 17 00:00:00 2001 From: Benjamin Webb Date: Fri, 6 Nov 2026 18:16:13 -0500 Subject: [PATCH 2/2] Cleanup file --- pygeoapi_plugins/formatter/jsonfg.py | 48 ---------------------------- 1 file changed, 48 deletions(-) diff --git a/pygeoapi_plugins/formatter/jsonfg.py b/pygeoapi_plugins/formatter/jsonfg.py index f680aa6..ec875c2 100644 --- a/pygeoapi_plugins/formatter/jsonfg.py +++ b/pygeoapi_plugins/formatter/jsonfg.py @@ -129,55 +129,7 @@ def write( jsonfg['features'].append(feature) - # The following code is an alternative implementation that uses GDAL - # to convert GeoJSON to JSON-FG. It is currently commented out because - # the direct manipulation of the GeoJSON structure is more - # straightforward and does not require GDAL as a dependency. However, - # if there are issues with the direct approach or if there is a need - # for more complex transformations, the GDAL-based implementation can - # be considered. - # - # try: - # links = data.get('links') - # output = geojson2jsonfg(data=data) - # output['links'] = links - # return to_json(output) - # except ValueError as err: - # LOGGER.error(err) - # raise FormatterSerializationError('Error writing JSONFG output') - return super().write(options, jsonfg) def __repr__(self): return f' {self.name}' - - -# def geojson2jsonfg(data: dict) -> dict: -# """ -# Return JSON-FG from a GeoJSON content. - -# :param data: dict of data - -# :returns: dict of converted GeoJSON (JSON-FG) -# """ -# gdal.UseExceptions() -# LOGGER.debug('Dump GeoJSON content into a data source') -# try: -# with gdal.OpenEx(json.dumps(data)) as srcDS: -# tmpfile = f'/vsimem/{uuid.uuid1()}.json' -# LOGGER.debug('Translate GeoJSON into a JSONFG memory file') -# gdal.VectorTranslate(tmpfile, srcDS, format='JSONFG') -# LOGGER.debug('Read JSONFG content from a memory file') -# data = gdal.VSIFOpenL(tmpfile, 'rb') -# if not data: -# raise ValueError('Failed to read JSONFG content') -# gdal.VSIFSeekL(data, 0, 2) -# length = gdal.VSIFTellL(data) -# gdal.VSIFSeekL(data, 0, 0) -# jsonfg = json.loads(gdal.VSIFReadL(1, length, data).decode()) -# return jsonfg -# except Exception as e: -# LOGGER.error(f'Failed to convert GeoJSON to JSON-FG: {e}') -# raise -# finally: -# gdal.VSIFCloseL(data)