From 4f80c86da16e59a50bd7c7de92dc6916e2b96878 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Fri, 5 May 2023 21:15:57 +0200 Subject: [PATCH 1/7] Collection Search, first draft --- src/components/ApiCapabilitiesMixin.js | 14 +- src/components/Catalogs.vue | 49 +++- src/components/Items.vue | 6 +- .../{ItemFilter.vue => SearchFilter.vue} | 16 +- src/components/StacHeader.vue | 15 +- src/locales/de/texts.json | 4 + src/locales/en/texts.json | 10 +- src/models/stac.js | 2 +- src/store/index.js | 30 ++- src/utils.js | 24 +- src/views/Catalog.vue | 6 +- src/views/Search.vue | 209 ++++++++++++------ 12 files changed, 257 insertions(+), 128 deletions(-) rename src/components/{ItemFilter.vue => SearchFilter.vue} (96%) diff --git a/src/components/ApiCapabilitiesMixin.js b/src/components/ApiCapabilitiesMixin.js index 201b9c803..369901277 100644 --- a/src/components/ApiCapabilitiesMixin.js +++ b/src/components/ApiCapabilitiesMixin.js @@ -15,7 +15,7 @@ import { mapGetters } from "vuex"; export const TYPES = { // OGC / STAC API - Features - Features: { + Items: { BasicFilters: [ 'https://api.stacspec.org/v1.*/ogcapi-features', 'http://www.opengis.net/spec/ogcapi-features-1/1.*/conf/core' @@ -39,11 +39,17 @@ export const TYPES = { }, // OGC / STAC API - Collections Collections: { - BasicFilters: [], + BasicFilters: [ + 'https://api.stacspec.org/v*/collection-search' + ], Collections: false, Items: false, - CqlFilters: [], - Sort: [] + CqlFilters: [ + 'https://api.stacspec.org/v*/collection-search#filter' + ], + Sort: [ + 'https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort' + ] } }; diff --git a/src/components/Catalogs.vue b/src/components/Catalogs.vue index 0afdc3ae6..5aa391e90 100644 --- a/src/components/Catalogs.vue +++ b/src/components/Catalogs.vue @@ -9,17 +9,23 @@ + {{ $t('catalogs.noMatches') }} - - - - {{ $t('catalogs.loadMore') }} +
+ + + + +
+ + {{ $t('catalogs.loadMore') }} \ No newline at end of file diff --git a/src/components/Items.vue b/src/components/Items.vue index 38d69a463..52cd7e305 100644 --- a/src/components/Items.vue +++ b/src/components/Items.vue @@ -12,8 +12,8 @@ {{ $t('items.filter') }} - @@ -51,7 +51,7 @@ export default { BCollapse, BIconSearch, Item, - ItemFilter: () => import('./ItemFilter.vue'), + SearchFilter: () => import('./SearchFilter.vue'), Loading, Pagination, SortButtons: () => import('./SortButtons.vue') diff --git a/src/components/ItemFilter.vue b/src/components/SearchFilter.vue similarity index 96% rename from src/components/ItemFilter.vue rename to src/components/SearchFilter.vue index 0b89412d0..030b17851 100644 --- a/src/components/ItemFilter.vue +++ b/src/components/SearchFilter.vue @@ -59,7 +59,7 @@ -
+
@@ -84,7 +84,7 @@
-
+
0; + }, sortOptions() { return [ { value: null, text: this.$t('default') }, @@ -211,7 +215,7 @@ export default { ]; }, collections() { - if (this.hasMoreCollections || !this.conformances.Collections) { + if (this.nextCollectionsLink || !this.conformances.Collections) { return []; } return this.apiCollections diff --git a/src/components/StacHeader.vue b/src/components/StacHeader.vue index 5a8792f11..a5d58e6cf 100644 --- a/src/components/StacHeader.vue +++ b/src/components/StacHeader.vue @@ -22,7 +22,7 @@ {{ $t('browse') }} - + {{ $t('search.title') }} @@ -61,7 +61,7 @@ export default { }, computed: { ...mapState(['allowSelectCatalog', 'authConfig', 'authData', 'catalogUrl', 'data', 'url', 'title']), - ...mapGetters(['root', 'parentLink', 'collectionLink', 'toBrowserPath']), + ...mapGetters(['canSearch', 'root', 'parentLink', 'collectionLink', 'toBrowserPath']), stacVersion() { return this.data?.stac_version; }, @@ -102,15 +102,10 @@ export default { if (dataLink) { return `/search${this.data.getBrowserPath()}`; } - else if (rootLink) { - if (!this.allowSelectCatalog) { - return '/search'; - } - else { - return `/search${this.root.getBrowserPath()}`; - } + else if (rootLink && this.allowSelectCatalog) { + return `/search${this.root.getBrowserPath()}`; } - return null; + return '/search'; }, containerLink() { // Check two cases where this page is the root... diff --git a/src/locales/de/texts.json b/src/locales/de/texts.json index 170e1d8ab..d9e144aef 100644 --- a/src/locales/de/texts.json +++ b/src/locales/de/texts.json @@ -224,6 +224,10 @@ "title": "Titel" }, "spatialExtent": "Räumliche Ausdehnung", + "tabs": { + "collections": "Sammlungen durchsuchen", + "items": "Elemente durchsuchen" + }, "temporalExtent": "Zeitliche Ausdehnung", "title": "Suche" }, diff --git a/src/locales/en/texts.json b/src/locales/en/texts.json index b1959b639..4dc2e9dd1 100644 --- a/src/locales/en/texts.json +++ b/src/locales/en/texts.json @@ -145,11 +145,11 @@ "title": "Metadata" }, "multiselect": { + "andMore": "and {count} more", + "deselectLabel": "Press enter to remove", "placeholder": "Select option", "selectLabel": "Press enter to select", - "selectedLabel": "Selected", - "deselectLabel": "Press enter to remove", - "andMore": "and {count} more" + "selectedLabel": "Selected" }, "open": "Open", "pagination": { @@ -224,6 +224,10 @@ "title": "Title" }, "spatialExtent": "Spatial Extent", + "tabs": { + "collections": "Search Collections", + "items": "Search Items" + }, "temporalExtent": "Temporal Extent", "title": "Search" }, diff --git a/src/models/stac.js b/src/models/stac.js index 8a14d267f..c05d20b99 100644 --- a/src/models/stac.js +++ b/src/models/stac.js @@ -145,7 +145,7 @@ class STAC { } getSearchLink() { - let links = this.getStacLinksWithRel('search'); + let links = this.getStacLinksWithRel('search').map(link => Object.assign({}, link, {href: Utils.toAbsolute(link.href, this._url)})); // Prefer POST if present let post = links.find(link => Utils.hasText(link.method) && link.method.toUpperCase() === 'POST'); return post || links[0] || null; diff --git a/src/store/index.js b/src/store/index.js index 36bf2a7a5..de720726d 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,13 +5,14 @@ import axios from "axios"; import URI from "urijs"; import i18n from '../i18n'; -import { ogcQueryables, stacBrowserSpecialHandling, stacPagination } from "../rels"; +import { ogcQueryables, stacBrowserSpecialHandling } from "../rels"; import Utils, { schemaMediaType, BrowserError } from '../utils'; import STAC from '../models/stac'; import Queryable from '../models/cql2/queryable'; import { addQueryIfNotExists, isAuthenticationError, Loading, processSTAC, proxyUrl, unproxyUrl, stacRequest } from './utils'; import { getBest } from '../locale-id'; +import { TYPES } from "../components/ApiCapabilitiesMixin"; function getStore(config, router) { // Local settings (e.g. for currently loaded STAC entity) @@ -189,6 +190,16 @@ function getStore(config, router) { return Utils.supportsExtension(state.data, schemaUri); }, + canSearch: (state, getters) => { + return getters.canSearchCollections || getters.canSearchItems; + }, + canSearchItems: (state, getters) => { + return getters.supportsConformance(TYPES.Items.BasicFilters); + }, + canSearchCollections: (state, getters) => { + return getters.supportsConformance(TYPES.Collections.BasicFilters); + }, + items: state => { if (state.apiItems.length > 0) { return state.apiItems; @@ -212,7 +223,6 @@ function getStore(config, router) { } return catalogs; }, - hasMoreCollections: state => Boolean(state.nextCollectionsLink), // hasAsset also checks whether the assets have a href and thus are not item asset definitions hasAssets: (state, getters) => Boolean(Object.values(getters.assets).find(asset => Utils.isObject(asset) && typeof asset.href === 'string')), @@ -548,12 +558,7 @@ function getStore(config, router) { } // Handle pagination links - let pageLinks = Utils.getLinksWithRels(data.links, stacPagination); - let pages = {}; - for (let pageLink of pageLinks) { - let rel = pageLink.rel === 'previous' ? 'prev' : pageLink.rel; - pages[rel] = pageLink; - } + let pages = Utils.getPaginationLinks(data.links); if (show) { state.apiItemsPagination = pages; @@ -812,14 +817,7 @@ function getStore(config, router) { baseUrl = stac.getAbsoluteUrl(); } - if (!Utils.isObject(filters)) { - filters = {}; - } - if (typeof filters.limit !== 'number') { - filters.limit = cx.state.itemsPerPage; - } - - link = Utils.addFiltersToLink(link, filters); + link = Utils.addFiltersToLink(link, filters, cx.state.itemsPerPage); let response = await stacRequest(cx, link); if (!Utils.isObject(response.data) || !Array.isArray(response.data.features)) { diff --git a/src/utils.js b/src/utils.js index 6f19c2125..fb70e906d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,6 @@ import URI from 'urijs'; import removeMd from 'remove-markdown'; +import { stacPagination } from "./rels"; export const commonFileNames = ['catalog', 'collection', 'item']; @@ -237,7 +238,17 @@ export default class Utils { }).join('/'); } - static addFiltersToLink(link, filters = {}) { + static getPaginationLinks(data) { + let pageLinks = Utils.getLinksWithRels(data.links, stacPagination); + let pages = {}; + for (let pageLink of pageLinks) { + let rel = pageLink.rel === 'previous' ? 'prev' : pageLink.rel; + pages[rel] = pageLink; + } + return pages; + } + + static addFiltersToLink(link, filters = {}, itemsPerPage = null) { let isEmpty = value => { return (value === null || (typeof value === 'number' && !Number.isFinite(value)) @@ -245,6 +256,17 @@ export default class Utils { || (typeof value === 'object' && Utils.size(value) === 0)); }; + if (!Utils.isObject(filters)) { + filters = {}; + } + else { + filters = Object.assign({}, filters); + } + + if (typeof filters.limit !== 'number' && typeof itemsPerPage === 'number') { + filters.limit = itemsPerPage; + } + if (Utils.hasText(link.method) && link.method.toUpperCase() === 'POST') { let body = Object.assign({}, link.body); diff --git a/src/views/Catalog.vue b/src/views/Catalog.vue index a8de6950e..4145d8b49 100644 --- a/src/views/Catalog.vue +++ b/src/views/Catalog.vue @@ -40,7 +40,7 @@ - + {{ $t('search.notSupported') }} - + + + + + + + + - - {{ $t('search.modifyCriteria') }} - {{ $t('search.noItemsFound') }} - + -