@jupytergis/base 0.11.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/lib/commands/BaseCommandIDs.d.ts +1 -0
  2. package/lib/commands/BaseCommandIDs.js +1 -0
  3. package/lib/commands/index.js +52 -0
  4. package/lib/constants.d.ts +3 -2
  5. package/lib/constants.js +3 -0
  6. package/lib/dialogs/symbology/hooks/useGetBandInfo.d.ts +0 -6
  7. package/lib/dialogs/symbology/hooks/useGetBandInfo.js +2 -2
  8. package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +4 -4
  9. package/lib/formbuilder/objectform/StoryEditorForm.d.ts +3 -2
  10. package/lib/formbuilder/objectform/StoryEditorForm.js +24 -1
  11. package/lib/mainview/mainView.d.ts +18 -0
  12. package/lib/mainview/mainView.js +243 -18
  13. package/lib/panelview/{components/filter-panel → filter-panel}/Filter.js +1 -1
  14. package/lib/panelview/leftpanel.js +26 -17
  15. package/lib/panelview/rightpanel.d.ts +2 -0
  16. package/lib/panelview/rightpanel.js +22 -14
  17. package/lib/panelview/{components/story-maps → story-maps}/PreviewModeSwitch.js +3 -2
  18. package/lib/panelview/story-maps/StoryEditorPanel.d.ts +9 -0
  19. package/lib/panelview/story-maps/StoryEditorPanel.js +34 -0
  20. package/lib/panelview/{components/story-maps → story-maps}/StoryNavBar.d.ts +2 -1
  21. package/lib/panelview/{components/story-maps → story-maps}/StoryNavBar.js +3 -3
  22. package/lib/panelview/story-maps/StoryViewerPanel.d.ts +13 -0
  23. package/lib/panelview/{components/story-maps → story-maps}/StoryViewerPanel.js +37 -24
  24. package/lib/panelview/story-maps/components/StoryContentSection.d.ts +6 -0
  25. package/lib/panelview/story-maps/components/StoryContentSection.js +10 -0
  26. package/lib/panelview/story-maps/components/StoryImageSection.d.ts +15 -0
  27. package/lib/panelview/story-maps/components/StoryImageSection.js +13 -0
  28. package/lib/panelview/story-maps/components/StorySubtitleSection.d.ts +11 -0
  29. package/lib/panelview/story-maps/components/StorySubtitleSection.js +9 -0
  30. package/lib/panelview/story-maps/components/StoryTitleSection.d.ts +12 -0
  31. package/lib/panelview/story-maps/components/StoryTitleSection.js +8 -0
  32. package/lib/shared/components/Combobox.d.ts +21 -0
  33. package/lib/shared/components/Combobox.js +32 -0
  34. package/lib/shared/components/Command.js +10 -10
  35. package/lib/shared/components/Input.d.ts +3 -0
  36. package/lib/shared/components/Input.js +18 -0
  37. package/lib/shared/components/Pagination.js +3 -2
  38. package/lib/shared/components/Select.d.ts +19 -0
  39. package/lib/shared/components/Select.js +28 -0
  40. package/lib/shared/components/SingleDatePicker.d.ts +11 -0
  41. package/lib/shared/components/SingleDatePicker.js +16 -0
  42. package/lib/stacBrowser/components/StacPanel.d.ts +9 -1
  43. package/lib/stacBrowser/components/StacPanel.js +53 -9
  44. package/lib/stacBrowser/components/filter-extension/QueryableComboBox.d.ts +9 -0
  45. package/lib/stacBrowser/components/filter-extension/QueryableComboBox.js +179 -0
  46. package/lib/stacBrowser/components/filter-extension/QueryableRow.d.ts +16 -0
  47. package/lib/stacBrowser/components/filter-extension/QueryableRow.js +16 -0
  48. package/lib/stacBrowser/components/filter-extension/StacFilterExtensionPanel.d.ts +7 -0
  49. package/lib/stacBrowser/components/filter-extension/StacFilterExtensionPanel.js +49 -0
  50. package/lib/stacBrowser/components/filter-extension/StacQueryableFilters.d.ts +11 -0
  51. package/lib/stacBrowser/components/filter-extension/StacQueryableFilters.js +19 -0
  52. package/lib/stacBrowser/components/{StacFilterSection.d.ts → geodes/StacFilterSection.d.ts} +1 -1
  53. package/lib/stacBrowser/components/{StacFilterSection.js → geodes/StacFilterSection.js} +3 -3
  54. package/lib/stacBrowser/components/geodes/StacGeodesFilterPanel.d.ts +7 -0
  55. package/lib/stacBrowser/components/geodes/StacGeodesFilterPanel.js +69 -0
  56. package/lib/stacBrowser/components/shared/StacPanelResults.d.ts +3 -0
  57. package/lib/stacBrowser/components/shared/StacPanelResults.js +68 -0
  58. package/lib/stacBrowser/components/shared/StacSpatialExtent.d.ts +8 -0
  59. package/lib/stacBrowser/components/shared/StacSpatialExtent.js +10 -0
  60. package/lib/stacBrowser/components/shared/StacTemporalExtent.d.ts +9 -0
  61. package/lib/stacBrowser/components/shared/StacTemporalExtent.js +9 -0
  62. package/lib/stacBrowser/context/StacResultsContext.d.ts +33 -0
  63. package/lib/stacBrowser/context/StacResultsContext.js +269 -0
  64. package/lib/stacBrowser/hooks/useGeodesSearch.d.ts +24 -0
  65. package/lib/stacBrowser/hooks/useGeodesSearch.js +178 -0
  66. package/lib/stacBrowser/hooks/useStacFilterExtension.d.ts +30 -0
  67. package/lib/stacBrowser/hooks/useStacFilterExtension.js +262 -0
  68. package/lib/stacBrowser/hooks/useStacSearch.d.ts +5 -16
  69. package/lib/stacBrowser/hooks/useStacSearch.js +30 -184
  70. package/lib/stacBrowser/types/types.d.ts +86 -3
  71. package/lib/toolbar/widget.d.ts +5 -0
  72. package/lib/toolbar/widget.js +23 -2
  73. package/lib/tools.d.ts +2 -8
  74. package/lib/tools.js +67 -18
  75. package/package.json +2 -3
  76. package/style/base.css +54 -11
  77. package/style/shared/button.css +5 -4
  78. package/style/shared/calendar.css +7 -1
  79. package/style/shared/combobox.css +75 -0
  80. package/style/shared/command.css +178 -0
  81. package/style/shared/input.css +59 -0
  82. package/style/shared/pagination.css +1 -1
  83. package/style/shared/popover.css +1 -0
  84. package/style/shared/tabs.css +1 -7
  85. package/style/shared/toggle.css +1 -1
  86. package/style/stacBrowser.css +169 -16
  87. package/style/statusBar.css +1 -0
  88. package/style/storyPanel.css +122 -3
  89. package/style/tabPanel.css +0 -86
  90. package/lib/panelview/components/story-maps/StoryEditorPanel.d.ts +0 -7
  91. package/lib/panelview/components/story-maps/StoryEditorPanel.js +0 -29
  92. package/lib/panelview/components/story-maps/StoryViewerPanel.d.ts +0 -7
  93. package/lib/stacBrowser/components/StacPanelFilters.d.ts +0 -14
  94. package/lib/stacBrowser/components/StacPanelFilters.js +0 -81
  95. package/lib/stacBrowser/components/StacPanelResults.d.ts +0 -13
  96. package/lib/stacBrowser/components/StacPanelResults.js +0 -48
  97. /package/lib/panelview/{components/filter-panel → filter-panel}/Filter.d.ts +0 -0
  98. /package/lib/panelview/{components/filter-panel → filter-panel}/FilterRow.d.ts +0 -0
  99. /package/lib/panelview/{components/filter-panel → filter-panel}/FilterRow.js +0 -0
  100. /package/lib/panelview/{components/identify-panel → identify-panel}/IdentifyPanel.d.ts +0 -0
  101. /package/lib/panelview/{components/identify-panel → identify-panel}/IdentifyPanel.js +0 -0
  102. /package/lib/panelview/{components/story-maps → story-maps}/PreviewModeSwitch.d.ts +0 -0
@@ -0,0 +1,262 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { endOfToday, startOfToday } from 'date-fns';
13
+ import { useCallback, useEffect, useState } from 'react';
14
+ import useIsFirstRender from "../../shared/hooks/useIsFirstRender";
15
+ import { useStacResultsContext } from "../context/StacResultsContext";
16
+ import { useStacSearch } from "./useStacSearch";
17
+ import { GlobalStateDbManager } from "../../store";
18
+ import { fetchWithProxies } from "../../tools";
19
+ const STAC_FILTER_EXTENSION_STATE_KEY = 'jupytergis:stac-filter-extension-state';
20
+ /**
21
+ * Hook for searching STAC catalogs that support the Filter Extension (CQL2-JSON).
22
+ * Fetches collections and queryables, and builds filter queries using the STAC Filter Extension.
23
+ */
24
+ export function useStacFilterExtension({ model, baseUrl, limit = 12, }) {
25
+ const isFirstRender = useIsFirstRender();
26
+ const { registerBuildQuery, executeQuery } = useStacResultsContext();
27
+ // Get temporal/spatial filters from useStacSearch
28
+ const { startTime, endTime, setStartTime, setEndTime, currentBBox, useWorldBBox, setUseWorldBBox, } = useStacSearch({
29
+ model,
30
+ });
31
+ const [queryableFields, setQueryableFields] = useState();
32
+ const [collections, setCollections] = useState([]);
33
+ const [selectedCollection, setSelectedCollection] = useState('');
34
+ const [selectedQueryables, setSelectedQueryables] = useState({});
35
+ const [filterOperator, setFilterOperator] = useState('and');
36
+ const stateDb = GlobalStateDbManager.getInstance().getStateDb();
37
+ // On mount, load saved filter state from StateDB (if present)
38
+ useEffect(() => {
39
+ async function loadFilterExtensionStateFromDb() {
40
+ const savedFilterState = (await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.fetch(STAC_FILTER_EXTENSION_STATE_KEY)));
41
+ if (savedFilterState) {
42
+ if (savedFilterState.selectedCollection) {
43
+ setSelectedCollection(savedFilterState.selectedCollection);
44
+ }
45
+ if (savedFilterState.queryableFilters) {
46
+ // Convert null back to undefined for inputValue
47
+ const restoredFilters = {};
48
+ Object.entries(savedFilterState.queryableFilters).forEach(([key, filter]) => {
49
+ restoredFilters[key] = {
50
+ operator: filter.operator,
51
+ inputValue: filter.inputValue === null ? undefined : filter.inputValue,
52
+ };
53
+ });
54
+ setSelectedQueryables(restoredFilters);
55
+ }
56
+ if (savedFilterState.filterOperator) {
57
+ setFilterOperator(savedFilterState.filterOperator);
58
+ }
59
+ }
60
+ }
61
+ loadFilterExtensionStateFromDb();
62
+ }, [stateDb]);
63
+ // Save filter state to StateDB on change
64
+ useEffect(() => {
65
+ async function saveFilterExtensionStateToDb() {
66
+ // Clean queryableFilters to ensure JSON serialization works
67
+ const cleanedQueryableFilters = {};
68
+ Object.entries(selectedQueryables).forEach(([key, filter]) => {
69
+ var _a;
70
+ cleanedQueryableFilters[key] = {
71
+ operator: filter.operator,
72
+ inputValue: (_a = filter.inputValue) !== null && _a !== void 0 ? _a : null,
73
+ };
74
+ });
75
+ await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.save(STAC_FILTER_EXTENSION_STATE_KEY, {
76
+ selectedCollection: selectedCollection || undefined,
77
+ queryableFilters: Object.keys(cleanedQueryableFilters).length > 0
78
+ ? cleanedQueryableFilters
79
+ : undefined,
80
+ filterOperator,
81
+ }));
82
+ }
83
+ saveFilterExtensionStateToDb();
84
+ }, [selectedCollection, selectedQueryables, filterOperator, stateDb]);
85
+ // Reset all state when URL changes
86
+ useEffect(() => {
87
+ setQueryableFields(undefined);
88
+ setCollections([]);
89
+ setSelectedCollection('');
90
+ setSelectedQueryables({});
91
+ setFilterOperator('and');
92
+ }, [baseUrl]);
93
+ // for collections
94
+ useEffect(() => {
95
+ if (!model) {
96
+ return;
97
+ }
98
+ const fetchCollections = async () => {
99
+ if (!baseUrl) {
100
+ return;
101
+ }
102
+ const collectionsUrl = baseUrl.endsWith('/')
103
+ ? `${baseUrl}collections`
104
+ : `${baseUrl}/collections`;
105
+ const data = await fetchWithProxies(collectionsUrl, model, async (response) => await response.json(), undefined);
106
+ const collections = data.collections
107
+ .map((collection) => {
108
+ var _a;
109
+ return ({
110
+ title: (_a = collection.title) !== null && _a !== void 0 ? _a : collection.id,
111
+ id: collection.id,
112
+ });
113
+ })
114
+ .sort((a, b) => {
115
+ var _a, _b, _c, _d;
116
+ const titleA = (_b = (_a = a.title) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : '';
117
+ const titleB = (_d = (_c = b.title) === null || _c === void 0 ? void 0 : _c.toLowerCase()) !== null && _d !== void 0 ? _d : '';
118
+ return titleA.localeCompare(titleB);
119
+ });
120
+ setCollections(collections);
121
+ // Set first collection as default if one isn't loaded
122
+ if (collections.length > 0 && !(selectedCollection === '')) {
123
+ setSelectedCollection(collections[0].id);
124
+ }
125
+ };
126
+ fetchCollections();
127
+ }, [model, baseUrl]);
128
+ // for queryables
129
+ // ! TODO - support multiple collection selections
130
+ useEffect(() => {
131
+ if (!model) {
132
+ return;
133
+ }
134
+ const fetchQueryables = async () => {
135
+ if (!baseUrl) {
136
+ return;
137
+ }
138
+ const queryablesUrl = baseUrl.endsWith('/')
139
+ ? `${baseUrl}queryables`
140
+ : `${baseUrl}/queryables`;
141
+ const data = await fetchWithProxies(queryablesUrl, model, async (response) => await response.json(), undefined);
142
+ setQueryableFields(Object.entries(data.properties));
143
+ };
144
+ fetchQueryables();
145
+ }, [model, baseUrl]);
146
+ const updateSelectedQueryables = useCallback((qKey, filter) => {
147
+ setSelectedQueryables(prev => {
148
+ // If filter is null, remove the key entirely
149
+ if (filter === null) {
150
+ const _a = prev, _b = qKey, _ = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
151
+ return rest;
152
+ }
153
+ // If inputValue is undefined but filter exists, keep it (user might be entering value)
154
+ // Only remove if explicitly set to null
155
+ return Object.assign(Object.assign({}, prev), { [qKey]: filter });
156
+ });
157
+ }, []);
158
+ const buildQuery = useCallback(() => {
159
+ const st = startTime
160
+ ? startTime.toISOString()
161
+ : startOfToday().toISOString();
162
+ const et = endTime ? endTime.toISOString() : endOfToday().toISOString();
163
+ // Build filter object from selectedQueryables
164
+ const filterConditions = Object.entries(selectedQueryables)
165
+ .filter(([, filter]) => filter.inputValue !== undefined)
166
+ .map(([property, filter]) => {
167
+ var _a, _b;
168
+ // Check if this property is a datetime type
169
+ const queryableField = queryableFields === null || queryableFields === void 0 ? void 0 : queryableFields.find(([key]) => key === property);
170
+ const isDateTime = queryableField &&
171
+ ((_a = queryableField[1]) === null || _a === void 0 ? void 0 : _a.type) === 'string' &&
172
+ ((_b = queryableField[1]) === null || _b === void 0 ? void 0 : _b.format) === 'date-time';
173
+ // For datetime values, wrap in timestamp object; otherwise use value directly
174
+ const value = isDateTime
175
+ ? { timestamp: filter.inputValue }
176
+ : filter.inputValue;
177
+ const condition = {
178
+ op: filter.operator,
179
+ args: [{ property }, value],
180
+ };
181
+ return condition;
182
+ });
183
+ const body = {
184
+ bbox: currentBBox,
185
+ collections: [selectedCollection],
186
+ datetime: `${st}/${et}`,
187
+ limit,
188
+ 'filter-lang': 'cql2-json',
189
+ };
190
+ // Only add filter if there are any conditions
191
+ if (filterConditions.length > 0) {
192
+ body.filter = {
193
+ op: filterOperator,
194
+ args: filterConditions,
195
+ };
196
+ }
197
+ return body;
198
+ }, [
199
+ startTime,
200
+ endTime,
201
+ currentBBox,
202
+ selectedCollection,
203
+ limit,
204
+ selectedQueryables,
205
+ filterOperator,
206
+ queryableFields,
207
+ ]);
208
+ // Register buildQuery with context
209
+ useEffect(() => {
210
+ registerBuildQuery(() => buildQuery());
211
+ }, [registerBuildQuery, buildQuery, baseUrl]);
212
+ const handleSubmit = useCallback(async () => {
213
+ if (!model) {
214
+ return;
215
+ }
216
+ // Build query body and execute query
217
+ const queryBody = buildQuery();
218
+ const searchUrl = baseUrl.endsWith('/')
219
+ ? `${baseUrl}search`
220
+ : `${baseUrl}/search`;
221
+ await executeQuery(queryBody, searchUrl);
222
+ }, [model, executeQuery, buildQuery, baseUrl]);
223
+ // Handle search when filters change
224
+ useEffect(() => {
225
+ if (model && !isFirstRender && selectedCollection !== '') {
226
+ const queryBody = buildQuery();
227
+ const searchUrl = baseUrl.endsWith('/')
228
+ ? `${baseUrl}search`
229
+ : `${baseUrl}/search`;
230
+ executeQuery(queryBody, searchUrl);
231
+ }
232
+ }, [
233
+ model,
234
+ isFirstRender,
235
+ selectedCollection,
236
+ selectedQueryables,
237
+ filterOperator,
238
+ startTime,
239
+ endTime,
240
+ currentBBox,
241
+ buildQuery,
242
+ executeQuery,
243
+ baseUrl,
244
+ ]);
245
+ return {
246
+ queryableFields,
247
+ collections,
248
+ selectedCollection,
249
+ setSelectedCollection,
250
+ handleSubmit,
251
+ startTime,
252
+ endTime,
253
+ setStartTime,
254
+ setEndTime,
255
+ useWorldBBox,
256
+ setUseWorldBBox,
257
+ selectedQueryables,
258
+ updateSelectedQueryables,
259
+ filterOperator,
260
+ setFilterOperator,
261
+ };
262
+ }
@@ -1,30 +1,19 @@
1
1
  import { IJupyterGISModel } from '@jupytergis/schema';
2
- import { IStacItem, StacFilterState, StacFilterSetters } from "../types/types";
3
2
  interface IUseStacSearchProps {
4
3
  model: IJupyterGISModel | undefined;
5
4
  }
6
5
  interface IUseStacSearchReturn {
7
- filterState: StacFilterState;
8
- filterSetters: StacFilterSetters;
9
- results: IStacItem[];
10
6
  startTime: Date | undefined;
11
7
  setStartTime: (date: Date | undefined) => void;
12
8
  endTime: Date | undefined;
13
9
  setEndTime: (date: Date | undefined) => void;
14
- totalPages: number;
15
- currentPage: number;
16
- totalResults: number;
17
- handlePaginationClick: (page: number) => Promise<void>;
18
- handleResultClick: (id: string) => Promise<void>;
19
- formatResult: (item: IStacItem) => string;
20
- isLoading: boolean;
10
+ currentBBox: [number, number, number, number];
11
+ setCurrentBBox: (bbox: [number, number, number, number]) => void;
21
12
  useWorldBBox: boolean;
22
13
  setUseWorldBBox: (val: boolean) => void;
23
14
  }
24
15
  /**
25
- * Custom hook for managing STAC search functionality
26
- * @param props - Configuration object containing datasets, platforms, products, and model
27
- * @returns Object containing state and handlers for STAC search
16
+ * Base hook for managing STAC search - handles temporal/spatial filters
28
17
  */
29
- declare function useStacSearch({ model }: IUseStacSearchProps): IUseStacSearchReturn;
30
- export default useStacSearch;
18
+ export declare function useStacSearch({ model, }: IUseStacSearchProps): IUseStacSearchReturn;
19
+ export {};
@@ -1,79 +1,47 @@
1
- var _a;
2
- import { UUID } from '@lumino/coreutils';
3
- import { startOfYesterday } from 'date-fns';
4
1
  import { useEffect, useState } from 'react';
5
- import useIsFirstRender from "../../shared/hooks/useIsFirstRender";
6
- import { products } from "../constants";
7
2
  import { GlobalStateDbManager } from "../../store";
8
- import { fetchWithProxies } from "../../tools";
9
- const API_URL = 'https://geodes-portal.cnes.fr/api/stac/search';
10
- const XSRF_TOKEN = (_a = document.cookie.match(/_xsrf=([^;]+)/)) === null || _a === void 0 ? void 0 : _a[1];
11
- const STAC_FILTERS_KEY = 'jupytergis:stac-filters';
3
+ const STAC_SEARCH_STATE_KEY = 'jupytergis:stac-search-state';
12
4
  /**
13
- * Custom hook for managing STAC search functionality
14
- * @param props - Configuration object containing datasets, platforms, products, and model
15
- * @returns Object containing state and handlers for STAC search
5
+ * Base hook for managing STAC search - handles temporal/spatial filters
16
6
  */
17
- function useStacSearch({ model }) {
18
- const isFirstRender = useIsFirstRender();
19
- const stateDb = GlobalStateDbManager.getInstance().getStateDb();
20
- const [results, setResults] = useState([]);
21
- const [isLoading, setIsLoading] = useState(false);
22
- const [totalPages, setTotalPages] = useState(1);
23
- const [currentPage, setCurrentPage] = useState(1);
24
- const [totalResults, setTotalResults] = useState(0);
7
+ export function useStacSearch({ model, }) {
25
8
  const [startTime, setStartTime] = useState(undefined);
26
9
  const [endTime, setEndTime] = useState(undefined);
27
10
  const [currentBBox, setCurrentBBox] = useState([-180, -90, 180, 90]);
28
11
  const [useWorldBBox, setUseWorldBBox] = useState(false);
29
- const [filterState, setFilterState] = useState({
30
- collections: new Set(),
31
- datasets: new Set(),
32
- platforms: new Set(),
33
- products: new Set(),
34
- });
35
- const filterSetters = {
36
- collections: val => setFilterState(s => (Object.assign(Object.assign({}, s), { collections: new Set(val) }))),
37
- datasets: val => setFilterState(s => (Object.assign(Object.assign({}, s), { datasets: new Set(val) }))),
38
- platforms: val => setFilterState(s => (Object.assign(Object.assign({}, s), { platforms: new Set(val) }))),
39
- products: val => setFilterState(s => (Object.assign(Object.assign({}, s), { products: new Set(val) }))),
40
- };
41
- // On mount, fetch filterState and times from StateDB (if present)
12
+ const stateDb = GlobalStateDbManager.getInstance().getStateDb();
13
+ // Load saved state from StateDB on mount
42
14
  useEffect(() => {
43
- async function loadStacStateFromDb() {
44
- var _a, _b, _c, _d;
45
- const savedFilterState = (await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.fetch(STAC_FILTERS_KEY)));
46
- setFilterState({
47
- collections: new Set((_a = savedFilterState === null || savedFilterState === void 0 ? void 0 : savedFilterState.collections) !== null && _a !== void 0 ? _a : []),
48
- datasets: new Set((_b = savedFilterState === null || savedFilterState === void 0 ? void 0 : savedFilterState.datasets) !== null && _b !== void 0 ? _b : []),
49
- platforms: new Set((_c = savedFilterState === null || savedFilterState === void 0 ? void 0 : savedFilterState.platforms) !== null && _c !== void 0 ? _c : []),
50
- products: new Set((_d = savedFilterState === null || savedFilterState === void 0 ? void 0 : savedFilterState.products) !== null && _d !== void 0 ? _d : []),
51
- });
15
+ async function loadStacSearchStateFromDb() {
16
+ const savedState = (await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.fetch(STAC_SEARCH_STATE_KEY)));
17
+ if (savedState) {
18
+ if (savedState.startTime) {
19
+ setStartTime(new Date(savedState.startTime));
20
+ }
21
+ if (savedState.endTime) {
22
+ setEndTime(new Date(savedState.endTime));
23
+ }
24
+ if (savedState.useWorldBBox !== undefined) {
25
+ setUseWorldBBox(savedState.useWorldBBox);
26
+ }
27
+ }
52
28
  }
53
- loadStacStateFromDb();
29
+ loadStacSearchStateFromDb();
54
30
  }, [stateDb]);
55
- // Save filterState to StateDB on change
31
+ // Save state to StateDB on change
56
32
  useEffect(() => {
57
- async function saveStacFilterStateToDb() {
58
- await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.save(STAC_FILTERS_KEY, {
59
- collections: Array.from(filterState.collections),
60
- datasets: Array.from(filterState.datasets),
61
- platforms: Array.from(filterState.platforms),
62
- products: Array.from(filterState.products),
33
+ async function saveStacSearchStateToDb() {
34
+ await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.save(STAC_SEARCH_STATE_KEY, {
35
+ startTime: startTime === null || startTime === void 0 ? void 0 : startTime.toISOString(),
36
+ endTime: endTime === null || endTime === void 0 ? void 0 : endTime.toISOString(),
37
+ useWorldBBox,
63
38
  }));
64
39
  }
65
- saveStacFilterStateToDb();
66
- }, [filterState, stateDb]);
67
- // Handle search when filters change
68
- useEffect(() => {
69
- if (model && !isFirstRender && filterState.datasets.size > 0) {
70
- setCurrentPage(1);
71
- fetchResults(1);
72
- }
73
- }, [filterState, startTime, endTime, currentBBox]);
40
+ saveStacSearchStateToDb();
41
+ }, [startTime, endTime, useWorldBBox, stateDb]);
74
42
  // Listen for model updates to get current bounding box
75
43
  useEffect(() => {
76
- const listenToModel = (sender, bBoxIn4326) => {
44
+ const listenToModel = (_sender, bBoxIn4326) => {
77
45
  if (useWorldBBox) {
78
46
  setCurrentBBox([-180, -90, 180, 90]);
79
47
  }
@@ -86,136 +54,14 @@ function useStacSearch({ model }) {
86
54
  model === null || model === void 0 ? void 0 : model.updateBboxSignal.disconnect(listenToModel);
87
55
  };
88
56
  }, [model, useWorldBBox]);
89
- const fetchResults = async (page = 1) => {
90
- const processingLevel = new Set();
91
- const productType = new Set();
92
- filterState.products.forEach(productCode => {
93
- products
94
- .filter(product => product.productCode === productCode)
95
- .forEach(product => {
96
- if (product.processingLevel) {
97
- processingLevel.add(product.processingLevel);
98
- }
99
- if (product.productType) {
100
- product.productType.forEach(type => productType.add(type));
101
- }
102
- });
103
- });
104
- const body = {
105
- bbox: currentBBox,
106
- limit: 12,
107
- page,
108
- query: Object.assign(Object.assign(Object.assign(Object.assign({ latest: { eq: true }, dataset: { in: Array.from(filterState.datasets) }, end_datetime: {
109
- gte: startTime
110
- ? startTime.toISOString()
111
- : startOfYesterday().toISOString(),
112
- } }, (endTime && {
113
- start_datetime: { lte: endTime.toISOString() },
114
- })), (filterState.platforms.size > 0 && {
115
- platform: { in: Array.from(filterState.platforms) },
116
- })), (processingLevel.size > 0 && {
117
- 'processing:level': { in: Array.from(processingLevel) },
118
- })), (productType.size > 0 && {
119
- 'product:type': { in: Array.from(productType) },
120
- })),
121
- sortBy: [{ direction: 'desc', field: 'start_datetime' }],
122
- };
123
- try {
124
- setIsLoading(true);
125
- const options = {
126
- method: 'POST',
127
- headers: {
128
- 'Content-Type': 'application/json',
129
- 'X-XSRFToken': XSRF_TOKEN,
130
- credentials: 'include',
131
- },
132
- body: JSON.stringify(body),
133
- };
134
- if (!model) {
135
- return;
136
- }
137
- const data = (await fetchWithProxies(API_URL, model, async (response) => await response.json(),
138
- //@ts-expect-error Jupyter requires X-XSRFToken header
139
- options, 'internal'));
140
- if (!data) {
141
- console.debug('STAC search failed -- no results found');
142
- setResults([]);
143
- setTotalPages(1);
144
- setTotalResults(0);
145
- return;
146
- }
147
- setResults(data.features);
148
- const pages = data.context.matched / data.context.limit;
149
- setTotalPages(Math.ceil(pages));
150
- setTotalResults(data.context.matched);
151
- }
152
- catch (error) {
153
- console.error('STAC search failed -- error fetching data:', error);
154
- setResults([]);
155
- setTotalPages(1);
156
- setTotalResults(0);
157
- }
158
- finally {
159
- setIsLoading(false);
160
- }
161
- };
162
- /**
163
- * Handles clicking on a result item
164
- * @param id - ID of the clicked result
165
- */
166
- const handleResultClick = async (id) => {
167
- var _a;
168
- if (!results) {
169
- return;
170
- }
171
- const layerId = UUID.uuid4();
172
- const stacData = results.find(item => item.id === id);
173
- if (!stacData) {
174
- console.error('Result not found:', id);
175
- return;
176
- }
177
- const layerModel = {
178
- type: 'StacLayer',
179
- parameters: { data: stacData },
180
- visible: true,
181
- name: (_a = stacData.properties.title) !== null && _a !== void 0 ? _a : stacData.id,
182
- };
183
- model && model.addLayer(layerId, layerModel);
184
- };
185
- /**
186
- * Handles pagination clicks
187
- * @param page - Page number to navigate to
188
- */
189
- const handlePaginationClick = async (page) => {
190
- setCurrentPage(page);
191
- model && fetchResults(page);
192
- };
193
- /**
194
- * Formats a result item for display
195
- * @param item - STAC item to format
196
- * @returns Formatted string representation of the item
197
- */
198
- const formatResult = (item) => {
199
- var _a;
200
- return (_a = item.properties.title) !== null && _a !== void 0 ? _a : item.id;
201
- };
202
57
  return {
203
- filterState,
204
- filterSetters,
205
- results,
206
58
  startTime,
207
59
  setStartTime,
208
60
  endTime,
209
61
  setEndTime,
210
- totalPages,
211
- currentPage,
212
- totalResults,
213
- handlePaginationClick,
214
- handleResultClick,
215
- formatResult,
216
- isLoading,
62
+ currentBBox,
63
+ setCurrentBBox,
217
64
  useWorldBBox,
218
65
  setUseWorldBBox,
219
66
  };
220
67
  }
221
- export default useStacSearch;
@@ -1,3 +1,7 @@
1
+ export interface IStacCollectionsReturn {
2
+ collections: IStacCollection[];
3
+ links: IStacLink[];
4
+ }
1
5
  export interface IStacCollection {
2
6
  type: 'Collection';
3
7
  stac_version: string;
@@ -22,13 +26,13 @@ export interface IStacRange {
22
26
  maximum: number | string;
23
27
  }
24
28
  export interface IStacExtent {
25
- spatial: IStacSpacialExtent;
29
+ spatial: IStacSpatialExtent;
26
30
  temporal: IStacTemporalExtent;
27
31
  }
28
32
  export interface IStacTemporalExtent {
29
33
  interval: Array<[string | null, string | null]>;
30
34
  }
31
- export interface IStacSpacialExtent {
35
+ export interface IStacSpatialExtent {
32
36
  bbox: number[][];
33
37
  }
34
38
  export interface IStacProvider {
@@ -43,6 +47,14 @@ export interface IStacLink {
43
47
  type?: string;
44
48
  title?: string;
45
49
  }
50
+ /**
51
+ * Extended STAC link with optional method and body for pagination.
52
+ * Used for pagination links that may include HTTP method and request body.
53
+ */
54
+ export interface IStacPaginationLink extends IStacLink {
55
+ method?: string;
56
+ body?: IStacQueryBodyUnion;
57
+ }
46
58
  export interface IStacAsset {
47
59
  href: string;
48
60
  title?: string;
@@ -91,7 +103,72 @@ export interface IStacSearchResult {
91
103
  stac_version: string;
92
104
  type: 'FeatureCollection';
93
105
  }
94
- export interface IStacQueryBody {
106
+ /**
107
+ * Comparison operators for STAC filter conditions.
108
+ */
109
+ export type Operator = '=' | '!=' | '<' | '<=' | '>' | '>=';
110
+ /**
111
+ * CQL2-JSON filter condition structure for STAC Filter Extension queries.
112
+ * For datetime values, the second argument is wrapped in a timestamp object.
113
+ */
114
+ export interface IStacFilterCondition {
115
+ op: Operator;
116
+ args: [{
117
+ property: string;
118
+ }, string | number | {
119
+ timestamp: string;
120
+ }];
121
+ }
122
+ export type FilterOperator = 'and' | 'or';
123
+ /**
124
+ * CQL2-JSON filter structure for STAC Filter Extension queries.
125
+ */
126
+ export interface IStacCql2Filter {
127
+ op: FilterOperator;
128
+ args: IStacFilterCondition[];
129
+ }
130
+ export interface IQueryableFilter {
131
+ operator: Operator;
132
+ inputValue: string | number | undefined;
133
+ }
134
+ export type UpdateSelectedQueryables = (qKey: string, filter: IQueryableFilter | null) => void;
135
+ /**
136
+ * JSON Schema structure for STAC queryables.
137
+ * Based on the STAC Filter Extension queryables endpoint response.
138
+ * Different endpoints may have varying structures, so most fields are optional
139
+ * and we allow additional properties to accommodate variations.
140
+ */
141
+ export interface IStacQueryableSchema {
142
+ type?: 'string' | 'number' | 'integer';
143
+ title?: string;
144
+ description?: string;
145
+ format?: string;
146
+ enum?: (string | number)[];
147
+ pattern?: string;
148
+ minLength?: number;
149
+ maximum?: number;
150
+ minimum?: number;
151
+ $ref?: string;
152
+ [key: string]: unknown;
153
+ }
154
+ /**
155
+ * Type for queryables array: array of [propertyName, schema] tuples.
156
+ */
157
+ export type IStacQueryables = [string, IStacQueryableSchema][];
158
+ /**
159
+ * Query body for STAC catalogs that support the Filter Extension (CQL2-JSON).
160
+ * Used for generic STAC searches with filter extension support.
161
+ */
162
+ export interface IStacFilterExtensionQueryBody {
163
+ bbox: [number, number, number, number];
164
+ collections: string[];
165
+ datetime: string;
166
+ limit: number;
167
+ 'filter-lang': 'cql2-json';
168
+ filter?: IStacCql2Filter;
169
+ token?: string;
170
+ }
171
+ export interface IStacGeodesQueryBody {
95
172
  bbox: [number, number, number, number];
96
173
  limit?: number;
97
174
  page?: number;
@@ -116,9 +193,15 @@ export interface IStacQueryBody {
116
193
  }
117
194
  ];
118
195
  }
196
+ /**
197
+ * Union type for all STAC query body formats.
198
+ * Used in contexts that need to accept multiple query formats.
199
+ */
200
+ export type IStacQueryBodyUnion = IStacGeodesQueryBody | IStacFilterExtensionQueryBody;
119
201
  export type StacFilterKey = 'collections' | 'datasets' | 'platforms' | 'products';
120
202
  export type StacFilterState = Record<StacFilterKey, Set<string>>;
121
203
  export type StacFilterStateStateDb = {
122
204
  [K in keyof StacFilterState]: string[];
123
205
  };
124
206
  export type StacFilterSetters = Record<StacFilterKey, (val: Set<string>) => void>;
207
+ export type SetResultsFunction = (results: IStacItem[], totalResults: string, totalPages: number) => void;