@jupytergis/base 0.14.0 → 0.15.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 (62) hide show
  1. package/lib/commands/BaseCommandIDs.d.ts +1 -1
  2. package/lib/commands/BaseCommandIDs.js +1 -1
  3. package/lib/commands/index.js +28 -34
  4. package/lib/constants.js +1 -0
  5. package/lib/dialogs/symbology/classificationModes.js +12 -16
  6. package/lib/dialogs/symbology/colorRampUtils.d.ts +47 -3
  7. package/lib/dialogs/symbology/colorRampUtils.js +112 -13
  8. package/lib/dialogs/symbology/components/color_ramp/ColorRampSelector.js +6 -14
  9. package/lib/dialogs/symbology/components/color_ramp/ColorRampSelectorEntry.d.ts +2 -2
  10. package/lib/dialogs/symbology/components/color_ramp/ColorRampSelectorEntry.js +3 -11
  11. package/lib/dialogs/symbology/components/color_ramp/RgbaColorPicker.d.ts +13 -0
  12. package/lib/dialogs/symbology/components/color_ramp/RgbaColorPicker.js +98 -0
  13. package/lib/dialogs/symbology/components/color_stops/StopContainer.js +3 -1
  14. package/lib/dialogs/symbology/components/color_stops/StopRow.d.ts +1 -1
  15. package/lib/dialogs/symbology/components/color_stops/StopRow.js +12 -7
  16. package/lib/dialogs/symbology/symbologyDialog.d.ts +2 -1
  17. package/lib/dialogs/symbology/symbologyUtils.d.ts +2 -2
  18. package/lib/dialogs/symbology/symbologyUtils.js +58 -40
  19. package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +14 -2
  20. package/lib/dialogs/symbology/vector_layer/VectorRendering.js +6 -5
  21. package/lib/dialogs/symbology/vector_layer/components/ValueSelect.js +3 -1
  22. package/lib/dialogs/symbology/vector_layer/types/Canonical.js +70 -5
  23. package/lib/dialogs/symbology/vector_layer/types/Categorized.js +81 -34
  24. package/lib/dialogs/symbology/vector_layer/types/Graduated.js +155 -43
  25. package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.js +31 -16
  26. package/lib/formbuilder/formselectors.js +4 -1
  27. package/lib/formbuilder/objectform/components/WmsTileSourceUrlInput.d.ts +3 -0
  28. package/lib/formbuilder/objectform/components/WmsTileSourceUrlInput.js +84 -0
  29. package/lib/formbuilder/objectform/source/index.d.ts +1 -0
  30. package/lib/formbuilder/objectform/source/index.js +1 -0
  31. package/lib/formbuilder/objectform/source/wmsTileSource.d.ts +4 -0
  32. package/lib/formbuilder/objectform/source/wmsTileSource.js +78 -0
  33. package/lib/formbuilder/objectform/useSchemaFormState.d.ts +1 -1
  34. package/lib/mainview/mainView.d.ts +3 -1
  35. package/lib/mainview/mainView.js +170 -23
  36. package/lib/menus.js +4 -0
  37. package/lib/panelview/components/layers.js +19 -2
  38. package/lib/panelview/components/legendItem.js +14 -4
  39. package/lib/panelview/filter-panel/Filter.d.ts +3 -0
  40. package/lib/panelview/filter-panel/Filter.js +9 -9
  41. package/lib/panelview/leftpanel.js +0 -7
  42. package/lib/panelview/story-maps/SpectaPanel.js +2 -2
  43. package/lib/panelview/story-maps/StoryViewerPanel.d.ts +1 -2
  44. package/lib/panelview/story-maps/StoryViewerPanel.js +1 -1
  45. package/lib/panelview/story-maps/components/SpectaDesktopView.d.ts +2 -1
  46. package/lib/panelview/story-maps/components/SpectaDesktopView.js +4 -4
  47. package/lib/panelview/story-maps/hooks/useStoryMap.d.ts +1 -0
  48. package/lib/panelview/story-maps/hooks/useStoryMap.js +3 -0
  49. package/lib/stacBrowser/components/filter-extension/QueryableComboBox.js +61 -20
  50. package/lib/stacBrowser/hooks/useStacFilterExtension.d.ts +1 -1
  51. package/lib/stacBrowser/hooks/useStacFilterExtension.js +195 -111
  52. package/lib/stacBrowser/hooks/useStacSearch.d.ts +1 -0
  53. package/lib/stacBrowser/hooks/useStacSearch.js +18 -10
  54. package/lib/tools.d.ts +1 -1
  55. package/lib/tools.js +3 -3
  56. package/lib/types.d.ts +7 -1
  57. package/package.json +5 -2
  58. package/style/shared/button.css +2 -5
  59. package/style/shared/input.css +2 -2
  60. package/style/shared/tabs.css +2 -2
  61. package/style/storyPanel.css +7 -0
  62. package/style/symbologyDialog.css +45 -1
@@ -16,6 +16,7 @@ export function getSpectaPresentationStyle(story) {
16
16
  return style;
17
17
  }
18
18
  export function useStoryMap({ model, overrideLayerEntriesRef, removeLayer, addLayer, panelRef, isSpecta, }) {
19
+ var _a;
19
20
  const [currentIndex, setCurrentIndex] = useState(() => { var _a; return (_a = model.getCurrentSegmentIndex()) !== null && _a !== void 0 ? _a : 0; });
20
21
  const [storyData, setStoryData] = useState(() => { var _a; return (_a = model.getSelectedStory().story) !== null && _a !== void 0 ? _a : null; });
21
22
  const storySegments = useMemo(() => {
@@ -32,6 +33,7 @@ export function useStoryMap({ model, overrideLayerEntriesRef, removeLayer, addLa
32
33
  const activeSlide = useMemo(() => currentStorySegment === null || currentStorySegment === void 0 ? void 0 : currentStorySegment.parameters, [currentStorySegment]);
33
34
  const layerName = useMemo(() => { var _a; return (_a = currentStorySegment === null || currentStorySegment === void 0 ? void 0 : currentStorySegment.name) !== null && _a !== void 0 ? _a : ''; }, [currentStorySegment]);
34
35
  const currentStorySegmentId = useMemo(() => storySegmentIds === null || storySegmentIds === void 0 ? void 0 : storySegmentIds[currentIndex], [storySegmentIds, currentIndex]);
36
+ const showGradient = (_a = storyData === null || storyData === void 0 ? void 0 : storyData.showGradient) !== null && _a !== void 0 ? _a : true;
35
37
  const hasPrev = currentIndex > 0;
36
38
  const hasNext = currentIndex < segmentCount - 1;
37
39
  const clearOverrideLayers = useCallback(() => {
@@ -236,6 +238,7 @@ export function useStoryMap({ model, overrideLayerEntriesRef, removeLayer, addLa
236
238
  storyData,
237
239
  storySegments,
238
240
  currentIndex,
241
+ showGradient,
239
242
  clearOverrideLayers,
240
243
  setIndex,
241
244
  handlePrev,
@@ -1,10 +1,55 @@
1
- import React, { useMemo } from 'react';
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 React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
2
13
  import { Combobox } from "../../../shared/components/Combobox";
3
14
  import { Input } from "../../../shared/components/Input";
4
15
  import { Select } from "../../../shared/components/Select";
5
16
  import QueryableRow from "./QueryableRow";
17
+ import { debounce } from "../../../tools";
6
18
  import SingleDatePicker from '../../../shared/components/SingleDatePicker';
7
19
  export function QueryableComboBox({ queryables, selectedQueryables, updateSelectedQueryables, }) {
20
+ const [draftValues, setDraftValues] = useState({});
21
+ const selectedQueryablesRef = useRef(selectedQueryables);
22
+ const debouncedCommitByKeyRef = useRef({});
23
+ useEffect(() => {
24
+ selectedQueryablesRef.current = selectedQueryables;
25
+ }, [selectedQueryables]);
26
+ const normalizeInputValue = useCallback((schema, value) => {
27
+ let valueToStore = value;
28
+ if (schema.type === 'string' &&
29
+ schema.format === 'date-time' &&
30
+ typeof value === 'string') {
31
+ try {
32
+ const localDate = new Date(value);
33
+ valueToStore = localDate.toISOString();
34
+ }
35
+ catch (_a) {
36
+ valueToStore = value;
37
+ }
38
+ }
39
+ return valueToStore;
40
+ }, []);
41
+ const scheduleQueryableCommit = useCallback((key, value) => {
42
+ if (!debouncedCommitByKeyRef.current[key]) {
43
+ debouncedCommitByKeyRef.current[key] = debounce((nextValue) => {
44
+ const latestFilter = selectedQueryablesRef.current[key];
45
+ if (!latestFilter) {
46
+ return;
47
+ }
48
+ updateSelectedQueryables(key, Object.assign(Object.assign({}, latestFilter), { inputValue: nextValue }));
49
+ }, 500);
50
+ }
51
+ debouncedCommitByKeyRef.current[key](value);
52
+ }, [updateSelectedQueryables]);
8
53
  // Derive selected items from selectedQueryables
9
54
  const selectedItems = useMemo(() => {
10
55
  return queryables.filter(([key]) => key in selectedQueryables);
@@ -14,6 +59,11 @@ export function QueryableComboBox({ queryables, selectedQueryables, updateSelect
14
59
  const isCurrentlySelected = key in selectedQueryables;
15
60
  if (isCurrentlySelected) {
16
61
  // Remove if already selected - pass null to explicitly remove
62
+ delete debouncedCommitByKeyRef.current[key];
63
+ setDraftValues(prev => {
64
+ const _a = prev, _b = key, _ = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
65
+ return rest;
66
+ });
17
67
  updateSelectedQueryables(key, null);
18
68
  }
19
69
  else {
@@ -105,9 +155,7 @@ export function QueryableComboBox({ queryables, selectedQueryables, updateSelect
105
155
  };
106
156
  return (React.createElement(SingleDatePicker, { date: parseDate(currentValue), onDateChange: handleDateChange, dateFormat: "P", showIcon: true, placeholder: "Select date", className: "jgis-queryable-combo-input jgis-queryable-combo-input-date-picker" }));
107
157
  }
108
- return (React.createElement(Input, { type: "text", className: "jgis-queryable-combo-input",
109
- // style={{borderRadius: 0}}
110
- value: currentValue || '', onChange: e => onChange(e.target.value) }));
158
+ return (React.createElement(Input, { type: "text", value: currentValue || '', onChange: e => onChange(e.target.value) }));
111
159
  case 'number':
112
160
  case 'integer':
113
161
  if (val.enum) {
@@ -154,26 +202,19 @@ export function QueryableComboBox({ queryables, selectedQueryables, updateSelect
154
202
  operator: ((_b = operators[0]) === null || _b === void 0 ? void 0 : _b.value) || '=',
155
203
  inputValue: undefined,
156
204
  };
205
+ const inputValue = draftValues[key] !== undefined
206
+ ? draftValues[key]
207
+ : currentFilter.inputValue;
157
208
  const handleInputChange = (value) => {
158
- // For datetime values, convert local time to UTC ISO string
159
- let valueToStore = value;
160
- if (val.type === 'string' &&
161
- val.format === 'date-time' &&
162
- typeof value === 'string') {
163
- try {
164
- // Parse local time and convert to UTC ISO string
165
- const localDate = new Date(value);
166
- valueToStore = localDate.toISOString();
167
- }
168
- catch (_a) {
169
- valueToStore = value;
170
- }
171
- }
172
- updateSelectedQueryables(key, Object.assign(Object.assign({}, currentFilter), { inputValue: valueToStore }));
209
+ const normalizedValue = normalizeInputValue(val, value);
210
+ setDraftValues(prev => (Object.assign(Object.assign({}, prev), { [key]: normalizedValue })));
211
+ // Uses a stable per-field debounced function
212
+ // inline debounce would recreate each render and reset its timer
213
+ scheduleQueryableCommit(key, normalizedValue);
173
214
  };
174
215
  const handleOperatorChange = (operator) => {
175
216
  updateSelectedQueryables(key, Object.assign(Object.assign({}, currentFilter), { operator }));
176
217
  };
177
- return (React.createElement(QueryableRow, { key: key, qKey: key, qVal: val, operators: operators, currentFilter: currentFilter, inputComponent: getInputBasedOnType(val, currentFilter.inputValue, handleInputChange), onOperatorChange: handleOperatorChange }));
218
+ return (React.createElement(QueryableRow, { key: key, qKey: key, qVal: val, operators: operators, currentFilter: currentFilter, inputComponent: getInputBasedOnType(val, inputValue, handleInputChange), onOperatorChange: handleOperatorChange }));
178
219
  }))));
179
220
  }
@@ -14,7 +14,7 @@ export declare function useStacFilterExtension({ model, baseUrl, limit, }: IUseS
14
14
  queryableFields: IStacQueryables | undefined;
15
15
  collections: FilteredCollection[];
16
16
  selectedCollection: string;
17
- setSelectedCollection: import("react").Dispatch<import("react").SetStateAction<string>>;
17
+ setSelectedCollection: (nextSelectedCollection: string) => void;
18
18
  handleSubmit: () => Promise<void>;
19
19
  startTime: Date | undefined;
20
20
  endTime: Date | undefined;
@@ -10,13 +10,15 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { endOfToday, startOfToday } from 'date-fns';
13
- import { useCallback, useEffect, useState } from 'react';
13
+ import { useCallback, useEffect, useRef, useState } from 'react';
14
14
  import useIsFirstRender from "../../shared/hooks/useIsFirstRender";
15
15
  import { useStacResultsContext } from "../context/StacResultsContext";
16
16
  import { useStacSearch } from "./useStacSearch";
17
17
  import { GlobalStateDbManager } from "../../store";
18
18
  import { fetchWithProxies } from "../../tools";
19
19
  const STAC_FILTER_EXTENSION_STATE_KEY = 'jupytergis:stac-filter-extension-state';
20
+ const STAC_COLLECTIONS_CACHE_STATE_KEY = 'jupytergis:stac-collections-cache';
21
+ const STAC_QUERYABLES_CACHE_STATE_KEY = 'jupytergis:stac-queryables-cache';
20
22
  /**
21
23
  * Hook for searching STAC catalogs that support the Filter Extension (CQL2-JSON).
22
24
  * Fetches collections and queryables, and builds filter queries using the STAC Filter Extension.
@@ -25,7 +27,7 @@ export function useStacFilterExtension({ model, baseUrl, limit = 12, }) {
25
27
  const isFirstRender = useIsFirstRender();
26
28
  const { registerBuildQuery, executeQuery } = useStacResultsContext();
27
29
  // Get temporal/spatial filters from useStacSearch
28
- const { startTime, endTime, setStartTime, setEndTime, currentBBox, useWorldBBox, setUseWorldBBox, } = useStacSearch({
30
+ const { startTime, endTime, setStartTime, setEndTime, currentBBox, useWorldBBox, setUseWorldBBox, hasLoadedInitialSearchState, } = useStacSearch({
29
31
  model,
30
32
  });
31
33
  const [queryableFields, setQueryableFields] = useState();
@@ -33,30 +35,132 @@ export function useStacFilterExtension({ model, baseUrl, limit = 12, }) {
33
35
  const [selectedCollection, setSelectedCollection] = useState('');
34
36
  const [selectedQueryables, setSelectedQueryables] = useState({});
35
37
  const [filterOperator, setFilterOperator] = useState('and');
38
+ const hasLoadedInitialFilterStateRef = useRef(false);
39
+ const hasLoadedInitialQueryablesRef = useRef(false);
40
+ /** Last auto-search request; skips duplicate consecutive fetches (React churn). */
41
+ const lastAutoQueryKeyRef = useRef(null);
36
42
  const stateDb = GlobalStateDbManager.getInstance().getStateDb();
43
+ const getCollectionsCacheKey = useCallback(() => `${STAC_COLLECTIONS_CACHE_STATE_KEY}:${baseUrl}`, [baseUrl]);
44
+ const getQueryablesCacheKey = useCallback(() => `${STAC_QUERYABLES_CACHE_STATE_KEY}:${baseUrl}:${selectedCollection}`, [baseUrl, selectedCollection]);
45
+ const updateSelectedQueryables = useCallback((qKey, filter) => {
46
+ setSelectedQueryables(prev => {
47
+ // If filter is null, remove the key entirely
48
+ if (filter === null) {
49
+ const _a = prev, _b = qKey, _ = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
50
+ return rest;
51
+ }
52
+ // If inputValue is undefined but filter exists, keep it (user might be
53
+ // entering value). Only remove if explicitly set to null.
54
+ return Object.assign(Object.assign({}, prev), { [qKey]: filter });
55
+ });
56
+ }, []);
57
+ const handleSelectedCollectionChange = useCallback((nextSelectedCollection) => {
58
+ if (selectedCollection !== '' &&
59
+ nextSelectedCollection !== '' &&
60
+ selectedCollection !== nextSelectedCollection) {
61
+ setSelectedQueryables({});
62
+ setQueryableFields(undefined);
63
+ setFilterOperator('and');
64
+ }
65
+ setSelectedCollection(nextSelectedCollection);
66
+ }, [selectedCollection]);
67
+ const buildQuery = useCallback(() => {
68
+ const st = startTime
69
+ ? startTime.toISOString()
70
+ : startOfToday().toISOString();
71
+ const et = endTime ? endTime.toISOString() : endOfToday().toISOString();
72
+ // Build filter object from selectedQueryables
73
+ const filterConditions = Object.entries(selectedQueryables)
74
+ .filter(([, filter]) => filter.inputValue !== undefined)
75
+ .map(([property, filter]) => {
76
+ var _a, _b;
77
+ // Check if this property is a datetime type
78
+ const queryableField = queryableFields === null || queryableFields === void 0 ? void 0 : queryableFields.find(([key]) => key === property);
79
+ const isDateTime = queryableField &&
80
+ ((_a = queryableField[1]) === null || _a === void 0 ? void 0 : _a.type) === 'string' &&
81
+ ((_b = queryableField[1]) === null || _b === void 0 ? void 0 : _b.format) === 'date-time';
82
+ // For datetime values, wrap in timestamp object; otherwise use value directly
83
+ const value = isDateTime
84
+ ? { timestamp: filter.inputValue }
85
+ : filter.inputValue;
86
+ const condition = {
87
+ op: filter.operator,
88
+ args: [{ property }, value],
89
+ };
90
+ return condition;
91
+ });
92
+ const body = {
93
+ bbox: currentBBox,
94
+ collections: [selectedCollection],
95
+ datetime: `${st}/${et}`,
96
+ limit,
97
+ 'filter-lang': 'cql2-json',
98
+ };
99
+ // Only add filter if there are any conditions
100
+ if (filterConditions.length > 0) {
101
+ body.filter = {
102
+ op: filterOperator,
103
+ args: filterConditions,
104
+ };
105
+ }
106
+ return body;
107
+ }, [
108
+ startTime,
109
+ endTime,
110
+ currentBBox,
111
+ selectedCollection,
112
+ limit,
113
+ selectedQueryables,
114
+ filterOperator,
115
+ queryableFields,
116
+ ]);
117
+ // Register buildQuery with context
118
+ useEffect(() => {
119
+ registerBuildQuery(() => buildQuery());
120
+ }, [registerBuildQuery, buildQuery, baseUrl]);
121
+ const handleSubmit = useCallback(async () => {
122
+ if (!model) {
123
+ return;
124
+ }
125
+ // Build query body and execute query
126
+ const queryBody = buildQuery();
127
+ const searchUrl = baseUrl.endsWith('/')
128
+ ? `${baseUrl}search`
129
+ : `${baseUrl}/search`;
130
+ lastAutoQueryKeyRef.current = JSON.stringify({
131
+ searchUrl,
132
+ queryBody,
133
+ });
134
+ await executeQuery(queryBody, searchUrl);
135
+ }, [model, buildQuery, baseUrl]);
37
136
  // On mount, load saved filter state from StateDB (if present)
38
137
  useEffect(() => {
39
138
  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);
139
+ hasLoadedInitialFilterStateRef.current = false;
140
+ try {
141
+ const savedFilterState = (await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.fetch(STAC_FILTER_EXTENSION_STATE_KEY)));
142
+ if (savedFilterState) {
143
+ if (savedFilterState.selectedCollection) {
144
+ handleSelectedCollectionChange(savedFilterState.selectedCollection);
145
+ }
146
+ if (savedFilterState.queryableFilters) {
147
+ const restoredFilters = {};
148
+ Object.entries(savedFilterState.queryableFilters).forEach(([key, filter]) => {
149
+ restoredFilters[key] = {
150
+ operator: filter.operator,
151
+ inputValue: filter.inputValue === null ? undefined : filter.inputValue,
152
+ };
153
+ });
154
+ setSelectedQueryables(restoredFilters);
155
+ }
156
+ if (savedFilterState.filterOperator) {
157
+ setFilterOperator(savedFilterState.filterOperator);
158
+ }
58
159
  }
59
160
  }
161
+ finally {
162
+ hasLoadedInitialFilterStateRef.current = true;
163
+ }
60
164
  }
61
165
  loadFilterExtensionStateFromDb();
62
166
  }, [stateDb]);
@@ -84,6 +188,8 @@ export function useStacFilterExtension({ model, baseUrl, limit = 12, }) {
84
188
  }, [selectedCollection, selectedQueryables, filterOperator, stateDb]);
85
189
  // Reset all state when URL changes
86
190
  useEffect(() => {
191
+ lastAutoQueryKeyRef.current = null;
192
+ hasLoadedInitialQueryablesRef.current = false;
87
193
  setQueryableFields(undefined);
88
194
  setCollections([]);
89
195
  setSelectedCollection('');
@@ -96,14 +202,37 @@ export function useStacFilterExtension({ model, baseUrl, limit = 12, }) {
96
202
  return;
97
203
  }
98
204
  const fetchCollections = async () => {
205
+ var _a;
99
206
  if (!baseUrl) {
100
207
  return;
101
208
  }
209
+ const cachedCollections = (await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.fetch(getCollectionsCacheKey())));
210
+ if (cachedCollections && cachedCollections.length > 0) {
211
+ setCollections(cachedCollections);
212
+ if (hasLoadedInitialQueryablesRef.current &&
213
+ selectedCollection === '') {
214
+ handleSelectedCollectionChange(cachedCollections[0].id);
215
+ }
216
+ return;
217
+ }
102
218
  const collectionsUrl = baseUrl.endsWith('/')
103
219
  ? `${baseUrl}collections`
104
220
  : `${baseUrl}/collections`;
105
- const data = await fetchWithProxies(collectionsUrl, model, async (response) => await response.json(), undefined);
106
- const collections = data.collections
221
+ const allCollections = [];
222
+ let nextUrl = collectionsUrl;
223
+ while (nextUrl) {
224
+ const page = await fetchWithProxies(nextUrl, model, async (response) => await response.json(), undefined);
225
+ if (!page) {
226
+ break;
227
+ }
228
+ allCollections.push(...page.collections);
229
+ const currentPageUrl = nextUrl;
230
+ const nextLinkHref = (_a = page.links.find(link => link.rel === 'next')) === null || _a === void 0 ? void 0 : _a.href;
231
+ nextUrl = nextLinkHref
232
+ ? new URL(nextLinkHref, currentPageUrl).toString()
233
+ : null;
234
+ }
235
+ const collections = allCollections
107
236
  .map((collection) => {
108
237
  var _a;
109
238
  return ({
@@ -118,13 +247,23 @@ export function useStacFilterExtension({ model, baseUrl, limit = 12, }) {
118
247
  return titleA.localeCompare(titleB);
119
248
  });
120
249
  setCollections(collections);
250
+ await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.save(getCollectionsCacheKey(), collections));
121
251
  // Set first collection as default if one isn't loaded
122
- if (collections.length > 0 && !(selectedCollection === '')) {
123
- setSelectedCollection(collections[0].id);
252
+ if (hasLoadedInitialQueryablesRef.current &&
253
+ collections.length > 0 &&
254
+ !(selectedCollection === '')) {
255
+ handleSelectedCollectionChange(collections[0].id);
124
256
  }
125
257
  };
126
258
  fetchCollections();
127
- }, [model, baseUrl]);
259
+ }, [
260
+ model,
261
+ baseUrl,
262
+ stateDb,
263
+ selectedCollection,
264
+ getCollectionsCacheKey,
265
+ handleSelectedCollectionChange,
266
+ ]);
128
267
  // for queryables
129
268
  // ! TODO - support multiple collection selections
130
269
  useEffect(() => {
@@ -132,102 +271,46 @@ export function useStacFilterExtension({ model, baseUrl, limit = 12, }) {
132
271
  return;
133
272
  }
134
273
  const fetchQueryables = async () => {
135
- if (!baseUrl) {
274
+ if (!baseUrl || selectedCollection === '') {
275
+ return;
276
+ }
277
+ hasLoadedInitialQueryablesRef.current = false;
278
+ const cachedQueryables = (await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.fetch(getQueryablesCacheKey())));
279
+ if (cachedQueryables !== undefined) {
280
+ setQueryableFields(Object.entries(cachedQueryables));
281
+ hasLoadedInitialQueryablesRef.current = true;
136
282
  return;
137
283
  }
138
284
  const queryablesUrl = baseUrl.endsWith('/')
139
- ? `${baseUrl}queryables`
140
- : `${baseUrl}/queryables`;
285
+ ? `${baseUrl}collections/${encodeURIComponent(selectedCollection)}/queryables`
286
+ : `${baseUrl}/collections/${encodeURIComponent(selectedCollection)}/queryables`;
141
287
  const data = await fetchWithProxies(queryablesUrl, model, async (response) => await response.json(), undefined);
142
- setQueryableFields(Object.entries(data.properties));
288
+ const queryableProperties = data.properties;
289
+ setQueryableFields(Object.entries(queryableProperties));
290
+ await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.save(getQueryablesCacheKey(), queryableProperties));
291
+ hasLoadedInitialQueryablesRef.current = true;
143
292
  };
144
293
  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]);
294
+ }, [model, baseUrl, selectedCollection, stateDb, getQueryablesCacheKey]);
223
295
  // Handle search when filters change
224
296
  useEffect(() => {
225
- if (model && !isFirstRender && selectedCollection !== '') {
297
+ const hasLoadedInitialFilterState = hasLoadedInitialFilterStateRef.current &&
298
+ hasLoadedInitialQueryablesRef.current;
299
+ if (model &&
300
+ !isFirstRender &&
301
+ selectedCollection !== '' &&
302
+ hasLoadedInitialFilterState &&
303
+ hasLoadedInitialSearchState) {
226
304
  const queryBody = buildQuery();
227
305
  const searchUrl = baseUrl.endsWith('/')
228
306
  ? `${baseUrl}search`
229
307
  : `${baseUrl}/search`;
230
- executeQuery(queryBody, searchUrl);
308
+ const autoQueryKey = JSON.stringify({ searchUrl, queryBody });
309
+ if (lastAutoQueryKeyRef.current === autoQueryKey) {
310
+ return;
311
+ }
312
+ lastAutoQueryKeyRef.current = autoQueryKey;
313
+ void executeQuery(queryBody, searchUrl);
231
314
  }
232
315
  }, [
233
316
  model,
@@ -235,18 +318,19 @@ export function useStacFilterExtension({ model, baseUrl, limit = 12, }) {
235
318
  selectedCollection,
236
319
  selectedQueryables,
237
320
  filterOperator,
321
+ queryableFields,
238
322
  startTime,
239
323
  endTime,
240
324
  currentBBox,
241
325
  buildQuery,
242
- executeQuery,
243
326
  baseUrl,
327
+ hasLoadedInitialSearchState,
244
328
  ]);
245
329
  return {
246
330
  queryableFields,
247
331
  collections,
248
332
  selectedCollection,
249
- setSelectedCollection,
333
+ setSelectedCollection: handleSelectedCollectionChange,
250
334
  handleSubmit,
251
335
  startTime,
252
336
  endTime,
@@ -11,6 +11,7 @@ interface IUseStacSearchReturn {
11
11
  setCurrentBBox: (bbox: [number, number, number, number]) => void;
12
12
  useWorldBBox: boolean;
13
13
  setUseWorldBBox: (val: boolean) => void;
14
+ hasLoadedInitialSearchState: boolean;
14
15
  }
15
16
  /**
16
17
  * Base hook for managing STAC search - handles temporal/spatial filters
@@ -9,22 +9,29 @@ export function useStacSearch({ model, }) {
9
9
  const [endTime, setEndTime] = useState(undefined);
10
10
  const [currentBBox, setCurrentBBox] = useState([-180, -90, 180, 90]);
11
11
  const [useWorldBBox, setUseWorldBBox] = useState(false);
12
+ const [hasLoadedInitialSearchState, setHasLoadedInitialSearchState] = useState(false);
12
13
  const stateDb = GlobalStateDbManager.getInstance().getStateDb();
13
14
  // Load saved state from StateDB on mount
14
15
  useEffect(() => {
15
16
  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);
17
+ setHasLoadedInitialSearchState(false);
18
+ try {
19
+ const savedState = (await (stateDb === null || stateDb === void 0 ? void 0 : stateDb.fetch(STAC_SEARCH_STATE_KEY)));
20
+ if (savedState) {
21
+ if (savedState.startTime) {
22
+ setStartTime(new Date(savedState.startTime));
23
+ }
24
+ if (savedState.endTime) {
25
+ setEndTime(new Date(savedState.endTime));
26
+ }
27
+ if (savedState.useWorldBBox !== undefined) {
28
+ setUseWorldBBox(savedState.useWorldBBox);
29
+ }
26
30
  }
27
31
  }
32
+ finally {
33
+ setHasLoadedInitialSearchState(true);
34
+ }
28
35
  }
29
36
  loadStacSearchStateFromDb();
30
37
  }, [stateDb]);
@@ -63,5 +70,6 @@ export function useStacSearch({ model, }) {
63
70
  setCurrentBBox,
64
71
  useWorldBBox,
65
72
  setUseWorldBBox,
73
+ hasLoadedInitialSearchState,
66
74
  };
67
75
  }
package/lib/tools.d.ts CHANGED
@@ -27,7 +27,7 @@ export declare function getLayerTileInfo(tileUrl: string, mapOptions: Pick<IJGIS
27
27
  export interface IParsedStyle {
28
28
  fillColor: string;
29
29
  strokeColor: string;
30
- strokeWidth: number;
30
+ strokeWidth: string;
31
31
  joinStyle: string;
32
32
  capStyle: string;
33
33
  radius?: number;
package/lib/tools.js CHANGED
@@ -6,6 +6,7 @@ import { compressors } from 'hyparquet-compressors';
6
6
  import Protobuf from 'pbf';
7
7
  import shp from 'shpjs';
8
8
  import LAYER_GALLERY from "../layer_gallery.json";
9
+ import { DEFAULT_STROKE_WIDTH } from "./dialogs/symbology/colorRampUtils";
9
10
  export const debounce = (func, timeout = 100) => {
10
11
  let timeoutId;
11
12
  return (...args) => {
@@ -219,7 +220,7 @@ export function parseColor(style) {
219
220
  radius: (_a = style['circle-radius']) !== null && _a !== void 0 ? _a : 5,
220
221
  fillColor: (_c = (_b = style['circle-fill-color']) !== null && _b !== void 0 ? _b : style['fill-color']) !== null && _c !== void 0 ? _c : '#3399CC',
221
222
  strokeColor: (_e = (_d = style['circle-stroke-color']) !== null && _d !== void 0 ? _d : style['stroke-color']) !== null && _e !== void 0 ? _e : '#3399CC',
222
- strokeWidth: (_g = (_f = style['circle-stroke-width']) !== null && _f !== void 0 ? _f : style['stroke-width']) !== null && _g !== void 0 ? _g : 1.25,
223
+ strokeWidth: String((_g = (_f = style['circle-stroke-width']) !== null && _f !== void 0 ? _f : style['stroke-width']) !== null && _g !== void 0 ? _g : DEFAULT_STROKE_WIDTH),
223
224
  joinStyle: (_j = (_h = style['circle-stroke-line-join']) !== null && _h !== void 0 ? _h : style['stroke-line-join']) !== null && _j !== void 0 ? _j : 'round',
224
225
  capStyle: (_l = (_k = style['circle-stroke-line-cap']) !== null && _k !== void 0 ? _k : style['stroke-line-cap']) !== null && _l !== void 0 ? _l : 'round',
225
226
  };
@@ -767,8 +768,7 @@ export const getNumericFeatureAttributes = (featureProperties) => {
767
768
  */
768
769
  export const getColorCodeFeatureAttributes = (featureProperties) => {
769
770
  return getFeatureAttributes(featureProperties, (_, value) => {
770
- const regex = new RegExp('^#[0-9a-f]{6}$');
771
- return typeof value === 'string' && regex.test(value);
771
+ return typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value);
772
772
  });
773
773
  };
774
774
  export function downloadFile(content, fileName, mimeType) {