@jupytergis/base 0.10.1 → 0.12.0
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.
- package/lib/commands/BaseCommandIDs.d.ts +2 -0
- package/lib/commands/BaseCommandIDs.js +3 -0
- package/lib/commands/index.js +66 -0
- package/lib/constants.js +4 -0
- package/lib/dialogs/symbology/hooks/useGetBandInfo.d.ts +0 -6
- package/lib/dialogs/symbology/hooks/useGetBandInfo.js +2 -2
- package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +4 -4
- package/lib/dialogs/symbology/vector_layer/types/Categorized.js +1 -5
- package/lib/formbuilder/formselectors.js +5 -1
- package/lib/formbuilder/objectform/StoryEditorForm.d.ts +9 -0
- package/lib/formbuilder/objectform/StoryEditorForm.js +16 -0
- package/lib/formbuilder/objectform/components/StorySegmentReset.d.ts +8 -0
- package/lib/formbuilder/objectform/components/StorySegmentReset.js +24 -0
- package/lib/formbuilder/objectform/layer/index.d.ts +1 -0
- package/lib/formbuilder/objectform/layer/index.js +1 -0
- package/lib/formbuilder/objectform/layer/storySegmentLayerForm.d.ts +5 -0
- package/lib/formbuilder/objectform/layer/storySegmentLayerForm.js +32 -0
- package/lib/mainview/mainView.d.ts +18 -0
- package/lib/mainview/mainView.js +293 -14
- package/lib/panelview/components/layers.d.ts +2 -1
- package/lib/panelview/components/layers.js +31 -23
- package/lib/panelview/{components/filter-panel → filter-panel}/Filter.js +1 -1
- package/lib/panelview/leftpanel.js +89 -7
- package/lib/panelview/rightpanel.d.ts +2 -0
- package/lib/panelview/rightpanel.js +41 -4
- package/lib/panelview/story-maps/PreviewModeSwitch.d.ts +7 -0
- package/lib/panelview/story-maps/PreviewModeSwitch.js +13 -0
- package/lib/panelview/story-maps/StoryEditorPanel.d.ts +9 -0
- package/lib/panelview/story-maps/StoryEditorPanel.js +34 -0
- package/lib/panelview/story-maps/StoryNavBar.d.ts +10 -0
- package/lib/panelview/story-maps/StoryNavBar.js +11 -0
- package/lib/panelview/story-maps/StoryViewerPanel.d.ts +13 -0
- package/lib/panelview/story-maps/StoryViewerPanel.js +179 -0
- package/lib/panelview/story-maps/components/StoryContentSection.d.ts +6 -0
- package/lib/panelview/story-maps/components/StoryContentSection.js +10 -0
- package/lib/panelview/story-maps/components/StoryImageSection.d.ts +15 -0
- package/lib/panelview/story-maps/components/StoryImageSection.js +13 -0
- package/lib/panelview/story-maps/components/StorySubtitleSection.d.ts +11 -0
- package/lib/panelview/story-maps/components/StorySubtitleSection.js +9 -0
- package/lib/panelview/story-maps/components/StoryTitleSection.d.ts +12 -0
- package/lib/panelview/story-maps/components/StoryTitleSection.js +8 -0
- package/lib/shared/components/Calendar.d.ts +1 -1
- package/lib/shared/components/Combobox.d.ts +21 -0
- package/lib/shared/components/Combobox.js +32 -0
- package/lib/shared/components/Command.d.ts +18 -0
- package/lib/shared/components/Command.js +60 -0
- package/lib/shared/components/Dialog.d.ts +15 -0
- package/lib/shared/components/Dialog.js +62 -0
- package/lib/shared/components/Input.d.ts +3 -0
- package/lib/shared/components/Input.js +18 -0
- package/lib/shared/components/Pagination.js +3 -2
- package/lib/shared/components/RadioGroup.d.ts +5 -0
- package/lib/shared/components/RadioGroup.js +26 -0
- package/lib/shared/components/Select.d.ts +19 -0
- package/lib/shared/components/Select.js +28 -0
- package/lib/shared/components/SingleDatePicker.d.ts +11 -0
- package/lib/shared/components/SingleDatePicker.js +16 -0
- package/lib/shared/components/Switch.d.ts +4 -0
- package/lib/shared/components/Switch.js +20 -0
- package/lib/stacBrowser/components/StacPanel.d.ts +9 -1
- package/lib/stacBrowser/components/StacPanel.js +53 -9
- package/lib/stacBrowser/components/filter-extension/QueryableComboBox.d.ts +9 -0
- package/lib/stacBrowser/components/filter-extension/QueryableComboBox.js +179 -0
- package/lib/stacBrowser/components/filter-extension/QueryableRow.d.ts +16 -0
- package/lib/stacBrowser/components/filter-extension/QueryableRow.js +16 -0
- package/lib/stacBrowser/components/filter-extension/StacFilterExtensionPanel.d.ts +7 -0
- package/lib/stacBrowser/components/filter-extension/StacFilterExtensionPanel.js +49 -0
- package/lib/stacBrowser/components/filter-extension/StacQueryableFilters.d.ts +11 -0
- package/lib/stacBrowser/components/filter-extension/StacQueryableFilters.js +19 -0
- package/lib/stacBrowser/components/{StacFilterSection.d.ts → geodes/StacFilterSection.d.ts} +1 -1
- package/lib/stacBrowser/components/{StacFilterSection.js → geodes/StacFilterSection.js} +3 -3
- package/lib/stacBrowser/components/geodes/StacGeodesFilterPanel.d.ts +7 -0
- package/lib/stacBrowser/components/geodes/StacGeodesFilterPanel.js +69 -0
- package/lib/stacBrowser/components/shared/StacPanelResults.d.ts +3 -0
- package/lib/stacBrowser/components/shared/StacPanelResults.js +68 -0
- package/lib/stacBrowser/components/shared/StacSpatialExtent.d.ts +8 -0
- package/lib/stacBrowser/components/shared/StacSpatialExtent.js +10 -0
- package/lib/stacBrowser/components/shared/StacTemporalExtent.d.ts +9 -0
- package/lib/stacBrowser/components/shared/StacTemporalExtent.js +9 -0
- package/lib/stacBrowser/context/StacResultsContext.d.ts +33 -0
- package/lib/stacBrowser/context/StacResultsContext.js +269 -0
- package/lib/stacBrowser/hooks/useGeodesSearch.d.ts +24 -0
- package/lib/stacBrowser/hooks/useGeodesSearch.js +178 -0
- package/lib/stacBrowser/hooks/useStacFilterExtension.d.ts +30 -0
- package/lib/stacBrowser/hooks/useStacFilterExtension.js +262 -0
- package/lib/stacBrowser/hooks/useStacSearch.d.ts +5 -16
- package/lib/stacBrowser/hooks/useStacSearch.js +30 -184
- package/lib/stacBrowser/types/types.d.ts +86 -3
- package/lib/toolbar/widget.d.ts +15 -0
- package/lib/toolbar/widget.js +70 -0
- package/lib/tools.d.ts +0 -7
- package/lib/tools.js +56 -15
- package/package.json +8 -3
- package/style/base.css +42 -3
- package/style/leftPanel.css +18 -0
- package/style/shared/button.css +6 -5
- package/style/shared/calendar.css +7 -1
- package/style/shared/combobox.css +75 -0
- package/style/shared/command.css +178 -0
- package/style/shared/dialog.css +177 -0
- package/style/shared/input.css +59 -0
- package/style/shared/pagination.css +1 -1
- package/style/shared/popover.css +1 -0
- package/style/shared/radioGroup.css +55 -0
- package/style/shared/switch.css +63 -0
- package/style/shared/tabs.css +4 -3
- package/style/shared/toggle.css +1 -1
- package/style/stacBrowser.css +169 -16
- package/style/statusBar.css +1 -0
- package/style/storyPanel.css +185 -0
- package/style/tabPanel.css +1 -88
- package/lib/stacBrowser/components/StacPanelFilters.d.ts +0 -14
- package/lib/stacBrowser/components/StacPanelFilters.js +0 -81
- package/lib/stacBrowser/components/StacPanelResults.d.ts +0 -13
- package/lib/stacBrowser/components/StacPanelResults.js +0 -48
- /package/lib/panelview/{components/filter-panel → filter-panel}/Filter.d.ts +0 -0
- /package/lib/panelview/{components/filter-panel → filter-panel}/FilterRow.d.ts +0 -0
- /package/lib/panelview/{components/filter-panel → filter-panel}/FilterRow.js +0 -0
- /package/lib/panelview/{components/identify-panel → identify-panel}/IdentifyPanel.d.ts +0 -0
- /package/lib/panelview/{components/identify-panel → identify-panel}/IdentifyPanel.js +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
|
-
|
|
15
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
30
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
29
|
+
loadStacSearchStateFromDb();
|
|
54
30
|
}, [stateDb]);
|
|
55
|
-
// Save
|
|
31
|
+
// Save state to StateDB on change
|
|
56
32
|
useEffect(() => {
|
|
57
|
-
async function
|
|
58
|
-
await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.save(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
}, [
|
|
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 = (
|
|
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
|
-
|
|
211
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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;
|