Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions pygeoapi_plugins/formatter/json_.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
79 changes: 14 additions & 65 deletions pygeoapi_plugins/formatter/jsonfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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')
Expand Down Expand Up @@ -132,55 +129,7 @@ def write(self, options: dict = {}, data: dict | None = {}) -> str:

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 to_json(jsonfg)
return super().write(options, jsonfg)

def __repr__(self):
return f'<JSONFGFormatter> {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)
50 changes: 50 additions & 0 deletions tests/test_jsonfg_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
# =================================================================

import json

import pytest

from pygeoapi_plugins.formatter.json_ import GeoJSONGeometry
from pygeoapi_plugins.formatter.jsonfg import JSONFGFormatter


Expand Down Expand Up @@ -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'] = '<b>Dinagat</b>'
result = JSONFGFormatter({}).write(data=fixture)

# Written as JSONFormatter writes JSON: bytes, < and > not escaped
assert isinstance(result, bytes)
assert b'<b>Dinagat</b>' in result
assert b'&lt;' 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])
Loading