@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.
Files changed (48) hide show
  1. package/lib/commands/BaseCommandIDs.d.ts +1 -0
  2. package/lib/commands/BaseCommandIDs.js +1 -0
  3. package/lib/commands/index.js +28 -9
  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/types/Canonical.js +70 -5
  21. package/lib/dialogs/symbology/vector_layer/types/Categorized.js +81 -34
  22. package/lib/dialogs/symbology/vector_layer/types/Graduated.js +155 -43
  23. package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.js +31 -16
  24. package/lib/formbuilder/formselectors.js +4 -1
  25. package/lib/formbuilder/objectform/components/WmsTileSourceUrlInput.d.ts +3 -0
  26. package/lib/formbuilder/objectform/components/WmsTileSourceUrlInput.js +84 -0
  27. package/lib/formbuilder/objectform/source/index.d.ts +1 -0
  28. package/lib/formbuilder/objectform/source/index.js +1 -0
  29. package/lib/formbuilder/objectform/source/wmsTileSource.d.ts +4 -0
  30. package/lib/formbuilder/objectform/source/wmsTileSource.js +78 -0
  31. package/lib/formbuilder/objectform/useSchemaFormState.d.ts +1 -1
  32. package/lib/mainview/mainView.d.ts +3 -1
  33. package/lib/mainview/mainView.js +170 -23
  34. package/lib/menus.js +4 -0
  35. package/lib/panelview/components/layers.js +19 -2
  36. package/lib/panelview/components/legendItem.js +14 -4
  37. package/lib/stacBrowser/components/filter-extension/QueryableComboBox.js +60 -17
  38. package/lib/stacBrowser/hooks/useStacFilterExtension.d.ts +1 -1
  39. package/lib/stacBrowser/hooks/useStacFilterExtension.js +195 -111
  40. package/lib/stacBrowser/hooks/useStacSearch.d.ts +1 -0
  41. package/lib/stacBrowser/hooks/useStacSearch.js +18 -10
  42. package/lib/tools.d.ts +1 -1
  43. package/lib/tools.js +3 -3
  44. package/lib/types.d.ts +6 -0
  45. package/package.json +5 -2
  46. package/style/shared/tabs.css +2 -2
  47. package/style/storyPanel.css +2 -0
  48. 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
- 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) {
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.14.1",
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.14.1",
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",
@@ -17,7 +17,7 @@
17
17
  gap: 1rem;
18
18
  width: 100%;
19
19
  font-size: 9px;
20
- overflow-x: scroll;
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: scroll;
63
+ overflow-y: auto;
64
64
  /* max-height: 480px; */
65
65
  padding-top: 1rem;
66
66
  }
@@ -159,6 +159,8 @@
159
159
  top: unset;
160
160
  box-shadow: unset;
161
161
  max-height: unset;
162
+ border-bottom-right-radius: 0;
163
+ border-top-right-radius: 0;
162
164
  }
163
165
 
164
166
  .jgis-specta-story-panel-container {
@@ -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
+ }