Skip to content

Commit 11d3a3a

Browse files
author
dick
committed
Changes to be committed:
Issue #3 1. implementation of search api modified: pygeoapi/api.py modified: pygeoapi/flask_app.py modified: pygeoapi/openapi.py modified: pygeoapi/provider/hateoas.py
1 parent 73ad377 commit 11d3a3a

File tree

4 files changed

+1051
-990
lines changed

4 files changed

+1051
-990
lines changed

pygeoapi/api.py

+109-4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import pytz
5959
from shapely.errors import WKTReadingError
6060
from shapely.wkt import loads as shapely_loads
61+
import shapely.geometry
6162

6263
from pygeoapi import __version__, l10n
6364
from pygeoapi.formatter.base import FormatterSerializationError
@@ -88,6 +89,11 @@
8889
modify_pygeofilter, CrsTransformSpec,
8990
transform_bbox)
9091

92+
import json
93+
from itertools import repeat
94+
from collections import Counter
95+
import urllib
96+
import pprint
9197
LOGGER = logging.getLogger(__name__)
9298

9399
#: Return headers for requests (e.g:X-Powered-By)
@@ -112,7 +118,7 @@
112118
(F_JSON, 'application/json'),
113119
(F_PNG, 'image/png'),
114120
(F_MVT, 'application/vnd.mapbox-vector-tile'),
115-
(F_NETCDF, 'application/x-netcdf'),
121+
(F_NETCDF, 'application/x-netcdf')
116122
))
117123

118124
#: Locale used for system responses (e.g. exceptions)
@@ -178,7 +184,8 @@
178184
"https://api.stacspec.org/v1.0.0/collections",
179185
"https://api.stacspec.org/v1.0.0/core",
180186
"https://api.stacspec.org/v1.0.0/ogcapi-features",
181-
"https://api.stacspec.org/v1.0.0/item-search"
187+
"https://api.stacspec.org/v1.0.0/item-search",
188+
"https://api.stacspec.org/v1.0.0/item-search#sort"
182189
]
183190
}
184191

@@ -3871,6 +3878,13 @@ def get_stac_root(
38713878
"href": f"{self.base_url}/openapi?f=html"
38723879
})
38733880

3881+
content['links'].append({
3882+
"rel": "search",
3883+
"type": "application/geo+json",
3884+
"title": "STAC search",
3885+
"href": f"{self.base_url}/stac/search",
3886+
"method": "GET"
3887+
})
38743888
stac_collections = filter_dict_by_key_value(self.config['resources'],
38753889
'type', 'stac-collection')
38763890

@@ -3885,7 +3899,7 @@ def get_stac_root(
38853899
'href': f'{stac_url}/{key}',
38863900
'type': FORMAT_TYPES[F_HTML]
38873901
})
3888-
3902+
LOGGER.debug(content)
38893903
if request.format == F_HTML: # render
38903904
content = render_j2_template(self.tpl_config,
38913905
'stac/collection.html',
@@ -3971,7 +3985,12 @@ def get_stac_path(self, request: Union[APIRequest, Any],
39713985
if (len(list(stac_collections[dataset]['links'][0].keys())) > 0):
39723986
content['links'].extend(stac_collections[dataset]['links'])
39733987
# LOGGER.debug(content['links'])
3974-
3988+
if (request.format != F_HTML or request.format is None):
3989+
if content['type'] == 'Feature':
3990+
try:
3991+
del content['assets']['default']
3992+
except KeyError:
3993+
pass
39753994
if request.format == F_HTML: # render
39763995
content['path'] = path
39773996
if 'assets' in content: # item view
@@ -4011,6 +4030,92 @@ def get_stac_path(self, request: Union[APIRequest, Any],
40114030
headers.pop('Content-Type', None)
40124031
return headers, HTTPStatus.OK, stac_data
40134032

4033+
@gzip
4034+
@pre_process
4035+
@jsonldify
4036+
def get_stac_search(self, request: Union[APIRequest, Any]) -> Tuple[dict, int, str]:
4037+
4038+
"""
4039+
Provide STAC Search
4040+
4041+
:param request: APIRequest instance with query params
4042+
4043+
:returns: tuple of headers, status code, content
4044+
"""
4045+
if not request.is_valid():
4046+
return self.get_format_exception(request)
4047+
headers = request.get_response_headers(**self.api_headers)
4048+
headers['Content-Type'] = 'application/geo+json'
4049+
# - b'{"limit": 10, "collections": ["eu_l8_EVI"], "sortby": [{"field": "collection", "direction": "asc"}]}'
4050+
LOGGER.debug(f'STAC search (POST data) : {request._data}')
4051+
queries = json.loads(request._data.decode("utf-8"))
4052+
limit = queries['limit']
4053+
sortby = queries['sortby']
4054+
del queries['limit']
4055+
del queries['sortby']
4056+
criteria = list(queries.keys())
4057+
# Search : Start from collections level (biggest), then with others criteria for filtering.
4058+
# If the search doesn't comes with collections param , make one.
4059+
if ('collections' not in criteria):
4060+
LOGGER.debug('STAC search doesn\'t contain collections')
4061+
root_result = self.get_stac_root(request)
4062+
root_result = root_result[2]
4063+
root_result = json.loads(root_result)['links']
4064+
root_result = [r['href'] for r in root_result if (r['rel'] == 'child')]
4065+
root_result = [r.split('/')[-1].split('?')[0] for r in root_result if (r.endswith('json'))]
4066+
LOGGER.debug(f'STAC search find Collections : {root_result}')
4067+
criteria.append('collections')
4068+
queries['collections'] = root_result
4069+
# Get items under each collections - super set
4070+
collections = queries['collections']
4071+
result = list(map(self.get_stac_path, repeat(request), collections))
4072+
result = [r[2] for r in result]
4073+
result = list(map(json.loads, result))
4074+
LOGGER.debug(f'STAC search collections :{result}')
4075+
result = [r['links'] for r in result]
4076+
result = [o['href'] for r in result for o in r if (o['rel'] == 'item')]
4077+
result = ['/'.join(r.split('/')[-2:]) for r in result]
4078+
LOGGER.debug(f'STAC search items:{result}')
4079+
result = list(map(self.get_stac_path, repeat(request), result))
4080+
result = [r[2] for r in result]
4081+
result = list(map(json.loads, result))
4082+
LOGGER.debug(f'STAC search collections item result :{len(result)}')
4083+
# Filter's implememtation - create subset
4084+
def _bboxWrapper(l):
4085+
return shapely.geometry.box(*l)
4086+
filter_idx = Counter()
4087+
got_filter = False
4088+
for filter_ in criteria:
4089+
if(filter_ == 'bbox'):
4090+
got_filter = True
4091+
bbox = queries['bbox'] # Assume WGS84
4092+
LOGGER.debug(f'STAC search bbox filter: {bbox}')
4093+
bbox = shapely.geometry.box(*bbox)
4094+
items_bbox = [ obj['bbox'] for obj in result ]
4095+
items_bbox = list(map(_bboxWrapper,items_bbox))
4096+
items_bbox = list(map(bbox.intersects,items_bbox))
4097+
find_idx = [ i for i,x in enumerate(items_bbox) if (x) ]
4098+
LOGGER.debug(f'STAC search bbox filter found : {find_idx}')
4099+
filter_idx.update(find_idx)
4100+
# 'assets': {'image': {'href': 'https://storage.googleapis.com/download/storage/v1/b/isaac_shared/o/europe_EVI%2Feu_l8_EVI%2Feu_l8_EVI0000000000-0000023296.tif?generation=1697631155438880&alt=media'
4101+
4102+
find_idx = list(filter_idx.keys()) if (got_filter is True) else list(range(len(result)))
4103+
result = [ r for i,r in enumerate(result) if (i in find_idx)]
4104+
for r in result:
4105+
try:
4106+
s=r['assets']['image']['href']
4107+
s=urllib.parse.unquote(s)
4108+
s=s.split('?')[0]
4109+
r['assets']['image']['href']="/".join(s.split('/')[7:8]+s.split('/')[9:])
4110+
except KeyError:
4111+
pass
4112+
# "context": { "returned":len(result), "limit":"0", "matched":len(find_idx) }
4113+
result = { "type": "FeatureCollection", "features":result }
4114+
LOGGER.debug(f'STAC search return results : {pprint.pprint(result)}')
4115+
4116+
4117+
return headers, HTTPStatus.OK, to_json(result, self.pretty_print)
4118+
40144119
def get_exception(self, status, headers, format_, code,
40154120
description) -> Tuple[dict, int, str]:
40164121
"""

pygeoapi/flask_app.py

+8
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,14 @@ def stac_catalog_path(path):
478478
"""
479479
return get_response(api_.get_stac_path(request, path))
480480

481+
@BLUEPRINT.route('/stac/search',methods=['GET', 'POST'])
482+
def stac_catalog_search():
483+
"""
484+
STAC root endpoint
485+
486+
:returns: HTTP response
487+
"""
488+
return get_response(api_.get_stac_search(request))
481489

482490
@ADMIN_BLUEPRINT.route('/admin/config', methods=['GET', 'PUT', 'PATCH'])
483491
def admin_config():

0 commit comments

Comments
 (0)