@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.
- package/lib/commands/BaseCommandIDs.d.ts +1 -1
- package/lib/commands/BaseCommandIDs.js +1 -1
- package/lib/commands/index.js +28 -34
- package/lib/constants.js +1 -0
- package/lib/dialogs/symbology/classificationModes.js +12 -16
- package/lib/dialogs/symbology/colorRampUtils.d.ts +47 -3
- package/lib/dialogs/symbology/colorRampUtils.js +112 -13
- package/lib/dialogs/symbology/components/color_ramp/ColorRampSelector.js +6 -14
- package/lib/dialogs/symbology/components/color_ramp/ColorRampSelectorEntry.d.ts +2 -2
- package/lib/dialogs/symbology/components/color_ramp/ColorRampSelectorEntry.js +3 -11
- package/lib/dialogs/symbology/components/color_ramp/RgbaColorPicker.d.ts +13 -0
- package/lib/dialogs/symbology/components/color_ramp/RgbaColorPicker.js +98 -0
- package/lib/dialogs/symbology/components/color_stops/StopContainer.js +3 -1
- package/lib/dialogs/symbology/components/color_stops/StopRow.d.ts +1 -1
- package/lib/dialogs/symbology/components/color_stops/StopRow.js +12 -7
- package/lib/dialogs/symbology/symbologyDialog.d.ts +2 -1
- package/lib/dialogs/symbology/symbologyUtils.d.ts +2 -2
- package/lib/dialogs/symbology/symbologyUtils.js +58 -40
- package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +14 -2
- package/lib/dialogs/symbology/vector_layer/VectorRendering.js +6 -5
- package/lib/dialogs/symbology/vector_layer/components/ValueSelect.js +3 -1
- package/lib/dialogs/symbology/vector_layer/types/Canonical.js +70 -5
- package/lib/dialogs/symbology/vector_layer/types/Categorized.js +81 -34
- package/lib/dialogs/symbology/vector_layer/types/Graduated.js +155 -43
- package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.js +31 -16
- package/lib/formbuilder/formselectors.js +4 -1
- package/lib/formbuilder/objectform/components/WmsTileSourceUrlInput.d.ts +3 -0
- package/lib/formbuilder/objectform/components/WmsTileSourceUrlInput.js +84 -0
- package/lib/formbuilder/objectform/source/index.d.ts +1 -0
- package/lib/formbuilder/objectform/source/index.js +1 -0
- package/lib/formbuilder/objectform/source/wmsTileSource.d.ts +4 -0
- package/lib/formbuilder/objectform/source/wmsTileSource.js +78 -0
- package/lib/formbuilder/objectform/useSchemaFormState.d.ts +1 -1
- package/lib/mainview/mainView.d.ts +3 -1
- package/lib/mainview/mainView.js +170 -23
- package/lib/menus.js +4 -0
- package/lib/panelview/components/layers.js +19 -2
- package/lib/panelview/components/legendItem.js +14 -4
- package/lib/panelview/filter-panel/Filter.d.ts +3 -0
- package/lib/panelview/filter-panel/Filter.js +9 -9
- package/lib/panelview/leftpanel.js +0 -7
- package/lib/panelview/story-maps/SpectaPanel.js +2 -2
- package/lib/panelview/story-maps/StoryViewerPanel.d.ts +1 -2
- package/lib/panelview/story-maps/StoryViewerPanel.js +1 -1
- package/lib/panelview/story-maps/components/SpectaDesktopView.d.ts +2 -1
- package/lib/panelview/story-maps/components/SpectaDesktopView.js +4 -4
- package/lib/panelview/story-maps/hooks/useStoryMap.d.ts +1 -0
- package/lib/panelview/story-maps/hooks/useStoryMap.js +3 -0
- package/lib/stacBrowser/components/filter-extension/QueryableComboBox.js +61 -20
- package/lib/stacBrowser/hooks/useStacFilterExtension.d.ts +1 -1
- package/lib/stacBrowser/hooks/useStacFilterExtension.js +195 -111
- package/lib/stacBrowser/hooks/useStacSearch.d.ts +1 -0
- package/lib/stacBrowser/hooks/useStacSearch.js +18 -10
- package/lib/tools.d.ts +1 -1
- package/lib/tools.js +3 -3
- package/lib/types.d.ts +7 -1
- package/package.json +5 -2
- package/style/shared/button.css +2 -5
- package/style/shared/input.css +2 -2
- package/style/shared/tabs.css +2 -2
- package/style/storyPanel.css +7 -0
- 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
|
-
|
|
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",
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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,
|
|
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:
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
106
|
-
|
|
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 (
|
|
123
|
-
|
|
252
|
+
if (hasLoadedInitialQueryablesRef.current &&
|
|
253
|
+
collections.length > 0 &&
|
|
254
|
+
!(selectedCollection === '')) {
|
|
255
|
+
handleSelectedCollectionChange(collections[0].id);
|
|
124
256
|
}
|
|
125
257
|
};
|
|
126
258
|
fetchCollections();
|
|
127
|
-
}, [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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:
|
|
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 :
|
|
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
|
-
|
|
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) {
|