@jupytergis/base 0.14.1 → 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 -0
- package/lib/commands/BaseCommandIDs.js +1 -0
- package/lib/commands/index.js +28 -9
- 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/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/stacBrowser/components/filter-extension/QueryableComboBox.js +60 -17
- 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 +6 -0
- package/package.json +5 -2
- package/style/shared/tabs.css +2 -2
- package/style/storyPanel.css +2 -0
- package/style/symbologyDialog.css +45 -1
|
@@ -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) {
|
package/lib/types.d.ts
CHANGED
|
@@ -30,10 +30,16 @@ declare global {
|
|
|
30
30
|
declare const classificationModes: readonly ["quantile", "equal interval", "jenks", "pretty", "logarithmic", "continuous"];
|
|
31
31
|
export type ClassificationMode = (typeof classificationModes)[number];
|
|
32
32
|
export declare const SYMBOLOGY_VALID_LAYER_TYPES: string[];
|
|
33
|
+
export interface IWmsLayerInfo {
|
|
34
|
+
name: string;
|
|
35
|
+
title: string;
|
|
36
|
+
}
|
|
33
37
|
/** Form context passed to SchemaForm and custom fields. */
|
|
34
38
|
export interface IJupyterGISFormContext<TFormData = IDict | undefined> {
|
|
35
39
|
model: IJupyterGISModel;
|
|
36
40
|
formData: TFormData;
|
|
41
|
+
wmsAvailableLayers?: IWmsLayerInfo[];
|
|
42
|
+
setWmsAvailableLayers?: (layers: IWmsLayerInfo[]) => void;
|
|
37
43
|
formSchemaRegistry?: IJGISFormSchemaRegistry;
|
|
38
44
|
}
|
|
39
45
|
/** Optional form state (schema, extraErrors). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupytergis/base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "A JupyterLab extension for 3D modelling.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"clean:all": "jlpm run clean:lib",
|
|
38
38
|
"watch": "tspc -w"
|
|
39
39
|
},
|
|
40
|
+
"packageManager": "yarn@3.5.0",
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
|
42
43
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"@jupyter/collaboration": "^4",
|
|
45
46
|
"@jupyter/react-components": "^0.16.6",
|
|
46
47
|
"@jupyter/ydoc": "^2.0.0 || ^3.0.0",
|
|
47
|
-
"@jupytergis/schema": "^0.
|
|
48
|
+
"@jupytergis/schema": "^0.15.0",
|
|
48
49
|
"@jupyterlab/application": "^4.3.0",
|
|
49
50
|
"@jupyterlab/apputils": "^4.3.0",
|
|
50
51
|
"@jupyterlab/completer": "^4.3.0",
|
|
@@ -71,6 +72,7 @@
|
|
|
71
72
|
"clsx": "^2.1.1",
|
|
72
73
|
"cmdk": "^1.1.1",
|
|
73
74
|
"colormap": "^2.3.2",
|
|
75
|
+
"d3-scale-chromatic": "^3.0.0",
|
|
74
76
|
"date-fns": "^4.1.0",
|
|
75
77
|
"gdal3.js": "^2.8.1",
|
|
76
78
|
"geojson-vt": "^4.0.2",
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
"proj4-list": "1.0.4",
|
|
88
90
|
"radix-ui": "^1.4.3",
|
|
89
91
|
"react": "^18.0.1",
|
|
92
|
+
"react-colorful": "^5.6.1",
|
|
90
93
|
"react-day-picker": "^9.7.0",
|
|
91
94
|
"react-markdown": "^10.1.0",
|
|
92
95
|
"shpjs": "^6.1.0",
|
package/style/shared/tabs.css
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
gap: 1rem;
|
|
18
18
|
width: 100%;
|
|
19
19
|
font-size: 9px;
|
|
20
|
-
overflow-x:
|
|
20
|
+
overflow-x: auto;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
.jgis-tabs-list:active {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
.jgis-tabs-content {
|
|
61
61
|
outline: none;
|
|
62
62
|
width: 100%;
|
|
63
|
-
overflow-y:
|
|
63
|
+
overflow-y: auto;
|
|
64
64
|
/* max-height: 480px; */
|
|
65
65
|
padding-top: 1rem;
|
|
66
66
|
}
|
package/style/storyPanel.css
CHANGED
|
@@ -27,7 +27,8 @@ select option {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
.jp-gis-symbology-row > .jp-select-wrapper,
|
|
30
|
-
.jp-gis-symbology-row > .jp-mod-styled
|
|
30
|
+
.jp-gis-symbology-row > .jp-mod-styled,
|
|
31
|
+
.jp-gis-symbology-row > .jp-gis-rgba-picker {
|
|
31
32
|
flex: 1 0 50%;
|
|
32
33
|
max-width: 50%;
|
|
33
34
|
}
|
|
@@ -228,3 +229,46 @@ select option {
|
|
|
228
229
|
.jp-gis-selected-entry {
|
|
229
230
|
width: 100%;
|
|
230
231
|
}
|
|
232
|
+
|
|
233
|
+
.jp-gis-rgba-inputs {
|
|
234
|
+
display: flex;
|
|
235
|
+
flex-wrap: wrap;
|
|
236
|
+
gap: 4px;
|
|
237
|
+
margin-top: 6px;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.jp-gis-rgba-field {
|
|
241
|
+
display: flex;
|
|
242
|
+
flex-direction: column;
|
|
243
|
+
align-items: center;
|
|
244
|
+
flex: 0 0 auto;
|
|
245
|
+
gap: 2px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.jp-gis-rgba-field label {
|
|
249
|
+
font-size: var(--jp-ui-font-size0);
|
|
250
|
+
color: var(--jp-ui-font-color2);
|
|
251
|
+
line-height: 1;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.jp-gis-rgba-field input {
|
|
255
|
+
width: 4ch;
|
|
256
|
+
text-align: center;
|
|
257
|
+
padding: 1px 2px;
|
|
258
|
+
-moz-appearance: textfield;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.jp-gis-rgba-field input::-webkit-outer-spin-button,
|
|
262
|
+
.jp-gis-rgba-field input::-webkit-inner-spin-button {
|
|
263
|
+
-webkit-appearance: none;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.jp-gis-transparent-label {
|
|
267
|
+
display: flex;
|
|
268
|
+
align-items: center;
|
|
269
|
+
gap: 4px;
|
|
270
|
+
margin-top: 6px;
|
|
271
|
+
font-size: var(--jp-ui-font-size1);
|
|
272
|
+
cursor: pointer;
|
|
273
|
+
user-select: none;
|
|
274
|
+
}
|