@jupytergis/base 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/lib/commands/BaseCommandIDs.d.ts +1 -1
  2. package/lib/commands/BaseCommandIDs.js +1 -1
  3. package/lib/commands/index.js +28 -34
  4. package/lib/constants.js +1 -0
  5. package/lib/dialogs/symbology/classificationModes.js +12 -16
  6. package/lib/dialogs/symbology/colorRampUtils.d.ts +47 -3
  7. package/lib/dialogs/symbology/colorRampUtils.js +112 -13
  8. package/lib/dialogs/symbology/components/color_ramp/ColorRampSelector.js +6 -14
  9. package/lib/dialogs/symbology/components/color_ramp/ColorRampSelectorEntry.d.ts +2 -2
  10. package/lib/dialogs/symbology/components/color_ramp/ColorRampSelectorEntry.js +3 -11
  11. package/lib/dialogs/symbology/components/color_ramp/RgbaColorPicker.d.ts +13 -0
  12. package/lib/dialogs/symbology/components/color_ramp/RgbaColorPicker.js +98 -0
  13. package/lib/dialogs/symbology/components/color_stops/StopContainer.js +3 -1
  14. package/lib/dialogs/symbology/components/color_stops/StopRow.d.ts +1 -1
  15. package/lib/dialogs/symbology/components/color_stops/StopRow.js +12 -7
  16. package/lib/dialogs/symbology/symbologyDialog.d.ts +2 -1
  17. package/lib/dialogs/symbology/symbologyUtils.d.ts +2 -2
  18. package/lib/dialogs/symbology/symbologyUtils.js +58 -40
  19. package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +14 -2
  20. package/lib/dialogs/symbology/vector_layer/VectorRendering.js +6 -5
  21. package/lib/dialogs/symbology/vector_layer/components/ValueSelect.js +3 -1
  22. package/lib/dialogs/symbology/vector_layer/types/Canonical.js +70 -5
  23. package/lib/dialogs/symbology/vector_layer/types/Categorized.js +81 -34
  24. package/lib/dialogs/symbology/vector_layer/types/Graduated.js +155 -43
  25. package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.js +31 -16
  26. package/lib/formbuilder/formselectors.js +4 -1
  27. package/lib/formbuilder/objectform/components/WmsTileSourceUrlInput.d.ts +3 -0
  28. package/lib/formbuilder/objectform/components/WmsTileSourceUrlInput.js +84 -0
  29. package/lib/formbuilder/objectform/source/index.d.ts +1 -0
  30. package/lib/formbuilder/objectform/source/index.js +1 -0
  31. package/lib/formbuilder/objectform/source/wmsTileSource.d.ts +4 -0
  32. package/lib/formbuilder/objectform/source/wmsTileSource.js +78 -0
  33. package/lib/formbuilder/objectform/useSchemaFormState.d.ts +1 -1
  34. package/lib/mainview/mainView.d.ts +3 -1
  35. package/lib/mainview/mainView.js +170 -23
  36. package/lib/menus.js +4 -0
  37. package/lib/panelview/components/layers.js +19 -2
  38. package/lib/panelview/components/legendItem.js +14 -4
  39. package/lib/panelview/filter-panel/Filter.d.ts +3 -0
  40. package/lib/panelview/filter-panel/Filter.js +9 -9
  41. package/lib/panelview/leftpanel.js +0 -7
  42. package/lib/panelview/story-maps/SpectaPanel.js +2 -2
  43. package/lib/panelview/story-maps/StoryViewerPanel.d.ts +1 -2
  44. package/lib/panelview/story-maps/StoryViewerPanel.js +1 -1
  45. package/lib/panelview/story-maps/components/SpectaDesktopView.d.ts +2 -1
  46. package/lib/panelview/story-maps/components/SpectaDesktopView.js +4 -4
  47. package/lib/panelview/story-maps/hooks/useStoryMap.d.ts +1 -0
  48. package/lib/panelview/story-maps/hooks/useStoryMap.js +3 -0
  49. package/lib/stacBrowser/components/filter-extension/QueryableComboBox.js +61 -20
  50. package/lib/stacBrowser/hooks/useStacFilterExtension.d.ts +1 -1
  51. package/lib/stacBrowser/hooks/useStacFilterExtension.js +195 -111
  52. package/lib/stacBrowser/hooks/useStacSearch.d.ts +1 -0
  53. package/lib/stacBrowser/hooks/useStacSearch.js +18 -10
  54. package/lib/tools.d.ts +1 -1
  55. package/lib/tools.js +3 -3
  56. package/lib/types.d.ts +7 -1
  57. package/package.json +5 -2
  58. package/style/shared/button.css +2 -5
  59. package/style/shared/input.css +2 -2
  60. package/style/shared/tabs.css +2 -2
  61. package/style/storyPanel.css +7 -0
  62. package/style/symbologyDialog.css +45 -1
@@ -1,6 +1,9 @@
1
+ import { UUID } from '@lumino/coreutils';
1
2
  import React, { useEffect, useState } from 'react';
2
3
  import { VectorClassifications } from "../../classificationModes";
4
+ import { colorToRgba, DEFAULT_COLOR, DEFAULT_STROKE_WIDTH, getColorMapList, isColor, } from "../../colorRampUtils";
3
5
  import ColorRampControls from "../../components/color_ramp/ColorRampControls";
6
+ import RgbaColorPicker from "../../components/color_ramp/RgbaColorPicker";
4
7
  import StopContainer from "../../components/color_stops/StopContainer";
5
8
  import { useOkSignal } from "../../hooks/useOkSignal";
6
9
  import { saveSymbology, Utils, VectorUtils, } from "../../symbologyUtils";
@@ -19,15 +22,23 @@ const Graduated = ({ model, okSignalPromise, layerId, symbologyTab, selectableAt
19
22
  const [colorStopRows, setColorStopRows] = useState([]);
20
23
  const [radiusStopRows, setRadiusStopRows] = useState([]);
21
24
  const [colorRampOptions, setColorRampOptions] = useState();
25
+ const [fallbackColor, setFallbackColor] = useState([0, 0, 0, 0]);
26
+ const [strokeFollowsFill, setStrokeFollowsFill] = useState(false);
27
+ const fallbackColorRef = useLatest(fallbackColor);
28
+ const strokeFollowsFillRef = useLatest(strokeFollowsFill);
22
29
  const [colorManualStyle, setColorManualStyle] = useState({
23
- strokeColor: '#3399CC',
24
- strokeWidth: 1.25,
30
+ strokeColor: DEFAULT_COLOR,
31
+ strokeWidth: String(DEFAULT_STROKE_WIDTH),
25
32
  });
26
33
  const [radiusManualStyle, setRadiusManualStyle] = useState({
27
34
  radius: 5,
28
35
  });
36
+ const [vmin, setVmin] = useState('');
37
+ const [vmax, setVmax] = useState('');
29
38
  const selectableAttributeRef = useLatest(selectedAttribute);
30
39
  const symbologyTabRef = useLatest(symbologyTab);
40
+ const vminRef = useLatest(vmin);
41
+ const vmaxRef = useLatest(vmax);
31
42
  const colorStopRowsRef = useLatest(colorStopRows);
32
43
  const radiusStopRowsRef = useLatest(radiusStopRows);
33
44
  const colorRampOptionsRef = useLatest(colorRampOptions);
@@ -51,31 +62,54 @@ const Graduated = ({ model, okSignalPromise, layerId, symbologyTab, selectableAt
51
62
  updateStopRowsBasedOnLayer();
52
63
  }, []);
53
64
  useEffect(() => {
65
+ var _a, _b, _c, _d;
54
66
  if (params.color) {
55
67
  const strokeColor = params.color['stroke-color'];
56
68
  const circleStrokeColor = params.color['circle-stroke-color'];
57
- const isSimpleColor = (val) => typeof val === 'string' && /^#?[0-9A-Fa-f]{3,8}$/.test(val);
69
+ const effectiveStroke = isColor(strokeColor)
70
+ ? strokeColor
71
+ : isColor(circleStrokeColor)
72
+ ? circleStrokeColor
73
+ : DEFAULT_COLOR;
58
74
  setColorManualStyle({
59
- strokeColor: isSimpleColor(strokeColor)
60
- ? strokeColor
61
- : isSimpleColor(circleStrokeColor)
62
- ? circleStrokeColor
63
- : '#3399CC',
64
- strokeWidth: params.color['stroke-width'] ||
75
+ strokeColor: colorToRgba(effectiveStroke),
76
+ strokeWidth: String(params.color['stroke-width'] ||
65
77
  params.color['circle-stroke-width'] ||
66
- 1.25,
78
+ DEFAULT_STROKE_WIDTH),
67
79
  });
68
80
  setRadiusManualStyle({
69
81
  radius: params.color['circle-radius'] || 5,
70
82
  });
71
83
  }
84
+ setFallbackColor(colorToRgba((_b = (_a = params.symbologyState) === null || _a === void 0 ? void 0 : _a.fallbackColor) !== null && _b !== void 0 ? _b : [0, 0, 0, 0]));
85
+ setStrokeFollowsFill((_d = (_c = params.symbologyState) === null || _c === void 0 ? void 0 : _c.strokeFollowsFill) !== null && _d !== void 0 ? _d : false);
72
86
  }, [layerId]);
73
87
  useEffect(() => {
74
88
  var _a;
75
- const attribute = ((_a = params.symbologyState) === null || _a === void 0 ? void 0 : _a.value) ||
76
- Object.keys(selectableAttributesAndValues)[0];
89
+ const savedValue = (_a = params.symbologyState) === null || _a === void 0 ? void 0 : _a.value;
90
+ const attribute = savedValue && savedValue in selectableAttributesAndValues
91
+ ? savedValue
92
+ : Object.keys(selectableAttributesAndValues)[0];
77
93
  setSelectedAttribute(attribute);
78
94
  }, [selectableAttributesAndValues]);
95
+ useEffect(() => {
96
+ var _a, _b;
97
+ if (!selectedAttribute ||
98
+ !selectableAttributesAndValues[selectedAttribute]) {
99
+ return;
100
+ }
101
+ if (((_a = params.symbologyState) === null || _a === void 0 ? void 0 : _a.vmin) !== undefined) {
102
+ setVmin(String(params.symbologyState.vmin));
103
+ setVmax(String((_b = params.symbologyState.vmax) !== null && _b !== void 0 ? _b : ''));
104
+ return;
105
+ }
106
+ const values = Array.from(selectableAttributesAndValues[selectedAttribute]).filter(Number.isFinite);
107
+ if (values.length === 0) {
108
+ return;
109
+ }
110
+ setVmin(String(Math.min(...values)));
111
+ setVmax(String(Math.max(...values)));
112
+ }, [selectedAttribute]);
79
113
  const updateStopRowsBasedOnLayer = () => {
80
114
  if (!layer) {
81
115
  return;
@@ -88,27 +122,42 @@ const Graduated = ({ model, okSignalPromise, layerId, symbologyTab, selectableAt
88
122
  const newStyle = Object.assign({}, params.color);
89
123
  // Apply color symbology
90
124
  if (colorStopRowsRef.current.length > 0) {
91
- const colorExpr = [
125
+ const interpolateExpr = [
92
126
  'interpolate',
93
127
  ['linear'],
94
128
  ['get', selectableAttributeRef.current],
95
129
  ];
96
130
  colorStopRowsRef.current.forEach(stop => {
97
- colorExpr.push(stop.stop);
98
- colorExpr.push(stop.output);
131
+ interpolateExpr.push(stop.stop);
132
+ interpolateExpr.push(stop.output);
99
133
  });
134
+ // Wrap in case so features missing the attribute use the fallback color
135
+ // instead of causing OL to throw at render time.
136
+ const colorExpr = [
137
+ 'case',
138
+ ['has', selectableAttributeRef.current],
139
+ interpolateExpr,
140
+ fallbackColorRef.current,
141
+ ];
100
142
  newStyle['fill-color'] = colorExpr;
101
143
  newStyle['circle-fill-color'] = colorExpr;
102
- newStyle['stroke-color'] = colorExpr;
103
- newStyle['circle-stroke-color'] = colorExpr;
144
+ if (strokeFollowsFillRef.current) {
145
+ newStyle['stroke-color'] = colorExpr;
146
+ newStyle['circle-stroke-color'] = colorExpr;
147
+ }
148
+ else {
149
+ newStyle['stroke-color'] = colorManualStyleRef.current.strokeColor;
150
+ newStyle['circle-stroke-color'] =
151
+ colorManualStyleRef.current.strokeColor;
152
+ }
104
153
  }
105
154
  else {
106
155
  // use manual style
156
+ newStyle['stroke-color'] = colorManualStyleRef.current.strokeColor;
157
+ newStyle['circle-stroke-color'] = colorManualStyleRef.current.strokeColor;
107
158
  }
108
- newStyle['stroke-color'] = colorManualStyleRef.current.strokeColor;
109
- newStyle['circle-stroke-color'] = colorManualStyleRef.current.strokeColor;
110
- newStyle['stroke-width'] = colorManualStyleRef.current.strokeWidth;
111
- newStyle['circle-stroke-width'] = colorManualStyleRef.current.strokeWidth;
159
+ newStyle['stroke-width'] = Math.max(0, parseFloat(colorManualStyleRef.current.strokeWidth));
160
+ newStyle['circle-stroke-width'] = Math.max(0, parseFloat(colorManualStyleRef.current.strokeWidth));
112
161
  // Apply radius symbology
113
162
  if (radiusStopRowsRef.current.length > 0) {
114
163
  const radiusExpr = [
@@ -125,15 +174,9 @@ const Graduated = ({ model, okSignalPromise, layerId, symbologyTab, selectableAt
125
174
  else {
126
175
  newStyle['circle-radius'] = radiusManualStyleRef.current.radius;
127
176
  }
128
- const symbologyState = {
129
- renderType: 'Graduated',
130
- value: selectableAttributeRef.current,
131
- method: symbologyTabRef.current,
132
- colorRamp: (_a = colorRampOptionsRef.current) === null || _a === void 0 ? void 0 : _a.selectedRamp,
133
- nClasses: (_b = colorRampOptionsRef.current) === null || _b === void 0 ? void 0 : _b.numberOfShades,
134
- mode: (_c = colorRampOptionsRef.current) === null || _c === void 0 ? void 0 : _c.selectedMode,
135
- reverseRamp: (_d = colorRampOptionsRef.current) === null || _d === void 0 ? void 0 : _d.reverseRamp,
136
- };
177
+ const parsedVmin = parseFloat(vminRef.current);
178
+ const parsedVmax = parseFloat(vmaxRef.current);
179
+ const symbologyState = Object.assign(Object.assign({ renderType: 'Graduated', value: selectableAttributeRef.current, method: symbologyTabRef.current, colorRamp: (_a = colorRampOptionsRef.current) === null || _a === void 0 ? void 0 : _a.selectedRamp, nClasses: (_b = colorRampOptionsRef.current) === null || _b === void 0 ? void 0 : _b.numberOfShades, mode: (_c = colorRampOptionsRef.current) === null || _c === void 0 ? void 0 : _c.selectedMode, reverseRamp: (_d = colorRampOptionsRef.current) === null || _d === void 0 ? void 0 : _d.reverseRamp, fallbackColor: fallbackColorRef.current, strokeFollowsFill: strokeFollowsFillRef.current }, (Number.isFinite(parsedVmin) && { vmin: parsedVmin })), (Number.isFinite(parsedVmax) && { vmax: parsedVmax }));
137
180
  saveSymbology({
138
181
  model,
139
182
  layerId,
@@ -159,30 +202,73 @@ const Graduated = ({ model, okSignalPromise, layerId, symbologyTab, selectableAt
159
202
  reverseRamp,
160
203
  });
161
204
  let stops;
162
- const values = Array.from(selectableAttributesAndValues[selectedAttribute]);
205
+ if (!selectableAttributesAndValues[selectedAttribute]) {
206
+ return;
207
+ }
208
+ const allValues = Array.from(selectableAttributesAndValues[selectedAttribute]);
209
+ const parsed = (s) => {
210
+ const n = parseFloat(s);
211
+ return Number.isFinite(n) ? n : undefined;
212
+ };
213
+ const parsedVmin = parsed(vmin);
214
+ const parsedVmax = parsed(vmax);
215
+ const values = allValues.filter(v => {
216
+ if (!Number.isFinite(v)) {
217
+ return false;
218
+ }
219
+ if (parsedVmin !== undefined && v < parsedVmin) {
220
+ return false;
221
+ }
222
+ if (parsedVmax !== undefined && v > parsedVmax) {
223
+ return false;
224
+ }
225
+ return true;
226
+ });
227
+ const dataMin = Math.min(...values);
228
+ const dataMax = Math.max(...values);
229
+ const rangeMin = parsedVmin !== null && parsedVmin !== void 0 ? parsedVmin : dataMin;
230
+ const rangeMax = parsedVmax !== null && parsedVmax !== void 0 ? parsedVmax : dataMax;
231
+ const rangeValues = [rangeMin, rangeMax];
163
232
  switch (selectedMode) {
164
233
  case 'quantile':
165
234
  stops = VectorClassifications.calculateQuantileBreaks(values, numberOfShades);
166
235
  break;
167
236
  case 'equal interval':
168
- stops = VectorClassifications.calculateEqualIntervalBreaks(values, numberOfShades);
237
+ stops = VectorClassifications.calculateEqualIntervalBreaks(rangeValues, numberOfShades);
169
238
  break;
170
239
  case 'jenks':
171
240
  stops = VectorClassifications.calculateJenksBreaks(values, numberOfShades);
172
241
  break;
173
242
  case 'pretty':
174
- stops = VectorClassifications.calculatePrettyBreaks(values, numberOfShades);
243
+ stops = VectorClassifications.calculatePrettyBreaks(rangeValues, numberOfShades);
175
244
  break;
176
245
  case 'logarithmic':
177
- stops = VectorClassifications.calculateLogarithmicBreaks(values, numberOfShades);
246
+ stops = VectorClassifications.calculateLogarithmicBreaks(rangeValues, numberOfShades);
178
247
  break;
179
248
  default:
180
249
  console.warn('No mode selected');
181
250
  return;
182
251
  }
183
- const stopOutputPairs = symbologyTab === 'radius'
184
- ? stops.map(v => ({ stop: v, output: v }))
185
- : Utils.getValueColorPairs(stops, selectedRamp, numberOfShades, reverseRamp);
252
+ // Pin outer stops to the user-specified range for all modes.
253
+ // Range-based modes (equal interval, pretty, logarithmic) already receive
254
+ // rangeValues so their outer stops are correct; this clamp ensures
255
+ // data-driven modes (quantile, jenks) also honour vmin/vmax at the edges,
256
+ // which is useful e.g. for excluding outliers while keeping the ramp
257
+ // anchored to the chosen range.
258
+ if (stops.length > 0) {
259
+ stops[0] = rangeMin;
260
+ stops[stops.length - 1] = rangeMax;
261
+ }
262
+ const colorRamp = getColorMapList().find(c => c.name === selectedRamp);
263
+ const getStopOutputPairs = () => {
264
+ if (symbologyTab === 'radius') {
265
+ return stops.map(v => ({ id: UUID.uuid4(), stop: v, output: v }));
266
+ }
267
+ return colorRamp
268
+ ? Utils.getValueColorPairs(stops, colorRamp, numberOfShades, reverseRamp)
269
+ : [];
270
+ };
271
+ const stopOutputPairs = getStopOutputPairs();
186
272
  if (symbologyTab === 'radius') {
187
273
  setRadiusStopRows(stopOutputPairs);
188
274
  }
@@ -225,20 +311,46 @@ const Graduated = ({ model, okSignalPromise, layerId, symbologyTab, selectableAt
225
311
  "symbology."),
226
312
  React.createElement("div", { className: "jp-gis-symbology-row" },
227
313
  React.createElement("label", null, "Stroke Color:"),
228
- React.createElement("input", { type: "color", className: "jp-mod-styled", value: colorManualStyle.strokeColor, onChange: e => {
229
- setColorManualStyle(Object.assign(Object.assign({}, colorManualStyle), { strokeColor: e.target.value }));
230
- } })),
314
+ React.createElement("div", { style: {
315
+ display: 'flex',
316
+ alignItems: 'center',
317
+ gap: 8,
318
+ flex: '1 0 50%',
319
+ maxWidth: '50%',
320
+ } },
321
+ React.createElement("div", { style: {
322
+ opacity: strokeFollowsFill ? 0.3 : 1,
323
+ pointerEvents: strokeFollowsFill ? 'none' : 'auto',
324
+ } },
325
+ React.createElement(RgbaColorPicker, { color: colorManualStyle.strokeColor, onChange: color => setColorManualStyle(prev => (Object.assign(Object.assign({}, prev), { strokeColor: color }))) })),
326
+ React.createElement("label", { style: {
327
+ display: 'flex',
328
+ alignItems: 'center',
329
+ gap: 4,
330
+ whiteSpace: 'nowrap',
331
+ } },
332
+ React.createElement("input", { type: "checkbox", checked: strokeFollowsFill, onChange: e => setStrokeFollowsFill(e.target.checked) }),
333
+ "match fill"))),
231
334
  React.createElement("div", { className: "jp-gis-symbology-row" },
232
335
  React.createElement("label", null, "Stroke Width:"),
233
- React.createElement("input", { type: "number", className: "jp-mod-styled", value: colorManualStyle.strokeWidth, onChange: e => {
234
- setColorManualStyle(Object.assign(Object.assign({}, colorManualStyle), { strokeWidth: +e.target.value }));
235
- } })))),
336
+ React.createElement("input", { type: "text", className: "jp-mod-styled", value: colorManualStyle.strokeWidth, onChange: e => {
337
+ setColorManualStyle(Object.assign(Object.assign({}, colorManualStyle), { strokeWidth: e.target.value }));
338
+ } })),
339
+ React.createElement("div", { className: "jp-gis-symbology-row" },
340
+ React.createElement("label", null, "Fallback Color:"),
341
+ React.createElement(RgbaColorPicker, { color: fallbackColor, onChange: setFallbackColor })))),
236
342
  symbologyTab === 'radius' && (React.createElement("div", { className: "jp-gis-symbology-row" },
237
343
  React.createElement("label", null, "Circle Radius:"),
238
344
  React.createElement("input", { type: "number", className: "jp-mod-styled", value: radiusManualStyle.radius, onChange: e => {
239
345
  handleReset('radius');
240
346
  setRadiusManualStyle(Object.assign(Object.assign({}, radiusManualStyle), { radius: +e.target.value }));
241
347
  } })))),
348
+ React.createElement("div", { className: "jp-gis-symbology-row" },
349
+ React.createElement("label", null, "Min value:"),
350
+ React.createElement("input", { type: "text", className: "jp-mod-styled", placeholder: "auto", value: vmin, onChange: e => setVmin(e.target.value) })),
351
+ React.createElement("div", { className: "jp-gis-symbology-row" },
352
+ React.createElement("label", null, "Max value:"),
353
+ React.createElement("input", { type: "text", className: "jp-mod-styled", placeholder: "auto", value: vmax, onChange: e => setVmax(e.target.value) })),
242
354
  React.createElement(ColorRampControls, { layerParams: params, modeOptions: modeOptions, classifyFunc: buildColorInfoFromClassification, showModeRow: true, showRampSelector: symbologyTab === 'color' }),
243
355
  React.createElement(StopContainer, { selectedMethod: symbologyTab || 'color', stopRows: symbologyTab === 'color' ? colorStopRows : radiusStopRows, setStopRows: symbologyTab === 'color' ? setColorStopRows : setRadiusStopRows })));
244
356
  }
@@ -1,4 +1,7 @@
1
1
  import React, { useEffect, useState } from 'react';
2
+ import { colorToRgba, DEFAULT_COLOR, } from "../../colorRampUtils";
3
+ import { DEFAULT_STROKE_WIDTH } from "../../colorRampUtils";
4
+ import RgbaColorPicker from "../../components/color_ramp/RgbaColorPicker";
2
5
  import { useEffectiveSymbologyParams } from "../../hooks/useEffectiveSymbologyParams";
3
6
  import { useOkSignal } from "../../hooks/useOkSignal";
4
7
  import { saveSymbology, } from "../../symbologyUtils";
@@ -10,10 +13,14 @@ const SimpleSymbol = ({ model, okSignalPromise, layerId, symbologyTab, isStorySe
10
13
  joinStyle: 'round',
11
14
  strokeColor: '#3399CC',
12
15
  capStyle: 'round',
13
- strokeWidth: 1.25,
16
+ strokeWidth: String(DEFAULT_STROKE_WIDTH),
14
17
  radius: 5,
15
18
  });
16
19
  const styleRef = useLatest(style);
20
+ const [fillRgba, setFillRgba] = useState(DEFAULT_COLOR);
21
+ const [strokeRgba, setStrokeRgba] = useState(DEFAULT_COLOR);
22
+ const fillRgbaRef = useLatest(fillRgba);
23
+ const strokeRgbaRef = useLatest(strokeRgba);
17
24
  const layer = layerId !== undefined ? model.getLayer(layerId) : null;
18
25
  const params = useEffectiveSymbologyParams({
19
26
  model,
@@ -23,7 +30,7 @@ const SimpleSymbol = ({ model, okSignalPromise, layerId, symbologyTab, isStorySe
23
30
  segmentId,
24
31
  });
25
32
  useEffect(() => {
26
- var _a;
33
+ var _a, _b, _c;
27
34
  if (!params) {
28
35
  return;
29
36
  }
@@ -32,25 +39,33 @@ const SimpleSymbol = ({ model, okSignalPromise, layerId, symbologyTab, isStorySe
32
39
  if (parsed) {
33
40
  setStyle(parsed);
34
41
  }
42
+ const fillColor = (_b = params.color['circle-fill-color']) !== null && _b !== void 0 ? _b : params.color['fill-color'];
43
+ const strokeColor = (_c = params.color['circle-stroke-color']) !== null && _c !== void 0 ? _c : params.color['stroke-color'];
44
+ if (fillColor !== undefined) {
45
+ setFillRgba(colorToRgba(fillColor));
46
+ }
47
+ if (strokeColor !== undefined) {
48
+ setStrokeRgba(colorToRgba(strokeColor));
49
+ }
35
50
  }
36
51
  }, [params]);
37
52
  const handleOk = () => {
38
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
53
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
39
54
  if (!layerId || !(layer === null || layer === void 0 ? void 0 : layer.parameters)) {
40
55
  return;
41
56
  }
42
57
  const styleExpr = {
43
58
  'circle-radius': (_a = styleRef.current) === null || _a === void 0 ? void 0 : _a.radius,
44
- 'circle-fill-color': (_b = styleRef.current) === null || _b === void 0 ? void 0 : _b.fillColor,
45
- 'circle-stroke-color': (_c = styleRef.current) === null || _c === void 0 ? void 0 : _c.strokeColor,
46
- 'circle-stroke-width': (_d = styleRef.current) === null || _d === void 0 ? void 0 : _d.strokeWidth,
47
- 'circle-stroke-line-join': (_e = styleRef.current) === null || _e === void 0 ? void 0 : _e.joinStyle,
48
- 'circle-stroke-line-cap': (_f = styleRef.current) === null || _f === void 0 ? void 0 : _f.capStyle,
49
- 'fill-color': (_g = styleRef.current) === null || _g === void 0 ? void 0 : _g.fillColor,
50
- 'stroke-color': (_h = styleRef.current) === null || _h === void 0 ? void 0 : _h.strokeColor,
51
- 'stroke-width': (_j = styleRef.current) === null || _j === void 0 ? void 0 : _j.strokeWidth,
52
- 'stroke-line-join': (_k = styleRef.current) === null || _k === void 0 ? void 0 : _k.joinStyle,
53
- 'stroke-line-cap': (_l = styleRef.current) === null || _l === void 0 ? void 0 : _l.capStyle,
59
+ 'circle-fill-color': fillRgbaRef.current,
60
+ 'circle-stroke-color': strokeRgbaRef.current,
61
+ 'circle-stroke-width': Math.max(0, parseFloat((_c = (_b = styleRef.current) === null || _b === void 0 ? void 0 : _b.strokeWidth) !== null && _c !== void 0 ? _c : '0')),
62
+ 'circle-stroke-line-join': (_d = styleRef.current) === null || _d === void 0 ? void 0 : _d.joinStyle,
63
+ 'circle-stroke-line-cap': (_e = styleRef.current) === null || _e === void 0 ? void 0 : _e.capStyle,
64
+ 'fill-color': fillRgbaRef.current,
65
+ 'stroke-color': strokeRgbaRef.current,
66
+ 'stroke-width': Math.max(0, parseFloat((_g = (_f = styleRef.current) === null || _f === void 0 ? void 0 : _f.strokeWidth) !== null && _g !== void 0 ? _g : '0')),
67
+ 'stroke-line-join': (_h = styleRef.current) === null || _h === void 0 ? void 0 : _h.joinStyle,
68
+ 'stroke-line-cap': (_j = styleRef.current) === null || _j === void 0 ? void 0 : _j.capStyle,
54
69
  };
55
70
  const symbologyState = {
56
71
  renderType: 'Single Symbol',
@@ -80,13 +95,13 @@ const SimpleSymbol = ({ model, okSignalPromise, layerId, symbologyTab, isStorySe
80
95
  const renderColorTab = () => (React.createElement(React.Fragment, null,
81
96
  React.createElement("div", { className: "jp-gis-symbology-row" },
82
97
  React.createElement("label", { htmlFor: 'vector-value-select' }, "Fill Color:"),
83
- React.createElement("input", { type: "color", value: style.fillColor, className: "jp-mod-styled", onChange: event => setStyle(prevState => (Object.assign(Object.assign({}, prevState), { fillColor: event.target.value }))) })),
98
+ React.createElement(RgbaColorPicker, { color: fillRgba, onChange: setFillRgba })),
84
99
  React.createElement("div", { className: "jp-gis-symbology-row" },
85
100
  React.createElement("label", { htmlFor: 'vector-value-select' }, "Stroke Color:"),
86
- React.createElement("input", { type: "color", value: style.strokeColor, className: "jp-mod-styled", onChange: event => setStyle(prevState => (Object.assign(Object.assign({}, prevState), { strokeColor: event.target.value }))) })),
101
+ React.createElement(RgbaColorPicker, { color: strokeRgba, onChange: setStrokeRgba })),
87
102
  React.createElement("div", { className: "jp-gis-symbology-row" },
88
103
  React.createElement("label", { htmlFor: 'vector-value-select' }, "Stroke Width:"),
89
- React.createElement("input", { type: "number", value: style.strokeWidth, className: "jp-mod-styled", onChange: event => setStyle(prevState => (Object.assign(Object.assign({}, prevState), { strokeWidth: +event.target.value }))) })),
104
+ React.createElement("input", { type: "text", value: style.strokeWidth, className: "jp-mod-styled", onChange: event => setStyle(prevState => (Object.assign(Object.assign({}, prevState), { strokeWidth: event.target.value }))) })),
90
105
  React.createElement("div", { className: "jp-gis-symbology-row" },
91
106
  React.createElement("label", { htmlFor: 'vector-join-select' }, "Join Style:"),
92
107
  React.createElement("div", { className: "jp-select-wrapper" },
@@ -1,5 +1,5 @@
1
1
  import { HeatmapLayerPropertiesForm, HillshadeLayerPropertiesForm, StorySegmentLayerPropertiesForm, LayerPropertiesForm, VectorLayerPropertiesForm, WebGlLayerPropertiesForm, } from './objectform/layer';
2
- import { GeoJSONSourcePropertiesForm, GeoTiffSourcePropertiesForm, PathBasedSourcePropertiesForm, TileSourcePropertiesForm, SourcePropertiesForm, } from './objectform/source';
2
+ import { GeoJSONSourcePropertiesForm, GeoTiffSourcePropertiesForm, PathBasedSourcePropertiesForm, TileSourcePropertiesForm, WmsTileSourceForm, SourcePropertiesForm, } from './objectform/source';
3
3
  export function getLayerTypeForm(layerType) {
4
4
  let LayerForm = LayerPropertiesForm;
5
5
  switch (layerType) {
@@ -38,6 +38,9 @@ export function getSourceTypeForm(sourceType) {
38
38
  case 'GeoTiffSource':
39
39
  SourceForm = GeoTiffSourcePropertiesForm;
40
40
  break;
41
+ case 'WmsTileSource':
42
+ SourceForm = WmsTileSourceForm;
43
+ break;
41
44
  case 'RasterSource':
42
45
  case 'VectorTileSource':
43
46
  SourceForm = TileSourcePropertiesForm;
@@ -0,0 +1,3 @@
1
+ import { WidgetProps } from '@rjsf/utils';
2
+ import React from 'react';
3
+ export declare function WmsTileSourceUrlInput(props: WidgetProps<string>): React.ReactElement;
@@ -0,0 +1,84 @@
1
+ import React, { useState } from 'react';
2
+ import { WMS_AVAILABLE_LAYERS_CACHE } from "../source";
3
+ import { Button } from "../../../shared/components/Button";
4
+ import { Input } from "../../../shared/components/Input";
5
+ import { GlobalStateDbManager } from "../../../store";
6
+ import { fetchWithProxies } from "../../../tools";
7
+ export function WmsTileSourceUrlInput(props) {
8
+ var _a;
9
+ const { value, formContext, onChange, id, name, onBlur, onFocus, disabled, readonly, } = props;
10
+ const context = formContext;
11
+ const model = context === null || context === void 0 ? void 0 : context.model;
12
+ const layers = (_a = context === null || context === void 0 ? void 0 : context.wmsAvailableLayers) !== null && _a !== void 0 ? _a : [];
13
+ const setWmsAvailableLayers = context === null || context === void 0 ? void 0 : context.setWmsAvailableLayers;
14
+ const stateDb = GlobalStateDbManager.getInstance().getStateDb();
15
+ const text = !value ? '' : String(value);
16
+ const [isLoading, setIsLoading] = useState(false);
17
+ const [error, setError] = useState(undefined);
18
+ const handleChange = (event) => {
19
+ onChange(event.target.value);
20
+ };
21
+ const handleConnect = async () => {
22
+ var _a, _b, _c;
23
+ if (!model || !setWmsAvailableLayers) {
24
+ return null;
25
+ }
26
+ setIsLoading(true);
27
+ setError(undefined);
28
+ const slash = text.endsWith('/') ? '' : '/';
29
+ const url = `${text}${slash}?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities`;
30
+ try {
31
+ if (stateDb) {
32
+ const cacheKey = `${WMS_AVAILABLE_LAYERS_CACHE}:${text}`;
33
+ const cached = (await stateDb.fetch(cacheKey));
34
+ if (cached && cached.length > 0) {
35
+ setWmsAvailableLayers(cached);
36
+ return;
37
+ }
38
+ }
39
+ const xmlText = await fetchWithProxies(url, model, (response) => response.text());
40
+ const xml = typeof xmlText === 'string' ? xmlText : '';
41
+ const doc = new DOMParser().parseFromString(xml, 'text/xml');
42
+ const hasParseError = Boolean(doc.querySelector('parsererror'));
43
+ const serviceException = doc.querySelector('ServiceExceptionReport');
44
+ if (hasParseError || serviceException) {
45
+ setError((_b = (_a = serviceException === null || serviceException === void 0 ? void 0 : serviceException.textContent) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : 'Failed to parse WMS GetCapabilities XML.');
46
+ return;
47
+ }
48
+ const rootLayer = doc.querySelector('Capability > Layer');
49
+ const layerEls = Array.from((_c = rootLayer === null || rootLayer === void 0 ? void 0 : rootLayer.querySelectorAll(':scope > Layer')) !== null && _c !== void 0 ? _c : []);
50
+ const parsed = layerEls
51
+ .map(layerEl => {
52
+ var _a, _b, _c, _d, _e, _f;
53
+ const name = (_c = (_b = (_a = layerEl.querySelector('Name')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : '';
54
+ const title = (_f = (_e = (_d = layerEl.querySelector('Title')) === null || _d === void 0 ? void 0 : _d.textContent) === null || _e === void 0 ? void 0 : _e.trim()) !== null && _f !== void 0 ? _f : name;
55
+ return { name, title };
56
+ })
57
+ .filter(layer => layer.name !== '' || layer.title !== '');
58
+ setWmsAvailableLayers(parsed);
59
+ if (stateDb) {
60
+ const cacheKey = `${WMS_AVAILABLE_LAYERS_CACHE}:${text}`;
61
+ await stateDb.save(cacheKey, parsed);
62
+ }
63
+ }
64
+ catch (e) {
65
+ setError(e instanceof Error ? e.message : String(e));
66
+ }
67
+ finally {
68
+ setIsLoading(false);
69
+ }
70
+ };
71
+ return (React.createElement(React.Fragment, null,
72
+ React.createElement("div", { style: {
73
+ display: 'flex',
74
+ gap: '0.5rem',
75
+ alignItems: 'center',
76
+ margin: '0 7px',
77
+ } },
78
+ React.createElement(Input, { id: id, name: name, type: "text", value: text, onChange: handleChange, onBlur: e => onBlur(id, e.target.value), onFocus: e => onFocus(id, e.target.value), disabled: disabled, readOnly: readonly, placeholder: "Enter WMS URL", style: { flexGrow: 1 } }),
79
+ React.createElement(Button, { variant: "outline", size: "sm", type: "button", onClick: handleConnect, disabled: isLoading }, isLoading ? 'Connecting…' : 'Connect')),
80
+ error && (React.createElement("div", { style: { marginTop: '0.5rem', color: 'var(--jp-error-color1)' } }, error)),
81
+ layers.length > 0 && (React.createElement("div", { style: { marginTop: '0.5rem' } },
82
+ layers.length,
83
+ " layer(s) found. Choose one in the `params.layers` dropdown."))));
84
+ }
@@ -3,3 +3,4 @@ export * from './geotiffsource';
3
3
  export * from './pathbasedsource';
4
4
  export * from './sourceform';
5
5
  export * from './tilesourceform';
6
+ export * from './wmsTileSource';
@@ -3,3 +3,4 @@ export * from './geotiffsource';
3
3
  export * from './pathbasedsource';
4
4
  export * from './sourceform';
5
5
  export * from './tilesourceform';
6
+ export * from './wmsTileSource';
@@ -0,0 +1,4 @@
1
+ import { ReactElement } from 'react';
2
+ import type { ISourceFormProps } from "./sourceform";
3
+ export declare const WMS_AVAILABLE_LAYERS_CACHE = "jgis:wmsTileSource:availableLayers";
4
+ export declare function WmsTileSourceForm(props: ISourceFormProps): ReactElement | null;
@@ -0,0 +1,78 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { SchemaForm } from "../SchemaForm";
3
+ import { WmsTileSourceUrlInput } from "../components/WmsTileSourceUrlInput";
4
+ import { processBaseSchema, removeFormEntry, } from "../schemaUtils";
5
+ import { useSchemaFormState } from "../useSchemaFormState";
6
+ import { GlobalStateDbManager } from "../../../store";
7
+ import { deepCopy } from "../../../tools";
8
+ export const WMS_AVAILABLE_LAYERS_CACHE = 'jgis:wmsTileSource:availableLayers';
9
+ export function WmsTileSourceForm(props) {
10
+ const { schema: schemaProp, sourceData, syncData, model, filePath, formContext, dialogOptions, cancel, formErrorSignal, } = props;
11
+ const { formData, schema, formContextValue, hasSchema, handleChangeBase, handleSubmitBase, } = useSchemaFormState({
12
+ sourceData,
13
+ schemaProp,
14
+ model,
15
+ syncData,
16
+ cancel,
17
+ onAfterChange: dialogOptions
18
+ ? (data) => {
19
+ dialogOptions.sourceData = Object.assign({}, data);
20
+ }
21
+ : undefined,
22
+ });
23
+ const [wmsAvailableLayers, setWmsAvailableLayers] = useState([]);
24
+ const stateDb = GlobalStateDbManager.getInstance().getStateDb();
25
+ // Rehydrate available WMS layers from StateDB to avoid having to refetch on remount.
26
+ useEffect(() => {
27
+ const wmsUrl = formData === null || formData === void 0 ? void 0 : formData.url;
28
+ if (!stateDb || !wmsUrl) {
29
+ return;
30
+ }
31
+ const db = stateDb;
32
+ const cacheKey = `${WMS_AVAILABLE_LAYERS_CACHE}:${wmsUrl}`;
33
+ async function loadLayersFromCache() {
34
+ var _a, _b;
35
+ const cached = (await db.fetch(cacheKey));
36
+ if (cached && cached.length > 0) {
37
+ setWmsAvailableLayers(cached);
38
+ }
39
+ else {
40
+ setWmsAvailableLayers([]);
41
+ handleChangeBase(Object.assign(Object.assign({}, (formData !== null && formData !== void 0 ? formData : {})), { params: Object.assign(Object.assign({}, ((_b = ((_a = formData === null || formData === void 0 ? void 0 : formData.params) !== null && _a !== void 0 ? _a : {})) !== null && _b !== void 0 ? _b : {})), { layers: undefined }) }));
42
+ }
43
+ }
44
+ void loadLayersFromCache();
45
+ }, [stateDb, formData === null || formData === void 0 ? void 0 : formData.url]);
46
+ const uiSchema = useMemo(() => {
47
+ var _a, _b, _c, _d;
48
+ const builtUiSchema = {};
49
+ const dataCopy = deepCopy(formData);
50
+ processBaseSchema(dataCopy, schema, builtUiSchema, formContext, removeFormEntry);
51
+ const layerNames = wmsAvailableLayers
52
+ .map(layer => layer.name)
53
+ .filter(name => name !== '');
54
+ // Populate schema enum dynamically so RJSF renders a select for params.layers
55
+ const params = ((_b = (_a = schema.properties) === null || _a === void 0 ? void 0 : _a.params) !== null && _b !== void 0 ? _b : {});
56
+ const paramsProperties = ((_c = params.properties) !== null && _c !== void 0 ? _c : {});
57
+ if (paramsProperties.layers) {
58
+ // Keep select options in sync with the cached/available layers list.
59
+ if (layerNames.length > 0) {
60
+ paramsProperties.layers.enum = layerNames;
61
+ }
62
+ else {
63
+ // Avoid invalid schema (`enum` must be a non-empty array).
64
+ delete paramsProperties.layers.enum;
65
+ }
66
+ }
67
+ builtUiSchema.url = {
68
+ 'ui:widget': WmsTileSourceUrlInput,
69
+ };
70
+ builtUiSchema.params = Object.assign(Object.assign({}, builtUiSchema.params), { 'ui:title': false, layers: Object.assign(Object.assign({}, (_d = builtUiSchema.params) === null || _d === void 0 ? void 0 : _d.layers), { 'ui:widget': 'select', 'ui:placeholder': 'Select a layer', 'ui:enumNames': wmsAvailableLayers.map(layer => layer.title) }) });
71
+ return builtUiSchema;
72
+ }, [schema, formData, formContext, wmsAvailableLayers]);
73
+ if (!hasSchema) {
74
+ return null;
75
+ }
76
+ return (React.createElement(SchemaForm, { schema: schema, formData: formData, onChange: handleChangeBase, onSubmit: handleSubmitBase, formContext: Object.assign(Object.assign({}, formContextValue), { wmsAvailableLayers,
77
+ setWmsAvailableLayers }), filePath: filePath, uiSchema: uiSchema, formErrorSignal: formErrorSignal }));
78
+ }
@@ -29,7 +29,7 @@ export interface IUseSchemaFormStateResult {
29
29
  setFormData: Dispatch<SetStateAction<IDict>>;
30
30
  /** Schema to pass to SchemaForm (deep copy of schemaProp). */
31
31
  schema: RJSFSchema;
32
- /** Form context value { model, formData } for SchemaForm. */
32
+ /** Form context value for SchemaForm (available to custom fields/widgets). */
33
33
  formContextValue: {
34
34
  model: IJupyterGISModel;
35
35
  formData: IDict;
@@ -42,7 +42,7 @@ export declare class MainView extends React.Component<IMainViewProps, IStates> {
42
42
  componentDidMount(): Promise<void>;
43
43
  componentDidUpdate(prevProps: IMainViewProps, prevState: IStates): void;
44
44
  componentWillUnmount(): void;
45
- generateMap(center: number[], zoom: number): Promise<void>;
45
+ generateMap(center: number[], zoom: number, projection?: string): Promise<void>;
46
46
  updateCenter: () => void;
47
47
  getViewBbox: (targetProjection?: string) => import("ol/extent").Extent;
48
48
  createSelectInteraction: () => void;
@@ -189,6 +189,7 @@ export declare class MainView extends React.Component<IMainViewProps, IStates> {
189
189
  private _onZoomToPosition;
190
190
  private _moveToPosition;
191
191
  private _flyToPosition;
192
+ private _lastPointerCoord;
192
193
  private _onPointerMove;
193
194
  private _syncPointer;
194
195
  private _addMarker;
@@ -205,6 +206,7 @@ export declare class MainView extends React.Component<IMainViewProps, IStates> {
205
206
  private _commands;
206
207
  private _isPositionInitialized;
207
208
  private divRef;
209
+ private mainViewRef;
208
210
  private controlsToolbarRef;
209
211
  private spectaContainerRef;
210
212
  private storyViewerPanelRef;