@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
@@ -2,7 +2,8 @@ import { faTrash } from '@fortawesome/free-solid-svg-icons';
2
2
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3
3
  import { Button } from '@jupyterlab/ui-components';
4
4
  import React, { useEffect, useRef } from 'react';
5
- import { ensureHexColorCode, hexToRgb, } from "../../colorRampUtils";
5
+ import { colorToRgba } from "../../colorRampUtils";
6
+ import RgbaColorPicker from "../color_ramp/RgbaColorPicker";
6
7
  const StopRow = ({ index, dataValue, symbologyValue, stopRows, setStopRows, deleteRow, useNumber, }) => {
7
8
  const inputRef = useRef(null);
8
9
  useEffect(() => {
@@ -13,7 +14,8 @@ const StopRow = ({ index, dataValue, symbologyValue, stopRows, setStopRows, dele
13
14
  }, [stopRows]);
14
15
  const handleStopChange = (event) => {
15
16
  const newRows = [...stopRows];
16
- newRows[index].stop = +event.target.value;
17
+ const value = event.target.value;
18
+ newRows[index].stop = useNumber ? +value : value;
17
19
  setStopRows(newRows);
18
20
  };
19
21
  const handleBlur = () => {
@@ -31,14 +33,17 @@ const StopRow = ({ index, dataValue, symbologyValue, stopRows, setStopRows, dele
31
33
  };
32
34
  const handleOutputChange = (event) => {
33
35
  const newRows = [...stopRows];
34
- useNumber
35
- ? (newRows[index].output = +event.target.value)
36
- : (newRows[index].output = hexToRgb(event.target.value));
36
+ newRows[index].output = +event.target.value;
37
+ setStopRows(newRows);
38
+ };
39
+ const handleColorOutputChange = (color) => {
40
+ const newRows = [...stopRows];
41
+ newRows[index].output = color;
37
42
  setStopRows(newRows);
38
43
  };
39
44
  return (React.createElement("div", { className: "jp-gis-color-row" },
40
- React.createElement("input", { id: `jp-gis-color-value-${index}`, type: "number", value: dataValue, onChange: handleStopChange, onBlur: handleBlur, className: "jp-mod-styled jp-gis-color-row-value-input" }),
41
- useNumber ? (React.createElement("input", { type: "number", ref: inputRef, value: symbologyValue, onChange: handleOutputChange, className: "jp-mod-styled jp-gis-color-row-output-input" })) : (React.createElement("input", { id: `jp-gis-color-color-${index}`, ref: inputRef, value: ensureHexColorCode(symbologyValue), type: "color", onChange: handleOutputChange, className: "jp-mod-styled jp-gis-color-row-output-input" })),
45
+ React.createElement("input", { id: `jp-gis-color-value-${index}`, type: useNumber ? 'number' : 'text', value: dataValue, onChange: handleStopChange, onBlur: handleBlur, className: "jp-mod-styled jp-gis-color-row-value-input" }),
46
+ useNumber ? (React.createElement("input", { type: "number", ref: inputRef, value: symbologyValue, onChange: handleOutputChange, className: "jp-mod-styled jp-gis-color-row-output-input" })) : (React.createElement(RgbaColorPicker, { color: colorToRgba(symbologyValue), onChange: handleColorOutputChange })),
42
47
  React.createElement(Button, { id: `jp-gis-remove-color-${index}`, className: "jp-Button jp-gis-filter-icon" },
43
48
  React.createElement(FontAwesomeIcon, { icon: faTrash, onClick: deleteRow }))));
44
49
  };
@@ -25,7 +25,8 @@ export interface ISymbologyWidgetOptions {
25
25
  segmentId?: string;
26
26
  }
27
27
  export interface IStopRow {
28
- stop: number;
28
+ id: string;
29
+ stop: number | string;
29
30
  output: SymbologyValue;
30
31
  }
31
32
  export declare class SymbologyWidget extends Dialog<boolean> {
@@ -1,5 +1,5 @@
1
1
  import { IJGISLayer, IJupyterGISModel, IVectorLayer, IWebGlLayer } from '@jupytergis/schema';
2
- import { ColorRampName } from './colorRampUtils';
2
+ import { IColorMap } from './colorRampUtils';
3
3
  import { IStopRow } from './symbologyDialog';
4
4
  /** Payload when saving symbology; shape matches vector or WebGl layer params. */
5
5
  export interface ISymbologyPayload {
@@ -29,5 +29,5 @@ export declare namespace VectorUtils {
29
29
  const buildRadiusInfo: (layer: IJGISLayer) => IStopRow[];
30
30
  }
31
31
  export declare namespace Utils {
32
- const getValueColorPairs: (stops: number[], selectedRamp: ColorRampName, nClasses: number, reverse?: boolean) => IStopRow[];
32
+ const getValueColorPairs: (stops: number[], colorRamp: IColorMap, nClasses: number, reverse?: boolean) => IStopRow[];
33
33
  }
@@ -1,4 +1,6 @@
1
+ import { UUID } from '@lumino/coreutils';
1
2
  import colormap from 'colormap';
3
+ import { findExprNode } from './colorRampUtils';
2
4
  const COLOR_EXPR_STOPS_START = 3;
3
5
  /**
4
6
  * Resolve the effective symbology params for this dialog: either the layer's
@@ -90,35 +92,38 @@ export var VectorUtils;
90
92
  if (!color[key]) {
91
93
  continue;
92
94
  }
93
- switch (color[key][0]) {
94
- case 'interpolate':
95
- // First element is interpolate for linear selection
96
- // Second element is type of interpolation (ie linear)
97
- // Third is input value that stop values are compared with
98
- // Fourth and on is value:color pairs
99
- for (let i = COLOR_EXPR_STOPS_START; i < color[key].length; i += 2) {
100
- const pairKey = `${color[key][i]}-${color[key][i + 1]}`;
101
- if (!seenPairs.has(pairKey)) {
102
- valueColorPairs.push({
103
- stop: color[key][i],
104
- output: color[key][i + 1],
105
- });
106
- seenPairs.add(pairKey);
107
- }
95
+ const interpolate = findExprNode(color[key], 'interpolate');
96
+ if (interpolate) {
97
+ // Graduated: value:color pairs starting at index 3
98
+ for (let i = COLOR_EXPR_STOPS_START; i < interpolate.length; i += 2) {
99
+ const pairKey = `${interpolate[i]}-${interpolate[i + 1]}`;
100
+ if (!seenPairs.has(pairKey)) {
101
+ valueColorPairs.push({
102
+ id: UUID.uuid4(),
103
+ stop: interpolate[i],
104
+ output: interpolate[i + 1],
105
+ });
106
+ seenPairs.add(pairKey);
108
107
  }
109
- break;
110
- case 'case':
111
- for (let i = 1; i < color[key].length - 1; i += 2) {
112
- const pairKey = `${color[key][i][2]}-${color[key][i + 1]}`;
108
+ }
109
+ }
110
+ else {
111
+ const caseExpr = findExprNode(color[key], 'case');
112
+ if (caseExpr) {
113
+ // Categorized: alternating [condition, color] pairs, last element is fallback
114
+ for (let i = 1; i < caseExpr.length - 1; i += 2) {
115
+ const condition = caseExpr[i];
116
+ const pairKey = `${condition[2]}-${caseExpr[i + 1]}`;
113
117
  if (!seenPairs.has(pairKey)) {
114
118
  valueColorPairs.push({
115
- stop: color[key][i][2],
116
- output: color[key][i + 1],
119
+ id: UUID.uuid4(),
120
+ stop: condition[2],
121
+ output: caseExpr[i + 1],
117
122
  });
118
123
  seenPairs.add(pairKey);
119
124
  }
120
125
  }
121
- break;
126
+ }
122
127
  }
123
128
  }
124
129
  return valueColorPairs;
@@ -141,6 +146,7 @@ export var VectorUtils;
141
146
  }
142
147
  for (let i = COLOR_EXPR_STOPS_START; i < circleRadius.length; i += 2) {
143
148
  const obj = {
149
+ id: UUID.uuid4(),
144
150
  stop: circleRadius[i],
145
151
  output: circleRadius[i + 1],
146
152
  };
@@ -151,29 +157,41 @@ export var VectorUtils;
151
157
  })(VectorUtils || (VectorUtils = {}));
152
158
  export var Utils;
153
159
  (function (Utils) {
154
- Utils.getValueColorPairs = (stops, selectedRamp, nClasses, reverse = false) => {
155
- let colorMap = colormap({
156
- colormap: selectedRamp,
157
- nshades: nClasses > 9 ? nClasses : 9,
158
- format: 'rgba',
159
- });
160
+ Utils.getValueColorPairs = (stops, colorRamp, nClasses, reverse = false) => {
161
+ const isCategorical = colorRamp.type === 'categorical';
162
+ let colorMap;
163
+ if (isCategorical) {
164
+ colorMap = [...colorRamp.colors];
165
+ if (colorMap.length < nClasses) {
166
+ colorMap = Array.from({ length: nClasses }, (_, i) => {
167
+ return colorMap[i % colorMap.length];
168
+ });
169
+ }
170
+ else {
171
+ colorMap = colorMap.slice(0, nClasses);
172
+ }
173
+ }
174
+ else {
175
+ const nShades = Math.max(nClasses, 9);
176
+ colorMap = colormap({
177
+ colormap: colorRamp.name,
178
+ nshades: nShades,
179
+ format: 'rgba',
180
+ });
181
+ }
160
182
  if (reverse) {
161
183
  colorMap = [...colorMap].reverse();
162
184
  }
163
185
  const valueColorPairs = [];
164
- // colormap requires 9 classes to generate the ramp
165
- // so we do some tomfoolery to make it work with less than 9 stops
166
- if (nClasses < 9) {
167
- const midIndex = Math.floor(nClasses / 2);
168
- // Get the first n/2 elements from the second array
169
- const firstPart = colorMap.slice(0, midIndex);
170
- // Get the last n/2 elements from the second array
171
- const secondPart = colorMap.slice(colorMap.length - (stops.length - firstPart.length));
172
- // Create the new array by combining the first and last parts
173
- colorMap = firstPart.concat(secondPart);
174
- }
175
186
  for (let i = 0; i < nClasses; i++) {
176
- valueColorPairs.push({ stop: stops[i], output: colorMap[i] });
187
+ const colorIndex = isCategorical
188
+ ? i
189
+ : Math.round((i / (nClasses - 1)) * (colorMap.length - 1));
190
+ valueColorPairs.push({
191
+ id: UUID.uuid4(),
192
+ stop: stops[i],
193
+ output: colorMap[colorIndex],
194
+ });
177
195
  }
178
196
  return valueColorPairs;
179
197
  };
@@ -1,4 +1,5 @@
1
1
  import { Button } from '@jupyterlab/ui-components';
2
+ import { UUID } from '@lumino/coreutils';
2
3
  import React, { useEffect, useState } from 'react';
3
4
  import { GeoTiffClassifications } from "../../classificationModes";
4
5
  import ColorRampControls from "../../components/color_ramp/ColorRampControls";
@@ -10,6 +11,7 @@ import BandRow from "../components/BandRow";
10
11
  import { LoadingOverlay } from "../../../../shared/components/loading";
11
12
  import { useLatest } from "../../../../shared/hooks/useLatest";
12
13
  import { GlobalStateDbManager } from "../../../../store";
14
+ import { getColorMapList } from '../../colorRampUtils';
13
15
  import { useEffectiveSymbologyParams } from '../../hooks/useEffectiveSymbologyParams';
14
16
  const SingleBandPseudoColor = ({ model, okSignalPromise, layerId, isStorySegmentOverride, segmentId, }) => {
15
17
  if (!layerId) {
@@ -83,6 +85,7 @@ const SingleBandPseudoColor = ({ model, okSignalPromise, layerId, isStorySegment
83
85
  // Sixth and on is value:color pairs
84
86
  for (let i = 5; i < color.length; i += 2) {
85
87
  const obj = {
88
+ id: UUID.uuid4(),
86
89
  stop: scaleValue(Number(color[i]), isQuantile),
87
90
  output: color[i + 1],
88
91
  };
@@ -103,6 +106,7 @@ const SingleBandPseudoColor = ({ model, okSignalPromise, layerId, isStorySegment
103
106
  ? color[i][2]
104
107
  : color[i]);
105
108
  const obj = {
109
+ id: UUID.uuid4(),
106
110
  stop: scaleValue(stopVal, isQuantile),
107
111
  output: color[i + 1],
108
112
  };
@@ -211,6 +215,7 @@ const SingleBandPseudoColor = ({ model, okSignalPromise, layerId, isStorySegment
211
215
  const addStopRow = () => {
212
216
  setStopRows([
213
217
  {
218
+ id: UUID.uuid4(),
214
219
  stop: 0,
215
220
  output: [0, 0, 0, 1],
216
221
  },
@@ -252,7 +257,11 @@ const SingleBandPseudoColor = ({ model, okSignalPromise, layerId, isStorySegment
252
257
  return;
253
258
  }
254
259
  setIsLoading(false);
255
- const valueColorPairs = Utils.getValueColorPairs(stops, selectedRamp, nClasses, reverseRamp);
260
+ const colorRamp = getColorMapList().find(c => c.name === selectedRamp);
261
+ if (!colorRamp) {
262
+ return;
263
+ }
264
+ const valueColorPairs = Utils.getValueColorPairs(stops, colorRamp, nClasses, reverseRamp);
256
265
  setStopRows(valueColorPairs);
257
266
  };
258
267
  const scaleValue = (bandValue, isQuantile) => {
@@ -265,6 +274,9 @@ const SingleBandPseudoColor = ({ model, okSignalPromise, layerId, isStorySegment
265
274
  return (bandValue * (max - min)) / (1 - 0) + min;
266
275
  };
267
276
  const unscaleValue = (value, isQuantile) => {
277
+ if (typeof value !== 'number') {
278
+ throw new Error('unscaleValue expects a number');
279
+ }
268
280
  const currentBand = bandRowsRef.current[selectedBand - 1];
269
281
  const min = isQuantile ? 1 : currentBand.stats.minimum;
270
282
  const max = isQuantile ? 65535 : currentBand.stats.maximum;
@@ -294,7 +306,7 @@ const SingleBandPseudoColor = ({ model, okSignalPromise, layerId, isStorySegment
294
306
  ? '='
295
307
  : ''),
296
308
  React.createElement("span", null, "Output Value")),
297
- stopRows.map((stop, index) => (React.createElement(StopRow, { key: `${index}-${stop.output}`, index: index, dataValue: stop.stop, symbologyValue: stop.output, stopRows: stopRows, setStopRows: setStopRows, deleteRow: () => deleteStopRow(index) })))),
309
+ stopRows.map((stop, index) => (React.createElement(StopRow, { key: stop.id, index: index, dataValue: stop.stop, symbologyValue: stop.output, stopRows: stopRows, setStopRows: setStopRows, deleteRow: () => deleteStopRow(index) })))),
298
310
  React.createElement("div", { className: "jp-gis-symbology-button-container" },
299
311
  React.createElement(Button, { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: addStopRow }, "Add Stop"))));
300
312
  };
@@ -1,11 +1,22 @@
1
1
  import React, { useEffect, useState } from 'react';
2
+ import { colorToRgba, DEFAULT_COLOR, DEFAULT_STROKE_WIDTH, isColor, } from "../../colorRampUtils";
3
+ import RgbaColorPicker from "../../components/color_ramp/RgbaColorPicker";
2
4
  import { useOkSignal } from "../../hooks/useOkSignal";
3
5
  import { saveSymbology } from "../../symbologyUtils";
4
6
  import ValueSelect from "../components/ValueSelect";
5
7
  import { useLatest } from "../../../../shared/hooks/useLatest";
8
+ const TRANSPARENT = [0, 0, 0, 0];
6
9
  const Canonical = ({ model, okSignalPromise, layerId, selectableAttributesAndValues, isStorySegmentOverride, segmentId, }) => {
7
10
  const [selectedValue, setSelectedValue] = useState('');
11
+ const [fallbackColor, setFallbackColor] = useState(TRANSPARENT);
12
+ const [strokeFollowsFill, setStrokeFollowsFill] = useState(true);
13
+ const [strokeColor, setStrokeColor] = useState(DEFAULT_COLOR);
14
+ const [strokeWidth, setStrokeWidth] = useState(String(DEFAULT_STROKE_WIDTH));
8
15
  const selectedValueRef = useLatest(selectedValue);
16
+ const fallbackColorRef = useLatest(fallbackColor);
17
+ const strokeFollowsFillRef = useLatest(strokeFollowsFill);
18
+ const strokeColorRef = useLatest(strokeColor);
19
+ const strokeWidthRef = useLatest(strokeWidth);
9
20
  if (!layerId) {
10
21
  return;
11
22
  }
@@ -14,23 +25,49 @@ const Canonical = ({ model, okSignalPromise, layerId, selectableAttributesAndVal
14
25
  return;
15
26
  }
16
27
  useEffect(() => {
17
- var _a, _b;
28
+ var _a, _b, _c, _d, _e, _f, _g, _h;
18
29
  const layerParams = layer.parameters;
19
- const value = (_b = (_a = layerParams.symbologyState) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : Object.keys(selectableAttributesAndValues)[0];
30
+ const savedValue = (_a = layerParams.symbologyState) === null || _a === void 0 ? void 0 : _a.value;
31
+ const value = savedValue && savedValue in selectableAttributesAndValues
32
+ ? savedValue
33
+ : Object.keys(selectableAttributesAndValues)[0];
20
34
  setSelectedValue(value);
35
+ setFallbackColor(colorToRgba((_c = (_b = layerParams.symbologyState) === null || _b === void 0 ? void 0 : _b.fallbackColor) !== null && _c !== void 0 ? _c : TRANSPARENT));
36
+ setStrokeFollowsFill((_e = (_d = layerParams.symbologyState) === null || _d === void 0 ? void 0 : _d.strokeFollowsFill) !== null && _e !== void 0 ? _e : true);
37
+ const savedStroke = (_f = layerParams.color) === null || _f === void 0 ? void 0 : _f['stroke-color'];
38
+ setStrokeColor(isColor(savedStroke) ? colorToRgba(savedStroke) : DEFAULT_COLOR);
39
+ setStrokeWidth(String((_h = (_g = layerParams.color) === null || _g === void 0 ? void 0 : _g['stroke-width']) !== null && _h !== void 0 ? _h : DEFAULT_STROKE_WIDTH));
21
40
  }, [selectableAttributesAndValues]);
22
41
  const handleOk = () => {
23
42
  if (!layer.parameters) {
24
43
  return;
25
44
  }
26
- const colorExpr = ['get', selectedValueRef.current];
45
+ // Use coalesce so that features missing the color property (e.g. boundary
46
+ // or line features in a multi-layer MVT) fall back to the user-chosen color
47
+ // instead of returning undefined, which would cause OL to throw at render time.
48
+ const colorExpr = [
49
+ 'coalesce',
50
+ ['get', selectedValueRef.current],
51
+ fallbackColorRef.current,
52
+ ];
27
53
  const newStyle = Object.assign({}, layer.parameters.color);
28
54
  newStyle['fill-color'] = colorExpr;
29
- newStyle['stroke-color'] = colorExpr;
30
55
  newStyle['circle-fill-color'] = colorExpr;
56
+ if (strokeFollowsFillRef.current) {
57
+ newStyle['stroke-color'] = colorExpr;
58
+ newStyle['circle-stroke-color'] = colorExpr;
59
+ }
60
+ else {
61
+ newStyle['stroke-color'] = strokeColorRef.current;
62
+ newStyle['circle-stroke-color'] = strokeColorRef.current;
63
+ newStyle['stroke-width'] = Math.max(0, parseFloat(strokeWidthRef.current));
64
+ newStyle['circle-stroke-width'] = Math.max(0, parseFloat(strokeWidthRef.current));
65
+ }
31
66
  const symbologyState = {
32
67
  renderType: 'Canonical',
33
68
  value: selectedValueRef.current,
69
+ fallbackColor: fallbackColorRef.current,
70
+ strokeFollowsFill: strokeFollowsFillRef.current,
34
71
  };
35
72
  saveSymbology({
36
73
  model,
@@ -60,6 +97,34 @@ const Canonical = ({ model, okSignalPromise, layerId, selectableAttributesAndVal
60
97
  })();
61
98
  return (React.createElement("div", { className: "jp-gis-layer-symbology-container" },
62
99
  React.createElement("p", null, "Color features based on an attribute containing a hex color code."),
63
- body));
100
+ body,
101
+ React.createElement("div", { className: "jp-gis-symbology-row" },
102
+ React.createElement("label", null, "Stroke Color:"),
103
+ React.createElement("div", { style: {
104
+ display: 'flex',
105
+ alignItems: 'center',
106
+ gap: 8,
107
+ flex: '1 0 50%',
108
+ maxWidth: '50%',
109
+ } },
110
+ React.createElement("div", { style: {
111
+ opacity: strokeFollowsFill ? 0.3 : 1,
112
+ pointerEvents: strokeFollowsFill ? 'none' : 'auto',
113
+ } },
114
+ React.createElement(RgbaColorPicker, { color: strokeColor, onChange: setStrokeColor })),
115
+ React.createElement("label", { style: {
116
+ display: 'flex',
117
+ alignItems: 'center',
118
+ gap: 4,
119
+ whiteSpace: 'nowrap',
120
+ } },
121
+ React.createElement("input", { type: "checkbox", checked: strokeFollowsFill, onChange: e => setStrokeFollowsFill(e.target.checked) }),
122
+ "match fill"))),
123
+ React.createElement("div", { className: "jp-gis-symbology-row" },
124
+ React.createElement("label", null, "Stroke Width:"),
125
+ React.createElement("input", { type: "text", className: "jp-mod-styled", value: strokeWidth, onChange: e => setStrokeWidth(e.target.value) })),
126
+ React.createElement("div", { className: "jp-gis-symbology-row" },
127
+ React.createElement("label", null, "Fallback Color:"),
128
+ React.createElement(RgbaColorPicker, { color: fallbackColor, onChange: setFallbackColor }))));
64
129
  };
65
130
  export default Canonical;
@@ -1,5 +1,7 @@
1
1
  import React, { useEffect, useState } from 'react';
2
+ import { colorToRgba, DEFAULT_COLOR, DEFAULT_STROKE_WIDTH, getColorMapList, isColor, } from "../../colorRampUtils";
2
3
  import ColorRampControls from "../../components/color_ramp/ColorRampControls";
4
+ import RgbaColorPicker from "../../components/color_ramp/RgbaColorPicker";
3
5
  import StopContainer from "../../components/color_stops/StopContainer";
4
6
  import { useOkSignal } from "../../hooks/useOkSignal";
5
7
  import { Utils, VectorUtils, saveSymbology, } from "../../symbologyUtils";
@@ -10,10 +12,14 @@ const Categorized = ({ model, okSignalPromise, layerId, symbologyTab, selectable
10
12
  const [selectedAttribute, setSelectedAttribute] = useState('');
11
13
  const [stopRows, setStopRows] = useState([]);
12
14
  const [colorRampOptions, setColorRampOptions] = useState();
15
+ const [fallbackColor, setFallbackColor] = useState([0, 0, 0, 0]);
16
+ const [strokeFollowsFill, setStrokeFollowsFill] = useState(false);
17
+ const fallbackColorRef = useLatest(fallbackColor);
18
+ const strokeFollowsFillRef = useLatest(strokeFollowsFill);
13
19
  const [manualStyle, setManualStyle] = useState({
14
- fillColor: '#3399CC',
15
- strokeColor: '#3399CC',
16
- strokeWidth: 1.25,
20
+ fillColor: DEFAULT_COLOR,
21
+ strokeColor: DEFAULT_COLOR,
22
+ strokeWidth: String(DEFAULT_STROKE_WIDTH),
17
23
  radius: 5,
18
24
  });
19
25
  const manualStyleRef = useLatest(manualStyle);
@@ -39,35 +45,40 @@ const Categorized = ({ model, okSignalPromise, layerId, symbologyTab, selectable
39
45
  setStopRows(valueColorPairs);
40
46
  }, []);
41
47
  useEffect(() => {
48
+ var _a, _b, _c, _d;
42
49
  if (params.color) {
43
50
  const fillColor = params.color['fill-color'];
44
51
  const circleFillColor = params.color['circle-fill-color'];
45
52
  const strokeColor = params.color['stroke-color'];
46
53
  const circleStrokeColor = params.color['circle-stroke-color'];
47
- const isSimpleColor = (val) => typeof val === 'string' && /^#?[0-9A-Fa-f]{3,8}$/.test(val);
54
+ const effectiveFill = isColor(fillColor)
55
+ ? fillColor
56
+ : isColor(circleFillColor)
57
+ ? circleFillColor
58
+ : DEFAULT_COLOR;
59
+ const effectiveStroke = isColor(strokeColor)
60
+ ? strokeColor
61
+ : isColor(circleStrokeColor)
62
+ ? circleStrokeColor
63
+ : DEFAULT_COLOR;
48
64
  setManualStyle({
49
- fillColor: isSimpleColor(fillColor)
50
- ? fillColor
51
- : isSimpleColor(circleFillColor)
52
- ? circleFillColor
53
- : '#3399CC',
54
- strokeColor: isSimpleColor(strokeColor)
55
- ? strokeColor
56
- : isSimpleColor(circleStrokeColor)
57
- ? circleStrokeColor
58
- : '#3399CC',
59
- strokeWidth: params.color['stroke-width'] ||
65
+ fillColor: colorToRgba(effectiveFill),
66
+ strokeColor: colorToRgba(effectiveStroke),
67
+ strokeWidth: String(params.color['stroke-width'] ||
60
68
  params.color['circle-stroke-width'] ||
61
- 1.25,
69
+ DEFAULT_STROKE_WIDTH),
62
70
  radius: params.color['circle-radius'] || 5,
63
71
  });
64
72
  }
73
+ setFallbackColor(colorToRgba((_b = (_a = params.symbologyState) === null || _a === void 0 ? void 0 : _a.fallbackColor) !== null && _b !== void 0 ? _b : [0, 0, 0, 0]));
74
+ setStrokeFollowsFill((_d = (_c = params.symbologyState) === null || _c === void 0 ? void 0 : _c.strokeFollowsFill) !== null && _d !== void 0 ? _d : false);
65
75
  }, [layerId]);
66
76
  useEffect(() => {
67
77
  var _a;
68
- // We only want number values here
69
- const attribute = ((_a = params.symbologyState) === null || _a === void 0 ? void 0 : _a.value) ||
70
- Object.keys(selectableAttributesAndValues)[0];
78
+ const savedValue = (_a = params.symbologyState) === null || _a === void 0 ? void 0 : _a.value;
79
+ const attribute = savedValue && savedValue in selectableAttributesAndValues
80
+ ? savedValue
81
+ : Object.keys(selectableAttributesAndValues)[0];
71
82
  setSelectedAttribute(attribute);
72
83
  }, [selectableAttributesAndValues]);
73
84
  const buildColorInfoFromClassification = (selectedMode, numberOfShades, selectedRamp, reverseRamp, setIsLoading) => {
@@ -78,8 +89,15 @@ const Categorized = ({ model, okSignalPromise, layerId, symbologyTab, selectable
78
89
  selectedMode,
79
90
  reverseRamp,
80
91
  });
92
+ if (!selectableAttributesAndValues[selectedAttribute]) {
93
+ return;
94
+ }
81
95
  const stops = Array.from(selectableAttributesAndValues[selectedAttribute]).sort((a, b) => a - b);
82
- const valueColorPairs = Utils.getValueColorPairs(stops, selectedRamp, stops.length, reverseRamp);
96
+ const colorRamp = getColorMapList().find(c => c.name === selectedRamp);
97
+ if (!colorRamp) {
98
+ return;
99
+ }
100
+ const valueColorPairs = Utils.getValueColorPairs(stops, colorRamp, stops.length, reverseRamp);
83
101
  setStopRows(valueColorPairs);
84
102
  };
85
103
  const handleOk = () => {
@@ -93,27 +111,36 @@ const Categorized = ({ model, okSignalPromise, layerId, symbologyTab, selectable
93
111
  expr.push(stop.output);
94
112
  });
95
113
  if (symbologyTab === 'color') {
96
- expr.push([0, 0, 0, 0.0]); // fallback color
114
+ expr.push(fallbackColorRef.current);
97
115
  newStyle['fill-color'] = expr;
98
116
  newStyle['circle-fill-color'] = expr;
99
- newStyle['stroke-color'] = expr;
100
- newStyle['circle-stroke-color'] = expr;
117
+ if (strokeFollowsFillRef.current) {
118
+ newStyle['stroke-color'] = expr;
119
+ newStyle['circle-stroke-color'] = expr;
120
+ }
121
+ else {
122
+ newStyle['stroke-color'] = manualStyleRef.current.strokeColor;
123
+ newStyle['circle-stroke-color'] = manualStyleRef.current.strokeColor;
124
+ }
101
125
  }
102
126
  }
103
127
  else {
104
128
  newStyle['fill-color'] = manualStyleRef.current.fillColor;
105
129
  newStyle['circle-fill-color'] = manualStyleRef.current.fillColor;
130
+ newStyle['stroke-color'] = manualStyleRef.current.strokeColor;
131
+ newStyle['circle-stroke-color'] = manualStyleRef.current.strokeColor;
106
132
  }
107
- newStyle['stroke-width'] = manualStyleRef.current.strokeWidth;
108
- newStyle['circle-stroke-width'] = manualStyleRef.current.strokeWidth;
133
+ newStyle['stroke-width'] = Math.max(0, parseFloat(manualStyleRef.current.strokeWidth));
134
+ newStyle['circle-stroke-width'] = Math.max(0, parseFloat(manualStyleRef.current.strokeWidth));
109
135
  newStyle['circle-radius'] = manualStyleRef.current.radius;
110
- newStyle['circle-stroke-color'] = manualStyleRef.current.strokeColor;
111
136
  const symbologyState = {
112
137
  renderType: 'Categorized',
113
138
  value: selectedAttributeRef.current,
114
139
  colorRamp: (_a = colorRampOptionsRef.current) === null || _a === void 0 ? void 0 : _a.selectedRamp,
115
140
  method: symbologyTab,
116
141
  reverseRamp: (_b = colorRampOptionsRef.current) === null || _b === void 0 ? void 0 : _b.reverseRamp,
142
+ fallbackColor: fallbackColorRef.current,
143
+ strokeFollowsFill: strokeFollowsFillRef.current,
117
144
  };
118
145
  saveSymbology({
119
146
  model,
@@ -165,20 +192,40 @@ const Categorized = ({ model, okSignalPromise, layerId, symbologyTab, selectable
165
192
  symbologyTab === 'color' && (React.createElement(React.Fragment, null,
166
193
  React.createElement("div", { className: "jp-gis-symbology-row" },
167
194
  React.createElement("label", null, "Fill Color:"),
168
- React.createElement("input", { type: "color", className: "jp-mod-styled", value: manualStyle.fillColor, onChange: e => {
195
+ React.createElement(RgbaColorPicker, { color: manualStyle.fillColor, onChange: color => {
169
196
  handleReset('color');
170
- setManualStyle(prev => (Object.assign(Object.assign({}, prev), { fillColor: e.target.value })));
197
+ setManualStyle(prev => (Object.assign(Object.assign({}, prev), { fillColor: color })));
171
198
  } })),
172
199
  React.createElement("div", { className: "jp-gis-symbology-row" },
173
200
  React.createElement("label", null, "Stroke Color:"),
174
- React.createElement("input", { type: "color", className: "jp-mod-styled", value: manualStyle.strokeColor, onChange: e => {
175
- setManualStyle(prev => (Object.assign(Object.assign({}, prev), { strokeColor: e.target.value })));
176
- } })),
201
+ React.createElement("div", { style: {
202
+ display: 'flex',
203
+ alignItems: 'center',
204
+ gap: 8,
205
+ flex: '1 0 50%',
206
+ maxWidth: '50%',
207
+ } },
208
+ React.createElement("div", { style: {
209
+ opacity: strokeFollowsFill ? 0.3 : 1,
210
+ pointerEvents: strokeFollowsFill ? 'none' : 'auto',
211
+ } },
212
+ React.createElement(RgbaColorPicker, { color: manualStyle.strokeColor, onChange: color => setManualStyle(prev => (Object.assign(Object.assign({}, prev), { strokeColor: color }))) })),
213
+ React.createElement("label", { style: {
214
+ display: 'flex',
215
+ alignItems: 'center',
216
+ gap: 4,
217
+ whiteSpace: 'nowrap',
218
+ } },
219
+ React.createElement("input", { type: "checkbox", checked: strokeFollowsFill, onChange: e => setStrokeFollowsFill(e.target.checked) }),
220
+ "match fill"))),
177
221
  React.createElement("div", { className: "jp-gis-symbology-row" },
178
222
  React.createElement("label", null, "Stroke Width:"),
179
- React.createElement("input", { type: "number", className: "jp-mod-styled", value: manualStyle.strokeWidth, onChange: e => {
180
- setManualStyle(prev => (Object.assign(Object.assign({}, prev), { strokeWidth: +e.target.value })));
181
- } })))),
223
+ React.createElement("input", { type: "text", className: "jp-mod-styled", value: manualStyle.strokeWidth, onChange: e => {
224
+ setManualStyle(prev => (Object.assign(Object.assign({}, prev), { strokeWidth: e.target.value })));
225
+ } })),
226
+ React.createElement("div", { className: "jp-gis-symbology-row" },
227
+ React.createElement("label", null, "Fallback Color:"),
228
+ React.createElement(RgbaColorPicker, { color: fallbackColor, onChange: setFallbackColor })))),
182
229
  symbologyTab === 'radius' && (React.createElement("div", { className: "jp-gis-symbology-row" },
183
230
  React.createElement("label", null, "Circle Radius:"),
184
231
  React.createElement("input", { type: "number", className: "jp-mod-styled", value: manualStyle.radius, onChange: e => {