@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,269 @@
|
|
|
1
|
+
import { UUID } from '@lumino/coreutils';
|
|
2
|
+
import React, { createContext, useContext, useState, useCallback, useRef, useEffect, } from 'react';
|
|
3
|
+
import { GlobalStateDbManager } from "../../store";
|
|
4
|
+
import { fetchWithProxies } from "../../tools";
|
|
5
|
+
const StacResultsContext = createContext(undefined);
|
|
6
|
+
const STAC_SELECTED_URL_STATE_KEY = 'jupytergis:stac-selected-url';
|
|
7
|
+
export function StacResultsProvider({ children, model, }) {
|
|
8
|
+
const [results, setResultsState] = useState([]);
|
|
9
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
10
|
+
const [totalResults, setTotalResults] = useState('0');
|
|
11
|
+
const [totalPages, setTotalPages] = useState(0);
|
|
12
|
+
const [paginationLinks, setPaginationLinksState] = useState([]);
|
|
13
|
+
const [selectedUrl, setSelectedUrlState] = useState('');
|
|
14
|
+
const [currentPage, setCurrentPageState] = useState(1);
|
|
15
|
+
const currentPageRef = useRef(1);
|
|
16
|
+
// Store hook-specific functions in refs (these are set by the hooks)
|
|
17
|
+
const addToMapRef = useRef();
|
|
18
|
+
const handlePaginationClickRef = useRef();
|
|
19
|
+
const buildQueryRef = useRef();
|
|
20
|
+
const stateDb = GlobalStateDbManager.getInstance().getStateDb();
|
|
21
|
+
// Load saved selected URL from StateDB on mount
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
async function loadStacSelectedUrlFromDb() {
|
|
24
|
+
const savedState = (await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.fetch(STAC_SELECTED_URL_STATE_KEY)));
|
|
25
|
+
if (savedState === null || savedState === void 0 ? void 0 : savedState.selectedUrl) {
|
|
26
|
+
setSelectedUrlState(savedState.selectedUrl);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
loadStacSelectedUrlFromDb();
|
|
30
|
+
}, [stateDb]);
|
|
31
|
+
// Save selected URL to StateDB on change
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
async function saveStacSelectedUrlToDb() {
|
|
34
|
+
await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.save(STAC_SELECTED_URL_STATE_KEY, {
|
|
35
|
+
selectedUrl,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
saveStacSelectedUrlToDb();
|
|
39
|
+
}, [selectedUrl, stateDb]);
|
|
40
|
+
// Keep ref in sync with state
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
currentPageRef.current = currentPage;
|
|
43
|
+
}, [currentPage]);
|
|
44
|
+
const setResults = useCallback((newResults, newTotalResults, newTotalPages) => {
|
|
45
|
+
setResultsState(newResults);
|
|
46
|
+
setTotalResults(newTotalResults);
|
|
47
|
+
setTotalPages(newTotalPages);
|
|
48
|
+
}, []);
|
|
49
|
+
const setPaginationLinks = useCallback((links) => {
|
|
50
|
+
setPaginationLinksState(links);
|
|
51
|
+
}, []);
|
|
52
|
+
const setSelectedUrl = useCallback((url) => {
|
|
53
|
+
setSelectedUrlState(url);
|
|
54
|
+
// Clear all registered handlers when provider changes to prevent stale handlers
|
|
55
|
+
handlePaginationClickRef.current = undefined;
|
|
56
|
+
addToMapRef.current = undefined;
|
|
57
|
+
buildQueryRef.current = undefined;
|
|
58
|
+
// Reset all state
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
setCurrentPageState(1);
|
|
61
|
+
currentPageRef.current = 1;
|
|
62
|
+
setResultsState([]);
|
|
63
|
+
setPaginationLinksState([]);
|
|
64
|
+
setTotalResults('0');
|
|
65
|
+
setTotalPages(0);
|
|
66
|
+
}, []);
|
|
67
|
+
const setCurrentPage = useCallback((page) => {
|
|
68
|
+
setCurrentPageState(page);
|
|
69
|
+
}, []);
|
|
70
|
+
const registerAddToMap = useCallback((addFn) => {
|
|
71
|
+
addToMapRef.current = addFn;
|
|
72
|
+
}, []);
|
|
73
|
+
const registerHandlePaginationClick = useCallback((handleFn) => {
|
|
74
|
+
handlePaginationClickRef.current = handleFn;
|
|
75
|
+
}, []);
|
|
76
|
+
const registerBuildQuery = useCallback((buildQueryFn) => {
|
|
77
|
+
buildQueryRef.current = buildQueryFn;
|
|
78
|
+
}, []);
|
|
79
|
+
// Helper to get search URL from base URL
|
|
80
|
+
const getSearchUrl = (baseUrl) => {
|
|
81
|
+
return baseUrl.endsWith('/') ? `${baseUrl}search` : `${baseUrl}/search`;
|
|
82
|
+
};
|
|
83
|
+
// Execute query using provided body
|
|
84
|
+
const executeQuery = useCallback(async (body, apiUrl, method) => {
|
|
85
|
+
var _a, _b, _c;
|
|
86
|
+
if (!model) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const XSRF_TOKEN = (_a = document.cookie.match(/_xsrf=([^;]+)/)) === null || _a === void 0 ? void 0 : _a[1];
|
|
90
|
+
const queryBody = body;
|
|
91
|
+
const urlToUse = apiUrl || getSearchUrl(selectedUrl);
|
|
92
|
+
const httpMethod = (method || 'POST').toUpperCase();
|
|
93
|
+
const options = {
|
|
94
|
+
method: httpMethod,
|
|
95
|
+
headers: {
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
'X-XSRFToken': XSRF_TOKEN,
|
|
98
|
+
credentials: 'include',
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify(queryBody),
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
// Update context with loading state
|
|
104
|
+
setIsLoading(true);
|
|
105
|
+
const data = (await fetchWithProxies(urlToUse, model, async (response) => await response.json(),
|
|
106
|
+
//@ts-expect-error Jupyter requires X-XSRFToken header
|
|
107
|
+
options));
|
|
108
|
+
if (!data) {
|
|
109
|
+
setResults([], '0', 0);
|
|
110
|
+
setIsLoading(false);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Filter assets to only include items with 'overview' or 'thumbnail' roles
|
|
114
|
+
if (data.features && data.features.length > 0) {
|
|
115
|
+
data.features.forEach((feature) => {
|
|
116
|
+
if (feature.assets) {
|
|
117
|
+
const originalAssets = feature.assets;
|
|
118
|
+
const filteredAssets = {};
|
|
119
|
+
for (const [key, asset] of Object.entries(originalAssets)) {
|
|
120
|
+
if (asset &&
|
|
121
|
+
typeof asset === 'object' &&
|
|
122
|
+
'roles' in asset &&
|
|
123
|
+
Array.isArray(asset.roles)) {
|
|
124
|
+
const roles = asset.roles;
|
|
125
|
+
if (roles.includes('thumbnail') ||
|
|
126
|
+
roles.includes('overview')) {
|
|
127
|
+
filteredAssets[key] = asset;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
feature.assets = filteredAssets;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Sort features by id before setting results
|
|
136
|
+
const sortedFeatures = [...data.features].sort((a, b) => a.id.localeCompare(b.id));
|
|
137
|
+
// Calculate total results from context if available
|
|
138
|
+
let totalResultsFromQuery;
|
|
139
|
+
let totalPagesFromQuery = 0;
|
|
140
|
+
if (data.context) {
|
|
141
|
+
totalResultsFromQuery = String(data.context.matched);
|
|
142
|
+
totalPagesFromQuery = Math.ceil(data.context.matched / data.context.limit);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// If no context, check pagination links to determine if there are more results
|
|
146
|
+
const hasNext = (_c = (_b = data.links) === null || _b === void 0 ? void 0 : _b.some(link => link.rel === 'next')) !== null && _c !== void 0 ? _c : false;
|
|
147
|
+
const featuresCount = data.features.length;
|
|
148
|
+
// If there are more results, show the number of results and a +
|
|
149
|
+
totalResultsFromQuery = hasNext
|
|
150
|
+
? `${featuresCount}+`
|
|
151
|
+
: String(featuresCount);
|
|
152
|
+
if (sortedFeatures.length > 0) {
|
|
153
|
+
// If results found but no context, use single page
|
|
154
|
+
totalPagesFromQuery = 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Update context with results
|
|
158
|
+
setResults(sortedFeatures, totalResultsFromQuery, totalPagesFromQuery);
|
|
159
|
+
// Store pagination links
|
|
160
|
+
if (data.links) {
|
|
161
|
+
setPaginationLinks(data.links);
|
|
162
|
+
}
|
|
163
|
+
setIsLoading(false);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
setResults([], '0', 0);
|
|
167
|
+
setIsLoading(false);
|
|
168
|
+
}
|
|
169
|
+
}, [model, selectedUrl, setResults, setPaginationLinks]);
|
|
170
|
+
// Wrapper function that takes a page number, builds query, and executes it
|
|
171
|
+
const executeQueryWithPage = useCallback(async (pageNumber) => {
|
|
172
|
+
if (!model || !buildQueryRef.current) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Build query body
|
|
176
|
+
let queryBody = buildQueryRef.current();
|
|
177
|
+
// Inject page number into the query
|
|
178
|
+
queryBody = Object.assign(Object.assign({}, queryBody), { page: pageNumber });
|
|
179
|
+
// Execute query with the modified body
|
|
180
|
+
await executeQuery(queryBody);
|
|
181
|
+
}, [model, executeQuery]);
|
|
182
|
+
// Use registered handler if provided, otherwise use context-created one
|
|
183
|
+
const handlePaginationClick = useCallback(async (dir) => {
|
|
184
|
+
if (handlePaginationClickRef.current) {
|
|
185
|
+
await handlePaginationClickRef.current(dir);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const currentLinks = paginationLinks;
|
|
189
|
+
// Find the pagination link by rel
|
|
190
|
+
const link = currentLinks.find(l => {
|
|
191
|
+
if (dir === 'next') {
|
|
192
|
+
return l.rel === 'next';
|
|
193
|
+
}
|
|
194
|
+
// For 'previous', accept both 'previous' and 'prev'
|
|
195
|
+
return ['prev', 'previous'].includes(l.rel);
|
|
196
|
+
});
|
|
197
|
+
// ! this is nice, if no body then link href should have search params - update eventually
|
|
198
|
+
// ! this actually doesny make sense
|
|
199
|
+
if (link && link.body) {
|
|
200
|
+
// Use executeQuery with the link's body, href, and method
|
|
201
|
+
await executeQuery(link.body, link.href, link.method);
|
|
202
|
+
}
|
|
203
|
+
}, [model, paginationLinks, executeQuery]);
|
|
204
|
+
const defaultAddToMap = useCallback((stacData) => {
|
|
205
|
+
var _a, _b;
|
|
206
|
+
if (!model) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const layerId = UUID.uuid4();
|
|
210
|
+
const layerModel = {
|
|
211
|
+
type: 'StacLayer',
|
|
212
|
+
parameters: { data: stacData },
|
|
213
|
+
visible: true,
|
|
214
|
+
name: (_b = (_a = stacData.properties) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : stacData.id,
|
|
215
|
+
};
|
|
216
|
+
model.addLayer(layerId, layerModel);
|
|
217
|
+
model.centerOnPosition(layerId);
|
|
218
|
+
}, [model]);
|
|
219
|
+
const handleResultClick = useCallback(async (id) => {
|
|
220
|
+
if (!model) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const currentResults = results;
|
|
224
|
+
const result = currentResults.find((r) => r.id === id);
|
|
225
|
+
if (result) {
|
|
226
|
+
// Use registered override if available, otherwise use default
|
|
227
|
+
if (addToMapRef.current) {
|
|
228
|
+
addToMapRef.current(result);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
defaultAddToMap(result);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}, [model, results, defaultAddToMap]);
|
|
235
|
+
const formatResult = useCallback((item) => {
|
|
236
|
+
var _a, _b;
|
|
237
|
+
return (_b = (_a = item.properties) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : item.id;
|
|
238
|
+
}, []);
|
|
239
|
+
return (React.createElement(StacResultsContext.Provider, { value: {
|
|
240
|
+
results,
|
|
241
|
+
isLoading,
|
|
242
|
+
totalResults,
|
|
243
|
+
totalPages,
|
|
244
|
+
handlePaginationClick,
|
|
245
|
+
handleResultClick,
|
|
246
|
+
formatResult,
|
|
247
|
+
paginationLinks,
|
|
248
|
+
selectedUrl,
|
|
249
|
+
setSelectedUrl,
|
|
250
|
+
currentPage,
|
|
251
|
+
setCurrentPage,
|
|
252
|
+
currentPageRef,
|
|
253
|
+
setResults,
|
|
254
|
+
setIsLoading,
|
|
255
|
+
setPaginationLinks,
|
|
256
|
+
registerAddToMap,
|
|
257
|
+
registerHandlePaginationClick,
|
|
258
|
+
registerBuildQuery,
|
|
259
|
+
executeQuery,
|
|
260
|
+
executeQueryWithPage,
|
|
261
|
+
} }, children));
|
|
262
|
+
}
|
|
263
|
+
export function useStacResultsContext() {
|
|
264
|
+
const context = useContext(StacResultsContext);
|
|
265
|
+
if (context === undefined) {
|
|
266
|
+
throw new Error('useStacResultsContext must be used within a StacResultsProvider');
|
|
267
|
+
}
|
|
268
|
+
return context;
|
|
269
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { IJupyterGISModel } from '@jupytergis/schema';
|
|
2
|
+
import { StacFilterState, StacFilterSetters } from "../types/types";
|
|
3
|
+
interface IUseGeodesSearchProps {
|
|
4
|
+
model: IJupyterGISModel | undefined;
|
|
5
|
+
}
|
|
6
|
+
interface IUseGeodesSearchReturn {
|
|
7
|
+
filterState: StacFilterState;
|
|
8
|
+
filterSetters: StacFilterSetters;
|
|
9
|
+
startTime: Date | undefined;
|
|
10
|
+
setStartTime: (date: Date | undefined) => void;
|
|
11
|
+
endTime: Date | undefined;
|
|
12
|
+
setEndTime: (date: Date | undefined) => void;
|
|
13
|
+
useWorldBBox: boolean;
|
|
14
|
+
setUseWorldBBox: (val: boolean) => void;
|
|
15
|
+
handleSubmit: () => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Custom hook for managing GEODES-specific STAC search functionality
|
|
19
|
+
* Focuses on query building with GEODES-specific filters
|
|
20
|
+
* @param props - Configuration object containing model and context setters
|
|
21
|
+
* @returns Object containing filter state and temporal/spatial filters
|
|
22
|
+
*/
|
|
23
|
+
declare function useGeodesSearch({ model, }: IUseGeodesSearchProps): IUseGeodesSearchReturn;
|
|
24
|
+
export default useGeodesSearch;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { startOfYesterday } from 'date-fns';
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import useIsFirstRender from "../../shared/hooks/useIsFirstRender";
|
|
4
|
+
import { products } from "../constants";
|
|
5
|
+
import { useStacResultsContext } from "../context/StacResultsContext";
|
|
6
|
+
import { useStacSearch } from "./useStacSearch";
|
|
7
|
+
import { GlobalStateDbManager } from "../../store";
|
|
8
|
+
const GEODES_STAC_FILTERS_KEY = 'jupytergis:geodes-stac-filters';
|
|
9
|
+
/**
|
|
10
|
+
* Custom hook for managing GEODES-specific STAC search functionality
|
|
11
|
+
* Focuses on query building with GEODES-specific filters
|
|
12
|
+
* @param props - Configuration object containing model and context setters
|
|
13
|
+
* @returns Object containing filter state and temporal/spatial filters
|
|
14
|
+
*/
|
|
15
|
+
function useGeodesSearch({ model, }) {
|
|
16
|
+
const isFirstRender = useIsFirstRender();
|
|
17
|
+
const stateDb = GlobalStateDbManager.getInstance().getStateDb();
|
|
18
|
+
const { currentPageRef, setCurrentPage, registerHandlePaginationClick, registerBuildQuery, executeQuery, selectedUrl, } = useStacResultsContext();
|
|
19
|
+
// Get temporal/spatial filters and fetch functions from useStacSearch
|
|
20
|
+
const { startTime, setStartTime, endTime, setEndTime, currentBBox, useWorldBBox, setUseWorldBBox, } = useStacSearch({
|
|
21
|
+
model,
|
|
22
|
+
});
|
|
23
|
+
const [filterState, setFilterState] = useState({
|
|
24
|
+
collections: new Set(),
|
|
25
|
+
datasets: new Set(),
|
|
26
|
+
platforms: new Set(),
|
|
27
|
+
products: new Set(),
|
|
28
|
+
});
|
|
29
|
+
const filterSetters = {
|
|
30
|
+
collections: val => setFilterState(s => (Object.assign(Object.assign({}, s), { collections: new Set(val) }))),
|
|
31
|
+
datasets: val => setFilterState(s => (Object.assign(Object.assign({}, s), { datasets: new Set(val) }))),
|
|
32
|
+
platforms: val => setFilterState(s => (Object.assign(Object.assign({}, s), { platforms: new Set(val) }))),
|
|
33
|
+
products: val => setFilterState(s => (Object.assign(Object.assign({}, s), { products: new Set(val) }))),
|
|
34
|
+
};
|
|
35
|
+
// On mount, fetch filterState and times from StateDB (if present)
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
async function loadStacStateFromDb() {
|
|
38
|
+
var _a, _b, _c, _d;
|
|
39
|
+
const savedFilterState = (await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.fetch(GEODES_STAC_FILTERS_KEY)));
|
|
40
|
+
setFilterState({
|
|
41
|
+
collections: new Set((_a = savedFilterState === null || savedFilterState === void 0 ? void 0 : savedFilterState.collections) !== null && _a !== void 0 ? _a : []),
|
|
42
|
+
datasets: new Set((_b = savedFilterState === null || savedFilterState === void 0 ? void 0 : savedFilterState.datasets) !== null && _b !== void 0 ? _b : []),
|
|
43
|
+
platforms: new Set((_c = savedFilterState === null || savedFilterState === void 0 ? void 0 : savedFilterState.platforms) !== null && _c !== void 0 ? _c : []),
|
|
44
|
+
products: new Set((_d = savedFilterState === null || savedFilterState === void 0 ? void 0 : savedFilterState.products) !== null && _d !== void 0 ? _d : []),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
loadStacStateFromDb();
|
|
48
|
+
}, [stateDb]);
|
|
49
|
+
// Save filterState to StateDB on change
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
async function saveStacFilterStateToDb() {
|
|
52
|
+
await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.save(GEODES_STAC_FILTERS_KEY, {
|
|
53
|
+
collections: Array.from(filterState.collections),
|
|
54
|
+
datasets: Array.from(filterState.datasets),
|
|
55
|
+
platforms: Array.from(filterState.platforms),
|
|
56
|
+
products: Array.from(filterState.products),
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
saveStacFilterStateToDb();
|
|
60
|
+
}, [filterState, stateDb]);
|
|
61
|
+
/**
|
|
62
|
+
* Builds GEODES-specific query
|
|
63
|
+
* @param page - Page number for pagination (defaults to currentPageRef.current)
|
|
64
|
+
*/
|
|
65
|
+
const buildGeodesQuery = useCallback((page) => {
|
|
66
|
+
const pageToUse = page !== null && page !== void 0 ? page : currentPageRef.current;
|
|
67
|
+
const processingLevel = new Set();
|
|
68
|
+
const productType = new Set();
|
|
69
|
+
filterState.products.forEach(productCode => {
|
|
70
|
+
products
|
|
71
|
+
.filter(product => product.productCode === productCode)
|
|
72
|
+
.forEach(product => {
|
|
73
|
+
if (product.processingLevel) {
|
|
74
|
+
processingLevel.add(product.processingLevel);
|
|
75
|
+
}
|
|
76
|
+
if (product.productType) {
|
|
77
|
+
product.productType.forEach(type => productType.add(type));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
bbox: currentBBox,
|
|
83
|
+
limit: 12,
|
|
84
|
+
page: pageToUse,
|
|
85
|
+
query: Object.assign(Object.assign(Object.assign(Object.assign({ latest: { eq: true }, dataset: { in: Array.from(filterState.datasets) }, end_datetime: {
|
|
86
|
+
gte: startTime
|
|
87
|
+
? startTime.toISOString()
|
|
88
|
+
: startOfYesterday().toISOString(),
|
|
89
|
+
} }, (endTime && {
|
|
90
|
+
start_datetime: { lte: endTime.toISOString() },
|
|
91
|
+
})), (filterState.platforms.size > 0 && {
|
|
92
|
+
platform: { in: Array.from(filterState.platforms) },
|
|
93
|
+
})), (processingLevel.size > 0 && {
|
|
94
|
+
'processing:level': { in: Array.from(processingLevel) },
|
|
95
|
+
})), (productType.size > 0 && {
|
|
96
|
+
'product:type': { in: Array.from(productType) },
|
|
97
|
+
})),
|
|
98
|
+
sortBy: [{ direction: 'desc', field: 'start_datetime' }],
|
|
99
|
+
};
|
|
100
|
+
}, [filterState, currentBBox, startTime, endTime, currentPageRef]);
|
|
101
|
+
// Register buildQuery with context - always use currentPageRef for latest page
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
registerBuildQuery(() => buildGeodesQuery(currentPageRef.current));
|
|
104
|
+
}, [registerBuildQuery, buildGeodesQuery, currentPageRef]);
|
|
105
|
+
/**
|
|
106
|
+
* Handles form submission - builds query and fetches results
|
|
107
|
+
*/
|
|
108
|
+
const handleSubmit = useCallback(async () => {
|
|
109
|
+
if (!model) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const searchUrl = selectedUrl.endsWith('/')
|
|
113
|
+
? `${selectedUrl}search`
|
|
114
|
+
: `${selectedUrl}/search`;
|
|
115
|
+
// Build query body and execute query
|
|
116
|
+
const queryBody = buildGeodesQuery();
|
|
117
|
+
await executeQuery(queryBody, searchUrl);
|
|
118
|
+
}, [model, executeQuery, buildGeodesQuery, selectedUrl]);
|
|
119
|
+
// Handle search when filters change
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (model && !isFirstRender && filterState.datasets.size > 0) {
|
|
122
|
+
handleSubmit();
|
|
123
|
+
}
|
|
124
|
+
}, [
|
|
125
|
+
model,
|
|
126
|
+
isFirstRender,
|
|
127
|
+
filterState,
|
|
128
|
+
startTime,
|
|
129
|
+
endTime,
|
|
130
|
+
currentBBox,
|
|
131
|
+
handleSubmit,
|
|
132
|
+
]);
|
|
133
|
+
/**
|
|
134
|
+
* Handles pagination clicks for GEODES
|
|
135
|
+
* Updates currentPage and executes query with new page number
|
|
136
|
+
* @param dir - Direction ('next' | 'previous')
|
|
137
|
+
*/
|
|
138
|
+
const handlePaginationClick = useCallback(async (dir) => {
|
|
139
|
+
if (!model) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Calculate new page number
|
|
143
|
+
const newPage = dir === 'next'
|
|
144
|
+
? currentPageRef.current + 1
|
|
145
|
+
: currentPageRef.current - 1;
|
|
146
|
+
// Update currentPage in context
|
|
147
|
+
setCurrentPage(newPage);
|
|
148
|
+
const searchUrl = selectedUrl.endsWith('/')
|
|
149
|
+
? `${selectedUrl}search`
|
|
150
|
+
: `${selectedUrl}/search`;
|
|
151
|
+
// Build query body with new page and execute query
|
|
152
|
+
const queryBody = buildGeodesQuery(newPage);
|
|
153
|
+
await executeQuery(queryBody, searchUrl);
|
|
154
|
+
}, [
|
|
155
|
+
model,
|
|
156
|
+
executeQuery,
|
|
157
|
+
setCurrentPage,
|
|
158
|
+
currentPageRef,
|
|
159
|
+
buildGeodesQuery,
|
|
160
|
+
selectedUrl,
|
|
161
|
+
]);
|
|
162
|
+
// Register handlePaginationClick with context
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
registerHandlePaginationClick(handlePaginationClick);
|
|
165
|
+
}, [handlePaginationClick, registerHandlePaginationClick]);
|
|
166
|
+
return {
|
|
167
|
+
filterState,
|
|
168
|
+
filterSetters,
|
|
169
|
+
startTime,
|
|
170
|
+
setStartTime,
|
|
171
|
+
endTime,
|
|
172
|
+
setEndTime,
|
|
173
|
+
useWorldBBox,
|
|
174
|
+
setUseWorldBBox,
|
|
175
|
+
handleSubmit,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export default useGeodesSearch;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IJupyterGISModel } from '@jupytergis/schema';
|
|
2
|
+
import { FilterOperator, IQueryableFilter, IStacCollection, IStacQueryables } from "../types/types";
|
|
3
|
+
type FilteredCollection = Pick<IStacCollection, 'id' | 'title'>;
|
|
4
|
+
interface IUseStacFilterExtensionProps {
|
|
5
|
+
model?: IJupyterGISModel;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
limit?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Hook for searching STAC catalogs that support the Filter Extension (CQL2-JSON).
|
|
11
|
+
* Fetches collections and queryables, and builds filter queries using the STAC Filter Extension.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useStacFilterExtension({ model, baseUrl, limit, }: IUseStacFilterExtensionProps): {
|
|
14
|
+
queryableFields: IStacQueryables | undefined;
|
|
15
|
+
collections: FilteredCollection[];
|
|
16
|
+
selectedCollection: string;
|
|
17
|
+
setSelectedCollection: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
18
|
+
handleSubmit: () => Promise<void>;
|
|
19
|
+
startTime: Date | undefined;
|
|
20
|
+
endTime: Date | undefined;
|
|
21
|
+
setStartTime: (date: Date | undefined) => void;
|
|
22
|
+
setEndTime: (date: Date | undefined) => void;
|
|
23
|
+
useWorldBBox: boolean;
|
|
24
|
+
setUseWorldBBox: (val: boolean) => void;
|
|
25
|
+
selectedQueryables: Record<string, IQueryableFilter>;
|
|
26
|
+
updateSelectedQueryables: (qKey: string, filter: IQueryableFilter | null) => void;
|
|
27
|
+
filterOperator: FilterOperator;
|
|
28
|
+
setFilterOperator: import("react").Dispatch<import("react").SetStateAction<FilterOperator>>;
|
|
29
|
+
};
|
|
30
|
+
export {};
|