@jupytergis/base 0.11.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.
Files changed (101) 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.js +3 -0
  5. package/lib/dialogs/symbology/hooks/useGetBandInfo.d.ts +0 -6
  6. package/lib/dialogs/symbology/hooks/useGetBandInfo.js +2 -2
  7. package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +4 -4
  8. package/lib/formbuilder/objectform/StoryEditorForm.d.ts +3 -2
  9. package/lib/formbuilder/objectform/StoryEditorForm.js +7 -1
  10. package/lib/mainview/mainView.d.ts +18 -0
  11. package/lib/mainview/mainView.js +243 -18
  12. package/lib/panelview/{components/filter-panel → filter-panel}/Filter.js +1 -1
  13. package/lib/panelview/leftpanel.js +4 -4
  14. package/lib/panelview/rightpanel.d.ts +2 -0
  15. package/lib/panelview/rightpanel.js +21 -14
  16. package/lib/panelview/{components/story-maps → story-maps}/PreviewModeSwitch.js +3 -2
  17. package/lib/panelview/story-maps/StoryEditorPanel.d.ts +9 -0
  18. package/lib/panelview/story-maps/StoryEditorPanel.js +34 -0
  19. package/lib/panelview/{components/story-maps → story-maps}/StoryNavBar.d.ts +2 -1
  20. package/lib/panelview/{components/story-maps → story-maps}/StoryNavBar.js +3 -3
  21. package/lib/panelview/story-maps/StoryViewerPanel.d.ts +13 -0
  22. package/lib/panelview/{components/story-maps → story-maps}/StoryViewerPanel.js +37 -24
  23. package/lib/panelview/story-maps/components/StoryContentSection.d.ts +6 -0
  24. package/lib/panelview/story-maps/components/StoryContentSection.js +10 -0
  25. package/lib/panelview/story-maps/components/StoryImageSection.d.ts +15 -0
  26. package/lib/panelview/story-maps/components/StoryImageSection.js +13 -0
  27. package/lib/panelview/story-maps/components/StorySubtitleSection.d.ts +11 -0
  28. package/lib/panelview/story-maps/components/StorySubtitleSection.js +9 -0
  29. package/lib/panelview/story-maps/components/StoryTitleSection.d.ts +12 -0
  30. package/lib/panelview/story-maps/components/StoryTitleSection.js +8 -0
  31. package/lib/shared/components/Combobox.d.ts +21 -0
  32. package/lib/shared/components/Combobox.js +32 -0
  33. package/lib/shared/components/Command.js +10 -10
  34. package/lib/shared/components/Input.d.ts +3 -0
  35. package/lib/shared/components/Input.js +18 -0
  36. package/lib/shared/components/Pagination.js +3 -2
  37. package/lib/shared/components/Select.d.ts +19 -0
  38. package/lib/shared/components/Select.js +28 -0
  39. package/lib/shared/components/SingleDatePicker.d.ts +11 -0
  40. package/lib/shared/components/SingleDatePicker.js +16 -0
  41. package/lib/stacBrowser/components/StacPanel.d.ts +9 -1
  42. package/lib/stacBrowser/components/StacPanel.js +53 -9
  43. package/lib/stacBrowser/components/filter-extension/QueryableComboBox.d.ts +9 -0
  44. package/lib/stacBrowser/components/filter-extension/QueryableComboBox.js +179 -0
  45. package/lib/stacBrowser/components/filter-extension/QueryableRow.d.ts +16 -0
  46. package/lib/stacBrowser/components/filter-extension/QueryableRow.js +16 -0
  47. package/lib/stacBrowser/components/filter-extension/StacFilterExtensionPanel.d.ts +7 -0
  48. package/lib/stacBrowser/components/filter-extension/StacFilterExtensionPanel.js +49 -0
  49. package/lib/stacBrowser/components/filter-extension/StacQueryableFilters.d.ts +11 -0
  50. package/lib/stacBrowser/components/filter-extension/StacQueryableFilters.js +19 -0
  51. package/lib/stacBrowser/components/{StacFilterSection.d.ts → geodes/StacFilterSection.d.ts} +1 -1
  52. package/lib/stacBrowser/components/{StacFilterSection.js → geodes/StacFilterSection.js} +3 -3
  53. package/lib/stacBrowser/components/geodes/StacGeodesFilterPanel.d.ts +7 -0
  54. package/lib/stacBrowser/components/geodes/StacGeodesFilterPanel.js +69 -0
  55. package/lib/stacBrowser/components/shared/StacPanelResults.d.ts +3 -0
  56. package/lib/stacBrowser/components/shared/StacPanelResults.js +68 -0
  57. package/lib/stacBrowser/components/shared/StacSpatialExtent.d.ts +8 -0
  58. package/lib/stacBrowser/components/shared/StacSpatialExtent.js +10 -0
  59. package/lib/stacBrowser/components/shared/StacTemporalExtent.d.ts +9 -0
  60. package/lib/stacBrowser/components/shared/StacTemporalExtent.js +9 -0
  61. package/lib/stacBrowser/context/StacResultsContext.d.ts +33 -0
  62. package/lib/stacBrowser/context/StacResultsContext.js +269 -0
  63. package/lib/stacBrowser/hooks/useGeodesSearch.d.ts +24 -0
  64. package/lib/stacBrowser/hooks/useGeodesSearch.js +178 -0
  65. package/lib/stacBrowser/hooks/useStacFilterExtension.d.ts +30 -0
  66. package/lib/stacBrowser/hooks/useStacFilterExtension.js +262 -0
  67. package/lib/stacBrowser/hooks/useStacSearch.d.ts +5 -16
  68. package/lib/stacBrowser/hooks/useStacSearch.js +30 -184
  69. package/lib/stacBrowser/types/types.d.ts +86 -3
  70. package/lib/toolbar/widget.d.ts +5 -0
  71. package/lib/toolbar/widget.js +23 -2
  72. package/lib/tools.d.ts +0 -7
  73. package/lib/tools.js +55 -14
  74. package/package.json +2 -2
  75. package/style/base.css +38 -3
  76. package/style/shared/button.css +5 -4
  77. package/style/shared/calendar.css +7 -1
  78. package/style/shared/combobox.css +75 -0
  79. package/style/shared/command.css +178 -0
  80. package/style/shared/input.css +59 -0
  81. package/style/shared/pagination.css +1 -1
  82. package/style/shared/popover.css +1 -0
  83. package/style/shared/tabs.css +1 -1
  84. package/style/shared/toggle.css +1 -1
  85. package/style/stacBrowser.css +169 -16
  86. package/style/statusBar.css +1 -0
  87. package/style/storyPanel.css +120 -3
  88. package/style/tabPanel.css +0 -86
  89. package/lib/panelview/components/story-maps/StoryEditorPanel.d.ts +0 -7
  90. package/lib/panelview/components/story-maps/StoryEditorPanel.js +0 -29
  91. package/lib/panelview/components/story-maps/StoryViewerPanel.d.ts +0 -7
  92. package/lib/stacBrowser/components/StacPanelFilters.d.ts +0 -14
  93. package/lib/stacBrowser/components/StacPanelFilters.js +0 -81
  94. package/lib/stacBrowser/components/StacPanelResults.d.ts +0 -13
  95. package/lib/stacBrowser/components/StacPanelResults.js +0 -48
  96. /package/lib/panelview/{components/filter-panel → filter-panel}/Filter.d.ts +0 -0
  97. /package/lib/panelview/{components/filter-panel → filter-panel}/FilterRow.d.ts +0 -0
  98. /package/lib/panelview/{components/filter-panel → filter-panel}/FilterRow.js +0 -0
  99. /package/lib/panelview/{components/identify-panel → identify-panel}/IdentifyPanel.d.ts +0 -0
  100. /package/lib/panelview/{components/identify-panel → identify-panel}/IdentifyPanel.js +0 -0
  101. /package/lib/panelview/{components/story-maps → story-maps}/PreviewModeSwitch.d.ts +0 -0
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import { Button } from "../../../shared/components/Button";
3
+ import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "../../../shared/components/Pagination";
4
+ import { LoadingIcon } from "../../../shared/components/loading";
5
+ import { useStacResultsContext } from "../../context/StacResultsContext";
6
+ function getPageItems(currentPage, totalPages) {
7
+ if (totalPages <= 5) {
8
+ return Array.from({ length: totalPages }, (_, i) => i + 1);
9
+ }
10
+ if (currentPage <= 3) {
11
+ return [1, 2, 3, 'ellipsis', totalPages];
12
+ }
13
+ if (currentPage >= totalPages - 2) {
14
+ return [
15
+ totalPages - 4,
16
+ totalPages - 3,
17
+ totalPages - 2,
18
+ totalPages - 1,
19
+ totalPages,
20
+ ];
21
+ }
22
+ return [
23
+ currentPage - 2,
24
+ currentPage - 1,
25
+ currentPage,
26
+ 'ellipsis',
27
+ totalPages,
28
+ ];
29
+ }
30
+ const StacPanelResults = () => {
31
+ const { results, handlePaginationClick, handleResultClick, formatResult, isLoading, paginationLinks, currentPage, setCurrentPage, totalPages, executeQueryWithPage, } = useStacResultsContext();
32
+ const isNext = paginationLinks.some(link => link.rel === 'next');
33
+ const isPrev = paginationLinks.some(link => ['prev', 'previous'].includes(link.rel));
34
+ return (React.createElement("div", { className: "jgis-stac-browser-filters-panel" },
35
+ React.createElement(Pagination, null,
36
+ React.createElement(PaginationContent, { className: "jgis-stac-panel-results-pagination" },
37
+ React.createElement(PaginationItem, null,
38
+ React.createElement(PaginationPrevious, { onClick: () => {
39
+ setCurrentPage(Math.max(currentPage - 1, 1));
40
+ handlePaginationClick('previous');
41
+ }, disabled: !isPrev })),
42
+ totalPages === 1 ? (
43
+ // One page, display current page number and keep active
44
+ React.createElement(PaginationItem, null,
45
+ React.createElement(PaginationLink, { isActive: true }, currentPage))) : results.length !== 0 ? (
46
+ // Multiple pages, display fancy pagination numbers
47
+ React.createElement(React.Fragment, null, getPageItems(currentPage, totalPages).map(pageNumber => {
48
+ if (pageNumber === 'ellipsis') {
49
+ return (React.createElement(PaginationItem, { key: "ellipsis" },
50
+ React.createElement(PaginationEllipsis, null)));
51
+ }
52
+ return (React.createElement(PaginationItem, { key: pageNumber },
53
+ React.createElement(PaginationLink, { isActive: pageNumber === currentPage, onClick: async () => {
54
+ setCurrentPage(pageNumber);
55
+ await executeQueryWithPage(pageNumber);
56
+ }, disabled: totalPages === 1 }, pageNumber)));
57
+ }))) : (
58
+ // No results
59
+ React.createElement(PaginationItem, null,
60
+ React.createElement(PaginationLink, { isActive: true, disabled: true }, "0"))),
61
+ React.createElement(PaginationItem, null,
62
+ React.createElement(PaginationNext, { onClick: () => {
63
+ setCurrentPage(currentPage + 1);
64
+ handlePaginationClick('next');
65
+ }, disabled: !isNext })))),
66
+ React.createElement("div", { className: "jgis-stac-browser-results-list" }, isLoading ? (React.createElement(LoadingIcon, { size: "3x" })) : (results.map(result => (React.createElement(Button, { key: result.id, className: "jgis-stac-browser-results-item", onClick: () => handleResultClick(result.id) }, formatResult(result))))))));
67
+ };
68
+ export default StacPanelResults;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ interface IStacSpatialExtentProps {
3
+ checked: boolean;
4
+ onCheckedChange: (checked: boolean) => void;
5
+ label: string;
6
+ }
7
+ declare const StacSpatialExtent: React.FC<IStacSpatialExtentProps>;
8
+ export default StacSpatialExtent;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import Checkbox from "../../../shared/components/Checkbox";
3
+ const StacSpatialExtent = ({ checked, onCheckedChange, label, }) => {
4
+ return (React.createElement(React.Fragment, null,
5
+ React.createElement("label", { className: "jgis-stac-filter-extension-label" }, "Spatial Extent"),
6
+ React.createElement("span", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' } },
7
+ React.createElement(Checkbox, { checked: checked, onCheckedChange: onCheckedChange }),
8
+ label)));
9
+ };
10
+ export default StacSpatialExtent;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface IStacTemporalExtentProps {
3
+ startTime: Date | undefined;
4
+ setStartTime: (date: Date | undefined) => void;
5
+ endTime: Date | undefined;
6
+ setEndTime: (date: Date | undefined) => void;
7
+ }
8
+ declare function StacTemporalExtent({ startTime, endTime, setStartTime, setEndTime, }: IStacTemporalExtentProps): React.JSX.Element;
9
+ export default StacTemporalExtent;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import SingleDatePicker from '../../../shared/components/SingleDatePicker';
3
+ function StacTemporalExtent({ startTime, endTime, setStartTime, setEndTime, }) {
4
+ return (React.createElement("div", { className: "jgis-stac-filter-extension-section" },
5
+ React.createElement("label", { className: "jgis-stac-filter-extension-label" }, "Temporal Extent"),
6
+ React.createElement(SingleDatePicker, { date: startTime, onDateChange: setStartTime, className: "jgis-stac-datepicker-full-width" }),
7
+ React.createElement(SingleDatePicker, { date: endTime, onDateChange: setEndTime, className: "jgis-stac-datepicker-full-width" })));
8
+ }
9
+ export default StacTemporalExtent;
@@ -0,0 +1,33 @@
1
+ import { IJupyterGISModel } from '@jupytergis/schema';
2
+ import React, { ReactNode } from 'react';
3
+ import { IStacItem, IStacPaginationLink, IStacQueryBodyUnion, SetResultsFunction } from "../types/types";
4
+ interface IStacResultsContext {
5
+ results: IStacItem[];
6
+ isLoading: boolean;
7
+ totalResults: string;
8
+ totalPages: number;
9
+ handlePaginationClick: (dir: 'next' | 'previous') => Promise<void>;
10
+ handleResultClick: (id: string) => Promise<void>;
11
+ formatResult: (item: IStacItem) => string;
12
+ paginationLinks: IStacPaginationLink[];
13
+ selectedUrl: string;
14
+ setSelectedUrl: (url: string) => void;
15
+ currentPage: number;
16
+ setCurrentPage: (page: number) => void;
17
+ currentPageRef: React.MutableRefObject<number>;
18
+ setResults: SetResultsFunction;
19
+ setIsLoading: (isLoading: boolean) => void;
20
+ setPaginationLinks: (links: IStacPaginationLink[]) => void;
21
+ registerAddToMap: (addFn: (stacData: IStacItem) => void) => void;
22
+ registerHandlePaginationClick: (handleFn: (dir: 'next' | 'previous') => Promise<void>) => void;
23
+ registerBuildQuery: (buildQueryFn: () => IStacQueryBodyUnion) => void;
24
+ executeQuery: (body: IStacQueryBodyUnion, apiUrl?: string, method?: string) => Promise<void>;
25
+ executeQueryWithPage: (pageNumber: number) => Promise<void>;
26
+ }
27
+ interface IStacResultsProviderProps {
28
+ children: ReactNode;
29
+ model?: IJupyterGISModel;
30
+ }
31
+ export declare function StacResultsProvider({ children, model, }: IStacResultsProviderProps): React.JSX.Element;
32
+ export declare function useStacResultsContext(): IStacResultsContext;
33
+ export {};
@@ -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 {};