Skip to content

Commit 82e9f3d

Browse files
committed
Add early implementation of items payload
1 parent e4b5ced commit 82e9f3d

7 files changed

Lines changed: 213 additions & 5 deletions

File tree

pygeoapi/api/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
F_JSON = 'json'
8383
F_COVERAGEJSON = 'json'
8484
F_HTML = 'html'
85+
F_JSONFG = 'jsonfg'
8586
F_JSONLD = 'jsonld'
8687
F_GZIP = 'gzip'
8788
F_PNG = 'png'
@@ -94,7 +95,7 @@
9495
(F_HTML, 'text/html'),
9596
(F_JSONLD, 'application/ld+json'),
9697
(F_JSON, 'application/json'),
97-
(F_PNG, 'image/png'),
98+
(F_JSONFG, 'application/vnd.geo+json'),
9899
(F_JPEG, 'image/jpeg'),
99100
(F_MVT, 'application/vnd.mapbox-vector-tile'),
100101
(F_NETCDF, 'application/x-netcdf'),
@@ -108,7 +109,7 @@
108109
'http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections',
109110
'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/landing-page',
110111
'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json',
111-
'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/html',
112+
'http://www.opengis.net/spec/json-fg-1/0.2/conf/core',
112113
'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/oas30'
113114
]
114115

pygeoapi/api/itemtypes.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from pygeoapi import l10n
5151
from pygeoapi.api import evaluate_limit
5252
from pygeoapi.formatter.base import FormatterSerializationError
53+
from pygeoapi.formatter.jsonfg import geojson2jsonfg
5354
from pygeoapi.linked_data import geojson2jsonld
5455
from pygeoapi.plugin import load_plugin, PLUGINS
5556
from pygeoapi.provider.base import (
@@ -62,7 +63,7 @@
6263
to_json, transform_bbox)
6364

6465
from . import (
65-
APIRequest, API, SYSTEM_LOCALE, F_JSON, FORMAT_TYPES, F_HTML, F_JSONLD,
66+
APIRequest, API, SYSTEM_LOCALE, F_JSON, FORMAT_TYPES, F_HTML, F_JSONFG, F_JSONLD,
6667
validate_bbox, validate_datetime
6768
)
6869

@@ -698,6 +699,27 @@ def get_collection_items(
698699

699700
return headers, HTTPStatus.OK, content
700701

702+
elif request.format == F_JSONFG:
703+
formatter = load_plugin('formatter',
704+
{'name': F_JSONFG, 'geom': True})
705+
706+
try:
707+
content = formatter.write(
708+
data=content,
709+
options={
710+
'provider_def': get_provider_by_type(
711+
collections[dataset]['providers'],
712+
'feature')
713+
}
714+
)
715+
except FormatterSerializationError:
716+
msg = 'Error serializing output'
717+
return api.get_exception(
718+
HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format,
719+
'NoApplicableCode', msg)
720+
721+
return headers, HTTPStatus.OK, content
722+
701723
return headers, HTTPStatus.OK, to_json(content, api.pretty_print)
702724

703725

@@ -982,6 +1004,30 @@ def get_collection_item(api: API, request: APIRequest,
9821004

9831005
return headers, HTTPStatus.OK, content
9841006

1007+
elif request.format == F_JSONFG:
1008+
# content = geojson2jsonfg(
1009+
# api, content, dataset, id_field=(p.uri_field or 'id')
1010+
# )
1011+
formatter = load_plugin('formatter',
1012+
{'name': F_JSONFG, 'geom': True})
1013+
1014+
try:
1015+
content = formatter.write(
1016+
data=content,
1017+
options={
1018+
'provider_def': get_provider_by_type(
1019+
collections[dataset]['providers'],
1020+
'feature')
1021+
}
1022+
)
1023+
except FormatterSerializationError:
1024+
msg = 'Error serializing output'
1025+
return api.get_exception(
1026+
HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format,
1027+
'NoApplicableCode', msg)
1028+
1029+
return headers, HTTPStatus.OK, content
1030+
9851031
return headers, HTTPStatus.OK, to_json(content, api.pretty_print)
9861032

9871033

pygeoapi/formatter/jsonfg.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# =================================================================
2+
#
3+
# Authors: Francesco Bartoli <xbartolone@gmail.com>
4+
#
5+
# Copyright (c) 2025 Francesco Bartoli
6+
#
7+
# Permission is hereby granted, free of charge, to any person
8+
# obtaining a copy of this software and associated documentation
9+
# files (the "Software"), to deal in the Software without
10+
# restriction, including without limitation the rights to use,
11+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
# copies of the Software, and to permit persons to whom the
13+
# Software is furnished to do so, subject to the following
14+
# conditions:
15+
#
16+
# The above copyright notice and this permission notice shall be
17+
# included in all copies or substantial portions of the Software.
18+
#
19+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26+
# OTHER DEALINGS IN THE SOFTWARE.
27+
#
28+
# =================================================================
29+
"""JSON-FG capabilities
30+
Returns content as JSON-FG representations
31+
"""
32+
33+
import json
34+
import logging
35+
import uuid
36+
from typing import Union
37+
38+
from osgeo import gdal
39+
40+
from pygeoapi.formatter.base import BaseFormatter, FormatterSerializationError
41+
42+
LOGGER = logging.getLogger(__name__)
43+
44+
45+
class JSONFGFormatter(BaseFormatter):
46+
"""JSON-FG formatter"""
47+
48+
def __init__(self, formatter_def: dict):
49+
"""
50+
Initialize object
51+
52+
:param formatter_def: formatter definition
53+
54+
:returns: `pygeoapi.formatter.jsonfg.JSONFGFormatter`
55+
"""
56+
57+
geom = False
58+
if "geom" in formatter_def:
59+
geom = formatter_def["geom"]
60+
61+
super().__init__({"name": "jsonfg", "geom": geom})
62+
self.mimetype = "application/vnd.ogc.fg+json"
63+
64+
def write(self, data: dict, options: dict = {}) -> str:
65+
"""
66+
Generate data in JSON-FG format
67+
68+
:param options: JSON-FG formatting options
69+
:param data: dict of GeoJSON data
70+
71+
:returns: string representation of format
72+
"""
73+
74+
try:
75+
fields = list(data["features"][0]["properties"].keys())
76+
except IndexError:
77+
LOGGER.error("no features")
78+
return str()
79+
80+
LOGGER.debug(f"JSONFG fields: {fields}")
81+
82+
try:
83+
output = geojson2jsonfg(data=data, dataset="items")
84+
return output
85+
except ValueError as err:
86+
LOGGER.error(err)
87+
raise FormatterSerializationError("Error writing JSONFG output")
88+
89+
def __repr__(self):
90+
return f"<JSONFGFormatter> {self.name}"
91+
92+
93+
def geojson2jsonfg(
94+
data: dict,
95+
dataset: str,
96+
identifier: Union[str, None] = None,
97+
id_field: str = "id",
98+
) -> str:
99+
"""
100+
Return JSON-FG from a GeoJSON content.
101+
102+
:param cls: API object
103+
:param data: dict of data:
104+
105+
:returns: string of rendered JSON (JSON-FG)
106+
"""
107+
gdal.UseExceptions()
108+
LOGGER.debug("Dump GeoJSON content into a data source")
109+
# breakpoint()
110+
try:
111+
with gdal.OpenEx(json.dumps(data)) as srcDS:
112+
tmpfile = f"/vsimem/{uuid.uuid1()}.json"
113+
LOGGER.debug("Translate GeoJSON into a JSONFG memory file")
114+
gdal.VectorTranslate(tmpfile, srcDS, format="JSONFG")
115+
LOGGER.debug("Read JSONFG content from a memory file")
116+
data = gdal.VSIFOpenL(tmpfile, "rb")
117+
if not data:
118+
raise ValueError("Failed to read JSONFG content")
119+
gdal.VSIFSeekL(data, 0, 2)
120+
length = gdal.VSIFTellL(data)
121+
gdal.VSIFSeekL(data, 0, 0)
122+
jsonfg = json.loads(gdal.VSIFReadL(1, length, data).decode())
123+
return jsonfg
124+
except Exception as e:
125+
LOGGER.error(f"Failed to convert GeoJSON to JSON-FG: {e}")
126+
raise
127+
finally:
128+
gdal.VSIFCloseL(data)

pygeoapi/plugin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@
7373
'xarray-edr': 'pygeoapi.provider.xarray_edr.XarrayEDRProvider'
7474
},
7575
'formatter': {
76-
'CSV': 'pygeoapi.formatter.csv_.CSVFormatter'
76+
'CSV': 'pygeoapi.formatter.csv_.CSVFormatter',
77+
'jsonfg': 'pygeoapi.formatter.jsonfg.JSONFGFormatter',
7778
},
7879
'process': {
7980
'HelloWorld': 'pygeoapi.process.hello_world.HelloWorldProcessor',

requirements-provider.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ cftime
44
elasticsearch
55
elasticsearch-dsl
66
fiona
7-
GDAL<=3.11.3
87
geoalchemy2
98
geopandas
109
netCDF4

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ Babel
22
click
33
filelock
44
Flask
5+
GDAL<=3.11.3
56
jinja2
67
jsonschema
78
pydantic
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# =================================================================
2+
#
3+
# Authors: Francesco Bartoli <xbartolone@gmail.com>
4+
#
5+
# Copyright (c) 2025 Francesco Bartoli
6+
#
7+
# Permission is hereby granted, free of charge, to any person
8+
# obtaining a copy of this software and associated documentation
9+
# files (the "Software"), to deal in the Software without
10+
# restriction, including without limitation the rights to use,
11+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
# copies of the Software, and to permit persons to whom the
13+
# Software is furnished to do so, subject to the following
14+
# conditions:
15+
#
16+
# The above copyright notice and this permission notice shall be
17+
# included in all copies or substantial portions of the Software.
18+
#
19+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26+
# OTHER DEALINGS IN THE SOFTWARE.
27+
#
28+
# =================================================================
29+
30+
import pytest
31+
32+
from pygeoapi.formatter.jsonfg import JSONFGFormatter

0 commit comments

Comments
 (0)