From b54bdd80a816d4f40df7e07d0c76ed0dd1ea50ad Mon Sep 17 00:00:00 2001 From: David Huard Date: Fri, 23 Feb 2024 01:13:24 -0300 Subject: [PATCH 001/279] Use get to fetch units attribute to avoid error if missing. (#1539) --- pygeoapi/provider/xarray_.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygeoapi/provider/xarray_.py b/pygeoapi/provider/xarray_.py index 44398c760..4a1f39665 100644 --- a/pygeoapi/provider/xarray_.py +++ b/pygeoapi/provider/xarray_.py @@ -439,10 +439,10 @@ def _get_coverage_properties(self): if coord.lower() == 'time': time_var = coord continue - if self._data.coords[coord].attrs['units'] == 'degrees_north': + if self._data.coords[coord].attrs.get('units') == 'degrees_north': y_var = coord continue - if self._data.coords[coord].attrs['units'] == 'degrees_east': + if self._data.coords[coord].attrs.get('units') == 'degrees_east': x_var = coord continue From 6bd1bc1118b2d6ac8010aa490f751477ebf0660f Mon Sep 17 00:00:00 2001 From: Bernhard Mallinger Date: Tue, 27 Feb 2024 13:52:02 +0100 Subject: [PATCH 002/279] Clear up space before running CI (#1550) * Clear up space before running CI With all the databases and dependencies more than the 19G is needed (this is the amount which is currenlty available on runners). There is a github action which meddles with the disc layout, which is probably too much for our use case, but it gave me the idea that you can delete files which are needed to support certain platforms such as dotnet and android. https://github.com/easimon/maximize-build-space With only manually removing those files, around 30G are available which should be enough for the foreseeable future * Update main.yml --------- Co-authored-by: Tom Kralidis --- .github/workflows/main.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6b154baed..291a79bf5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -44,6 +44,14 @@ jobs: PYGEOAPI_CONFIG: "$(pwd)/pygeoapi-config.yml" steps: + - name: Clear up GitHub runner diskspace + run: | + echo "Space before" + df -h / + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + echo "Space after" + df -h / - name: Chown user run: | sudo chown -R $USER:$USER $GITHUB_WORKSPACE From 4b2b60c1de42b18ddf8e1bce30d68123cde51b88 Mon Sep 17 00:00:00 2001 From: bojanKuzma <78640631+bojanKuzma@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:37:27 +0100 Subject: [PATCH 003/279] Added translations for Serbian and Bosnian language (#1553) Co-authored-by: Bojan Kuzmanovic --- locale/README.md | 17 +- locale/bs/LC_MESSAGES/messages.po | 536 ++++++++++++++++++++++++++++++ locale/sr/LC_MESSAGES/messages.po | 536 ++++++++++++++++++++++++++++++ 3 files changed, 1088 insertions(+), 1 deletion(-) create mode 100644 locale/bs/LC_MESSAGES/messages.po create mode 100644 locale/sr/LC_MESSAGES/messages.po diff --git a/locale/README.md b/locale/README.md index ee3c40741..49f8bdc19 100644 --- a/locale/README.md +++ b/locale/README.md @@ -1,4 +1,4 @@ -# Howto set up language files +# How to set up language files Inspired by https://phrase.com/blog/posts/i18n-advantages-babel-python/#Integration_with_Jinja2_templates @@ -22,6 +22,21 @@ Then extract the base messages from templates: This file is not persisted on github. +If the above command gives this ouput: +``` +AttributeError: module 'jinja2.ext' has no attribute 'autoescape' +``` + +Just comment out the extensions line in the `babel-mapping.ini` file: +``` +[python: **.py] +[jinja2: **/templates/**.html] +;extensions=jinja2.ext.i18n,jinja2.ext.autoescape,jinja2.ext.with_ +``` + +Then rerun the command for extracting base messages. + + Now setup a new language (french) using the init command: > pybabel init -d locale -l fr -i locale/messages.pot diff --git a/locale/bs/LC_MESSAGES/messages.po b/locale/bs/LC_MESSAGES/messages.po new file mode 100644 index 000000000..9188cbb40 --- /dev/null +++ b/locale/bs/LC_MESSAGES/messages.po @@ -0,0 +1,536 @@ +# Bosnian translations for PROJECT. +# Copyright (C) 2024 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2024-02-27 14:39+0100\n" +"PO-Revision-Date: 2024-02-27 14:40+0100\n" +"Last-Translator: Bojan Kuzmanović \n" +"Language: bs\n" +"Language-Team: bs \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: pygeoapi/templates/_base.html:51 +msgid "Admin" +msgstr "Admin" + +#: pygeoapi/templates/_base.html:67 pygeoapi/templates/landing_page.html:2 +msgid "Home" +msgstr "Početna" + +#: pygeoapi/templates/_base.html:75 pygeoapi/templates/_base.html:83 +msgid "json" +msgstr "json" + +#: pygeoapi/templates/_base.html:78 pygeoapi/templates/_base.html:86 +msgid "jsonld" +msgstr "jsonld" + +#: pygeoapi/templates/_base.html:105 +msgid "Powered by " +msgstr "Pokreće ga" + +#: pygeoapi/templates/conformance.html:2 pygeoapi/templates/conformance.html:4 +#: pygeoapi/templates/conformance.html:8 +#: pygeoapi/templates/landing_page.html:86 +msgid "Conformance" +msgstr "Usklađenost" + +#: pygeoapi/templates/exception.html:2 pygeoapi/templates/exception.html:5 +msgid "Exception" +msgstr "Izuzetak" + +#: pygeoapi/templates/landing_page.html:25 +msgid "Terms of service" +msgstr "Uslovi korišćenja" + +#: pygeoapi/templates/collections/collection.html:38 +#: pygeoapi/templates/landing_page.html:35 +msgid "License" +msgstr "Licensa" + +#: pygeoapi/templates/collections/collection.html:6 +#: pygeoapi/templates/collections/coverage/domainset.html:4 +#: pygeoapi/templates/collections/coverage/rangetype.html:4 +#: pygeoapi/templates/collections/edr/query.html:4 +#: pygeoapi/templates/collections/index.html:2 +#: pygeoapi/templates/collections/index.html:4 +#: pygeoapi/templates/collections/items/index.html:4 +#: pygeoapi/templates/collections/items/item.html:27 +#: pygeoapi/templates/collections/queryables.html:4 +#: pygeoapi/templates/collections/tiles/index.html:4 +#: pygeoapi/templates/collections/tiles/metadata.html:4 +#: pygeoapi/templates/landing_page.html:48 +#: pygeoapi/templates/stac/collection_base.html:19 +msgid "Collections" +msgstr "Kolekcije" + +#: pygeoapi/templates/landing_page.html:50 +msgid "View the collections in this service" +msgstr "Pogledajte kolekcije u ovoj usluzi" + +#: pygeoapi/templates/landing_page.html:56 +msgid "SpatioTemporal Assets" +msgstr "Prostorno-vremenski resursi" + +#: pygeoapi/templates/landing_page.html:58 +msgid "View the SpatioTemporal Assets in this service" +msgstr "Pretražite prostorno-vremenske resurse u ovoj usluzi" + +#: pygeoapi/templates/landing_page.html:64 +#: pygeoapi/templates/processes/index.html:2 +#: pygeoapi/templates/processes/index.html:4 +#: pygeoapi/templates/processes/process.html:4 +msgid "Processes" +msgstr "Procesi" + +#: pygeoapi/templates/landing_page.html:66 +msgid "View the processes in this service" +msgstr "Pogledajte procese u ovoj usluzi" + +#: pygeoapi/templates/jobs/index.html:2 pygeoapi/templates/jobs/index.html:4 +#: pygeoapi/templates/jobs/index.html:11 pygeoapi/templates/jobs/job.html:4 +#: pygeoapi/templates/jobs/results/index.html:4 +#: pygeoapi/templates/landing_page.html:70 +#: pygeoapi/templates/processes/process.html:76 +msgid "Jobs" +msgstr "Zadaci" + +#: pygeoapi/templates/landing_page.html:72 +#: pygeoapi/templates/processes/process.html:77 +msgid "Browse jobs" +msgstr "Pregledajte zadatke" + +#: pygeoapi/templates/landing_page.html:77 +msgid "API Definition" +msgstr "API definicija" + +#: pygeoapi/templates/landing_page.html:79 +msgid "Documentation" +msgstr "Dokumentacija" + +#: pygeoapi/templates/landing_page.html:79 +msgid "Swagger UI" +msgstr "Swagger UI" + +#: pygeoapi/templates/landing_page.html:79 +msgid "ReDoc" +msgstr "ReDoc" + +#: pygeoapi/templates/landing_page.html:82 +msgid "OpenAPI Document" +msgstr "OpenAPI dokument" + +#: pygeoapi/templates/landing_page.html:88 +msgid "View the conformance classes of this service" +msgstr "Poglejte klase usklađenosti ove usluge" + +#: pygeoapi/templates/landing_page.html:92 +msgid "Tile Matrix Sets" +msgstr "Matrica tile-ova" + +#: pygeoapi/templates/landing_page.html:94 +msgid "View the Tile Matrix Sets available on this service" +msgstr "Pogledajte matrice tile-ova dostupne u ovoj usluzi" + +#: pygeoapi/templates/landing_page.html:101 +msgid "Provider" +msgstr "Provajder" + +#: pygeoapi/templates/landing_page.html:110 +msgid "Contact point" +msgstr "Kontakt tačka" + +#: pygeoapi/templates/landing_page.html:113 +msgid "Address" +msgstr "Adresa" + +#: pygeoapi/templates/landing_page.html:122 +msgid "Email" +msgstr "Email" + +#: pygeoapi/templates/landing_page.html:125 +msgid "Telephone" +msgstr "Telefon" + +#: pygeoapi/templates/landing_page.html:129 +msgid "Fax" +msgstr "Fax" + +#: pygeoapi/templates/landing_page.html:133 +msgid "Contact URL" +msgstr "Konktakt URL" + +#: pygeoapi/templates/landing_page.html:137 +msgid "Hours" +msgstr "Sati" + +#: pygeoapi/templates/landing_page.html:141 +msgid "Contact instructions" +msgstr "Konktakt instrukcije" + +#: pygeoapi/templates/collections/collection.html:51 +msgid "Browse" +msgstr "Pretražite" + +#: pygeoapi/templates/collections/collection.html:55 +msgid "Browse Items" +msgstr "Pretražite stavke" + +#: pygeoapi/templates/collections/collection.html:56 +msgid "Browse through the items of" +msgstr "Pretražite kroz stavke od" + +#: pygeoapi/templates/collections/collection.html:59 +#: pygeoapi/templates/collections/queryables.html:6 +#: pygeoapi/templates/collections/queryables.html:17 +msgid "Queryables" +msgstr "Upitni" + +#: pygeoapi/templates/collections/collection.html:63 +msgid "Display Queryables" +msgstr "Prikaži upitne" + +#: pygeoapi/templates/collections/collection.html:64 +msgid "Display Queryables of" +msgstr "Prikaži upitne od" + +#: pygeoapi/templates/collections/collection.html:69 +#: pygeoapi/templates/collections/tiles/index.html:6 +#: pygeoapi/templates/collections/tiles/metadata.html:6 +msgid "Tiles" +msgstr "Tile-ovi" + +#: pygeoapi/templates/collections/collection.html:73 +msgid "Display Tiles" +msgstr "Prikaži tile-ove" + +#: pygeoapi/templates/collections/collection.html:73 +msgid "Display Tiles of" +msgstr "Prikaži tile-ove od" + +#: pygeoapi/templates/collections/collection.html:80 +#: pygeoapi/templates/jobs/job.html:50 +#: pygeoapi/templates/processes/process.html:78 +msgid "Links" +msgstr "Linkovi" + +#: pygeoapi/templates/collections/collection.html:90 +msgid "Reference Systems" +msgstr "Referentni sistemi" + +#: pygeoapi/templates/collections/collection.html:98 +msgid "Storage CRS" +msgstr "Skladišni CRS" + +#: pygeoapi/templates/collections/index.html:12 +#: pygeoapi/templates/processes/index.html:14 +#: pygeoapi/templates/stac/catalog.html:17 +#: pygeoapi/templates/stac/collection.html:17 +msgid "Name" +msgstr "Naziv" + +#: pygeoapi/templates/collections/index.html:13 +#: pygeoapi/templates/stac/catalog.html:18 +msgid "Type" +msgstr "Tip" + +#: pygeoapi/templates/collections/index.html:14 +#: pygeoapi/templates/processes/index.html:15 +#: pygeoapi/templates/processes/process.html:26 +#: pygeoapi/templates/processes/process.html:56 +#: pygeoapi/templates/stac/collection.html:18 +#: pygeoapi/templates/tilematrixsets/index.html:16 +msgid "Description" +msgstr "Opis" + +#: pygeoapi/templates/collections/coverage/domainset.html:11 +msgid "Coverage domain set" +msgstr "Pokrivenost domenskog skupa" + +#: pygeoapi/templates/collections/coverage/domainset.html:12 +msgid "Axis labels" +msgstr "Labele osa" + +#: pygeoapi/templates/collections/coverage/domainset.html:18 +msgid "Extent" +msgstr "Extent" + +#: pygeoapi/templates/collections/coverage/domainset.html:24 +msgid "Coordinate reference system" +msgstr "Koordinate referentnog sistema" + +#: pygeoapi/templates/collections/coverage/domainset.html:26 +#: pygeoapi/templates/stac/catalog.html:20 +#: pygeoapi/templates/stac/collection_base.html:34 +#: pygeoapi/templates/stac/item.html:34 +msgid "Size" +msgstr "Veličina" + +#: pygeoapi/templates/collections/coverage/domainset.html:28 +msgid "width" +msgstr "širina" + +#: pygeoapi/templates/collections/coverage/domainset.html:29 +msgid "height" +msgstr "visina" + +#: pygeoapi/templates/collections/coverage/domainset.html:31 +msgid "Resolution" +msgstr "Rezolucija" + +#: pygeoapi/templates/collections/coverage/domainset.html:33 +msgid "x" +msgstr "x" + +#: pygeoapi/templates/collections/coverage/domainset.html:34 +msgid "y" +msgstr "y" + +#: pygeoapi/templates/collections/coverage/rangetype.html:11 +msgid "Coverage range type" +msgstr "Pokrivenost tipa opsega" + +#: pygeoapi/templates/collections/coverage/rangetype.html:12 +msgid "Fields" +msgstr "Polja" + +#: pygeoapi/templates/collections/edr/query.html:11 +#: pygeoapi/templates/collections/items/index.html:11 +#: pygeoapi/templates/collections/items/item.html:33 +msgid "Items" +msgstr "Stavke" + +#: pygeoapi/templates/collections/items/index.html:25 +msgid "Items in this collection" +msgstr "Stavke u ovoj kolekciji" + +#: pygeoapi/templates/collections/items/index.html:38 +msgid "Warning: Higher limits not recommended!" +msgstr "Upozorenje: Veće granice nisu preporučljivi!" + +#: pygeoapi/templates/collections/items/index.html:43 +msgid "Limit" +msgstr "Granica" + +#: pygeoapi/templates/collections/items/index.html:45 +msgid "default" +msgstr "podrazumijevano" + +#: pygeoapi/templates/collections/items/index.html:66 +#: pygeoapi/templates/collections/items/item.html:62 +msgid "Prev" +msgstr "Prethodno" + +#: pygeoapi/templates/collections/items/index.html:68 +#: pygeoapi/templates/collections/items/item.html:64 +msgid "Next" +msgstr "Sljedeće" + +#: pygeoapi/templates/collections/items/index.html:139 +msgid "No items" +msgstr "Nema stavki" + +#: pygeoapi/templates/collections/items/item.html:77 +#: pygeoapi/templates/stac/collection_base.html:58 +#: pygeoapi/templates/stac/item.html:58 +msgid "Property" +msgstr "Svojstvo" + +#: pygeoapi/templates/collections/items/item.html:78 +#: pygeoapi/templates/stac/collection_base.html:59 +#: pygeoapi/templates/stac/item.html:59 +msgid "Value" +msgstr "Vrijednost" + +#: pygeoapi/templates/collections/tiles/index.html:31 +msgid "Tile Matrix Set" +msgstr "Matrica tile-ova" + +#: pygeoapi/templates/collections/tiles/index.html:42 +msgid "Metadata" +msgstr "Meta podaci" + +#: pygeoapi/templates/collections/tiles/metadata.html:15 +msgid "TileJSON" +msgstr "TileJSON" + +#: pygeoapi/templates/collections/tiles/metadata.html:17 +msgid "JSON" +msgstr "JSON" + +#: pygeoapi/templates/jobs/index.html:14 +msgid "Job ID" +msgstr "ID zadatka" + +#: pygeoapi/templates/jobs/index.html:15 +msgid "Process ID" +msgstr "ID procesa" + +#: pygeoapi/templates/jobs/index.html:16 +msgid "Start" +msgstr "Početak" + +#: pygeoapi/templates/jobs/index.html:17 pygeoapi/templates/jobs/job.html:37 +msgid "Duration" +msgstr "Trajanje" + +#: pygeoapi/templates/jobs/index.html:18 pygeoapi/templates/jobs/job.html:17 +#: pygeoapi/templates/jobs/job.html:35 +msgid "Progress" +msgstr "Napredak" + +#: pygeoapi/templates/jobs/index.html:19 pygeoapi/templates/jobs/job.html:16 +msgid "Status" +msgstr "Status" + +#: pygeoapi/templates/jobs/index.html:20 pygeoapi/templates/jobs/job.html:21 +msgid "Message" +msgstr "Poruka" + +#: pygeoapi/templates/jobs/job.html:2 pygeoapi/templates/jobs/job.html:10 +msgid "Job status" +msgstr "Status zadatka" + +#: pygeoapi/templates/jobs/job.html:26 +msgid "Parameters" +msgstr "Parametri" + +#: pygeoapi/templates/jobs/job.html:45 +msgid "Started processing" +msgstr "Započeto procesiranje" + +#: pygeoapi/templates/jobs/job.html:47 +msgid "Finished processing" +msgstr "Završeno procesiranje" + +#: pygeoapi/templates/jobs/results/index.html:2 +msgid "Job result" +msgstr "Rezuultat zadatka" + +#: pygeoapi/templates/jobs/results/index.html:6 +msgid "Results" +msgstr "Rezuultati" + +#: pygeoapi/templates/jobs/results/index.html:10 +msgid "Results of job" +msgstr "Rezultati zadatka" + +#: pygeoapi/templates/processes/index.html:8 +msgid "Processes in this service" +msgstr "Procesi u ovoj usluzi" + +#: pygeoapi/templates/processes/process.html:20 +msgid "Inputs" +msgstr "Ulazi" + +#: pygeoapi/templates/processes/process.html:23 +#: pygeoapi/templates/processes/process.html:54 +msgid "Id" +msgstr "Id" + +#: pygeoapi/templates/processes/process.html:24 +#: pygeoapi/templates/processes/process.html:55 +#: pygeoapi/templates/tilematrixsets/index.html:15 +msgid "Title" +msgstr "Naslov" + +#: pygeoapi/templates/processes/process.html:25 +msgid "Data Type" +msgstr "Vrst podataka" + +#: pygeoapi/templates/processes/process.html:51 +msgid "Outputs" +msgstr "Izlazi" + +#: pygeoapi/templates/processes/process.html:71 +msgid "Execution modes" +msgstr "Modovi izvršavanja" + +#: pygeoapi/templates/processes/process.html:73 +msgid "Synchronous" +msgstr "Sinhroni" + +#: pygeoapi/templates/processes/process.html:74 +msgid "Asynchronous" +msgstr "Asinhroni" + +#: pygeoapi/templates/stac/catalog.html:4 +#: pygeoapi/templates/stac/collection.html:2 +#: pygeoapi/templates/stac/collection.html:4 +#: pygeoapi/templates/stac/collection_base.html:4 +#: pygeoapi/templates/stac/item.html:4 +msgid "SpatioTemporal Asset Catalog" +msgstr "Prostorno-vremenski katalog resursa" + +#: pygeoapi/templates/stac/catalog.html:19 +msgid "Last modified" +msgstr "Zadnji put izmijenjeno" + +#: pygeoapi/templates/stac/collection.html:9 +msgid "STAC Version" +msgstr "STAC verzija" + +#: pygeoapi/templates/stac/collection_base.html:28 +#: pygeoapi/templates/stac/item.html:28 +msgid "Assets" +msgstr "Resursi" + +#: pygeoapi/templates/stac/collection_base.html:32 +#: pygeoapi/templates/stac/item.html:32 +msgid "URL" +msgstr "URL" + +#: pygeoapi/templates/stac/collection_base.html:33 +#: pygeoapi/templates/stac/item.html:33 +msgid "Last Modified" +msgstr "Zadnji put izmijenjeno" + +#: pygeoapi/templates/stac/collection_base.html:64 +#: pygeoapi/templates/stac/item.html:64 +msgid "id" +msgstr "id" + +#: pygeoapi/templates/stac/collection_base.html:68 +msgid "description" +msgstr "opis" + +#: pygeoapi/templates/stac/collection_base.html:72 +msgid "extent" +msgstr "extent" + +#: pygeoapi/templates/stac/collection_base.html:77 +msgid "cube:dimensions" +msgstr "kocka:dimenzije" + +#: pygeoapi/templates/stac/collection_base.html:83 +msgid "cube:variables" +msgstr "kocka:varijable" + +#: pygeoapi/templates/stac/item.html:19 +msgid "Item" +msgstr "Stavka" + +#: pygeoapi/templates/tilematrixsets/index.html:2 +#: pygeoapi/templates/tilematrixsets/index.html:4 +msgid "TileMatrixSets" +msgstr "" + +#: pygeoapi/templates/tilematrixsets/index.html:9 +msgid "Tile matrix sets available in this service" +msgstr "Matrica tile-ova dostupna u ovoj usluzi" + +#: pygeoapi/templates/tilematrixsets/tilematrixset.html:2 +#: pygeoapi/templates/tilematrixsets/tilematrixset.html:4 +msgid "TileMatrixSet" +msgstr "Matrica tile-ova" + diff --git a/locale/sr/LC_MESSAGES/messages.po b/locale/sr/LC_MESSAGES/messages.po new file mode 100644 index 000000000..4f3d35801 --- /dev/null +++ b/locale/sr/LC_MESSAGES/messages.po @@ -0,0 +1,536 @@ +# Serbian translations for PROJECT. +# Copyright (C) 2024 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2024-02-27 14:39+0100\n" +"PO-Revision-Date: 2024-02-27 14:40+0100\n" +"Last-Translator: Bojan Kuzmanović \n" +"Language: sr\n" +"Language-Team: sr \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: pygeoapi/templates/_base.html:51 +msgid "Admin" +msgstr "Admin" + +#: pygeoapi/templates/_base.html:67 pygeoapi/templates/landing_page.html:2 +msgid "Home" +msgstr "Početna" + +#: pygeoapi/templates/_base.html:75 pygeoapi/templates/_base.html:83 +msgid "json" +msgstr "json" + +#: pygeoapi/templates/_base.html:78 pygeoapi/templates/_base.html:86 +msgid "jsonld" +msgstr "jsonld" + +#: pygeoapi/templates/_base.html:105 +msgid "Powered by " +msgstr "Pokreće ga " + +#: pygeoapi/templates/conformance.html:2 pygeoapi/templates/conformance.html:4 +#: pygeoapi/templates/conformance.html:8 +#: pygeoapi/templates/landing_page.html:86 +msgid "Conformance" +msgstr "Usaglašenost" + +#: pygeoapi/templates/exception.html:2 pygeoapi/templates/exception.html:5 +msgid "Exception" +msgstr "Izuzetak" + +#: pygeoapi/templates/landing_page.html:25 +msgid "Terms of service" +msgstr "Uslovi korišćenja" + +#: pygeoapi/templates/collections/collection.html:38 +#: pygeoapi/templates/landing_page.html:35 +msgid "License" +msgstr "Licensa" + +#: pygeoapi/templates/collections/collection.html:6 +#: pygeoapi/templates/collections/coverage/domainset.html:4 +#: pygeoapi/templates/collections/coverage/rangetype.html:4 +#: pygeoapi/templates/collections/edr/query.html:4 +#: pygeoapi/templates/collections/index.html:2 +#: pygeoapi/templates/collections/index.html:4 +#: pygeoapi/templates/collections/items/index.html:4 +#: pygeoapi/templates/collections/items/item.html:27 +#: pygeoapi/templates/collections/queryables.html:4 +#: pygeoapi/templates/collections/tiles/index.html:4 +#: pygeoapi/templates/collections/tiles/metadata.html:4 +#: pygeoapi/templates/landing_page.html:48 +#: pygeoapi/templates/stac/collection_base.html:19 +msgid "Collections" +msgstr "Kolekcije" + +#: pygeoapi/templates/landing_page.html:50 +msgid "View the collections in this service" +msgstr "Pogledajte kolekcije u ovom servisu" + +#: pygeoapi/templates/landing_page.html:56 +msgid "SpatioTemporal Assets" +msgstr "Prostorno-vremenski resursi" + +#: pygeoapi/templates/landing_page.html:58 +msgid "View the SpatioTemporal Assets in this service" +msgstr "Pogledajte prostorno-vremenske resurse u ovom servisu" + +#: pygeoapi/templates/landing_page.html:64 +#: pygeoapi/templates/processes/index.html:2 +#: pygeoapi/templates/processes/index.html:4 +#: pygeoapi/templates/processes/process.html:4 +msgid "Processes" +msgstr "Procesi" + +#: pygeoapi/templates/landing_page.html:66 +msgid "View the processes in this service" +msgstr "Poglejte procese u ovom servisu" + +#: pygeoapi/templates/jobs/index.html:2 pygeoapi/templates/jobs/index.html:4 +#: pygeoapi/templates/jobs/index.html:11 pygeoapi/templates/jobs/job.html:4 +#: pygeoapi/templates/jobs/results/index.html:4 +#: pygeoapi/templates/landing_page.html:70 +#: pygeoapi/templates/processes/process.html:76 +msgid "Jobs" +msgstr "Zadaci" + +#: pygeoapi/templates/landing_page.html:72 +#: pygeoapi/templates/processes/process.html:77 +msgid "Browse jobs" +msgstr "Pogledajte zadatke" + +#: pygeoapi/templates/landing_page.html:77 +msgid "API Definition" +msgstr "API definicija" + +#: pygeoapi/templates/landing_page.html:79 +msgid "Documentation" +msgstr "Dokuemntacija" + +#: pygeoapi/templates/landing_page.html:79 +msgid "Swagger UI" +msgstr "Swagger UI" + +#: pygeoapi/templates/landing_page.html:79 +msgid "ReDoc" +msgstr "ReDoc" + +#: pygeoapi/templates/landing_page.html:82 +msgid "OpenAPI Document" +msgstr "OpenAPI dokument" + +#: pygeoapi/templates/landing_page.html:88 +msgid "View the conformance classes of this service" +msgstr "Pogledajte usaglašenost ovog servisa" + +#: pygeoapi/templates/landing_page.html:92 +msgid "Tile Matrix Sets" +msgstr "Matrica skupova tile-ova" + +#: pygeoapi/templates/landing_page.html:94 +msgid "View the Tile Matrix Sets available on this service" +msgstr "Pogledajte matrice skupova tile-ova dostupne u ovom servisu" + +#: pygeoapi/templates/landing_page.html:101 +msgid "Provider" +msgstr "Provajder" + +#: pygeoapi/templates/landing_page.html:110 +msgid "Contact point" +msgstr "Kontakt tačka" + +#: pygeoapi/templates/landing_page.html:113 +msgid "Address" +msgstr "Adresa" + +#: pygeoapi/templates/landing_page.html:122 +msgid "Email" +msgstr "Email" + +#: pygeoapi/templates/landing_page.html:125 +msgid "Telephone" +msgstr "Telefon" + +#: pygeoapi/templates/landing_page.html:129 +msgid "Fax" +msgstr "Fax" + +#: pygeoapi/templates/landing_page.html:133 +msgid "Contact URL" +msgstr "Kontakt URL" + +#: pygeoapi/templates/landing_page.html:137 +msgid "Hours" +msgstr "Sati" + +#: pygeoapi/templates/landing_page.html:141 +msgid "Contact instructions" +msgstr "Kontakt instrukcije" + +#: pygeoapi/templates/collections/collection.html:51 +msgid "Browse" +msgstr "Pretraži" + +#: pygeoapi/templates/collections/collection.html:55 +msgid "Browse Items" +msgstr "Pretraži stavke" + +#: pygeoapi/templates/collections/collection.html:56 +msgid "Browse through the items of" +msgstr "Pretrazite kroz stavke od" + +#: pygeoapi/templates/collections/collection.html:59 +#: pygeoapi/templates/collections/queryables.html:6 +#: pygeoapi/templates/collections/queryables.html:17 +msgid "Queryables" +msgstr "Upitni" + +#: pygeoapi/templates/collections/collection.html:63 +msgid "Display Queryables" +msgstr "Prikaži upitne" + +#: pygeoapi/templates/collections/collection.html:64 +msgid "Display Queryables of" +msgstr "Prikaži upitne od" + +#: pygeoapi/templates/collections/collection.html:69 +#: pygeoapi/templates/collections/tiles/index.html:6 +#: pygeoapi/templates/collections/tiles/metadata.html:6 +msgid "Tiles" +msgstr "Tile-ovi" + +#: pygeoapi/templates/collections/collection.html:73 +msgid "Display Tiles" +msgstr "Prikaži tile-ove" + +#: pygeoapi/templates/collections/collection.html:73 +msgid "Display Tiles of" +msgstr "Prikaži tile-ove od" + +#: pygeoapi/templates/collections/collection.html:80 +#: pygeoapi/templates/jobs/job.html:50 +#: pygeoapi/templates/processes/process.html:78 +msgid "Links" +msgstr "Linkovi" + +#: pygeoapi/templates/collections/collection.html:90 +msgid "Reference Systems" +msgstr "Referentni sistemi" + +#: pygeoapi/templates/collections/collection.html:98 +msgid "Storage CRS" +msgstr "Skaldišni CRS" + +#: pygeoapi/templates/collections/index.html:12 +#: pygeoapi/templates/processes/index.html:14 +#: pygeoapi/templates/stac/catalog.html:17 +#: pygeoapi/templates/stac/collection.html:17 +msgid "Name" +msgstr "Naziv" + +#: pygeoapi/templates/collections/index.html:13 +#: pygeoapi/templates/stac/catalog.html:18 +msgid "Type" +msgstr "Tip" + +#: pygeoapi/templates/collections/index.html:14 +#: pygeoapi/templates/processes/index.html:15 +#: pygeoapi/templates/processes/process.html:26 +#: pygeoapi/templates/processes/process.html:56 +#: pygeoapi/templates/stac/collection.html:18 +#: pygeoapi/templates/tilematrixsets/index.html:16 +msgid "Description" +msgstr "Opis" + +#: pygeoapi/templates/collections/coverage/domainset.html:11 +msgid "Coverage domain set" +msgstr "Pokrivenost domenskog skupa" + +#: pygeoapi/templates/collections/coverage/domainset.html:12 +msgid "Axis labels" +msgstr "Labele osa" + +#: pygeoapi/templates/collections/coverage/domainset.html:18 +msgid "Extent" +msgstr "Extent" + +#: pygeoapi/templates/collections/coverage/domainset.html:24 +msgid "Coordinate reference system" +msgstr "Koordiantni referentni sistem" + +#: pygeoapi/templates/collections/coverage/domainset.html:26 +#: pygeoapi/templates/stac/catalog.html:20 +#: pygeoapi/templates/stac/collection_base.html:34 +#: pygeoapi/templates/stac/item.html:34 +msgid "Size" +msgstr "Veličina" + +#: pygeoapi/templates/collections/coverage/domainset.html:28 +msgid "width" +msgstr "širina" + +#: pygeoapi/templates/collections/coverage/domainset.html:29 +msgid "height" +msgstr "visina" + +#: pygeoapi/templates/collections/coverage/domainset.html:31 +msgid "Resolution" +msgstr "rezolucija" + +#: pygeoapi/templates/collections/coverage/domainset.html:33 +msgid "x" +msgstr "x" + +#: pygeoapi/templates/collections/coverage/domainset.html:34 +msgid "y" +msgstr "y" + +#: pygeoapi/templates/collections/coverage/rangetype.html:11 +msgid "Coverage range type" +msgstr "Pokrivenost tipa opsega" + +#: pygeoapi/templates/collections/coverage/rangetype.html:12 +msgid "Fields" +msgstr "Polja" + +#: pygeoapi/templates/collections/edr/query.html:11 +#: pygeoapi/templates/collections/items/index.html:11 +#: pygeoapi/templates/collections/items/item.html:33 +msgid "Items" +msgstr "Stavke" + +#: pygeoapi/templates/collections/items/index.html:25 +msgid "Items in this collection" +msgstr "Stavke u ovoj kolekciji" + +#: pygeoapi/templates/collections/items/index.html:38 +msgid "Warning: Higher limits not recommended!" +msgstr "Upozorenje: Veća granica nije preporučljiva!" + +#: pygeoapi/templates/collections/items/index.html:43 +msgid "Limit" +msgstr "Granica" + +#: pygeoapi/templates/collections/items/index.html:45 +msgid "default" +msgstr "podrazumevano" + +#: pygeoapi/templates/collections/items/index.html:66 +#: pygeoapi/templates/collections/items/item.html:62 +msgid "Prev" +msgstr "Prethodno" + +#: pygeoapi/templates/collections/items/index.html:68 +#: pygeoapi/templates/collections/items/item.html:64 +msgid "Next" +msgstr "Sljedeće" + +#: pygeoapi/templates/collections/items/index.html:139 +msgid "No items" +msgstr "Nema stavki" + +#: pygeoapi/templates/collections/items/item.html:77 +#: pygeoapi/templates/stac/collection_base.html:58 +#: pygeoapi/templates/stac/item.html:58 +msgid "Property" +msgstr "Svojstvo" + +#: pygeoapi/templates/collections/items/item.html:78 +#: pygeoapi/templates/stac/collection_base.html:59 +#: pygeoapi/templates/stac/item.html:59 +msgid "Value" +msgstr "Vrednost" + +#: pygeoapi/templates/collections/tiles/index.html:31 +msgid "Tile Matrix Set" +msgstr "Matrica skupa tile-ova" + +#: pygeoapi/templates/collections/tiles/index.html:42 +msgid "Metadata" +msgstr "Metapodaci" + +#: pygeoapi/templates/collections/tiles/metadata.html:15 +msgid "TileJSON" +msgstr "TileJSON" + +#: pygeoapi/templates/collections/tiles/metadata.html:17 +msgid "JSON" +msgstr "JSON" + +#: pygeoapi/templates/jobs/index.html:14 +msgid "Job ID" +msgstr "ID zadatka" + +#: pygeoapi/templates/jobs/index.html:15 +msgid "Process ID" +msgstr "ID procesa" + +#: pygeoapi/templates/jobs/index.html:16 +msgid "Start" +msgstr "Započni" + +#: pygeoapi/templates/jobs/index.html:17 pygeoapi/templates/jobs/job.html:37 +msgid "Duration" +msgstr "Trajanje" + +#: pygeoapi/templates/jobs/index.html:18 pygeoapi/templates/jobs/job.html:17 +#: pygeoapi/templates/jobs/job.html:35 +msgid "Progress" +msgstr "Napredak" + +#: pygeoapi/templates/jobs/index.html:19 pygeoapi/templates/jobs/job.html:16 +msgid "Status" +msgstr "Status" + +#: pygeoapi/templates/jobs/index.html:20 pygeoapi/templates/jobs/job.html:21 +msgid "Message" +msgstr "Poruka" + +#: pygeoapi/templates/jobs/job.html:2 pygeoapi/templates/jobs/job.html:10 +msgid "Job status" +msgstr "Status zadatka" + +#: pygeoapi/templates/jobs/job.html:26 +msgid "Parameters" +msgstr "Parametri" + +#: pygeoapi/templates/jobs/job.html:45 +msgid "Started processing" +msgstr "Započeto procesiranje" + +#: pygeoapi/templates/jobs/job.html:47 +msgid "Finished processing" +msgstr "Završeno procesiranje" + +#: pygeoapi/templates/jobs/results/index.html:2 +msgid "Job result" +msgstr "Rezultat zadatka" + +#: pygeoapi/templates/jobs/results/index.html:6 +msgid "Results" +msgstr "Rezultati" + +#: pygeoapi/templates/jobs/results/index.html:10 +msgid "Results of job" +msgstr "Rezultati zadatka" + +#: pygeoapi/templates/processes/index.html:8 +msgid "Processes in this service" +msgstr "Processi u ovom servisu" + +#: pygeoapi/templates/processes/process.html:20 +msgid "Inputs" +msgstr "Unosi" + +#: pygeoapi/templates/processes/process.html:23 +#: pygeoapi/templates/processes/process.html:54 +msgid "Id" +msgstr "Id" + +#: pygeoapi/templates/processes/process.html:24 +#: pygeoapi/templates/processes/process.html:55 +#: pygeoapi/templates/tilematrixsets/index.html:15 +msgid "Title" +msgstr "Naslov" + +#: pygeoapi/templates/processes/process.html:25 +msgid "Data Type" +msgstr "Tip podatka" + +#: pygeoapi/templates/processes/process.html:51 +msgid "Outputs" +msgstr "Izlazi" + +#: pygeoapi/templates/processes/process.html:71 +msgid "Execution modes" +msgstr "Načini izvršavanja" + +#: pygeoapi/templates/processes/process.html:73 +msgid "Synchronous" +msgstr "Sinhrono" + +#: pygeoapi/templates/processes/process.html:74 +msgid "Asynchronous" +msgstr "Asinhrono" + +#: pygeoapi/templates/stac/catalog.html:4 +#: pygeoapi/templates/stac/collection.html:2 +#: pygeoapi/templates/stac/collection.html:4 +#: pygeoapi/templates/stac/collection_base.html:4 +#: pygeoapi/templates/stac/item.html:4 +msgid "SpatioTemporal Asset Catalog" +msgstr "Prostorno-vremenski katalog resursa" + +#: pygeoapi/templates/stac/catalog.html:19 +msgid "Last modified" +msgstr "Zadnja izmjena" + +#: pygeoapi/templates/stac/collection.html:9 +msgid "STAC Version" +msgstr "STAC verzija" + +#: pygeoapi/templates/stac/collection_base.html:28 +#: pygeoapi/templates/stac/item.html:28 +msgid "Assets" +msgstr "Resursi" + +#: pygeoapi/templates/stac/collection_base.html:32 +#: pygeoapi/templates/stac/item.html:32 +msgid "URL" +msgstr "URL" + +#: pygeoapi/templates/stac/collection_base.html:33 +#: pygeoapi/templates/stac/item.html:33 +msgid "Last Modified" +msgstr "Zadnja izmjena" + +#: pygeoapi/templates/stac/collection_base.html:64 +#: pygeoapi/templates/stac/item.html:64 +msgid "id" +msgstr "id" + +#: pygeoapi/templates/stac/collection_base.html:68 +msgid "description" +msgstr "opis" + +#: pygeoapi/templates/stac/collection_base.html:72 +msgid "extent" +msgstr "extent" + +#: pygeoapi/templates/stac/collection_base.html:77 +msgid "cube:dimensions" +msgstr "kocka:dimenzije" + +#: pygeoapi/templates/stac/collection_base.html:83 +msgid "cube:variables" +msgstr "kocka:varijable" + +#: pygeoapi/templates/stac/item.html:19 +msgid "Item" +msgstr "Stavka" + +#: pygeoapi/templates/tilematrixsets/index.html:2 +#: pygeoapi/templates/tilematrixsets/index.html:4 +msgid "TileMatrixSets" +msgstr "Matrica skupova tile-ova" + +#: pygeoapi/templates/tilematrixsets/index.html:9 +msgid "Tile matrix sets available in this service" +msgstr "Matrice skupova tile-ova dostupne u ovom servisu" + +#: pygeoapi/templates/tilematrixsets/tilematrixset.html:2 +#: pygeoapi/templates/tilematrixsets/tilematrixset.html:4 +msgid "TileMatrixSet" +msgstr "Matrica skupa tile-ova" + From 988d8aa7e223f50f10f1be53fca63c3e2b6ae885 Mon Sep 17 00:00:00 2001 From: Emmanuel Jolaiya Date: Tue, 27 Feb 2024 16:37:56 +0000 Subject: [PATCH 004/279] fixed bug with breadcrumb routes (#1554) * fixed bug with breadcrumb routes * removed example files in commit * Delete example-openapi.yml * Delete example-config.yml --- .gitignore | 4 +++ .../tilematrixsets/tilematrixset.html | 35 ++++++++++--------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index b205af713..3a434a1dd 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,10 @@ ENV/ # pygeoapi artifacts *.openapi.yml +# development setup examples +example-config.yml +example-openapi.yml + # misc *.swp .pytest_cache diff --git a/pygeoapi/templates/tilematrixsets/tilematrixset.html b/pygeoapi/templates/tilematrixsets/tilematrixset.html index 5aae2742a..9a7e1c0b1 100644 --- a/pygeoapi/templates/tilematrixsets/tilematrixset.html +++ b/pygeoapi/templates/tilematrixsets/tilematrixset.html @@ -1,18 +1,19 @@ -{% extends "_base.html" %} -{% block title %}{{ super() }} {% trans %}TileMatrixSet{% endtrans %} {% endblock %} -{% block crumbs %}{{ super() }} -/ {% trans %}TileMatrixSet{% endtrans %} -{% endblock %} -{% block body %} -
- -

{{ data['id'] }} tile matrix set

-
    -
  • CRS: {{ data['crs'] }}
  • -
  • Uri: {{ data['uri'] }}
  • -
  • Well Known Scale Set: {{ data['wellKnownScaleSet'] }}
  • -
- - -
+{% extends "_base.html" %} {% block title %}{{ super() }} {% trans +%}TileMatrixSet{% endtrans %} {% endblock %} {% block crumbs %}{{ super() }} / +{% trans %}TileMatrixSet{% endtrans %} +{% endblock %} {% block body %} +
+

{{ data['id'] }} tile matrix set

+
    +
  • + CRS: {{ data['crs'] }} +
  • +
  • + Uri: {{ data['uri'] }} +
  • +
  • + Well Known Scale Set: {{ data['wellKnownScaleSet'] }} +
  • +
+
{% endblock %} From 452e8c4254077c6dff3fb1cefb7734cdc3790ccb Mon Sep 17 00:00:00 2001 From: Tom Kralidis Date: Tue, 27 Feb 2024 13:25:51 -0500 Subject: [PATCH 005/279] align OACov to latest specification updates (#1548) * align OACov to latest specification updates * fix * fix * fix * fix --- .github/workflows/main.yml | 1 + .../data-publishing/ogcapi-coverages.rst | 6 +- docs/source/plugins.rst | 13 +- docs/source/tour.rst | 15 +- pygeoapi/api.py | 297 +++++++++--------- pygeoapi/django_/urls.py | 17 +- pygeoapi/django_/views.py | 57 ++-- pygeoapi/flask_app.py | 42 +-- pygeoapi/openapi.py | 64 ++-- pygeoapi/provider/base.py | 18 -- pygeoapi/provider/rasterio_.py | 113 ++----- pygeoapi/provider/xarray_.py | 147 ++------- pygeoapi/provider/xarray_edr.py | 13 +- pygeoapi/starlette_app.py | 55 ++-- .../collections/coverage/domainset.html | 37 --- .../collections/coverage/rangetype.html | 19 -- pygeoapi/templates/collections/schema.html | 46 +++ tests/test_api.py | 79 +++-- tests/test_rasterio_provider.py | 20 +- tests/test_xarray_netcdf_provider.py | 18 +- tests/test_xarray_zarr_provider.py | 20 +- 21 files changed, 384 insertions(+), 713 deletions(-) delete mode 100644 pygeoapi/templates/collections/coverage/domainset.html delete mode 100644 pygeoapi/templates/collections/coverage/rangetype.html create mode 100644 pygeoapi/templates/collections/schema.html diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 291a79bf5..d84aec178 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,7 @@ jobs: with: python-version: 3.8 architecture: x64 + cache: 'pip' - name: Checkout pygeoapi uses: actions/checkout@master - name: Install flake8 diff --git a/docs/source/data-publishing/ogcapi-coverages.rst b/docs/source/data-publishing/ogcapi-coverages.rst index 0861751cd..1390ee75c 100644 --- a/docs/source/data-publishing/ogcapi-coverages.rst +++ b/docs/source/data-publishing/ogcapi-coverages.rst @@ -103,10 +103,8 @@ Data access examples * http://localhost:5000/collections * overview of dataset * http://localhost:5000/collections/foo -* coverage rangetype - * http://localhost:5000/collections/foo/coverage/rangetype -* coverage domainset - * http://localhost:5000/collections/foo/coverage/domainset +* schema of dataset + * http://localhost:5000/collections/foo/schema * coverage access via CoverageJSON (default) * http://localhost:5000/collections/foo/coverage?f=json * coverage access via native format (as defined in ``provider.format.name``) diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index 018c13725..a0e8a9eb1 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -240,12 +240,15 @@ The below template provides a minimal example (let's call the file ``mycoolraste super().__init__(provider_def) self.num_bands = 4 self.axes = ['Lat', 'Long'] + self.fields = self.get_fields() - def get_coverage_domainset(self): - # return a CIS JSON DomainSet - - def get_coverage_rangetype(self): - # return a CIS JSON RangeType + def get_fields(self): + # generate a JSON Schema of coverage band metadata + return { + 'b1': { + 'type': 'number' + } + } def query(self, bands=[], subsets={}, format_='json', **kwargs): # process bands and subsets parameters diff --git a/docs/source/tour.rst b/docs/source/tour.rst index 06a6e363d..4fd4e51da 100644 --- a/docs/source/tour.rst +++ b/docs/source/tour.rst @@ -117,19 +117,12 @@ Delete an item from a collection: Raster data ----------- -Collection coverage domainset -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Collection coverage schema +^^^^^^^^^^^^^^^^^^^^^^^^^^ -This page provides information on a collection coverage spatial properties and axis information. +This page provides information on a collection coverage information. -http://localhost:5000/collections/gdps-temperature/coverage/domainset - -Collection coverage rangetype -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This page provides information on a collection coverage rangetype (bands) information. - -http://localhost:5000/collections/gdps-temperature/coverage/rangetype +http://localhost:5000/collections/gdps-temperature/schema Collection coverage data ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pygeoapi/api.py b/pygeoapi/api.py index a3941494b..d1674f21b 100644 --- a/pygeoapi/api.py +++ b/pygeoapi/api.py @@ -7,7 +7,7 @@ # Colin Blackburn # Ricardo Garcia Silva # -# Copyright (c) 2023 Tom Kralidis +# Copyright (c) 2024 Tom Kralidis # Copyright (c) 2022 Francesco Bartoli # Copyright (c) 2022 John A Stevenson and Colin Blackburn # Copyright (c) 2023 Ricardo Garcia Silva @@ -135,7 +135,9 @@ 'http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs', 'http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/queryables', 'http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/queryables-query-parameters', # noqa - 'http://www.opengis.net/spec/ogcapi-features-4/1.0/conf/create-replace-delete' # noqa + 'http://www.opengis.net/spec/ogcapi-features-4/1.0/conf/create-replace-delete', # noqa + 'http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/schemas', + 'http://www.opengis.net/spec/ogcapi-features-5/1.0/req/core-roles-features' # noqa ], 'coverage': [ 'http://www.opengis.net/spec/ogcapi-coverages-1/1.0/conf/core', @@ -1139,6 +1141,20 @@ def describe_collections(self, request: Union[APIRequest, Any], 'href': f'{self.get_collections_url()}/{k}?f={F_HTML}' }) + if collection_data_type in ['feature', 'coverage', 'record']: + collection['links'].append({ + 'type': FORMAT_TYPES[F_JSON], + 'rel': f'{OGC_RELTYPES_BASE}/schema', + 'title': 'Schema of collection in JSON', + 'href': f'{self.get_collections_url()}/{k}/schema?f={F_JSON}' # noqa + }) + collection['links'].append({ + 'type': FORMAT_TYPES[F_HTML], + 'rel': f'{OGC_RELTYPES_BASE}/schema', + 'title': 'Schema of collection in HTML', + 'href': f'{self.get_collections_url()}/{k}/schema?f={F_HTML}' # noqa + }) + if collection_data_type in ['feature', 'record', 'tile']: # TODO: translate collection['itemType'] = collection_data_type @@ -1184,44 +1200,6 @@ def describe_collections(self, request: Union[APIRequest, Any], elif collection_data_type == 'coverage': # TODO: translate LOGGER.debug('Adding coverage based links') - collection['links'].append({ - 'type': FORMAT_TYPES[F_JSON], - 'rel': 'collection', - 'title': 'Detailed Coverage metadata in JSON', - 'href': f'{self.get_collections_url()}/{k}?f={F_JSON}' - }) - collection['links'].append({ - 'type': FORMAT_TYPES[F_HTML], - 'rel': 'collection', - 'title': 'Detailed Coverage metadata in HTML', - 'href': f'{self.get_collections_url()}/{k}?f={F_HTML}' - }) - coverage_url = f'{self.get_collections_url()}/{k}/coverage' - - collection['links'].append({ - 'type': FORMAT_TYPES[F_JSON], - 'rel': f'{OGC_RELTYPES_BASE}/coverage-domainset', - 'title': 'Coverage domain set of collection in JSON', - 'href': f'{coverage_url}/domainset?f={F_JSON}' - }) - collection['links'].append({ - 'type': FORMAT_TYPES[F_HTML], - 'rel': f'{OGC_RELTYPES_BASE}/coverage-domainset', - 'title': 'Coverage domain set of collection in HTML', - 'href': f'{coverage_url}/domainset?f={F_HTML}' - }) - collection['links'].append({ - 'type': FORMAT_TYPES[F_JSON], - 'rel': f'{OGC_RELTYPES_BASE}/coverage-rangetype', - 'title': 'Coverage range type of collection in JSON', - 'href': f'{coverage_url}/rangetype?f={F_JSON}' - }) - collection['links'].append({ - 'type': FORMAT_TYPES[F_HTML], - 'rel': f'{OGC_RELTYPES_BASE}/coverage-rangetype', - 'title': 'Coverage range type of collection in HTML', - 'href': f'{coverage_url}/rangetype?f={F_HTML}' - }) collection['links'].append({ 'type': 'application/prs.coverage+json', 'rel': f'{OGC_RELTYPES_BASE}/coverage', @@ -1252,8 +1230,21 @@ def describe_collections(self, request: Union[APIRequest, Any], pass else: collection['crs'] = [p.crs] - collection['domainset'] = p.get_coverage_domainset() - collection['rangetype'] = p.get_coverage_rangetype() + collection['extent']['spatial'] = { + 'bbox': [ + p._coverage_properties['bbox'][0], + p._coverage_properties['bbox'][1], + p._coverage_properties['bbox'][2], + p._coverage_properties['bbox'][3] + ], + 'grid': [{ + 'cellsCount': p._coverage_properties['width'], + 'resolution': p._coverage_properties['resx'] + }, { + 'cellsCount': p._coverage_properties['height'], + 'resolution': p._coverage_properties['resy'] + }] + } try: tile = get_provider_by_type(v['providers'], 'tile') @@ -1319,8 +1310,21 @@ def describe_collections(self, request: Union[APIRequest, Any], parameters = p.get_fields() if parameters: collection['parameter_names'] = {} - for f in parameters['field']: - collection['parameter_names'][f['id']] = f + for key, value in parameters.items(): + collection['parameter_names'][key] = { + 'id': key, + 'type': 'Parameter', + 'name': value['title'], + 'unit': { + 'label': { + 'en': value['title'] + }, + 'symbol': { + 'value': value['x-ogc-unit'], + 'type': 'http://www.opengis.net/def/uom/UCUM/' # noqa + } + } + } for qt in p.get_query_types(): collection['links'].append({ @@ -1390,6 +1394,90 @@ def describe_collections(self, request: Union[APIRequest, Any], return headers, HTTPStatus.OK, to_json(fcm, self.pretty_print) + @gzip + @pre_process + @jsonldify + def get_collection_schema( + self, request: Union[APIRequest, Any], + dataset) -> Tuple[dict, int, str]: + """ + Returns a collection schema + + :param request: A request object + :param dataset: dataset name + + :returns: tuple of headers, status code, content + """ + + if not request.is_valid(): + return self.get_format_exception(request) + headers = request.get_response_headers(**self.api_headers) + + if any([dataset is None, + dataset not in self.config['resources'].keys()]): + + msg = 'Collection not found' + return self.get_exception( + HTTPStatus.NOT_FOUND, headers, request.format, 'NotFound', msg) + + LOGGER.debug('Creating collection schema') + try: + LOGGER.debug('Loading feature provider') + p = load_plugin('provider', get_provider_by_type( + self.config['resources'][dataset]['providers'], 'feature')) + except ProviderTypeError: + LOGGER.debug('Loading coverage provider') + p = load_plugin('provider', get_provider_by_type( + self.config['resources'][dataset]['providers'], 'coverage')) + except ProviderTypeError: + LOGGER.debug('Loading record provider') + p = load_plugin('provider', get_provider_by_type( + self.config['resources'][dataset]['providers'], 'record')) + except ProviderGenericError as err: + LOGGER.error(err) + return self.get_exception( + err.http_status_code, headers, request.format, + err.ogc_exception_code, err.message) + + schema = { + 'type': 'object', + 'title': l10n.translate( + self.config['resources'][dataset]['title'], request.locale), + 'properties': {}, + '$schema': 'http://json-schema.org/draft/2019-09/schema', + '$id': f'{self.get_collections_url()}/{dataset}/schema' + } + + if p.type != 'coverage': + schema['properties']['geometry'] = { + '$ref': 'https://geojson.org/schema/Geometry.json', + 'x-ogc-role': 'primary-geometry' + } + + for k, v in p.fields.items(): + schema['properties'][k] = v + + if k == p.id_field: + schema['properties'][k]['x-ogc-role'] = 'id' + if k == p.time_field: + schema['properties'][k]['x-ogc-role'] = 'primary-instant' + + if request.format == F_HTML: # render + schema['title'] = l10n.translate( + self.config['resources'][dataset]['title'], request.locale) + + schema['collections_path'] = self.get_collections_url() + + content = render_j2_template(self.tpl_config, + 'collections/schema.html', + schema, request.locale) + + return headers, HTTPStatus.OK, content + + headers['Content-Type'] = 'application/schema+json' + + return headers, HTTPStatus.OK, to_json(schema, self.pretty_print) + @gzip @pre_process @jsonldify @@ -1420,6 +1508,10 @@ def get_collection_queryables(self, request: Union[APIRequest, Any], LOGGER.debug('Loading feature provider') p = load_plugin('provider', get_provider_by_type( self.config['resources'][dataset]['providers'], 'feature')) + except ProviderTypeError: + LOGGER.debug('Loading coverage provider') + p = load_plugin('provider', get_provider_by_type( + self.config['resources'][dataset]['providers'], 'coverage')) except ProviderTypeError: LOGGER.debug('Loading record provider') p = load_plugin('provider', get_provider_by_type( @@ -1441,7 +1533,8 @@ def get_collection_queryables(self, request: Union[APIRequest, Any], if p.fields: queryables['properties']['geometry'] = { - '$ref': 'https://geojson.org/schema/Geometry.json' + '$ref': 'https://geojson.org/schema/Geometry.json', + 'x-ogc-role': 'primary-geometry' } for k, v in p.fields.items(): @@ -1460,6 +1553,11 @@ def get_collection_queryables(self, request: Union[APIRequest, Any], if 'values' in v: queryables['properties'][k]['enum'] = v['values'] + if k == p.id_field: + queryables['properties'][k]['x-ogc-role'] = 'id' + if k == p.time_field: + queryables['properties'][k]['x-ogc-role'] = 'primary-instant' # noqa + if request.format == F_HTML: # render queryables['title'] = l10n.translate( self.config['resources'][dataset]['title'], request.locale) @@ -2628,111 +2726,6 @@ def get_collection_coverage(self, request: Union[APIRequest, Any], else: return self.get_format_exception(request) - @gzip - @pre_process - @jsonldify - def get_collection_coverage_domainset( - self, request: Union[APIRequest, Any], - dataset) -> Tuple[dict, int, str]: - """ - Returns a collection coverage domainset - - :param request: A request object - :param dataset: dataset name - - :returns: tuple of headers, status code, content - """ - - format_ = request.format or F_JSON - headers = request.get_response_headers(**self.api_headers) - - LOGGER.debug('Loading provider') - try: - collection_def = get_provider_by_type( - self.config['resources'][dataset]['providers'], 'coverage') - - p = load_plugin('provider', collection_def) - - data = p.get_coverage_domainset() - except KeyError: - msg = 'collection does not exist' - return self.get_exception( - HTTPStatus.NOT_FOUND, headers, format_, - 'InvalidParameterValue', msg) - except ProviderGenericError as err: - LOGGER.error(err) - return self.get_exception( - err.http_status_code, headers, request.format, - err.ogc_exception_code, err.message) - - if format_ == F_JSON: - return headers, HTTPStatus.OK, to_json(data, self.pretty_print) - - elif format_ == F_HTML: - data['id'] = dataset - data['title'] = l10n.translate( - self.config['resources'][dataset]['title'], - self.default_locale) - data['collections_path'] = self.get_collections_url() - content = render_j2_template(self.tpl_config, - 'collections/coverage/domainset.html', - data, self.default_locale) - return headers, HTTPStatus.OK, content - else: - return self.get_format_exception(request) - - @gzip - @pre_process - @jsonldify - def get_collection_coverage_rangetype( - self, request: Union[APIRequest, Any], - dataset) -> Tuple[dict, int, str]: - """ - Returns a collection coverage rangetype - - :param request: A request object - :param dataset: dataset name - - :returns: tuple of headers, status code, content - """ - format_ = request.format or F_JSON - headers = request.get_response_headers(self.default_locale, - **self.api_headers) - LOGGER.debug('Loading provider') - try: - collection_def = get_provider_by_type( - self.config['resources'][dataset]['providers'], 'coverage') - - p = load_plugin('provider', collection_def) - - data = p.get_coverage_rangetype() - except KeyError: - msg = 'collection does not exist' - return self.get_exception( - HTTPStatus.NOT_FOUND, headers, format_, - 'InvalidParameterValue', msg) - except ProviderGenericError as err: - LOGGER.error(err) - return self.get_exception( - err.http_status_code, headers, request.format, - err.ogc_exception_code, err.message) - - if format_ == F_JSON: - return headers, HTTPStatus.OK, to_json(data, self.pretty_print) - - elif format_ == F_HTML: - data['id'] = dataset - data['title'] = l10n.translate( - self.config['resources'][dataset]['title'], - self.default_locale) - data['collections_path'] = self.get_collections_url() - content = render_j2_template(self.tpl_config, - 'collections/coverage/rangetype.html', - data, self.default_locale) - return headers, HTTPStatus.OK, content - else: - return self.get_format_exception(request) - @gzip @pre_process @jsonldify @@ -3769,8 +3762,8 @@ def get_collection_edr_query( HTTPStatus.BAD_REQUEST, headers, request.format, 'InvalidParameterValue', msg) - if parameternames and not any((fld['id'] in parameternames) - for fld in p.get_fields()['field']): + if parameternames and not any((fld in parameternames) + for fld in p.get_fields().keys()): msg = 'Invalid parameter_names' return self.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, diff --git a/pygeoapi/django_/urls.py b/pygeoapi/django_/urls.py index faf8bf469..7d254600f 100644 --- a/pygeoapi/django_/urls.py +++ b/pygeoapi/django_/urls.py @@ -8,7 +8,7 @@ # Copyright (c) 2022 Francesco Bartoli # Copyright (c) 2022 Luca Delucchi # Copyright (c) 2022 Krishna Lodha -# Copyright (c) 2022 Tom Kralidis +# Copyright (c) 2024 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -96,6 +96,11 @@ def apply_slash_rule(url: str): views.collections, name='collection-detail', ), + path( + apply_slash_rule('collections//schema/'), + views.collection_schema, + name='collection-schema', + ), path( apply_slash_rule('collections//queryables/'), views.collection_queryables, @@ -116,16 +121,6 @@ def apply_slash_rule(url: str): views.collection_coverage, name='collection-coverage', ), - path( - apply_slash_rule('collections//coverage/domainset/'), # noqa - views.collection_coverage_domainset, - name='collection-coverage-domainset', - ), - path( - apply_slash_rule('collections//coverage/rangetype/'), # noqa - views.collection_coverage_rangetype, - name='collection-coverage-rangetype', - ), path( 'collections//map', views.collection_map, diff --git a/pygeoapi/django_/views.py b/pygeoapi/django_/views.py index 7f6896297..95df4f024 100644 --- a/pygeoapi/django_/views.py +++ b/pygeoapi/django_/views.py @@ -126,6 +126,25 @@ def collections(request: HttpRequest, return response +def collection_schema(request: HttpRequest, + collection_id: Optional[str] = None) -> HttpResponse: + """ + OGC API collections schema endpoint + + :request Django HTTP Request + :param collection_id: collection identifier + + :returns: Django HTTP Response + """ + + response_ = _feed_response( + request, 'get_collection_schema', collection_id + ) + response = _to_django_response(*response_) + + return response + + def collection_queryables(request: HttpRequest, collection_id: Optional[str] = None) -> HttpResponse: """ @@ -268,44 +287,6 @@ def collection_coverage(request: HttpRequest, return response -def collection_coverage_domainset(request: HttpRequest, - collection_id: str) -> HttpResponse: - """ - OGC API - Coverages coverage domainset endpoint - - :request Django HTTP Request - :param collection_id: collection identifier - - :returns: Django HTTP response - """ - - response_ = _feed_response( - request, 'get_collection_coverage_domainset', collection_id - ) - response = _to_django_response(*response_) - - return response - - -def collection_coverage_rangetype(request: HttpRequest, - collection_id: str) -> HttpResponse: - """ - OGC API - Coverages coverage rangetype endpoint - - :request Django HTTP Request - :param collection_id: collection identifier - - :returns: Django HTTP response - """ - - response_ = _feed_response( - request, 'get_collection_coverage_rangetype', collection_id - ) - response = _to_django_response(*response_) - - return response - - def collection_tiles(request: HttpRequest, collection_id: str) -> HttpResponse: """ OGC API - Tiles collection tiles endpoint diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index ac5946268..b966cfc33 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -3,7 +3,7 @@ # Authors: Tom Kralidis # Norman Barker # -# Copyright (c) 2023 Tom Kralidis +# Copyright (c) 2024 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -189,10 +189,22 @@ def collections(collection_id=None): return get_response(api_.describe_collections(request, collection_id)) +@BLUEPRINT.route('/collections//schema') +def collection_schema(collection_id): + """ + OGC API - collections schema endpoint + + :param collection_id: collection identifier + + :returns: HTTP response + """ + return get_response(api_.get_collection_schema(request, collection_id)) + + @BLUEPRINT.route('/collections//queryables') def collection_queryables(collection_id=None): """ - OGC API collections querybles endpoint + OGC API collections queryables endpoint :param collection_id: collection identifier @@ -263,32 +275,6 @@ def collection_coverage(collection_id): return get_response(api_.get_collection_coverage(request, collection_id)) -@BLUEPRINT.route('/collections//coverage/domainset') -def collection_coverage_domainset(collection_id): - """ - OGC API - Coverages coverage domainset endpoint - - :param collection_id: collection identifier - - :returns: HTTP response - """ - return get_response(api_.get_collection_coverage_domainset( - request, collection_id)) - - -@BLUEPRINT.route('/collections//coverage/rangetype') -def collection_coverage_rangetype(collection_id): - """ - OGC API - Coverages coverage rangetype endpoint - - :param collection_id: collection identifier - - :returns: HTTP response - """ - return get_response(api_.get_collection_coverage_rangetype( - request, collection_id)) - - @BLUEPRINT.route('/collections//tiles') def get_collection_tiles(collection_id=None): """ diff --git a/pygeoapi/openapi.py b/pygeoapi/openapi.py index d697df206..c71b66bb5 100644 --- a/pygeoapi/openapi.py +++ b/pygeoapi/openapi.py @@ -4,7 +4,7 @@ # Authors: Francesco Bartoli # Authors: Ricardo Garcia Silva # -# Copyright (c) 2023 Tom Kralidis +# Copyright (c) 2024 Tom Kralidis # Copyright (c) 2022 Francesco Bartoli # Copyright (c) 2023 Ricardo Garcia Silva # @@ -640,6 +640,27 @@ def get_oas_30(cfg): paths[items_path]['get']['parameters'].append( {'$ref': f"{OPENAPI_YAML['oapir']}/parameters/q.yaml"}) if p.fields: + schema_path = f'{collection_name_path}/schemna' + + paths[schema_path] = { + 'get': { + 'summary': f'Get {title} schema', + 'description': desc, + 'tags': [name], + 'operationId': f'get{name.capitalize()}Queryables', + 'parameters': [ + items_f, + items_l + ], + 'responses': { + '200': {'$ref': '#/components/responses/Queryables'}, # noqa + '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa + '404': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/NotFound"}, # noqa + '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"}, # noqa + } + } + } + queryables_path = f'{collection_name_path}/queryables' paths[queryables_path] = { @@ -829,47 +850,6 @@ def get_oas_30(cfg): } } - coverage_domainset_path = f'{collection_name_path}/coverage/domainset' # noqa - - paths[coverage_domainset_path] = { - 'get': { - 'summary': f'Get {title} coverage domain set', - 'description': desc, - 'tags': [name], - 'operationId': f'get{name.capitalize()}CoverageDomainSet', - 'parameters': [ - items_f, - items_l - ], - 'responses': { - '200': {'$ref': f"{OPENAPI_YAML['oacov']}/schemas/cis_1.1/domainSet.yaml"}, # noqa - '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa - '404': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/NotFound"}, # noqa - '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa - } - } - } - - coverage_rangetype_path = f'{collection_name_path}/coverage/rangetype' # noqa - - paths[coverage_rangetype_path] = { - 'get': { - 'summary': f'Get {title} coverage range type', - 'description': desc, - 'tags': [name], - 'operationId': f'get{name.capitalize()}CoverageRangeType', - 'parameters': [ - items_f, - items_l - ], - 'responses': { - '200': {'$ref': f"{OPENAPI_YAML['oacov']}/schemas/cis_1.1/rangeType.yaml"}, # noqa - '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa - '404': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/NotFound"}, # noqa - '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa - } - } - } except ProviderTypeError: LOGGER.debug('collection is not coverage based') diff --git a/pygeoapi/provider/base.py b/pygeoapi/provider/base.py index 4f8728dda..6bdc7a2bb 100644 --- a/pygeoapi/provider/base.py +++ b/pygeoapi/provider/base.py @@ -181,24 +181,6 @@ def delete(self, identifier): raise NotImplementedError() - def get_coverage_domainset(self): - """ - Provide coverage domainset - - :returns: CIS JSON object of domainset metadata - """ - - raise NotImplementedError() - - def get_coverage_rangetype(self): - """ - Provide coverage rangetype - - :returns: CIS JSON object of rangetype metadata - """ - - raise NotImplementedError() - def _load_and_prepare_item(self, item, identifier=None, accept_missing_identifier=False, raise_if_exists=True): diff --git a/pygeoapi/provider/rasterio_.py b/pygeoapi/provider/rasterio_.py index 7414fcc3f..2bf25e3fb 100644 --- a/pygeoapi/provider/rasterio_.py +++ b/pygeoapi/provider/rasterio_.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2022 Tom Kralidis +# Copyright (c) 2024 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -59,107 +59,38 @@ def __init__(self, provider_def): self.axes = self._coverage_properties['axes'] self.crs = self._coverage_properties['bbox_crs'] self.num_bands = self._coverage_properties['num_bands'] - self.fields = [str(num) for num in range(1, self.num_bands+1)] + self.fields = self.get_fields() self.native_format = provider_def['format']['name'] except Exception as err: LOGGER.warning(err) raise ProviderConnectionError(err) - def get_coverage_domainset(self, *args, **kwargs): - """ - Provide coverage domainset - :returns: CIS JSON object of domainset metadata - """ + def get_fields(self): + fields = {} - domainset = { - 'type': 'DomainSet', - 'generalGrid': { - 'type': 'GeneralGridCoverage', - 'srsName': self._coverage_properties['bbox_crs'], - 'axisLabels': [ - self._coverage_properties['x_axis_label'], - self._coverage_properties['y_axis_label'] - ], - 'axis': [{ - 'type': 'RegularAxis', - 'axisLabel': self._coverage_properties['x_axis_label'], - 'lowerBound': self._coverage_properties['bbox'][0], - 'upperBound': self._coverage_properties['bbox'][2], - 'uomLabel': self._coverage_properties['bbox_units'], - 'resolution': self._coverage_properties['resx'] - }, { - 'type': 'RegularAxis', - 'axisLabel': self._coverage_properties['y_axis_label'], - 'lowerBound': self._coverage_properties['bbox'][1], - 'upperBound': self._coverage_properties['bbox'][3], - 'uomLabel': self._coverage_properties['bbox_units'], - 'resolution': self._coverage_properties['resy'] - }], - 'gridLimits': { - 'type': 'GridLimits', - 'srsName': 'http://www.opengis.net/def/crs/OGC/0/Index2D', - 'axisLabels': ['i', 'j'], - 'axis': [{ - 'type': 'IndexAxis', - 'axisLabel': 'i', - 'lowerBound': 0, - 'upperBound': self._coverage_properties['width'] - }, { - 'type': 'IndexAxis', - 'axisLabel': 'j', - 'lowerBound': 0, - 'upperBound': self._coverage_properties['height'] - }] - } - }, - '_meta': { - 'tags': self._coverage_properties['tags'] - } - } + for i, dtype in zip(self._data.indexes, self._data.dtypes): + LOGGER.debug(f'Adding field for band {i}') + i2 = str(i) - return domainset + parameter = _get_parameter_metadata( + self._data.profile['driver'], self._data.tags(i)) - def get_coverage_rangetype(self, *args, **kwargs): - """ - Provide coverage rangetype - :returns: CIS JSON object of rangetype metadata - """ + name = parameter['description'] + units = parameter.get('unit_label') - rangetype = { - 'type': 'DataRecord', - 'field': [] - } + dtype2 = dtype + if dtype.startswith('float'): + dtype2 = 'number' - for i, dtype, nodataval in zip(self._data.indexes, self._data.dtypes, - self._data.nodatavals): - LOGGER.debug(f'Determing rangetype for band {i}') - - name, units = None, None - if self._data.units[i-1] is None: - parameter = _get_parameter_metadata( - self._data.profile['driver'], self._data.tags(i)) - name = parameter['description'] - units = parameter['unit_label'] - - rangetype['field'].append({ - 'id': i, - 'type': 'Quantity', - 'name': name, - 'encodingInfo': { - 'dataType': f'http://www.opengis.net/def/dataType/OGC/0/{dtype}' # noqa - }, - 'nodata': nodataval, - 'uom': { - 'id': f'http://www.opengis.net/def/uom/UCUM/{units}', - 'type': 'UnitReference', - 'code': units - }, - '_meta': { - 'tags': self._data.tags(i) - } - }) + fields[i2] = { + 'title': name, + 'type': dtype2, + '_meta': self._data.tags(i) + } + if units is not None: + fields[i2]['x-ogc-unit'] = units - return rangetype + return fields def query(self, properties=[], subsets={}, bbox=None, bbox_crs=4326, datetime_=None, format_='json', **kwargs): diff --git a/pygeoapi/provider/xarray_.py b/pygeoapi/provider/xarray_.py index 4a1f39665..d1595c4d3 100644 --- a/pygeoapi/provider/xarray_.py +++ b/pygeoapi/provider/xarray_.py @@ -88,118 +88,28 @@ def __init__(self, provider_def): self._coverage_properties['y_axis_label'], self._coverage_properties['time_axis_label']] - self.fields = self._coverage_properties['fields'] + self.fields = self.get_fields() except Exception as err: LOGGER.warning(err) raise ProviderConnectionError(err) - def get_coverage_domainset(self, *args, **kwargs): - """ - Provide coverage domainset + def get_fields(self): + fields = {} - :returns: CIS JSON object of domainset metadata - """ + for key, value in self._data.variables.items(): + if len(value.shape) >= 3: + LOGGER.debug('Adding variable') + dtype = value.dtype + if dtype.name.startswith('float'): + dtype = 'number' - c_props = self._coverage_properties - domainset = { - 'type': 'DomainSet', - 'generalGrid': { - 'type': 'GeneralGridCoverage', - 'srsName': c_props['bbox_crs'], - 'axisLabels': [ - c_props['x_axis_label'], - c_props['y_axis_label'], - c_props['time_axis_label'] - ], - 'axis': [{ - 'type': 'RegularAxis', - 'axisLabel': c_props['x_axis_label'], - 'lowerBound': c_props['bbox'][0], - 'upperBound': c_props['bbox'][2], - 'uomLabel': c_props['bbox_units'], - 'resolution': c_props['resx'] - }, { - 'type': 'RegularAxis', - 'axisLabel': c_props['y_axis_label'], - 'lowerBound': c_props['bbox'][1], - 'upperBound': c_props['bbox'][3], - 'uomLabel': c_props['bbox_units'], - 'resolution': c_props['resy'] - }, - { - 'type': 'RegularAxis', - 'axisLabel': c_props['time_axis_label'], - 'lowerBound': c_props['time_range'][0], - 'upperBound': c_props['time_range'][1], - 'uomLabel': c_props['restime'], - 'resolution': c_props['restime'] - } - ], - 'gridLimits': { - 'type': 'GridLimits', - 'srsName': 'http://www.opengis.net/def/crs/OGC/0/Index2D', - 'axisLabels': ['i', 'j'], - 'axis': [{ - 'type': 'IndexAxis', - 'axisLabel': 'i', - 'lowerBound': 0, - 'upperBound': c_props['width'] - }, { - 'type': 'IndexAxis', - 'axisLabel': 'j', - 'lowerBound': 0, - 'upperBound': c_props['height'] - }] + fields[key] = { + 'type': dtype, + 'title': value.attrs['long_name'], + 'x-ogc-unit': value.attrs.get('units') } - }, - '_meta': { - 'tags': self._data.attrs - } - } - - return domainset - def get_coverage_rangetype(self, *args, **kwargs): - """ - Provide coverage rangetype - - :returns: CIS JSON object of rangetype metadata - """ - - rangetype = { - 'type': 'DataRecord', - 'field': [] - } - - for name, var in self._data.variables.items(): - LOGGER.debug(f'Determining rangetype for {name}') - - desc, units = None, None - if len(var.shape) >= 3: - parameter = self._get_parameter_metadata( - name, var.attrs) - desc = parameter['description'] - units = parameter['unit_label'] - - rangetype['field'].append({ - 'id': name, - 'type': 'Quantity', - 'name': var.attrs.get('long_name') or desc, - 'encodingInfo': { - 'dataType': f'http://www.opengis.net/def/dataType/OGC/0/{var.dtype}' # noqa - }, - 'nodata': 'null', - 'uom': { - 'id': f'http://www.opengis.net/def/uom/UCUM/{units}', - 'type': 'UnitReference', - 'code': units - }, - '_meta': { - 'tags': var.attrs - } - }) - - return rangetype + return fields def query(self, properties=[], subsets={}, bbox=[], bbox_crs=4326, datetime_=None, format_='json', **kwargs): @@ -224,7 +134,7 @@ def query(self, properties=[], subsets={}, bbox=[], bbox_crs=4326, return read_data(self.data) if len(properties) < 1: - properties = self.fields + properties = self.fields.keys() data = self._data[[*properties]] @@ -322,13 +232,13 @@ def query(self, properties=[], subsets={}, bbox=[], bbox_crs=4326, fp.seek(0) return fp.read() - def gen_covjson(self, metadata, data, range_type): + def gen_covjson(self, metadata, data, fields): """ Generate coverage as CoverageJSON representation :param metadata: coverage metadata :param data: rasterio DatasetReader object - :param range_type: range type list + :param fields: fields dict :returns: dict of CoverageJSON representation """ @@ -385,34 +295,31 @@ def gen_covjson(self, metadata, data, range_type): 'ranges': {} } - for variable in range_type: - pm = self._get_parameter_metadata( - variable, self._data[variable].attrs) - + for key, value in self.fields.items(): parameter = { 'type': 'Parameter', - 'description': pm['description'], + 'description': value['title'], 'unit': { - 'symbol': pm['unit_label'] + 'symbol': value['x-ogc-unit'] }, 'observedProperty': { - 'id': pm['observed_property_id'], + 'id': key, 'label': { - 'en': pm['observed_property_name'] + 'en': value['title'] } } } - cj['parameters'][pm['id']] = parameter + cj['parameters'][key] = parameter data = data.fillna(None) data = _convert_float32_to_float64(data) try: - for key in cj['parameters'].keys(): + for key, value in self.fields.items(): cj['ranges'][key] = { 'type': 'NdArray', - 'dataType': str(self._data[variable].dtype), + 'dataType': value['type'], 'axisNames': [ 'y', 'x', self._coverage_properties['time_axis_label'] ], @@ -435,6 +342,7 @@ def _get_coverage_properties(self): """ time_var, y_var, x_var = [None, None, None] + for coord in self._data.coords: if coord.lower() == 'time': time_var = coord @@ -503,9 +411,6 @@ def _get_coverage_properties(self): properties['time_axis_label'] ] - properties['fields'] = [name for name in self._data.variables - if len(self._data.variables[name].shape) >= 3] - return properties @staticmethod diff --git a/pygeoapi/provider/xarray_edr.py b/pygeoapi/provider/xarray_edr.py index f3c3e1af8..7bfe17833 100644 --- a/pygeoapi/provider/xarray_edr.py +++ b/pygeoapi/provider/xarray_edr.py @@ -57,15 +57,6 @@ def __init__(self, provider_def): BaseEDRProvider.__init__(self, provider_def) XarrayProvider.__init__(self, provider_def) - def get_fields(self): - """ - Get provider field information (names, types) - - :returns: dict of dicts of parameters - """ - - return self.get_coverage_rangetype() - @BaseEDRProvider.register() def position(self, **kwargs): """ @@ -118,7 +109,7 @@ def position(self, **kwargs): try: if select_properties: - self.fields = select_properties + self.fields = {k: v for k, v in self.fields.items() if k in select_properties} # noqa data = self._data[[*select_properties]] else: data = self._data @@ -215,7 +206,7 @@ def cube(self, **kwargs): LOGGER.debug(f'query parameters: {query_params}') try: if select_properties: - self.fields = select_properties + self.fields = {k: v for k, v in self.fields.items() if k in select_properties} # noqa data = self._data[[*select_properties]] else: data = self._data diff --git a/pygeoapi/starlette_app.py b/pygeoapi/starlette_app.py index 29db43e16..7af7a6160 100644 --- a/pygeoapi/starlette_app.py +++ b/pygeoapi/starlette_app.py @@ -5,7 +5,7 @@ # Abdulazeez Abdulazeez Adeshina # # Copyright (c) 2020 Francesco Bartoli -# Copyright (c) 2022 Tom Kralidis +# Copyright (c) 2024 Tom Kralidis # Copyright (c) 2022 Abdulazeez Abdulazeez Adeshina # # Permission is hereby granted, free of charge, to any person @@ -186,6 +186,22 @@ async def get_tilematrix_sets(request: Request): return await get_response(api_.tilematrixsets, request) +async def collection_schema(request: Request, collection_id=None): + """ + OGC API collections schema endpoint + + :param request: Starlette Request instance + :param collection_id: collection identifier + + :returns: Starlette HTTP Response + """ + if 'collection_id' in request.path_params: + collection_id = request.path_params['collection_id'] + + return await get_response( + api_.get_collection_schema, request, collection_id) + + async def collection_queryables(request: Request, collection_id=None): """ OGC API collections queryables endpoint @@ -197,6 +213,7 @@ async def collection_queryables(request: Request, collection_id=None): """ if 'collection_id' in request.path_params: collection_id = request.path_params['collection_id'] + return await get_response( api_.get_collection_queryables, request, collection_id) @@ -341,39 +358,6 @@ async def collection_coverage(request: Request, collection_id=None): api_.get_collection_coverage, request, collection_id) -async def collection_coverage_domainset(request: Request, collection_id=None): - """ - OGC API - Coverages coverage domainset endpoint - - :param request: Starlette Request instance - :param collection_id: collection identifier - - :returns: Starlette HTTP Response - """ - if 'collection_id' in request.path_params: - collection_id = request.path_params['collection_id'] - - return await get_response( - api_.get_collection_coverage_domainset, request, collection_id) - - -async def collection_coverage_rangetype(request: Request, collection_id=None): - """ - OGC API - Coverages coverage rangetype endpoint - - :param request: Starlette Request instance - :param collection_id: collection identifier - - :returns: Starlette HTTP Response - """ - - if 'collection_id' in request.path_params: - collection_id = request.path_params['collection_id'] - - return await get_response( - api_.get_collection_coverage_rangetype, request, collection_id) - - async def collection_map(request: Request, collection_id, style_id=None): """ OGC API - Maps map render endpoint @@ -637,6 +621,7 @@ async def __call__(self, scope: Scope, Route('/conformance', conformance), Route('/TileMatrixSets/{tileMatrixSetId}', get_tilematrix_set), Route('/TileMatrixSets', get_tilematrix_sets), + Route('/collections/{collection_id:path}/schema', collection_schema), Route('/collections/{collection_id:path}/queryables', collection_queryables), # noqa Route('/collections/{collection_id:path}/tiles', get_collection_tiles), Route('/collections/{collection_id:path}/tiles/{tileMatrixSetId}', get_collection_tiles_metadata), # noqa @@ -645,8 +630,6 @@ async def __call__(self, scope: Scope, Route('/collections/{collection_id:path}/items', collection_items, methods=['GET', 'POST', 'OPTIONS']), # noqa Route('/collections/{collection_id:path}/items/{item_id:path}', collection_items, methods=['GET', 'PUT', 'DELETE', 'OPTIONS']), # noqa Route('/collections/{collection_id:path}/coverage', collection_coverage), # noqa - Route('/collections/{collection_id:path}/coverage/domainset', collection_coverage_domainset), # noqa - Route('/collections/{collection_id:path}/coverage/rangetype', collection_coverage_rangetype), # noqa Route('/collections/{collection_id:path}/map', collection_map), Route('/collections/{collection_id:path}/styles/{style_id:path}/map', collection_map), # noqa Route('/processes', get_processes), diff --git a/pygeoapi/templates/collections/coverage/domainset.html b/pygeoapi/templates/collections/coverage/domainset.html deleted file mode 100644 index 8ca91a482..000000000 --- a/pygeoapi/templates/collections/coverage/domainset.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "_base.html" %} -{% block title %}{{ super() }} {{ data['title'] }} {% endblock %} -{% block crumbs %}{{ super() }} -/ {% trans %}Collections{% endtrans %} -/ {{ data['title'] }} -{% endblock %} -{% block body %} -
-

{{ data['title'] }}

-

{{ data['description'] }}

-

{% trans %}Coverage domain set{% endtrans %}

-

{% trans %}Axis labels{% endtrans %}

-
    - {% for al in data['generalGrid']['axisLabels'] %} -
  • {{ al }}
  • - {% endfor %} -
-

{% trans %}Extent{% endtrans %}

-
    -
  • minx: {{ data['generalGrid']['axis'][0]['lowerBound'] }}
  • -
  • miny: {{ data['generalGrid']['axis'][1]['lowerBound'] }}
  • -
  • maxx: {{ data['generalGrid']['axis'][0]['upperBound'] }}
  • -
  • maxy: {{ data['generalGrid']['axis'][1]['upperBound'] }}
  • -
  • {% trans %}Coordinate reference system{% endtrans %}: {{ data['generalGrid']['srsName'] }}
  • -
-

{% trans %}Size{% endtrans %}

-
    -
  • {% trans %}width{% endtrans %}: {{ data['generalGrid']['gridLimits']['axis'][0]['upperBound'] }}
  • -
  • {% trans %}height{% endtrans %}: {{ data['generalGrid']['gridLimits']['axis'][1]['upperBound'] }}
  • -
-

{% trans %}Resolution{% endtrans %}

-
    -
  • {% trans %}x{% endtrans %}: {{ data['generalGrid']['axis'][0]['resolution'] }}
  • -
  • {% trans %}y{% endtrans %}: {{ data['generalGrid']['axis'][1]['resolution'] }}
  • -
-
-{% endblock %} diff --git a/pygeoapi/templates/collections/coverage/rangetype.html b/pygeoapi/templates/collections/coverage/rangetype.html deleted file mode 100644 index d047594a1..000000000 --- a/pygeoapi/templates/collections/coverage/rangetype.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "_base.html" %} -{% block title %}{{ super() }} {{ data['title'] }} {% endblock %} -{% block crumbs %}{{ super() }} -/ {% trans %}Collections{% endtrans %} -/ {{ data['title'] }} -{% endblock %} -{% block body %} -
-

{{ data['title'] }}

-

{{ data['description'] }}

-

{% trans %}Coverage range type{% endtrans %}

-

{% trans %}Fields{% endtrans %}

-
    - {% for field in data['field'] %} -
  • {{ field['id'] }}: {{ field['name'] }} ({{ field['definition'] }})
  • - {% endfor %} -
-
-{% endblock %} diff --git a/pygeoapi/templates/collections/schema.html b/pygeoapi/templates/collections/schema.html new file mode 100644 index 000000000..908c60ddd --- /dev/null +++ b/pygeoapi/templates/collections/schema.html @@ -0,0 +1,46 @@ +{% extends "_base.html" %} +{% block title %}{{ super() }} {{ data['title'] }} {% endblock %} +{% block crumbs %}{{ super() }} +/ {% trans %}Collections{% endtrans %} +/ {{ data['title'] | truncate( 25 ) }} +/ {% trans %}Schema{% endtrans %} +{% endblock %} +{% block body %} +
+

{{ data['title'] }}

+

{{ data['description'] }}

+

+ {% for kw in data['keywords'] %} + {{ kw }} + {% endfor %} +

+

{% trans %}Schema{% endtrans %}

+ + + + + + +
    + {% for qname, qinfo in data['properties'].items() %} +
+ + + {% if qname == 'geometry' %} + + {% else %} + + {% endif %} + + + + {% endfor %} +
NameTitleTypeUnitsValues
{{ qname }}{{ qinfo['title'] }}{{ qname }} {{ qinfo['type'] }}{{ qinfo['x-ogc-unit'] }} +
    + {% for value in qinfo['enum'] %} +
  • {{ value }}
  • + {% endfor %} +
+
+
+{% endblock %} diff --git a/tests/test_api.py b/tests/test_api.py index 6732d2747..8560c2efd 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -609,7 +609,7 @@ def test_conformance(config, api_): assert isinstance(root, dict) assert 'conformsTo' in root - assert len(root['conformsTo']) == 34 + assert len(root['conformsTo']) == 36 assert 'http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs' \ in root['conformsTo'] @@ -703,7 +703,7 @@ def test_describe_collections(config, api_): assert collection['id'] == 'obs' assert collection['title'] == 'Observations' assert collection['description'] == 'My cool observations' - assert len(collection['links']) == 12 + assert len(collection['links']) == 14 assert collection['extent'] == { 'spatial': { 'bbox': [[-180, -90, 180, 90]], @@ -751,7 +751,11 @@ def test_describe_collections(config, api_): collection = json.loads(response) assert collection['id'] == 'gdps-temperature' - assert len(collection['links']) == 14 + assert len(collection['links']) == 10 + assert collection['extent']['spatial']['grid'][0]['cellsCount'] == 2400 + assert collection['extent']['spatial']['grid'][0]['resolution'] == 0.15000000000000002 # noqa + assert collection['extent']['spatial']['grid'][1]['cellsCount'] == 1201 + assert collection['extent']['spatial']['grid'][1]['resolution'] == 0.15 # hiearchical collections rsp_headers, code, response = api_.describe_collections( @@ -787,6 +791,35 @@ def test_describe_collections_hidden_resources( assert len(collections['collections']) == 1 +def test_get_collection_schema(config, api_): + req = mock_request() + rsp_headers, code, response = api_.get_collection_schema(req, + 'notfound') + assert code == HTTPStatus.NOT_FOUND + + req = mock_request({'f': 'html'}) + rsp_headers, code, response = api_.get_collection_schema(req, 'obs') + assert rsp_headers['Content-Type'] == FORMAT_TYPES[F_HTML] + + req = mock_request({'f': 'json'}) + rsp_headers, code, response = api_.get_collection_schema(req, 'obs') + assert rsp_headers['Content-Type'] == 'application/schema+json' + schema = json.loads(response) + + assert 'properties' in schema + assert len(schema['properties']) == 5 + + req = mock_request({'f': 'json'}) + rsp_headers, code, response = api_.get_collection_schema( + req, 'gdps-temperature') + assert rsp_headers['Content-Type'] == 'application/schema+json' + schema = json.loads(response) + + assert 'properties' in schema + assert len(schema['properties']) == 1 + assert schema['properties']['1']['type'] == 'number' + + def test_get_collection_queryables(config, api_): req = mock_request() rsp_headers, code, response = api_.get_collection_queryables(req, @@ -833,7 +866,7 @@ def test_describe_collections_json_ld(config, api_): assert len(expanded['http://schema.org/dataset']) == 1 dataset = expanded['http://schema.org/dataset'][0] assert dataset['@type'][0] == 'http://schema.org/Dataset' - assert len(dataset['http://schema.org/distribution']) == 12 + assert len(dataset['http://schema.org/distribution']) == 14 assert all(dist['@type'][0] == 'http://schema.org/DataDownload' for dist in dataset['http://schema.org/distribution']) @@ -1428,44 +1461,6 @@ def test_get_collection_item_json_ld(config, api_): assert rsp_headers['Content-Language'] == 'fr-CA' -def test_get_coverage_domainset(config, api_): - req = mock_request() - rsp_headers, code, response = api_.get_collection_coverage_domainset( - req, 'obs') - - assert code == HTTPStatus.BAD_REQUEST - - rsp_headers, code, response = api_.get_collection_coverage_domainset( - req, 'gdps-temperature') - - domainset = json.loads(response) - - assert domainset['type'] == 'DomainSet' - assert domainset['generalGrid']['axisLabels'] == ['Long', 'Lat'] - assert domainset['generalGrid']['gridLimits']['axisLabels'] == ['i', 'j'] - assert domainset['generalGrid']['gridLimits']['axis'][0]['upperBound'] == 2400 # noqa - assert domainset['generalGrid']['gridLimits']['axis'][1]['upperBound'] == 1201 # noqa - - -def test_get_collection_coverage_rangetype(config, api_): - req = mock_request() - rsp_headers, code, response = api_.get_collection_coverage_rangetype( - req, 'obs') - - assert code == HTTPStatus.BAD_REQUEST - - rsp_headers, code, response = api_.get_collection_coverage_rangetype( - req, 'gdps-temperature') - - rangetype = json.loads(response) - - assert rangetype['type'] == 'DataRecord' - assert len(rangetype['field']) == 1 - assert rangetype['field'][0]['id'] == 1 - assert rangetype['field'][0]['name'] == 'Temperature [C]' - assert rangetype['field'][0]['uom']['code'] == '[C]' - - def test_get_collection_coverage(config, api_): req = mock_request() rsp_headers, code, response = api_.get_collection_coverage( diff --git a/tests/test_rasterio_provider.py b/tests/test_rasterio_provider.py index f1b4471e4..e0dd1f2ac 100644 --- a/tests/test_rasterio_provider.py +++ b/tests/test_rasterio_provider.py @@ -61,24 +61,12 @@ def test_provider(config): assert p.axes == ['Long', 'Lat'] -def test_domainset(config): +def test_schema(config): p = RasterioProvider(config) - domainset = p.get_coverage_domainset() - assert isinstance(domainset, dict) - assert domainset['generalGrid']['axisLabels'] == ['Long', 'Lat'] - assert domainset['generalGrid']['gridLimits']['axisLabels'] == ['i', 'j'] - assert domainset['generalGrid']['gridLimits']['axis'][0]['upperBound'] == 2400 # noqa - assert domainset['generalGrid']['gridLimits']['axis'][1]['upperBound'] == 1201 # noqa - - -def test_rangetype(config): - p = RasterioProvider(config) - rangetype = p.get_coverage_rangetype() - - assert isinstance(rangetype, dict) - assert len(rangetype['field']) == 1 - assert rangetype['field'][0]['name'] == 'Temperature [C]' + assert isinstance(p.fields, dict) + assert len(p.fields) == 1 + assert p.fields['1']['title'] == 'Temperature [C]' def test_query(config): diff --git a/tests/test_xarray_netcdf_provider.py b/tests/test_xarray_netcdf_provider.py index f45b2df57..3546b902c 100644 --- a/tests/test_xarray_netcdf_provider.py +++ b/tests/test_xarray_netcdf_provider.py @@ -58,24 +58,12 @@ def test_provider(config): assert p.axes == ['COADSX', 'COADSY', 'TIME'] -def test_domainset(config): - p = XarrayProvider(config) - domainset = p.get_coverage_domainset() - - assert isinstance(domainset, dict) - assert domainset['generalGrid']['axisLabels'] == ['COADSX', 'COADSY', 'TIME'] # noqa - assert domainset['generalGrid']['gridLimits']['axisLabels'] == ['i', 'j'] - assert domainset['generalGrid']['gridLimits']['axis'][0]['upperBound'] == 180 # noqa - assert domainset['generalGrid']['gridLimits']['axis'][1]['upperBound'] == 90 # noqa - - def test_rangetype(config): p = XarrayProvider(config) - rangetype = p.get_coverage_rangetype() - assert isinstance(rangetype, dict) - assert len(rangetype['field']) == 4 - assert rangetype['field'][0]['name'] == 'SEA SURFACE TEMPERATURE' + assert isinstance(p.fields, dict) + assert len(p.fields) == 4 + assert p.fields['SST']['title'] == 'SEA SURFACE TEMPERATURE' def test_query(config): diff --git a/tests/test_xarray_zarr_provider.py b/tests/test_xarray_zarr_provider.py index 6e59906de..5163b32a6 100644 --- a/tests/test_xarray_zarr_provider.py +++ b/tests/test_xarray_zarr_provider.py @@ -61,24 +61,12 @@ def test_provider(config): assert p.axes == ['lon', 'lat', 'time'] -def test_domainset(config): +def test_schema(config): p = XarrayProvider(config) - domainset = p.get_coverage_domainset() - assert isinstance(domainset, dict) - assert domainset['generalGrid']['axisLabels'] == ['lon', 'lat', 'time'] - assert domainset['generalGrid']['gridLimits']['axisLabels'] == ['i', 'j'] - assert domainset['generalGrid']['gridLimits']['axis'][0]['upperBound'] == 101 # noqa - assert domainset['generalGrid']['gridLimits']['axis'][1]['upperBound'] == 101 # noqa - - -def test_rangetype(config): - p = XarrayProvider(config) - rangetype = p.get_coverage_rangetype() - - assert isinstance(rangetype, dict) - assert len(rangetype['field']) == 4 - assert rangetype['field'][0]['name'] == 'analysed sea surface temperature' + assert isinstance(p.fields, dict) + assert len(p.fields) == 4 + assert p.fields['analysed_sst']['title'] == 'analysed sea surface temperature' # noqa def test_query(config): From a640f7c487eddeb4c460d9c7202fe9fa03f65b09 Mon Sep 17 00:00:00 2001 From: PascalLike <1323093+PascalLike@users.noreply.github.com> Date: Tue, 27 Feb 2024 20:56:02 +0100 Subject: [PATCH 006/279] PostGIS MVT provider and docs (#1552) * Adding mvt Postgis provider * Fix test * Update docs * Rename mvt_postgis to mvt_proxy * Update ogcapi-tiles.rst * Update mvt_proxy.py * Remove not implemented method * Fix typo --------- Co-authored-by: Tom Kralidis --- docs/source/data-publishing/ogcapi-tiles.rst | 51 +++- pygeoapi/plugin.py | 1 + pygeoapi/provider/mvt_elastic.py | 8 - pygeoapi/provider/mvt_proxy.py | 256 +++++++++++++++++++ tests/pygeoapi-test-config-apirules.yml | 2 +- 5 files changed, 306 insertions(+), 12 deletions(-) create mode 100644 pygeoapi/provider/mvt_proxy.py diff --git a/docs/source/data-publishing/ogcapi-tiles.rst b/docs/source/data-publishing/ogcapi-tiles.rst index c3a40b677..acd07687d 100644 --- a/docs/source/data-publishing/ogcapi-tiles.rst +++ b/docs/source/data-publishing/ogcapi-tiles.rst @@ -20,6 +20,7 @@ pygeoapi core tile providers are listed below, along with supported features. `MVT-tippecanoe`_,❌,✅ `MVT-elastic`_,✅,❌ + `MVT-proxy`_, N/A , N/A Below are specific connection examples based on supported providers. @@ -50,7 +51,7 @@ This code block shows how to configure pygeoapi to read Mapbox vector tiles gene min: 0 max: 5 schemes: - - WorldCRS84Quad + - WebMercatorQuad format: name: pbf mimetype: application/vnd.mapbox-vector-tile @@ -59,7 +60,7 @@ This code block shows how to configure pygeoapi to read Mapbox vector tiles gene On `this tutorial `_ you can find detailed instructions on how-to generate tiles using tippecanoe and integrate them into pygeoapi. MVT-elastic -^^^^^^^^^^^^ +^^^^^^^^^^^ This provider gives support to serving tiles generated using `Elasticsearch `_. These tiles are rendered on-the-fly using the `Elasticsearch Vector tile search API `_. @@ -80,7 +81,7 @@ This code block shows how to configure pygeoapi to read Mapbox vector tiles from min: 0 max: 5 schemes: - - WorldCRS84Quad + - WebMercatorQuad format: name: pbf mimetype: application/vnd.mapbox-vector-tile @@ -88,6 +89,50 @@ This code block shows how to configure pygeoapi to read Mapbox vector tiles from .. tip:: On `this tutorial `_ you can find detailed instructions on publish tiles stored in an Elasticsearch endpoint. +MVT-proxy +^^^^^^^^^ + +This provider gives support to serving tiles from a generic tiles provider `{z}/{x}/{y}`. + +For example, you can get and publish tiles from PostGIS providers like `pg_tileserver `_ +or `martin `_. Both of them render tiles on the fly and provide properties. + +Following block shows how to configure pygeoapi to read Mapbox vector tiles from pg_tileserver endpoint. + +.. code-block:: yaml + + providers: + - type: tile + name: MVT-proxy + data: http://localhost:7800/public.ne_50m_admin_0_countries/{z}/{x}/{y}.mvt + options: + zoom: + min: 0 + max: 15 + schemes: + - WebMercatorQuad + format: + name: pbf + mimetype: application/vnd.mapbox-vector-tile + +Following code block shows how to configure pygeoapi to read Mapbox vector tiles from martin endpoint. + +.. code-block:: yaml + + providers: + - type: tile + name: MVT-proxy + data: http://localhost:3000/ne_50m_admin_0_countries/{z}/{x}/{y} + options: + zoom: + min: 0 + max: 15 + schemes: + - WebMercatorQuad + format: + name: pbf + mimetype: application/vnd.mapbox-vector-tile + Data access examples -------------------- diff --git a/pygeoapi/plugin.py b/pygeoapi/plugin.py index 312017a12..d882a9867 100644 --- a/pygeoapi/plugin.py +++ b/pygeoapi/plugin.py @@ -52,6 +52,7 @@ 'MongoDB': 'pygeoapi.provider.mongo.MongoProvider', 'MVT-tippecanoe': 'pygeoapi.provider.mvt_tippecanoe.MVTTippecanoeProvider', # noqa: E501 'MVT-elastic': 'pygeoapi.provider.mvt_elastic.MVTElasticProvider', # noqa: E501 + 'MVT-proxy': 'pygeoapi.provider.mvt_proxy.MVTProxyProvider', # noqa: E501 'OracleDB': 'pygeoapi.provider.oracle.OracleProvider', 'OGR': 'pygeoapi.provider.ogr.OGRProvider', 'PostgreSQL': 'pygeoapi.provider.postgresql.PostgreSQLProvider', diff --git a/pygeoapi/provider/mvt_elastic.py b/pygeoapi/provider/mvt_elastic.py index df739c18e..fb020c495 100644 --- a/pygeoapi/provider/mvt_elastic.py +++ b/pygeoapi/provider/mvt_elastic.py @@ -249,11 +249,3 @@ def get_default_metadata(self, dataset, server_url, layer, tileset, content.links = links return content.dict(exclude_none=True) - - def get_vendor_metadata(self, dataset, server_url, layer, tileset, - title, description, keywords, **kwargs): - """ - Gets tile metadata in tilejson format - """ - LOGGER.debug("Get tilejson metadata") - return "" diff --git a/pygeoapi/provider/mvt_proxy.py b/pygeoapi/provider/mvt_proxy.py new file mode 100644 index 000000000..fd80a995d --- /dev/null +++ b/pygeoapi/provider/mvt_proxy.py @@ -0,0 +1,256 @@ +# ================================================================= +# +# Authors: Antonio Cerciello +# +# Copyright (c) 2024 Antonio Cerciello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +import logging +import requests +from urllib.parse import urlparse + +from pygeoapi.provider.base_mvt import BaseMVTProvider +from pygeoapi.provider.base import (ProviderConnectionError, + ProviderGenericError, + ProviderInvalidQueryError) +from pygeoapi.models.provider.base import ( + TileSetMetadata, LinkType) +from pygeoapi.util import is_url, url_join + +LOGGER = logging.getLogger(__name__) + + +class MVTProxyProvider(BaseMVTProvider): + """ + MVT Proxy Provider + Provider for serving tiles rendered with an external + tiles provider + """ + + def __init__(self, BaseMVTProvider): + """ + Initialize object + + :param provider_def: provider definition + + :returns: pygeoapi.provider.mvt_proxy.pygeoapi/provider/mvt_proxy.py # noqa + """ + + super().__init__(BaseMVTProvider) + + if not is_url(self.data): + msg = 'Wrong input format for MVT' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + url = urlparse(self.data) + baseurl = f'{url.scheme}://{url.netloc}' + param_type = '?f=mvt' + layer = f'/{self.get_layer()}' + + LOGGER.debug('Extracting layer name from URL') + LOGGER.debug(f'Layer: {layer}') + + tilepath = f'{layer}/tiles' + servicepath = f'{tilepath}/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}{param_type}' # noqa + + self._service_url = url_join(baseurl, servicepath) + + self._service_metadata_url = url_join( + self.service_url.split('{tileMatrix}/{tileRow}/{tileCol}')[0], + 'metadata') + + def __repr__(self): + return f' {self.data}' + + @property + def service_url(self): + return self._service_url + + @property + def service_metadata_url(self): + return self._service_metadata_url + + def get_layer(self): + """ + Extracts layer name from url + + :returns: layer name + """ + + if not is_url(self.data): + msg = 'Wrong input format for MVT' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + url = urlparse(self.data) + + if ('/{z}/{x}/{y}' not in url.path): + msg = 'Wrong input format for MVT' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + layer = url.path.split('/{z}/{x}/{y}')[0] + + LOGGER.debug(layer) + LOGGER.debug('Removing leading "/"') + return layer[1:] + + def get_tiles_service(self, baseurl=None, servicepath=None, + dirpath=None, tile_type=None): + """ + Gets mvt service description + + :param baseurl: base URL of endpoint + :param servicepath: base path of URL + :param dirpath: directory basepath (equivalent of URL) + :param tile_type: tile format type + + :returns: `dict` of item tile service + """ + + super().get_tiles_service(baseurl, servicepath, + dirpath, tile_type) + + self._service_url = servicepath + return self.get_tms_links() + + def get_tiles(self, layer=None, tileset=None, + z=None, y=None, x=None, format_=None): + """ + Gets tile + + :param layer: mvt tile layer + :param tileset: mvt tileset + :param z: z index + :param y: y index + :param x: x index + :param format_: tile format + + :returns: an encoded mvt tile + """ + if format_ == 'mvt': + format_ = self.format_type + + if is_url(self.data): + url = urlparse(self.data) + base_url = f'{url.scheme}://{url.netloc}' + + if url.query: + url_query = f'?{url.query}' + else: + url_query = '' + + resp = None + + try: + with requests.Session() as session: + session.get(base_url) + if '.' in url.path: + resp = session.get(f'{base_url}/{layer}/{z}/{y}/{x}.{format_}{url_query}') # noqa + else: + resp = session.get(f'{base_url}/{layer}/{z}/{y}/{x}{url_query}') # noqa + + resp.raise_for_status() + return resp.content + except requests.exceptions.RequestException as e: + LOGGER.debug(e) + if resp and resp.status_code < 500: + raise ProviderInvalidQueryError # Client is sending an invalid request # noqa + raise ProviderGenericError # Server error + else: + msg = 'Wrong input format for MVT' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + def get_html_metadata(self, dataset, server_url, layer, tileset, + title, description, keywords, **kwargs): + + service_url = url_join( + server_url, + f'collections/{dataset}/tiles/{tileset}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f=mvt') # noqa + metadata_url = url_join( + server_url, + f'collections/{dataset}/tiles/{tileset}/metadata') + + metadata = dict() + metadata['id'] = dataset + metadata['title'] = title + metadata['tileset'] = tileset + metadata['collections_path'] = service_url + metadata['json_url'] = f'{metadata_url}?f=json' + + return metadata + + def get_default_metadata(self, dataset, server_url, layer, tileset, + title, description, keywords, **kwargs): + + service_url = url_join( + server_url, + f'collections/{dataset}/tiles/{tileset}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f=mvt') # noqa + + content = {} + tiling_schemes = self.get_tiling_schemes() + # Default values + tileMatrixSetURI = tiling_schemes[0].tileMatrixSetURI + crs = tiling_schemes[0].crs + # Checking the selected matrix in configured tiling_schemes + for schema in tiling_schemes: + if (schema.tileMatrixSet == tileset): + crs = schema.crs + tileMatrixSetURI = schema.tileMatrixSetURI + + tiling_scheme_url = url_join( + server_url, f'/TileMatrixSets/{schema.tileMatrixSet}') + tiling_scheme_url_type = "application/json" + tiling_scheme_url_title = f'{schema.tileMatrixSet} tile matrix set definition' # noqa + + tiling_scheme = LinkType(href=tiling_scheme_url, + rel="http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", # noqa + type=tiling_scheme_url_type, + title=tiling_scheme_url_title) + + if tiling_scheme is None: + msg = f'Could not identify a valid tiling schema' # noqa + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + content = TileSetMetadata(title=title, description=description, + keywords=keywords, crs=crs, + tileMatrixSetURI=tileMatrixSetURI) + + links = [] + service_url_link_type = "application/vnd.mapbox-vector-tile" + service_url_link_title = f'{tileset} vector tiles for {layer}' + service_url_link = LinkType(href=service_url, rel="item", + type=service_url_link_type, + title=service_url_link_title) + + links.append(tiling_scheme) + links.append(service_url_link) + + content.links = links + + return content.dict(exclude_none=True) diff --git a/tests/pygeoapi-test-config-apirules.yml b/tests/pygeoapi-test-config-apirules.yml index e8752f861..598903de6 100644 --- a/tests/pygeoapi-test-config-apirules.yml +++ b/tests/pygeoapi-test-config-apirules.yml @@ -212,7 +212,7 @@ resources: data: tests/data/ne_110m_lakes.geojson id_field: id - type: tile - name: MVT + name: MVT-tippecanoe # data: http://localhost:9000/ne_110m_lakes/{z}/{x}/{y} data: tests/data/tiles/ne_110m_lakes options: From f472a5f8a6663a45c29a1b8aefaa32836eedd6d2 Mon Sep 17 00:00:00 2001 From: Resad Hajdarpasic Date: Wed, 28 Feb 2024 09:34:46 +0100 Subject: [PATCH 007/279] fix Django EDR implementation on non-instance endpoints (#1528) (#1549) * fix Django EDR implementation on non-instance endpoints (#1528) * clean up code to follow PEP8 and fix docstring (#1528) * remove trailing comma from view definition args (#1528) --------- Co-authored-by: Resad Hajdarpasic --- pygeoapi/django_/views.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pygeoapi/django_/views.py b/pygeoapi/django_/views.py index 95df4f024..6a379cd50 100644 --- a/pygeoapi/django_/views.py +++ b/pygeoapi/django_/views.py @@ -36,6 +36,7 @@ """Integration module for Django""" from typing import Tuple, Dict, Mapping, Optional + from django.conf import settings from django.http import HttpRequest, HttpResponse @@ -328,7 +329,7 @@ def collection_tiles_metadata(request: HttpRequest, collection_id: str, def collection_item_tiles(request: HttpRequest, collection_id: str, tileMatrixSetId: str, tileMatrix: str, - tileRow: str, tileCol: str,) -> HttpResponse: + tileRow: str, tileCol: str) -> HttpResponse: """ OGC API - Tiles collection tiles data endpoint @@ -430,14 +431,16 @@ def job_results_resource(request: HttpRequest, process_id: str, job_id: str, return response -def get_collection_edr_query(request: HttpRequest, collection_id: str, - instance_id: str) -> HttpResponse: +def get_collection_edr_query( + request: HttpRequest, collection_id: str, + instance_id: Optional[str] = None +) -> HttpResponse: """ OGC API - EDR endpoint - :request Django HTTP Request - :param job_id: job identifier - :param resource: job resource + :param request: Django HTTP Request + :param collection_id: collection identifier + :param instance_id: optional instance identifier. default is None :returns: Django HTTP response """ From f4c2ff970fa2d42d3d5d5fd6952069162d5f823e Mon Sep 17 00:00:00 2001 From: Aleksandar Zec <52027726+acikaz@users.noreply.github.com> Date: Wed, 28 Feb 2024 11:48:27 +0100 Subject: [PATCH 008/279] #1545 fix: resolved issues on Breadcrumb routes for TMS endpoints (#1555) --- .../tilematrixsets/tilematrixset.html | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/pygeoapi/templates/tilematrixsets/tilematrixset.html b/pygeoapi/templates/tilematrixsets/tilematrixset.html index 9a7e1c0b1..1181d33dd 100644 --- a/pygeoapi/templates/tilematrixsets/tilematrixset.html +++ b/pygeoapi/templates/tilematrixsets/tilematrixset.html @@ -1,19 +1,18 @@ -{% extends "_base.html" %} {% block title %}{{ super() }} {% trans -%}TileMatrixSet{% endtrans %} {% endblock %} {% block crumbs %}{{ super() }} / -{% trans %}TileMatrixSet{% endtrans %} -{% endblock %} {% block body %} +{% extends "_base.html" %} +{% block title %}{{ super() }} {% trans %}TileMatrixSet{% endtrans %} {% endblock %} +{% block crumbs %}{{ super() }} +/ {% trans %}TileMatrixSets{% endtrans %} +/ {{ data['id'] }} +{% endblock %} +{% block body %}
+

{{ data['id'] }} tile matrix set

    -
  • - CRS: {{ data['crs'] }} -
  • -
  • - Uri: {{ data['uri'] }} -
  • -
  • - Well Known Scale Set: {{ data['wellKnownScaleSet'] }} -
  • +
  • CRS: {{ data['crs'] }}
  • +
  • Uri: {{ data['uri'] }}
  • +
  • Well Known Scale Set: {{ data['wellKnownScaleSet'] }}
+
{% endblock %} From 78c6e953bce12e32dc68f7cfdaf34d1cd2cf72ee Mon Sep 17 00:00:00 2001 From: Tom Kralidis Date: Wed, 28 Feb 2024 07:01:34 -0500 Subject: [PATCH 009/279] fix OACov based collection output (#1561) --- pygeoapi/api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pygeoapi/api.py b/pygeoapi/api.py index d1674f21b..f521e789e 100644 --- a/pygeoapi/api.py +++ b/pygeoapi/api.py @@ -1231,12 +1231,13 @@ def describe_collections(self, request: Union[APIRequest, Any], else: collection['crs'] = [p.crs] collection['extent']['spatial'] = { - 'bbox': [ + 'bbox': [[ p._coverage_properties['bbox'][0], p._coverage_properties['bbox'][1], p._coverage_properties['bbox'][2], p._coverage_properties['bbox'][3] - ], + ]], + 'crs': p.crs, 'grid': [{ 'cellsCount': p._coverage_properties['width'], 'resolution': p._coverage_properties['resx'] From 3ce68acdb85ec655e088b96f92b436f021beb57b Mon Sep 17 00:00:00 2001 From: Tom Kralidis Date: Wed, 28 Feb 2024 11:04:18 -0500 Subject: [PATCH 010/279] keep 4326 bbox and crs for coverages (#1564) --- pygeoapi/api.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/pygeoapi/api.py b/pygeoapi/api.py index f521e789e..d57f7fdb9 100644 --- a/pygeoapi/api.py +++ b/pygeoapi/api.py @@ -1229,23 +1229,13 @@ def describe_collections(self, request: Union[APIRequest, Any], except ProviderTypeError: pass else: - collection['crs'] = [p.crs] - collection['extent']['spatial'] = { - 'bbox': [[ - p._coverage_properties['bbox'][0], - p._coverage_properties['bbox'][1], - p._coverage_properties['bbox'][2], - p._coverage_properties['bbox'][3] - ]], - 'crs': p.crs, - 'grid': [{ - 'cellsCount': p._coverage_properties['width'], - 'resolution': p._coverage_properties['resx'] + collection['extent']['spatial']['grid'] = [{ + 'cellsCount': p._coverage_properties['width'], + 'resolution': p._coverage_properties['resx'] }, { - 'cellsCount': p._coverage_properties['height'], - 'resolution': p._coverage_properties['resy'] - }] - } + 'cellsCount': p._coverage_properties['height'], + 'resolution': p._coverage_properties['resy'] + }] try: tile = get_provider_by_type(v['providers'], 'tile') From 462b4f3566e88febf670b841c02908db7784fbe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Garn=C3=A6s?= Date: Thu, 29 Feb 2024 19:48:07 +0100 Subject: [PATCH 011/279] EDR /locations and /locations/{locId} endpoints (#1487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Expose EDR locations queries to the provider interface The EDR specification allows for locations queries to be performed: https://docs.ogc.org/is/19-086r6/19-086r6.html#_60c4d31c-62f2-4dc7-9a3e-6a1a9127d29e Providers should be allowed to implement these query types. * Generate correct OpenAPI spec for EDR locations collections * Imlement EDR locations query routing for providers in django --------- Co-authored-by: Peter Garnæs --- pygeoapi/api.py | 13 +++++++---- pygeoapi/django_/urls.py | 20 ++++++++++++++++ pygeoapi/django_/views.py | 12 +++++++--- pygeoapi/flask_app.py | 17 +++++++++++--- pygeoapi/openapi.py | 49 ++++++++++++++++++++++++++++++++++++++- pygeoapi/starlette_app.py | 4 ++++ 6 files changed, 103 insertions(+), 12 deletions(-) diff --git a/pygeoapi/api.py b/pygeoapi/api.py index d57f7fdb9..9fdd404e2 100644 --- a/pygeoapi/api.py +++ b/pygeoapi/api.py @@ -3651,7 +3651,8 @@ def delete_job( @pre_process def get_collection_edr_query( self, request: Union[APIRequest, Any], - dataset, instance, query_type) -> Tuple[dict, int, str]: + dataset, instance, query_type, + location_id=None) -> Tuple[dict, int, str]: """ Queries collection EDR @@ -3659,6 +3660,7 @@ def get_collection_edr_query( :param dataset: dataset name :param instance: instance name :param query_type: EDR query type + :param location_id: location id of a /location/ query :returns: tuple of headers, status code, content """ @@ -3694,11 +3696,11 @@ def get_collection_edr_query( parameternames = parameternames.split(',') bbox = None - if query_type == 'cube': + if query_type in ['cube', 'locations']: LOGGER.debug('Processing cube bbox') try: bbox = validate_bbox(request.params.get('bbox')) - if not bbox: + if not bbox and query_type == 'cube': raise ValueError('bbox parameter required by cube queries') except ValueError as err: return self.get_exception( @@ -3716,7 +3718,7 @@ def get_collection_edr_query( return self.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, 'InvalidParameterValue', msg) - elif query_type != 'cube': + elif query_type not in ['cube', 'locations']: msg = 'missing coords parameter' return self.get_exception( HTTPStatus.BAD_REQUEST, headers, request.format, @@ -3771,7 +3773,8 @@ def get_collection_edr_query( bbox=bbox, within=within, within_units=within_units, - limit=int(self.config['server']['limit']) + limit=int(self.config['server']['limit']), + location_id=location_id, ) try: diff --git a/pygeoapi/django_/urls.py b/pygeoapi/django_/urls.py index 7d254600f..25d4f59f8 100644 --- a/pygeoapi/django_/urls.py +++ b/pygeoapi/django_/urls.py @@ -182,6 +182,16 @@ def apply_slash_rule(url: str): views.get_collection_edr_query, name='collection-edr-corridor', ), + path( + 'collections//locations/', + views.get_collection_edr_query, + name='collection-edr-corridor', + ), + path( + 'collections//locations', + views.get_collection_edr_query, + name='collection-edr-corridor', + ), path( 'collections//instances//position', views.get_collection_edr_query, @@ -212,6 +222,16 @@ def apply_slash_rule(url: str): views.get_collection_edr_query, name='collection-edr-instance-corridor', ), + path( + 'collections//instances/locations/', # noqa + views.get_collection_edr_query, + name='collection-edr-corridor', + ), + path( + 'collections//instances/locations', + views.get_collection_edr_query, + name='collection-edr-corridor', + ), path(apply_slash_rule('processes/'), views.processes, name='processes'), path('processes/', views.processes, name='process-detail'), path(apply_slash_rule('jobs/'), views.jobs, name='jobs'), diff --git a/pygeoapi/django_/views.py b/pygeoapi/django_/views.py index 6a379cd50..3c235f172 100644 --- a/pygeoapi/django_/views.py +++ b/pygeoapi/django_/views.py @@ -433,7 +433,8 @@ def job_results_resource(request: HttpRequest, process_id: str, job_id: str, def get_collection_edr_query( request: HttpRequest, collection_id: str, - instance_id: Optional[str] = None + instance_id: Optional[str] = None, + location_id: Optional[str] = None ) -> HttpResponse: """ OGC API - EDR endpoint @@ -441,17 +442,22 @@ def get_collection_edr_query( :param request: Django HTTP Request :param collection_id: collection identifier :param instance_id: optional instance identifier. default is None + :param location_id: optional location identifier. default is None :returns: Django HTTP response """ - query_type = request.path.split('/')[-1] + if location_id: + query_type = 'locations' + else: + query_type = request.path.split('/')[-1] response_ = _feed_response( request, 'get_collection_edr_query', collection_id, instance_id, - query_type + query_type, + location_id ) response = _to_django_response(*response_) diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index b966cfc33..dc829783d 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -426,24 +426,35 @@ def get_job_result_resource(job_id, resource): @BLUEPRINT.route('/collections//radius') @BLUEPRINT.route('/collections//trajectory') @BLUEPRINT.route('/collections//corridor') +@BLUEPRINT.route('/collections//locations/') # noqa +@BLUEPRINT.route('/collections//locations') # noqa @BLUEPRINT.route('/collections//instances//position') # noqa @BLUEPRINT.route('/collections//instances//area') # noqa @BLUEPRINT.route('/collections//instances//cube') # noqa @BLUEPRINT.route('/collections//instances//radius') # noqa @BLUEPRINT.route('/collections//instances//trajectory') # noqa @BLUEPRINT.route('/collections//instances//corridor') # noqa -def get_collection_edr_query(collection_id, instance_id=None): +@BLUEPRINT.route('/collections//instances//locations/') # noqa +@BLUEPRINT.route('/collections//instances//locations') # noqa +def get_collection_edr_query(collection_id, instance_id=None, + location_id=None): """ OGC EDR API endpoints :param collection_id: collection identifier :param instance_id: instance identifier + :param location_id: location id of a /locations/ query :returns: HTTP response """ - query_type = request.path.split('/')[-1] + if location_id: + query_type = 'locations' + else: + query_type = request.path.split('/')[-1] + return get_response(api_.get_collection_edr_query(request, collection_id, - instance_id, query_type)) + instance_id, query_type, + location_id)) @BLUEPRINT.route('/stac') diff --git a/pygeoapi/openapi.py b/pygeoapi/openapi.py index c71b66bb5..2521ecc14 100644 --- a/pygeoapi/openapi.py +++ b/pygeoapi/openapi.py @@ -985,7 +985,7 @@ def get_oas_30(cfg): edr_query_endpoints = [] - for qt in ep.get_query_types(): + for qt in [qt for qt in ep.get_query_types() if qt != 'locations']: edr_query_endpoints.append({ 'path': f'{collection_name_path}/{qt}', 'qt': qt, @@ -1030,6 +1030,53 @@ def get_oas_30(cfg): } } } + if 'locations' in ep.get_query_types(): + paths[f'{collection_name_path}/locations'] = { + 'get': { + 'summary': f"Get pre-defined locations of {v['description']}", # noqa + 'description': v['description'], + 'tags': [k], + 'operationId': f'queryLOCATIONS{k.capitalize()}', + 'parameters': [ + {'$ref': f"{OPENAPI_YAML['oaedr']}/parameters/bbox.yaml"}, # noqa + {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/parameters/datetime"}, # noqa + {'$ref': '#/components/parameters/f'} + ], + 'responses': { + '200': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/Features"}, # noqa + '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa + '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa + } + } + } + paths[f'{collection_name_path}/locations/{{locId}}'] = { + 'get': { + 'summary': f"query {v['description']} by location", # noqa + 'description': v['description'], + 'tags': [k], + 'operationId': f'queryLOCATIONSBYID{k.capitalize()}', + 'parameters': [ + {'$ref': f"{OPENAPI_YAML['oaedr']}/parameters/{spatial_parameter}.yaml"}, # noqa + {'$ref': f"{OPENAPI_YAML['oaedr']}/parameters/locationId.yaml"}, # noqa + {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/parameters/datetime"}, # noqa + {'$ref': f"{OPENAPI_YAML['oaedr']}/parameters/parameter-name.yaml"}, # noqa + {'$ref': f"{OPENAPI_YAML['oaedr']}/parameters/z.yaml"}, # noqa + {'$ref': '#/components/parameters/f'} + ], + 'responses': { + '200': { + 'description': 'Response', + 'content': { + 'application/prs.coverage+json': { + 'schema': { + '$ref': f"{OPENAPI_YAML['oaedr']}/schemas/coverageJSON.yaml" # noqa + } + } + } + } + } + } + } LOGGER.debug('setting up maps endpoints') map_extension = filter_providers_by_type( diff --git a/pygeoapi/starlette_app.py b/pygeoapi/starlette_app.py index 7af7a6160..8264566dd 100644 --- a/pygeoapi/starlette_app.py +++ b/pygeoapi/starlette_app.py @@ -645,12 +645,16 @@ async def __call__(self, scope: Scope, Route('/collections/{collection_id:path}/radius', get_collection_edr_query), # noqa Route('/collections/{collection_id:path}/trajectory', get_collection_edr_query), # noqa Route('/collections/{collection_id:path}/corridor', get_collection_edr_query), # noqa + Route('/collections/{collection_id:path}/locations', get_collection_edr_query), # noqa + Route('/collections/{collection_id:path}/locations/{location_id}', get_collection_edr_query), # noqa Route('/collections/{collection_id:path}/instances/{instance_id}/position', get_collection_edr_query), # noqa Route('/collections/{collection_id:path}/instances/{instance_id}/area', get_collection_edr_query), # noqa Route('/collections/{collection_id:path}/instances/{instance_id}/cube', get_collection_edr_query), # noqa Route('/collections/{collection_id:path}/instances/{instance_id}/radius', get_collection_edr_query), # noqa Route('/collections/{collection_id:path}/instances/{instance_id}/trajectory', get_collection_edr_query), # noqa Route('/collections/{collection_id:path}/instances/{instance_id}/corridor', get_collection_edr_query), # noqa + Route('/collections/{collection_id:path}/instances/{instance_id}/locations', get_collection_edr_query), # noqa + Route('/collections/{collection_id:path}/instances/{instance_id}/locations/{location_id}', get_collection_edr_query), # noqa Route('/collections', collections), Route('/collections/{collection_id:path}', collections), Route('/stac', stac_catalog_root), From f092cf548ebbe405e030a6247a81ac5b6d276e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Garn=C3=A6s?= Date: Thu, 29 Feb 2024 19:48:57 +0100 Subject: [PATCH 012/279] Django server: EDR sanity test (#1530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * EDR queries without instance id works Django URL's without instance IDs called a method requiring instance IDs, resulting in a crash. Instance ID defaults to None, which is also standard for the provider that ultimately will be called. Regression test added * Use fixtures in django server tests --------- Co-authored-by: Peter Garnæs --- tests/test_django.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index acebab471..06cda4f09 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -4,15 +4,38 @@ from unittest import mock import django +import pytest from django.test import Client +from .util import get_test_file_path -@mock.patch.dict(os.environ, {"DJANGO_SETTINGS_MODULE": "django_.settings"}) + +@pytest.fixture +@mock.patch.dict(os.environ, { + "DJANGO_SETTINGS_MODULE": "django_.settings", + "PYGEOAPI_CONFIG": get_test_file_path('pygeoapi-test-config.yml'), + "PYGEOAPI_OPENAPI": get_test_file_path('pygeoapi-test-openapi.yml') +}) @mock.patch.object(sys, "path", sys.path + ["./pygeoapi"]) -def test_django_landing_page_loads(): +def django_(): django.setup() + return django + +def test_django_landing_page_loads(django_): response = Client(SERVER_NAME="localhost").get("/") assert response.status_code == HTTPStatus.OK assert response.json()["title"] == "pygeoapi default instance" + + +def test_django_edr_without_instance_id(django_): + edr_position_query = ("/collections/icoads-sst/position?coords=" + "POINT(12.779895 55.783523)&f=json") + response = Client(SERVER_NAME="localhost").get(edr_position_query) + + assert response.status_code == HTTPStatus.OK + # Validate CoverageJSON is returned + response_json = response.json() + assert response_json["type"] == "Coverage" + assert response_json["domain"]["domainType"] == "Grid" From 5764d3b926b0ff380655cda859bfcbb195d11178 Mon Sep 17 00:00:00 2001 From: Simon Seyock <8100558+simonseyock@users.noreply.github.com> Date: Fri, 1 Mar 2024 14:42:38 +0100 Subject: [PATCH 013/279] WMTS Provider (#1473) * feat: add WMTSFacadeProvider * feat: add tileset metadata to tile page * feat: add jpeg format * fix: minor adjustments * fix: remove tms_facade * chore: fix flake8 errors * docs: add docs for publishing with the WMTS provider * docs: spelling * fix: adjust metadata_format for WMTSFacade * fix: address review * fix: use mimetype as link type --- docs/source/data-publishing/ogcapi-tiles.rst | 62 ++-- pygeoapi/api.py | 14 +- pygeoapi/plugin.py | 1 + pygeoapi/provider/mvt_elastic.py | 4 +- pygeoapi/provider/mvt_proxy.py | 4 +- pygeoapi/provider/mvt_tippecanoe.py | 7 +- pygeoapi/provider/wmts_facade.py | 278 ++++++++++++++++++ .../templates/collections/tiles/index.html | 105 ++++--- 8 files changed, 405 insertions(+), 70 deletions(-) create mode 100644 pygeoapi/provider/wmts_facade.py diff --git a/docs/source/data-publishing/ogcapi-tiles.rst b/docs/source/data-publishing/ogcapi-tiles.rst index acd07687d..f16df73bd 100644 --- a/docs/source/data-publishing/ogcapi-tiles.rst +++ b/docs/source/data-publishing/ogcapi-tiles.rst @@ -7,7 +7,7 @@ Publishing tiles to OGC API - Tiles (map, vector, coverage, etc.). pygeoapi can publish tiles from local or remote data sources (including cloud -object storage or a tile service). +object storage or a tile service). Providers --------- @@ -21,20 +21,18 @@ pygeoapi core tile providers are listed below, along with supported features. `MVT-tippecanoe`_,❌,✅ `MVT-elastic`_,✅,❌ `MVT-proxy`_, N/A , N/A + `WMTSFacade`_,✅,❌ Below are specific connection examples based on supported providers. -.. note:: - Currently only `Mapbox Vector Tiles (MVT) `_ are supported in pygeoapi. - Connection examples ------------------- MVT-tippecanoe ^^^^^^^^^^^^^^ -This provider gives support to serving tiles generated using `Mapbox Tippecanoe `_. -The tiles can be integrated from a path on disk, or from a static url (e.g.: from an S3 or MinIO bucket). +This provider gives support to serving tiles generated using `Mapbox Tippecanoe `_. +The tiles can be integrated from a path on disk, or from a static url (e.g.: from an S3 or MinIO bucket). In both cases, they have to be rendered before using pygeoapi. This code block shows how to configure pygeoapi to read Mapbox vector tiles generated with tippecanoe, from disk or a URL. @@ -43,7 +41,7 @@ This code block shows how to configure pygeoapi to read Mapbox vector tiles gene providers: - type: tile - name: MVT-tippecanoe + name: MVT-tippecanoe data: tests/data/tiles/ne_110m_lakes # local directory tree # data: http://localhost:9000/ne_110m_lakes/{z}/{x}/{y}.pbf # tiles stored on a MinIO bucket options: @@ -53,7 +51,7 @@ This code block shows how to configure pygeoapi to read Mapbox vector tiles gene schemes: - WebMercatorQuad format: - name: pbf + name: pbf mimetype: application/vnd.mapbox-vector-tile .. tip:: @@ -62,7 +60,7 @@ This code block shows how to configure pygeoapi to read Mapbox vector tiles gene MVT-elastic ^^^^^^^^^^^ -This provider gives support to serving tiles generated using `Elasticsearch `_. +This provider gives support to serving tiles generated using `Elasticsearch `_. These tiles are rendered on-the-fly using the `Elasticsearch Vector tile search API `_. In order to use it, the only requirement is to have the data stored in an Elasticsearch index. @@ -72,9 +70,9 @@ This code block shows how to configure pygeoapi to read Mapbox vector tiles from providers: - type: tile - name: MVT-elastic + name: MVT-elastic data: http://localhost:9200/ne_110m_populated_places_simple2/_mvt/geometry/{z}/{x}/{y}?grid_precision=0 - # if you don't use precision 0, you will be requesting for aggregations which are not supported in the + # if you don't use precision 0, you will be requesting for aggregations which are not supported in the # free version of elastic options: zoom: @@ -83,18 +81,18 @@ This code block shows how to configure pygeoapi to read Mapbox vector tiles from schemes: - WebMercatorQuad format: - name: pbf + name: pbf mimetype: application/vnd.mapbox-vector-tile .. tip:: On `this tutorial `_ you can find detailed instructions on publish tiles stored in an Elasticsearch endpoint. - + MVT-proxy ^^^^^^^^^ This provider gives support to serving tiles from a generic tiles provider `{z}/{x}/{y}`. -For example, you can get and publish tiles from PostGIS providers like `pg_tileserver `_ +For example, you can get and publish tiles from PostGIS providers like `pg_tileserver `_ or `martin `_. Both of them render tiles on the fly and provide properties. Following block shows how to configure pygeoapi to read Mapbox vector tiles from pg_tileserver endpoint. @@ -103,7 +101,7 @@ Following block shows how to configure pygeoapi to read Mapbox vector tiles from providers: - type: tile - name: MVT-proxy + name: MVT-proxy data: http://localhost:7800/public.ne_50m_admin_0_countries/{z}/{x}/{y}.mvt options: zoom: @@ -112,7 +110,7 @@ Following block shows how to configure pygeoapi to read Mapbox vector tiles from schemes: - WebMercatorQuad format: - name: pbf + name: pbf mimetype: application/vnd.mapbox-vector-tile Following code block shows how to configure pygeoapi to read Mapbox vector tiles from martin endpoint. @@ -121,7 +119,7 @@ Following code block shows how to configure pygeoapi to read Mapbox vector tiles providers: - type: tile - name: MVT-proxy + name: MVT-proxy data: http://localhost:3000/ne_50m_admin_0_countries/{z}/{x}/{y} options: zoom: @@ -130,9 +128,37 @@ Following code block shows how to configure pygeoapi to read Mapbox vector tiles schemes: - WebMercatorQuad format: - name: pbf + name: pbf mimetype: application/vnd.mapbox-vector-tile + +WMTSFacade +^^^^^^^^^^ + +This provider gives support to serving map tiles generated using a WMTS. + +It is important that the WMTS provides a tile matrix set that matches exactly one of the configured schemes in pygeoapi. +Currently only `WebMercatorQuad` and `WorldCRS84Quad` are available in pygeopi. + +This code block shows how to configure pygeoapi to read map tiles from a WMTS. + +.. code-block:: yaml + providers: + - type: tile + name: WMTSFacade + data: http://127.0.0.1:8080/service + format: + name: png # png or jpeg + mimetype: image/png + options: + wmts_layer: bkg # the layer name of the wmts + wmts_tile_matrix_set: webmercator # the name of the tile matrix set of the wmts. + scheme: WebMercatorQuad # the aligning scheme in pygeoapi. + zoom: + min: 0 + max: 20 + + Data access examples -------------------- diff --git a/pygeoapi/api.py b/pygeoapi/api.py index 9fdd404e2..5084807e7 100644 --- a/pygeoapi/api.py +++ b/pygeoapi/api.py @@ -102,6 +102,7 @@ F_JSONLD = 'jsonld' F_GZIP = 'gzip' F_PNG = 'png' +F_JPEG = 'jpeg' F_MVT = 'mvt' F_NETCDF = 'NetCDF' @@ -111,6 +112,7 @@ (F_JSONLD, 'application/ld+json'), (F_JSON, 'application/json'), (F_PNG, 'image/png'), + (F_JPEG, 'image/jpeg'), (F_MVT, 'application/vnd.mapbox-vector-tile'), (F_NETCDF, 'application/x-netcdf'), )) @@ -1043,6 +1045,10 @@ def describe_collections(self, request: Union[APIRequest, Any], if 'format' in collection_data: collection_data_format = collection_data['format'] + is_vector_tile = (collection_data_type == 'tile' and + collection_data_format['name'] not + in [F_PNG, F_JPEG]) + collection = { 'id': k, 'title': l10n.translate(v['title'], request.locale), @@ -1155,7 +1161,7 @@ def describe_collections(self, request: Union[APIRequest, Any], 'href': f'{self.get_collections_url()}/{k}/schema?f={F_HTML}' # noqa }) - if collection_data_type in ['feature', 'record', 'tile']: + if is_vector_tile or collection_data_type in ['feature', 'record']: # TODO: translate collection['itemType'] = collection_data_type LOGGER.debug('Adding feature/record based links') @@ -2785,7 +2791,7 @@ def get_collection_tiles(self, request: Union[APIRequest, Any], tile_services = p.get_tiles_service( baseurl=self.base_url, - servicepath=f'{self.get_collections_url()}/{dataset}/tiles/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f=mvt' # noqa + servicepath=f'{self.get_collections_url()}/{dataset}/tiles/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f={p.format_type}' # noqa ) for service in tile_services['links']: @@ -2833,6 +2839,7 @@ def get_collection_tiles(self, request: Union[APIRequest, Any], tiles['minzoom'] = p.options['zoom']['min'] tiles['maxzoom'] = p.options['zoom']['max'] tiles['collections_path'] = self.get_collections_url() + tiles['tile_type'] = p.tile_type content = render_j2_template(self.tpl_config, 'collections/tiles/index.html', tiles, @@ -2953,7 +2960,8 @@ def get_collection_tiles_metadata( # Get provider language (if any) prv_locale = l10n.get_plugin_locale(t, request.raw_locale) - if matrix_id not in p.options['schemes']: + tiling_schemes = p.get_tiling_schemes() + if matrix_id not in [item.tileMatrixSet for item in tiling_schemes]: msg = 'tileset not found' return self.get_exception(HTTPStatus.NOT_FOUND, headers, request.format, 'NotFound', msg) diff --git a/pygeoapi/plugin.py b/pygeoapi/plugin.py index d882a9867..be8ba8431 100644 --- a/pygeoapi/plugin.py +++ b/pygeoapi/plugin.py @@ -62,6 +62,7 @@ 'Socrata': 'pygeoapi.provider.socrata.SODAServiceProvider', 'TinyDBCatalogue': 'pygeoapi.provider.tinydb_.TinyDBCatalogueProvider', 'WMSFacade': 'pygeoapi.provider.wms_facade.WMSFacadeProvider', + 'WMTSFacade': 'pygeoapi.provider.wmts_facade.WMTSFacadeProvider', 'xarray': 'pygeoapi.provider.xarray_.XarrayProvider', 'xarray-edr': 'pygeoapi.provider.xarray_edr.XarrayEDRProvider' }, diff --git a/pygeoapi/provider/mvt_elastic.py b/pygeoapi/provider/mvt_elastic.py index fb020c495..dd311bb0e 100644 --- a/pygeoapi/provider/mvt_elastic.py +++ b/pygeoapi/provider/mvt_elastic.py @@ -224,7 +224,7 @@ def get_default_metadata(self, dataset, server_url, layer, tileset, tiling_scheme = LinkType(href=tiling_scheme_url, rel="http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", # noqa - type=tiling_scheme_url_type, + type_=tiling_scheme_url_type, title=tiling_scheme_url_title) if tiling_scheme is None: @@ -240,7 +240,7 @@ def get_default_metadata(self, dataset, server_url, layer, tileset, service_url_link_type = "application/vnd.mapbox-vector-tile" service_url_link_title = f'{tileset} vector tiles for {layer}' service_url_link = LinkType(href=service_url, rel="item", - type=service_url_link_type, + type_=service_url_link_type, title=service_url_link_title) links.append(tiling_scheme) diff --git a/pygeoapi/provider/mvt_proxy.py b/pygeoapi/provider/mvt_proxy.py index fd80a995d..59d01f673 100644 --- a/pygeoapi/provider/mvt_proxy.py +++ b/pygeoapi/provider/mvt_proxy.py @@ -229,7 +229,7 @@ def get_default_metadata(self, dataset, server_url, layer, tileset, tiling_scheme = LinkType(href=tiling_scheme_url, rel="http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", # noqa - type=tiling_scheme_url_type, + type_=tiling_scheme_url_type, title=tiling_scheme_url_title) if tiling_scheme is None: @@ -245,7 +245,7 @@ def get_default_metadata(self, dataset, server_url, layer, tileset, service_url_link_type = "application/vnd.mapbox-vector-tile" service_url_link_title = f'{tileset} vector tiles for {layer}' service_url_link = LinkType(href=service_url, rel="item", - type=service_url_link_type, + type_=service_url_link_type, title=service_url_link_title) links.append(tiling_scheme) diff --git a/pygeoapi/provider/mvt_tippecanoe.py b/pygeoapi/provider/mvt_tippecanoe.py index 207524153..4bbda5e0d 100644 --- a/pygeoapi/provider/mvt_tippecanoe.py +++ b/pygeoapi/provider/mvt_tippecanoe.py @@ -72,14 +72,13 @@ def __init__(self, BaseMVTProvider): if is_url(self.data): url = urlparse(self.data) baseurl = f'{url.scheme}://{url.netloc}' - extension = Path(url.path).suffix # e.g. ".pbf" layer = f'/{self.get_layer()}' LOGGER.debug('Extracting layer name from URL') LOGGER.debug(f'Layer: {layer}') tilepath = f'{layer}/tiles' - servicepath = f'{tilepath}/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}{extension}' # noqa + servicepath = f'{tilepath}/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f=mvt' # noqa self._service_url = url_join(baseurl, servicepath) @@ -326,7 +325,7 @@ def get_default_metadata(self, dataset, server_url, layer, tileset, tiling_scheme = LinkType(href=tiling_scheme_url, rel="http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", # noqa - type=tiling_scheme_url_type, + type_=tiling_scheme_url_type, title=tiling_scheme_url_title) if tiling_scheme is None: @@ -343,7 +342,7 @@ def get_default_metadata(self, dataset, server_url, layer, tileset, service_url_link_type = "application/vnd.mapbox-vector-tile" service_url_link_title = f'{tileset} vector tiles for {layer}' service_url_link = LinkType(href=service_url, rel="item", - type=service_url_link_type, + type_=service_url_link_type, title=service_url_link_title) links.append(tiling_scheme) diff --git a/pygeoapi/provider/wmts_facade.py b/pygeoapi/provider/wmts_facade.py new file mode 100644 index 000000000..c3f122a18 --- /dev/null +++ b/pygeoapi/provider/wmts_facade.py @@ -0,0 +1,278 @@ +# ================================================================= +# +# Authors: Francesco Bartoli +# Authors: Tom Kralidis +# Authors: Simon Seyock +# +# Copyright (c) 2020 Francesco Bartoli +# Copyright (c) 2023 Tom Kralidis +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +import logging + +import requests +from urllib.parse import urlparse, urlencode + +from pygeoapi.provider.tile import ( + BaseTileProvider) +from pygeoapi.provider.base import ProviderConnectionError +from pygeoapi.models.provider.base import ( + TileMatrixSetEnum, TilesMetadataFormat, TileSetMetadata, LinkType) +from pygeoapi.util import is_url, url_join + +LOGGER = logging.getLogger(__name__) + + +class WMTSFacadeProvider(BaseTileProvider): + """WMTS Provider""" + + def __init__(self, provider_def): + """ + Initialize object + + :param provider_def: provider definition + + :returns: pygeoapi.provider.WMTSFacadeProvider + """ + + if provider_def['format']['name'] not in ['png', 'jpeg']: + raise RuntimeError("WMTS format has to be 'png' or 'jpeg'") + + super().__init__(provider_def) + + self.tile_type = 'raster' + + def __repr__(self): + return f' {self.data}' + + @property + def service_url(self): + return self._service_url + + @property + def service_metadata_url(self): + return self._service_metadata_url + + def get_tiling_schemes(self): + + tile_matrix_set_links_list = [ + TileMatrixSetEnum.WORLDCRS84QUAD.value, + TileMatrixSetEnum.WEBMERCATORQUAD.value + ] + tile_matrix_set_links = [ + item for item in tile_matrix_set_links_list + if item.tileMatrixSet == self.options['scheme'] + ] + + return tile_matrix_set_links + + def get_tiles_service(self, baseurl=None, servicepath=None, + dirpath=None, tile_type=None): + """ + Gets service description + + :param baseurl: base URL of endpoint + :param servicepath: base path of URL + :param dirpath: directory basepath (equivalent of URL) + :param tile_type: tile format type + + :returns: `dict` of item tile service + """ + + url = urlparse(self.data) + baseurl = baseurl or f'{url.scheme}://{url.netloc}' + tile_type = self.format_type + basepath = url.path.split('/{z}/{x}/{y}')[0] + servicepath = servicepath or f'{basepath}/tiles/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f={tile_type}' # noqa + + if servicepath.startswith(baseurl): + self._service_url = servicepath + else: + self._service_url = url_join(baseurl, servicepath) + + tile_matrix_set = self.service_url.split( + '/{tileMatrix}/{tileRow}/{tileCol}')[0] + self._service_metadata_url = url_join(tile_matrix_set, 'metadata') + + links = { + 'links': [ + { + 'type': self.mimetype, + 'rel': 'item', + 'title': 'This collection as image tiles', + 'href': self.service_url + }, + { + 'type': 'application/json', + 'rel': 'describedby', + 'title': 'Collection metadata in TileJSON format', + 'href': f'{self.service_metadata_url}?f=json' + } + ] + } + return links + + def get_tiles(self, layer=None, tileset=None, + z=None, y=None, x=None, format_=None): + """ + Gets tile + + :param layer: The layer name + :param tileset: The tileset name + :param z: z index + :param y: y index + :param x: x index + :param format_: tile format + + :returns: an image tile + """ + if is_url(self.data): + params = { + 'service': 'WMTS', + 'request': 'GetTile', + 'version': '1.0.0', + 'format': self.mimetype, + 'layer': self.options['wmts_layer'], + 'tileMatrixSet': self.options['wmts_tile_matrix_set'], + 'tileMatrix': z, + 'tileRow': y, + 'tileCol': x, + 'style': '' + } + + if '?' in self.data: + request_url = '&'.join([self.data, urlencode(params)]) + else: + request_url = '?'.join([self.data, urlencode(params)]) + + LOGGER.debug(f'WMTS 1.0.0 request url: {request_url}') + + with requests.Session() as session: + resp = session.get(request_url) + resp.raise_for_status() + return resp.content + else: + msg = f'Wrong data path configuration: {self.data}' + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + def get_metadata(self, dataset, server_url, layer=None, + tileset=None, metadata_format=None, title=None, + description=None, keywords=None, **kwargs): + """ + Gets tiles metadata + + :param dataset: dataset name + :param server_url: server base url + :param layer: mvt tile layer name + :param tileset: mvt tileset name + :param metadata_format: format for metadata, + enum TilesMetadataFormat + :param title: title name + :param description: description name + :param keywords: keywords list + + :returns: `dict` of JSON metadata + """ + + if metadata_format.upper() == TilesMetadataFormat.JSON: + return self.get_default_metadata(dataset, server_url, layer, + tileset, title, description, + keywords, **kwargs) + elif metadata_format.upper() == TilesMetadataFormat.HTML: + return self.get_html_metadata(dataset, server_url, layer, + tileset, title, description, + keywords, **kwargs) + else: + raise NotImplementedError(f"_{metadata_format.upper()}_ is not supported") # noqa + + def get_html_metadata(self, dataset, server_url, layer, tileset, + title, description, keywords, **kwargs): + + service_url = url_join( + server_url, + f'collections/{dataset}/tiles/{tileset}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f={self.format_type}') # noqa + metadata_url = url_join( + server_url, + f'collections/{dataset}/tiles/{tileset}/metadata') + + metadata = dict() + metadata['id'] = dataset + metadata['title'] = title + metadata['tileset'] = tileset + metadata['collections_path'] = service_url + metadata['json_url'] = f'{metadata_url}?f=json' + + return metadata + + def get_default_metadata(self, dataset, server_url, layer, tileset, + title, description, keywords, **kwargs): + + service_url = url_join( + server_url, + f'collections/{dataset}/tiles/{tileset}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f={self.format_type}') # noqa + + content = {} + tiling_schemes = self.get_tiling_schemes() + # Default values + tileMatrixSetURI = tiling_schemes[0].tileMatrixSetURI + crs = tiling_schemes[0].crs + # Checking the selected matrix in configured tiling_schemes + for schema in tiling_schemes: + if (schema.tileMatrixSet == tileset): + crs = schema.crs + tileMatrixSetURI = schema.tileMatrixSetURI + + tiling_scheme_url = url_join( + server_url, f'/TileMatrixSets/{schema.tileMatrixSet}') + tiling_scheme_url_type = "application/json" + tiling_scheme_url_title = f'{schema.tileMatrixSet} tile matrix set definition' # noqa + + tiling_scheme = LinkType(href=tiling_scheme_url, + rel="http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", # noqa + type_=tiling_scheme_url_type, + title=tiling_scheme_url_title) + + if tiling_scheme is None: + msg = f'Could not identify a valid tiling schema' # noqa + LOGGER.error(msg) + raise ProviderConnectionError(msg) + + content = TileSetMetadata(title=title, description=description, + keywords=keywords, crs=crs, + tileMatrixSetURI=tileMatrixSetURI) + + links = [] + service_url_link_title = f'{tileset} raster tiles for {dataset}' + service_url_link = LinkType(href=service_url, rel="item", + type_=self.mimetype, + title=service_url_link_title) + + links.append(tiling_scheme) + links.append(service_url_link) + + content.links = links + + return content.dict(exclude_none=True) diff --git a/pygeoapi/templates/collections/tiles/index.html b/pygeoapi/templates/collections/tiles/index.html index 8a789c49e..01e6d2e91 100644 --- a/pygeoapi/templates/collections/tiles/index.html +++ b/pygeoapi/templates/collections/tiles/index.html @@ -10,7 +10,7 @@