diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst
index bb07f1b..9a8e66a 100644
--- a/docs/source/configuration.rst
+++ b/docs/source/configuration.rst
@@ -147,6 +147,7 @@ default.
keywords: # list of related keywords
- observations
- monitoring
+ geojsonld: true # use default geojsonld behavior (see Linked Data section)
context: # linked data configuration (see Linked Data section)
- datetime: https://schema.org/DateTime
- vocab: https://example.com/vocab#
@@ -175,6 +176,7 @@ default.
name: CSV
data: tests/data/obs.csv # required: the data filesystem path or URL, depending on plugin setup
id_field: id # required for vector data, the field corresponding to the ID
+ uri_field: uri # optional field corresponding to the Uniform Resource Identifier (see Linked Data section)
time_field: datetimestamp # optional field corresponding to the temporal property of the dataset
title_field: foo # optional field of which property to display as title/label on HTML pages
format: # optional default format
@@ -232,12 +234,17 @@ The metadata for an instance is determined by the content of the `metadata`_ sec
This metadata is included automatically, and is sufficient for inclusion in major indices of datasets, including the
`Google Dataset Search`_.
-For collections, at the level of an item or items, by default the JSON-LD representation adds:
+For collections, at the level of an item or items, the default the JSON-LD representation adds:
- The GeoJSON JSON-LD `vocabulary and context `_ to the ``@context``.
- An ``@id`` for each item in a collection, that is the URL for that item (resolving to its HTML representation
in pygeoapi)
+The optional configuration options for collections, at the level of an item of items, are:
+
+- If ``geojsonld`` is not specified or is ``true``, the JSON-LD will conform to the default configuration presenting GeoJSON-LD. If ``geojsonld`` is ``false`` the individual item JSON-LD will conform to standard JSON-LD, and the ``@context`` will not include the geojson vocabulary, but only one specifying the ``@id`` property, the ``@type`` property, and schema.org/ vocabulary. In addition, properties block will be expanded into the main body, non-point geometry will be removed, and point-type geometry will be represented as https://schema.org geometry instead of geojson.
+- If ``uri_field`` is specified, JSON-LD will be updated such that ``@id:uri_field`` for each item in a collection.
+
.. note::
While this is enough to provide valid RDF (as GeoJSON-LD), it does not allow the *properties* of your items to be
unambiguously interpretable.
diff --git a/pygeoapi/api.py b/pygeoapi/api.py
index d384809..361cf9e 100644
--- a/pygeoapi/api.py
+++ b/pygeoapi/api.py
@@ -1045,6 +1045,8 @@ class API:
content['collections_path'] = '/'.join(path_info.split('/')[:-2])
content['startindex'] = startindex
+ if p.uri_field is not None:
+ content['uri_field'] = p.uri_field
if p.title_field is not None:
content['title_field'] = p.title_field
content['id_field'] = p.title_field
@@ -1074,8 +1076,10 @@ class API:
return headers_, 200, content
elif format_ == 'jsonld':
headers_['Content-Type'] = 'application/ld+json'
- content = geojson2geojsonld(self.config, content, dataset)
- return headers_, 200, content
+ content = geojson2geojsonld(
+ self.config, content, dataset, id_field=(p.uri_field or 'id')
+ )
+ return headers_, 200, to_json(content, self.pretty_print)
return headers_, 200, to_json(content, self.pretty_print)
@@ -1147,24 +1151,25 @@ class API:
msg = 'identifier not found'
return self.get_exception(400, headers_, format_, 'NotFound', msg)
+ uri = content['properties'].get(p.uri_field) if p.uri_field else \
+ '{}/collections/{}/items/{}'.format(
+ self.config['server']['url'], dataset, identifier)
+
content['links'] = [{
'rel': 'self' if not format_ or format_ == 'json' else 'alternate',
'type': 'application/geo+json',
'title': 'This document as GeoJSON',
- 'href': '{}/collections/{}/items/{}?f=json'.format(
- self.config['server']['url'], dataset, identifier)
+ 'href': '{}?f=json'.format(uri)
}, {
'rel': 'self' if format_ == 'jsonld' else 'alternate',
'type': 'application/ld+json',
'title': 'This document as RDF (JSON-LD)',
- 'href': '{}/collections/{}/items/{}?f=jsonld'.format(
- self.config['server']['url'], dataset, identifier)
+ 'href': '{}?f=jsonld'.format(uri)
}, {
'rel': 'self' if format_ == 'html' else 'alternate',
'type': 'text/html',
'title': 'This document as HTML',
- 'href': '{}/collections/{}/items/{}?f=html'.format(
- self.config['server']['url'], dataset, identifier)
+ 'href': '{}?f=html'.format(uri)
}, {
'rel': 'collection',
'type': 'application/json',
@@ -1174,13 +1179,11 @@ class API:
}, {
'rel': 'prev',
'type': 'application/geo+json',
- 'href': '{}/collections/{}/items/{}'.format(
- self.config['server']['url'], dataset, identifier)
+ 'href': uri
}, {
'rel': 'next',
'type': 'application/geo+json',
- 'href': '{}/collections/{}/items/{}'.format(
- self.config['server']['url'], dataset, identifier)
+ 'href': uri
}
]
@@ -1189,6 +1192,8 @@ class API:
content['title'] = collections[dataset]['title']
content['id_field'] = p.id_field
+ if p.uri_field is not None:
+ content['uri_field'] = p.uri_field
if p.title_field is not None:
content['title_field'] = p.title_field
@@ -1199,8 +1204,9 @@ class API:
elif format_ == 'jsonld':
headers_['Content-Type'] = 'application/ld+json'
content = geojson2geojsonld(
- self.config, content, dataset, identifier=identifier
+ self.config, content, dataset, uri, (p.uri_field or 'id')
)
+ content = to_json(content, self.pretty_print)
return headers_, 200, content
return headers_, 200, to_json(content, self.pretty_print)
diff --git a/pygeoapi/linked_data.py b/pygeoapi/linked_data.py
index fb89c05..7b717b8 100644
--- a/pygeoapi/linked_data.py
+++ b/pygeoapi/linked_data.py
@@ -30,7 +30,6 @@
Returns content as linked data representations
"""
-import json
import logging
from pygeoapi.util import is_url
@@ -161,7 +160,7 @@ def jsonldify_collection(cls, collection):
return dataset
-def geojson2geojsonld(config, data, dataset, identifier=None):
+def geojson2geojsonld(config, data, dataset, identifier=None, id_field='id'):
"""
Render GeoJSON-LD from a GeoJSON base. Inserts a @context that can be
read from, and extended by, the pygeoapi configuration for a particular
@@ -171,35 +170,96 @@ def geojson2geojsonld(config, data, dataset, identifier=None):
:param data: dict of data:
:param dataset: dataset identifier
:param identifier: item identifier (optional)
+ :param id_field: item identifier_field (optional)
:returns: string of rendered JSON (GeoJSON-LD)
"""
context = config['resources'][dataset].get('context', [])
- data['id'] = (
- '{}/collections/{}/items/{}' if identifier
- else '{}/collections/{}/items'
- ).format(
- *[config['server']['url'], dataset, identifier]
- )
+ geojsonld = config['resources'][dataset].get('geojsonld', True)
+
+ if identifier:
+ # Single geojsonld
+ if not geojsonld:
+ data, geocontext = make_jsonld(data)
+ data[id_field] = identifier
+ else:
+ data['id'] = identifier
+
+ else:
+ # Collection of geojsonld
+ data['@id'] = '{}/collections/{}/items/'.format(
+ config['server']['url'], dataset)
+ for i, feature in enumerate(data['features']):
+ identifier = feature.get(id_field,
+ feature['properties'].get(id_field, ''))
+ if not is_url(str(identifier)):
+ identifier = '{}/collections/{}/items/{}'.format(
+ config['server']['url'], dataset, feature['id'])
+
+ if not geojsonld:
+ feature, geocontext = make_jsonld(feature)
+ geocontext.append({
+ "features": "schema:itemListElement",
+ "FeatureCollection": "schema:itemList"
+ })
+ # Note: @id or https://schema.org/url, both or something else?
+ feature[id_field] = identifier
+ else:
+ feature['id'] = identifier
+
+ data['features'][i] = feature
+
if data.get('timeStamp', False):
data['https://schema.org/sdDatePublished'] = data.pop('timeStamp')
- defaultVocabulary = "https://geojson.org/geojson-ld/geojson-context.jsonld"
- ldjsonData = {
- "@context": [defaultVocabulary, *(context or [])],
- **data
- }
- isCollection = identifier is None
- if isCollection:
- for i, feature in enumerate(data['features']):
- featureId = feature.get(
- 'id', None
- ) or feature.get('properties', {}).get('id', None)
- if featureId is None:
- continue
- # Note: @id or https://schema.org/url or both or something else?
- if is_url(str(featureId)):
- feature['id'] = featureId
- else:
- feature['id'] = '{}/{}'.format(data['id'], featureId)
- return json.dumps(ldjsonData)
+ defaultVocabulary = "https://geojson.org/geojson-ld/geojson-context.jsonld"
+ if not geojsonld:
+ ldjsonData = {
+ "@context": [
+ {
+ "schema": "https://schema.org/",
+ id_field: "@id",
+ "type": "@type",
+ },
+ *(context or []),
+ *(geocontext or [])
+ ],
+ **data
+ }
+ else:
+ ldjsonData = {
+ "@context": [
+ defaultVocabulary,
+ *(context or [])
+ ],
+ **data
+ }
+
+ return ldjsonData
+
+
+def make_jsonld(feature):
+ # Expand properties block
+ feature = {**feature, **feature.get('properties')}
+ feature.pop('properties')
+
+ feature['type'] = 'schema:Place'
+
+ # Remove non-point geometry
+ if feature.get('geometry').get('type').lower() != 'point':
+ feature.pop('geometry')
+ geocontext = []
+ else:
+ feature.get('geometry').update({
+ "lat": feature.get('geometry').get('coordinates')[1],
+ "long": feature.get('geometry').get('coordinates')[0]
+ })
+ feature.get('geometry').pop('coordinates')
+ geocontext = [{
+ "geometry": "schema:geo",
+ "Point": "schema:GeoCoordinates",
+ "lat": "schema:latitude",
+ "long": "schema:longitude"
+ }, ]
+
+ return feature, geocontext
diff --git a/pygeoapi/provider/base.py b/pygeoapi/provider/base.py
index c477091..24ef993 100644
--- a/pygeoapi/provider/base.py
+++ b/pygeoapi/provider/base.py
@@ -53,6 +53,7 @@ class BaseProvider:
self.options = provider_def.get('options', None)
self.id_field = provider_def.get('id_field', None)
+ self.uri_field = provider_def.get('uri_field', None)
self.x_field = provider_def.get('x_field', None)
self.y_field = provider_def.get('y_field', None)
self.time_field = provider_def.get('time_field')
diff --git a/pygeoapi/templates/collections/items/index.html b/pygeoapi/templates/collections/items/index.html
index 390b5c4..451f73b 100644
--- a/pygeoapi/templates/collections/items/index.html
+++ b/pygeoapi/templates/collections/items/index.html
@@ -73,13 +73,16 @@
+ {% if data.get('uri_field') %}
+ | {{ data['uri_field'] }} |
+ {% endif %}
id |
{% if data['title_field'] %}
{{ data['title_field'] }} |
{% endif %}
{% for k, v in data['features'][0]['properties'].items() %}
{# start with id & title then take first 5 columns for table #}
- {% if loop.index < 5 and k != data['id_field'] and k != data['title_field'] %}
+ {% if loop.index < 5 and k not in [data['id_field'], data['title_field'], data['uri_field'], 'extent'] %}
{{ k }} |
{% endif %}
{% endfor %}
@@ -88,12 +91,15 @@
{% for ft in data['features'] %}
+ {% if data.get('uri_field') %}
+ | {{ft['properties'][data.get('uri_field')]}} |
+ {% endif %}
{{ ft.id | string | truncate( 12 ) }} |
{% if data['title_field'] %}
{{ ft['properties'][data['title_field']] | string | truncate( 35 ) }} |
{% endif %}
{% for k, v in ft['properties'].items() %}
- {% if loop.index < 5 and k not in [data['id_field'], data['title_field'], 'extent'] %}
+ {% if loop.index < 5 and k not in [data['id_field'], data['title_field'], data['uri_field'], 'extent'] %}
{{ v | string | truncate( 35 ) }} |
{% endif %}
{% endfor %}
diff --git a/pygeoapi/templates/collections/items/item.html b/pygeoapi/templates/collections/items/item.html
index 1fb5f74..5c0a819 100644
--- a/pygeoapi/templates/collections/items/item.html
+++ b/pygeoapi/templates/collections/items/item.html
@@ -77,6 +77,12 @@
+ {% if data.uri_field %}
+
+ | {{ data.uri_field }} |
+ {{ data['properties'].pop(data.uri_field) }} |
+
+ {% endif %}
| id |
{{ data.id }} |
@@ -86,7 +92,7 @@
| {{ k }} |
{% if k in ['links', 'associations'] %}
-
+ |
{% for l in v %}
{% if l['href'] %}
|