@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
@@ -8,6 +8,7 @@ export declare const addMarker = "jupytergis:addMarker";
8
8
  export declare const getGeolocation = "jupytergis:getGeolocation";
9
9
  export declare const openLayerBrowser = "jupytergis:openLayerBrowser";
10
10
  export declare const openNewRasterDialog = "jupytergis:openNewRasterDialog";
11
+ export declare const openNewWmsDialog = "jupytergis:openNewWmsDialog";
11
12
  export declare const openNewVectorTileDialog = "jupytergis:openNewVectorTileDialog";
12
13
  export declare const openNewShapefileDialog = "jupytergis:openNewShapefileDialog";
13
14
  export declare const openNewGeoJSONDialog = "jupytergis:openNewGeoJSONDialog";
@@ -16,6 +16,7 @@ export const getGeolocation = 'jupytergis:getGeolocation';
16
16
  export const openLayerBrowser = 'jupytergis:openLayerBrowser';
17
17
  // Layer and source
18
18
  export const openNewRasterDialog = 'jupytergis:openNewRasterDialog';
19
+ export const openNewWmsDialog = 'jupytergis:openNewWmsDialog';
19
20
  export const openNewVectorTileDialog = 'jupytergis:openNewVectorTileDialog';
20
21
  export const openNewShapefileDialog = 'jupytergis:openNewShapefileDialog';
21
22
  export const openNewGeoJSONDialog = 'jupytergis:openNewGeoJSONDialog';
@@ -267,7 +267,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
267
267
  /**
268
268
  * Source and layers
269
269
  */
270
- commands.addCommand(CommandIDs.openNewRasterDialog, Object.assign({ label: trans.__('Open New Raster Tile Layer Creation Dialog'), caption: 'Open a dialog to create a new raster tile layer and source in the current JupyterGIS document.', describedBy: {
270
+ commands.addCommand(CommandIDs.openNewRasterDialog, Object.assign({ label: trans.__('Raster Tile'), caption: 'Open a dialog to create a new raster tile layer and source in the current JupyterGIS document.', describedBy: {
271
271
  args: {
272
272
  type: 'object',
273
273
  properties: {},
@@ -286,7 +286,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
286
286
  sourceType: 'RasterSource',
287
287
  layerType: 'RasterLayer',
288
288
  }) }, icons.get(CommandIDs.openNewRasterDialog)));
289
- commands.addCommand(CommandIDs.openNewVectorTileDialog, Object.assign({ label: trans.__('Open New Vector Tile Layer Creation Dialog'), caption: 'Open a dialog to create a new vector tile layer and source in the current JupyterGIS document.', describedBy: {
289
+ commands.addCommand(CommandIDs.openNewVectorTileDialog, Object.assign({ label: trans.__('Vector Tile'), caption: 'Open a dialog to create a new vector tile layer and source in the current JupyterGIS document.', describedBy: {
290
290
  args: {
291
291
  type: 'object',
292
292
  properties: {},
@@ -305,7 +305,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
305
305
  sourceType: 'VectorTileSource',
306
306
  layerType: 'VectorTileLayer',
307
307
  }) }, icons.get(CommandIDs.openNewVectorTileDialog)));
308
- commands.addCommand(CommandIDs.openNewGeoParquetDialog, Object.assign({ label: trans.__('Open New GeoParquet Layer Creation Dialog'), caption: 'Open a dialog to create a new GeoParquet layer and source in the current JupyterGIS document.', describedBy: {
308
+ commands.addCommand(CommandIDs.openNewGeoParquetDialog, Object.assign({ label: trans.__('GeoParquet'), caption: 'Open a dialog to create a new GeoParquet layer and source in the current JupyterGIS document.', describedBy: {
309
309
  args: {
310
310
  type: 'object',
311
311
  properties: {},
@@ -325,7 +325,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
325
325
  sourceType: 'GeoParquetSource',
326
326
  layerType: 'VectorLayer',
327
327
  }) }, icons.get(CommandIDs.openNewGeoParquetDialog)));
328
- commands.addCommand(CommandIDs.openNewGeoJSONDialog, Object.assign({ label: trans.__('Open New GeoJSON Layer Creation Dialog'), caption: 'Open a dialog to create a new GeoJSON layer and source in the current JupyterGIS document.', describedBy: {
328
+ commands.addCommand(CommandIDs.openNewGeoJSONDialog, Object.assign({ label: trans.__('GeoJSON'), caption: 'Open a dialog to create a new GeoJSON layer and source in the current JupyterGIS document.', describedBy: {
329
329
  args: {
330
330
  type: 'object',
331
331
  properties: {},
@@ -344,9 +344,28 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
344
344
  sourceType: 'GeoJSONSource',
345
345
  layerType: 'VectorLayer',
346
346
  }) }, icons.get(CommandIDs.openNewGeoJSONDialog)));
347
+ commands.addCommand(CommandIDs.openNewWmsDialog, Object.assign({ label: trans.__('WMS Layer'), caption: 'Open a dialog to create a new WMS layer and source in the current JupyterGIS document.', describedBy: {
348
+ args: {
349
+ type: 'object',
350
+ properties: {},
351
+ },
352
+ }, isEnabled: () => {
353
+ return tracker.currentWidget
354
+ ? tracker.currentWidget.model.sharedModel.editable
355
+ : false;
356
+ }, execute: Private.createEntry({
357
+ tracker,
358
+ formSchemaRegistry,
359
+ title: 'Create WMS Layer',
360
+ createLayer: true,
361
+ createSource: true,
362
+ layerData: { name: 'Custom WMS Layer' },
363
+ sourceType: 'WmsTileSource',
364
+ layerType: 'WebGlLayer',
365
+ }) }, icons.get(CommandIDs.openNewWmsDialog)));
347
366
  //Add processing commands
348
367
  addProcessingCommands(app, commands, tracker, trans, formSchemaRegistry, Object.fromEntries(formSchemaRegistry.getSchemas()));
349
- commands.addCommand(CommandIDs.openNewHillshadeDialog, Object.assign({ label: trans.__('Open New Hillshade Layer Creation Dialog'), caption: 'Open a dialog to create a new hillshade layer and source in the current JupyterGIS document.', describedBy: {
368
+ commands.addCommand(CommandIDs.openNewHillshadeDialog, Object.assign({ label: trans.__('Hillshade'), caption: 'Open a dialog to create a new hillshade layer and source in the current JupyterGIS document.', describedBy: {
350
369
  args: {
351
370
  type: 'object',
352
371
  properties: {},
@@ -365,7 +384,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
365
384
  sourceType: 'RasterDemSource',
366
385
  layerType: 'HillshadeLayer',
367
386
  }) }, icons.get(CommandIDs.openNewHillshadeDialog)));
368
- commands.addCommand(CommandIDs.openNewImageDialog, Object.assign({ label: trans.__('Open New Image Layer Creation Dialog'), caption: 'Open a dialog to create a new image layer and source in the current JupyterGIS document.', describedBy: {
387
+ commands.addCommand(CommandIDs.openNewImageDialog, Object.assign({ label: trans.__('Image'), caption: 'Open a dialog to create a new image layer and source in the current JupyterGIS document.', describedBy: {
369
388
  args: {
370
389
  type: 'object',
371
390
  properties: {},
@@ -394,7 +413,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
394
413
  sourceType: 'ImageSource',
395
414
  layerType: 'ImageLayer',
396
415
  }) }, icons.get(CommandIDs.openNewImageDialog)));
397
- commands.addCommand(CommandIDs.openNewVideoDialog, Object.assign({ label: trans.__('Open New Video Layer Creation Dialog'), caption: 'Open a dialog to create a new video layer and source in the current JupyterGIS document.', describedBy: {
416
+ commands.addCommand(CommandIDs.openNewVideoDialog, Object.assign({ label: trans.__('Video'), caption: 'Open a dialog to create a new video layer and source in the current JupyterGIS document.', describedBy: {
398
417
  args: {
399
418
  type: 'object',
400
419
  properties: {},
@@ -426,7 +445,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
426
445
  sourceType: 'VideoSource',
427
446
  layerType: 'RasterLayer',
428
447
  }) }, icons.get(CommandIDs.openNewVideoDialog)));
429
- commands.addCommand(CommandIDs.openNewGeoTiffDialog, Object.assign({ label: trans.__('Open New GeoTiff Layer Creation Dialog'), caption: 'Open a dialog to create a new GeoTiff layer and source in the current JupyterGIS document.', describedBy: {
448
+ commands.addCommand(CommandIDs.openNewGeoTiffDialog, Object.assign({ label: trans.__('GeoTiff'), caption: 'Open a dialog to create a new GeoTiff layer and source in the current JupyterGIS document.', describedBy: {
430
449
  args: {
431
450
  type: 'object',
432
451
  properties: {},
@@ -449,7 +468,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
449
468
  sourceType: 'GeoTiffSource',
450
469
  layerType: 'WebGlLayer',
451
470
  }) }, icons.get(CommandIDs.openNewGeoTiffDialog)));
452
- commands.addCommand(CommandIDs.openNewShapefileDialog, Object.assign({ label: trans.__('Open New Shapefile Layer Creation Dialog'), caption: 'Open a dialog to create a new shapefile layer and source in the current JupyterGIS document.', describedBy: {
471
+ commands.addCommand(CommandIDs.openNewShapefileDialog, Object.assign({ label: trans.__('Shapefile'), caption: 'Open a dialog to create a new shapefile layer and source in the current JupyterGIS document.', describedBy: {
453
472
  args: {
454
473
  type: 'object',
455
474
  properties: {},
package/lib/constants.js CHANGED
@@ -23,6 +23,7 @@ const iconObject = {
23
23
  [CommandIDs.undo]: { icon: undoIcon },
24
24
  [CommandIDs.openLayerBrowser]: { icon: bookOpenIcon },
25
25
  [CommandIDs.openNewRasterDialog]: { icon: rasterIcon },
26
+ [CommandIDs.openNewWmsDialog]: { iconClass: 'fa fa-server' },
26
27
  [CommandIDs.openNewVectorTileDialog]: { icon: vectorSquareIcon },
27
28
  [CommandIDs.openNewGeoJSONDialog]: { icon: geoJSONIcon },
28
29
  [CommandIDs.openNewHillshadeDialog]: { icon: moundIcon },
@@ -202,22 +202,18 @@ export var VectorClassifications;
202
202
  return breaks;
203
203
  };
204
204
  VectorClassifications.calculateLogarithmicBreaks = (values, nClasses) => {
205
- const minimum = Math.min(...values);
206
- const maximum = Math.max(...values);
207
- let positiveMinimum = Number.MAX_VALUE;
208
- let breaks = [];
209
- positiveMinimum = minimum;
210
- const actualLogMin = Math.log10(positiveMinimum);
211
- let logMin = Math.floor(actualLogMin);
212
- const logMax = Math.ceil(Math.log10(maximum));
213
- let prettyBreaks = VectorClassifications.calculatePrettyBreaks([logMin, logMax], nClasses);
214
- while (prettyBreaks.length > 0 && prettyBreaks[0] < actualLogMin) {
215
- logMin += 1.0;
216
- prettyBreaks = VectorClassifications.calculatePrettyBreaks([logMin, logMax], nClasses);
217
- }
218
- breaks = prettyBreaks;
219
- for (let i = 0; i < breaks.length; i++) {
220
- breaks[i] = Math.pow(10, breaks[i]);
205
+ const positiveValues = values.filter(v => v > 0);
206
+ if (positiveValues.length === 0 || nClasses < 1) {
207
+ return [];
208
+ }
209
+ const minimum = Math.min(...positiveValues);
210
+ const maximum = Math.max(...positiveValues);
211
+ const logMin = Math.log10(minimum);
212
+ const logMax = Math.log10(maximum);
213
+ const breaks = [];
214
+ for (let i = 0; i < nClasses; i++) {
215
+ const t = nClasses === 1 ? 0 : i / (nClasses - 1);
216
+ breaks.push(Math.pow(10, logMin + t * (logMax - logMin)));
221
217
  }
222
218
  return breaks;
223
219
  };
@@ -1,10 +1,49 @@
1
+ export type RgbaColor = [number, number, number, number];
2
+ export declare const RGBA_INDEX: {
3
+ readonly r: 0;
4
+ readonly g: 1;
5
+ readonly b: 2;
6
+ readonly a: 3;
7
+ };
8
+ export type RgbaChannel = keyof typeof RGBA_INDEX;
9
+ /** OpenLayers default blue, used as the fallback color throughout symbology dialogs. */
10
+ export declare const DEFAULT_COLOR: RgbaColor;
11
+ /** Default stroke width in pixels, used as the initial value in all symbology dialogs. */
12
+ export declare const DEFAULT_STROKE_WIDTH = 1.25;
13
+ /**
14
+ * Returns true if `val` is a usable solid color: either a hex string or a
15
+ * plain [r,g,b,a] number array. Returns false for OL expression arrays like
16
+ * ['interpolate', ...] whose first element is a string.
17
+ */
18
+ export declare function isColor(val: unknown): boolean;
19
+ /**
20
+ * Recursively searches an OL expression tree for the first node whose first
21
+ * element matches `operator` (e.g. `'interpolate'`, `'case'`).
22
+ * Returns the matching sub-expression, or `null` if not found.
23
+ */
24
+ export declare function findExprNode(expr: unknown, operator: string): unknown[] | null;
1
25
  export interface IColorMap {
2
26
  name: ColorRampName;
3
27
  colors: string[];
28
+ type: 'continuous' | 'categorical';
4
29
  }
5
30
  export declare const COLOR_RAMP_NAMES: readonly ["jet", "hsv", "hot", "cool", "spring", "summer", "autumn", "winter", "bone", "copper", "greys", "YiGnBu", "greens", "YiOrRd", "bluered", "RdBu", "picnic", "rainbow", "portland", "blackbody", "earth", "electric", "viridis", "inferno", "magma", "plasma", "warm", "rainbow-soft", "bathymetry", "cdom", "chlorophyll", "density", "freesurface-blue", "freesurface-red", "oxygen", "par", "phase", "salinity", "temperature", "turbidity", "velocity-blue", "velocity-green", "cubehelix", "ice", "oxy", "matter", "amp", "tempo", "rain", "topo", "balance", "delta", "curl", "diff", "tarn"];
6
31
  export declare const COLOR_RAMP_DEFAULTS: Partial<Record<ColorRampName, number>>;
7
- export type ColorRampName = (typeof COLOR_RAMP_NAMES)[number];
32
+ export declare const D3_CATEGORICAL_SCHEMES: {
33
+ readonly schemeCategory10: readonly string[];
34
+ readonly schemeAccent: readonly string[];
35
+ readonly schemeDark2: readonly string[];
36
+ readonly schemeObservable10: readonly string[];
37
+ readonly schemePaired: readonly string[];
38
+ readonly schemePastel1: readonly string[];
39
+ readonly schemePastel2: readonly string[];
40
+ readonly schemeSet1: readonly string[];
41
+ readonly schemeSet2: readonly string[];
42
+ readonly schemeSet3: readonly string[];
43
+ readonly schemeTableau10: readonly string[];
44
+ };
45
+ export type D3SchemeName = keyof typeof D3_CATEGORICAL_SCHEMES;
46
+ export type ColorRampName = (typeof COLOR_RAMP_NAMES)[number] | D3SchemeName;
8
47
  export declare const getColorMapList: () => IColorMap[];
9
48
  /**
10
49
  * Hook that loads and sets color maps.
@@ -15,6 +54,11 @@ export declare const useColorMapList: (setColorMaps: (maps: IColorMap[]) => void
15
54
  */
16
55
  export declare const ensureHexColorCode: (color: number[] | string) => string;
17
56
  /**
18
- * Convert hex to [r,g,b,a] array.
57
+ * Convert any color value (hex string or [r,g,b,a] array) to RgbaColor.
58
+ * Alpha must be in 0-1 range; a warning is logged if it is not.
59
+ */
60
+ export declare function colorToRgba(color: unknown): RgbaColor;
61
+ /**
62
+ * Draw a color ramp to a canvas.
19
63
  */
20
- export declare function hexToRgb(hex: string): [number, number, number, number];
64
+ export declare const drawColorRamp: (canvas: HTMLCanvasElement, colorRamp: IColorMap) => void;
@@ -11,8 +11,46 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import colormap from 'colormap';
13
13
  import colorScale from 'colormap/colorScale.js';
14
+ import * as d3Chromatic from 'd3-scale-chromatic';
14
15
  import { useEffect } from 'react';
15
16
  import rawCmocean from "./components/color_ramp/cmocean.json";
17
+ import { objectEntries } from "../../tools";
18
+ export const RGBA_INDEX = { r: 0, g: 1, b: 2, a: 3 };
19
+ /** OpenLayers default blue, used as the fallback color throughout symbology dialogs. */
20
+ export const DEFAULT_COLOR = [51, 153, 204, 1];
21
+ /** Default stroke width in pixels, used as the initial value in all symbology dialogs. */
22
+ export const DEFAULT_STROKE_WIDTH = 1.25;
23
+ /**
24
+ * Returns true if `val` is a usable solid color: either a hex string or a
25
+ * plain [r,g,b,a] number array. Returns false for OL expression arrays like
26
+ * ['interpolate', ...] whose first element is a string.
27
+ */
28
+ export function isColor(val) {
29
+ if (typeof val === 'string') {
30
+ return /^#?[0-9A-Fa-f]{3,8}$/.test(val);
31
+ }
32
+ return Array.isArray(val) && val.length >= 3 && typeof val[0] === 'number';
33
+ }
34
+ /**
35
+ * Recursively searches an OL expression tree for the first node whose first
36
+ * element matches `operator` (e.g. `'interpolate'`, `'case'`).
37
+ * Returns the matching sub-expression, or `null` if not found.
38
+ */
39
+ export function findExprNode(expr, operator) {
40
+ if (!Array.isArray(expr)) {
41
+ return null;
42
+ }
43
+ if (expr[0] === operator) {
44
+ return expr;
45
+ }
46
+ for (const child of expr) {
47
+ const found = findExprNode(child, operator);
48
+ if (found) {
49
+ return found;
50
+ }
51
+ }
52
+ return null;
53
+ }
16
54
  const { __license__: _ } = rawCmocean, cmocean = __rest(rawCmocean, ["__license__"]);
17
55
  Object.assign(colorScale, cmocean);
18
56
  export const COLOR_RAMP_NAMES = [
@@ -78,6 +116,19 @@ export const COLOR_RAMP_DEFAULTS = {
78
116
  'rainbow-soft': 11,
79
117
  cubehelix: 16,
80
118
  };
119
+ export const D3_CATEGORICAL_SCHEMES = {
120
+ schemeCategory10: d3Chromatic.schemeCategory10,
121
+ schemeAccent: d3Chromatic.schemeAccent,
122
+ schemeDark2: d3Chromatic.schemeDark2,
123
+ schemeObservable10: d3Chromatic.schemeObservable10,
124
+ schemePaired: d3Chromatic.schemePaired,
125
+ schemePastel1: d3Chromatic.schemePastel1,
126
+ schemePastel2: d3Chromatic.schemePastel2,
127
+ schemeSet1: d3Chromatic.schemeSet1,
128
+ schemeSet2: d3Chromatic.schemeSet2,
129
+ schemeSet3: d3Chromatic.schemeSet3,
130
+ schemeTableau10: d3Chromatic.schemeTableau10,
131
+ };
81
132
  export const getColorMapList = () => {
82
133
  const colorMapList = [];
83
134
  COLOR_RAMP_NAMES.forEach(name => {
@@ -86,7 +137,14 @@ export const getColorMapList = () => {
86
137
  nshades: 255,
87
138
  format: 'rgbaString',
88
139
  });
89
- colorMapList.push({ name, colors: colorRamp });
140
+ colorMapList.push({ name, colors: colorRamp, type: 'continuous' });
141
+ });
142
+ objectEntries(D3_CATEGORICAL_SCHEMES).forEach(([name, colors]) => {
143
+ colorMapList.push({
144
+ name,
145
+ colors: colors.map(c => c.toString()),
146
+ type: 'categorical',
147
+ });
90
148
  });
91
149
  return colorMapList;
92
150
  };
@@ -115,18 +173,59 @@ export const ensureHexColorCode = (color) => {
115
173
  return '#' + hex;
116
174
  };
117
175
  /**
118
- * Convert hex to [r,g,b,a] array.
176
+ * Convert any color value (hex string or [r,g,b,a] array) to RgbaColor.
177
+ * Alpha must be in 0-1 range; a warning is logged if it is not.
119
178
  */
120
- export function hexToRgb(hex) {
121
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
122
- if (!result) {
123
- console.warn('Unable to parse hex value, defaulting to black');
124
- return [0, 0, 0, 255];
179
+ export function colorToRgba(color) {
180
+ if (typeof color === 'string') {
181
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
182
+ if (!result) {
183
+ console.warn('Unable to parse hex color, using default');
184
+ return DEFAULT_COLOR;
185
+ }
186
+ return [
187
+ parseInt(result[1], 16),
188
+ parseInt(result[2], 16),
189
+ parseInt(result[3], 16),
190
+ 1,
191
+ ];
125
192
  }
126
- return [
127
- parseInt(result[1], 16),
128
- parseInt(result[2], 16),
129
- parseInt(result[3], 16),
130
- 255, // TODO: Make alpha customizable?
131
- ];
193
+ if (isColor(color)) {
194
+ const [r, g, b, a] = color;
195
+ const alpha = a !== null && a !== void 0 ? a : 1;
196
+ if (alpha > 1) {
197
+ console.warn(`Color alpha ${alpha} is out of 0-1 range`);
198
+ }
199
+ return [r, g, b, alpha];
200
+ }
201
+ return DEFAULT_COLOR;
132
202
  }
203
+ /**
204
+ * Draw a color ramp to a canvas.
205
+ */
206
+ export const drawColorRamp = (canvas, colorRamp) => {
207
+ const ctx = canvas.getContext('2d');
208
+ const colors = colorRamp.colors;
209
+ if (!ctx || !colors || colors.length === 0) {
210
+ return;
211
+ }
212
+ const width = canvas.width;
213
+ const height = canvas.height;
214
+ if (colorRamp.type === 'categorical') {
215
+ const blockWidth = width / colors.length;
216
+ colors.forEach((color, i) => {
217
+ ctx.beginPath();
218
+ ctx.fillStyle = color;
219
+ ctx.fillRect(i * blockWidth, 0, blockWidth, height);
220
+ });
221
+ }
222
+ else {
223
+ const gradient = ctx.createLinearGradient(0, 0, width, 0);
224
+ const step = 1 / (colors.length - 1);
225
+ colors.forEach((color, i) => {
226
+ gradient.addColorStop(i * step, color);
227
+ });
228
+ ctx.fillStyle = gradient;
229
+ ctx.fillRect(0, 0, width, height);
230
+ }
231
+ };
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { Button } from '@jupyterlab/ui-components';
14
14
  import React, { useEffect, useRef, useState } from 'react';
15
- import { useColorMapList, } from "../../colorRampUtils";
15
+ import { useColorMapList, drawColorRamp, } from "../../colorRampUtils";
16
16
  import ColorRampSelectorEntry from './ColorRampSelectorEntry';
17
17
  const ColorRampSelector = ({ selectedRamp, setSelected, reverse, setReverse, }) => {
18
18
  const containerRef = useRef(null);
@@ -49,23 +49,15 @@ const ColorRampSelector = ({ selectedRamp, setSelected, reverse, setReverse, })
49
49
  return;
50
50
  }
51
51
  canvas.style.visibility = 'hidden';
52
- const ctx = canvas.getContext('2d');
53
- if (!ctx) {
52
+ const ramp = colorMaps.find(c => c.name === rampName);
53
+ if (!ramp) {
54
54
  return;
55
55
  }
56
- const ramp = colorMaps.filter(c => c.name === rampName)[0];
57
- let colors = ramp.colors;
58
- if (reverse) {
59
- colors = [...colors].reverse();
60
- }
56
+ const displayRamp = reverse
57
+ ? Object.assign(Object.assign({}, ramp), { colors: [...ramp.colors].reverse() }) : ramp;
61
58
  canvas.width = canvasWidth;
62
59
  canvas.height = canvasHeight;
63
- for (let i = 0; i <= 255; i++) {
64
- ctx.beginPath();
65
- const color = colors[i];
66
- ctx.fillStyle = color;
67
- ctx.fillRect(i * 2, 0, 2, canvasHeight);
68
- }
60
+ drawColorRamp(canvas, displayRamp);
69
61
  canvas.style.visibility = 'initial';
70
62
  };
71
63
  useEffect(() => {
@@ -10,11 +10,11 @@
10
10
  * - `onClick`: Callback fired with the ramp name when clicked.
11
11
  */
12
12
  import React from 'react';
13
- import { IColorMap } from "../../colorRampUtils";
13
+ import { ColorRampName, IColorMap } from "../../colorRampUtils";
14
14
  interface IColorRampSelectorEntryProps {
15
15
  index: number;
16
16
  colorMap: IColorMap;
17
- onClick: (item: any) => void;
17
+ onClick: (item: ColorRampName) => void;
18
18
  }
19
19
  declare const ColorRampSelectorEntry: React.FC<IColorRampSelectorEntryProps>;
20
20
  export default ColorRampSelectorEntry;
@@ -10,6 +10,7 @@
10
10
  * - `onClick`: Callback fired with the ramp name when clicked.
11
11
  */
12
12
  import React, { useEffect } from 'react';
13
+ import { drawColorRamp, } from "../../colorRampUtils";
13
14
  const ColorRampSelectorEntry = ({ index, colorMap, onClick, }) => {
14
15
  const canvasWidth = 512;
15
16
  const canvasHeight = 30;
@@ -20,17 +21,8 @@ const ColorRampSelectorEntry = ({ index, colorMap, onClick, }) => {
20
21
  }
21
22
  canvas.width = canvasWidth;
22
23
  canvas.height = canvasHeight;
23
- const ctx = canvas.getContext('2d');
24
- if (!ctx) {
25
- return;
26
- }
27
- for (let i = 0; i <= 255; i++) {
28
- ctx.beginPath();
29
- const color = colorMap.colors[i];
30
- ctx.fillStyle = color;
31
- ctx.fillRect(i * 2, 0, 2, canvasHeight);
32
- }
33
- }, []);
24
+ drawColorRamp(canvas, colorMap);
25
+ }, [colorMap, index]);
34
26
  return (React.createElement("div", { key: colorMap.name, onClick: () => onClick(colorMap.name), className: "jp-gis-color-ramp-entry" },
35
27
  React.createElement("span", { className: "jp-gis-color-label" }, colorMap.name),
36
28
  React.createElement("canvas", { id: `cv-${index}`, width: canvasWidth, height: canvasHeight, className: "jp-gis-color-canvas" })));
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { RgbaColor } from "../../colorRampUtils";
3
+ export type { RgbaColor };
4
+ interface IRgbaColorPickerProps {
5
+ color: RgbaColor;
6
+ onChange: (color: RgbaColor) => void;
7
+ }
8
+ /**
9
+ * A swatch button that opens a floating RGBA color picker on click.
10
+ * Color is stored as [r, g, b, a] where r/g/b are 0-255 and a is 0-1.
11
+ */
12
+ declare const RgbaColorPicker: React.FC<IRgbaColorPickerProps>;
13
+ export default RgbaColorPicker;
@@ -0,0 +1,98 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { RgbaColorPicker as ReactColorfulRgba } from 'react-colorful';
3
+ import { RGBA_INDEX, } from "../../colorRampUtils";
4
+ /**
5
+ * A swatch button that opens a floating RGBA color picker on click.
6
+ * Color is stored as [r, g, b, a] where r/g/b are 0-255 and a is 0-1.
7
+ */
8
+ const RgbaColorPicker = ({ color, onChange, }) => {
9
+ const [open, setOpen] = useState(false);
10
+ const containerRef = useRef(null);
11
+ const [r, g, b, a] = color;
12
+ const [inputs, setInputs] = useState({
13
+ r: String(Math.round(r)),
14
+ g: String(Math.round(g)),
15
+ b: String(Math.round(b)),
16
+ a: String(Math.round(a * 100)),
17
+ });
18
+ // Sync text inputs when color changes externally (e.g. picker drag)
19
+ useEffect(() => {
20
+ setInputs({
21
+ r: String(Math.round(r)),
22
+ g: String(Math.round(g)),
23
+ b: String(Math.round(b)),
24
+ a: String(Math.round(a * 100)),
25
+ });
26
+ }, [r, g, b, a]);
27
+ const swatchStyle = {
28
+ background: `rgba(${r},${g},${b},${a})`,
29
+ width: 28,
30
+ height: 28,
31
+ borderRadius: 4,
32
+ border: '1px solid var(--jp-border-color1, #ccc)',
33
+ cursor: 'pointer',
34
+ flexShrink: 0,
35
+ };
36
+ const handlePickerChange = useCallback((c) => {
37
+ onChange([c.r, c.g, c.b, c.a]);
38
+ }, [onChange]);
39
+ const handleChannelInput = (channel, value) => {
40
+ setInputs(prev => (Object.assign(Object.assign({}, prev), { [channel]: value })));
41
+ const num = Number(value);
42
+ if (channel === 'a') {
43
+ if (num >= 0 && num <= 100) {
44
+ onChange([r, g, b, num / 100]);
45
+ }
46
+ }
47
+ else {
48
+ if (num >= 0 && num <= 255) {
49
+ const newColor = [...color];
50
+ newColor[RGBA_INDEX[channel]] = Math.round(num);
51
+ onChange(newColor);
52
+ }
53
+ }
54
+ };
55
+ const handleTransparentChange = (checked) => {
56
+ // Always restore to fully opaque rather than the previous alpha,
57
+ // which could have been near-zero and appear broken to the user.
58
+ onChange([r, g, b, checked ? 0 : 1]);
59
+ };
60
+ useEffect(() => {
61
+ if (!open) {
62
+ return;
63
+ }
64
+ const handleClickOutside = (e) => {
65
+ if (containerRef.current &&
66
+ !containerRef.current.contains(e.target)) {
67
+ setOpen(false);
68
+ }
69
+ };
70
+ document.addEventListener('mousedown', handleClickOutside);
71
+ return () => document.removeEventListener('mousedown', handleClickOutside);
72
+ }, [open]);
73
+ return (React.createElement("div", { ref: containerRef, className: "jp-gis-rgba-picker", style: { position: 'relative' } },
74
+ React.createElement("div", { style: swatchStyle, onClick: () => setOpen(v => !v) }),
75
+ open && (React.createElement("div", { style: {
76
+ position: 'absolute',
77
+ zIndex: 1000,
78
+ top: '110%',
79
+ left: 0,
80
+ background: 'var(--jp-layout-color1, #fff)',
81
+ border: '1px solid var(--jp-border-color1, #ccc)',
82
+ borderRadius: 6,
83
+ padding: 8,
84
+ boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
85
+ } },
86
+ React.createElement(ReactColorfulRgba, { color: { r, g, b, a }, onChange: handlePickerChange }),
87
+ React.createElement("div", { className: "jp-gis-rgba-inputs" },
88
+ ['r', 'g', 'b'].map(ch => (React.createElement("div", { key: ch, className: "jp-gis-rgba-field" },
89
+ React.createElement("label", null, ch.toUpperCase()),
90
+ React.createElement("input", { className: "jp-mod-styled", type: "number", min: 0, max: 255, value: inputs[ch], onChange: e => handleChannelInput(ch, e.target.value) })))),
91
+ React.createElement("div", { className: "jp-gis-rgba-field" },
92
+ React.createElement("label", null, "A%"),
93
+ React.createElement("input", { className: "jp-mod-styled", type: "number", min: 0, max: 100, value: inputs.a, onChange: e => handleChannelInput('a', e.target.value) }))),
94
+ React.createElement("label", { className: "jp-gis-transparent-label" },
95
+ React.createElement("input", { type: "checkbox", checked: a === 0, onChange: e => handleTransparentChange(e.target.checked) }),
96
+ "No color")))));
97
+ };
98
+ export default RgbaColorPicker;
@@ -1,10 +1,12 @@
1
1
  import { Button } from '@jupyterlab/ui-components';
2
+ import { UUID } from '@lumino/coreutils';
2
3
  import React from 'react';
3
4
  import StopRow from './StopRow';
4
5
  const StopContainer = ({ selectedMethod, stopRows, setStopRows, }) => {
5
6
  const addStopRow = () => {
6
7
  setStopRows([
7
8
  {
9
+ id: UUID.uuid4(),
8
10
  stop: 0,
9
11
  output: [0, 0, 0, 1],
10
12
  },
@@ -21,7 +23,7 @@ const StopContainer = ({ selectedMethod, stopRows, setStopRows, }) => {
21
23
  React.createElement("div", { className: "jp-gis-stop-labels", style: { display: 'flex', gap: 6 } },
22
24
  React.createElement("span", { style: { flex: '0 0 18%' } }, "Value"),
23
25
  React.createElement("span", null, "Output Value")),
24
- 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), useNumber: selectedMethod === 'radius' ? true : false })))),
26
+ 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), useNumber: selectedMethod === 'radius' ? true : false })))),
25
27
  React.createElement("div", { className: "jp-gis-symbology-button-container" },
26
28
  React.createElement(Button, { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: addStopRow }, "Add Stop"))));
27
29
  };
@@ -3,7 +3,7 @@ import { IStopRow } from "../../symbologyDialog";
3
3
  import { SymbologyValue } from "../../../../types";
4
4
  declare const StopRow: React.FC<{
5
5
  index: number;
6
- dataValue: number;
6
+ dataValue: number | string;
7
7
  symbologyValue: SymbologyValue;
8
8
  stopRows: IStopRow[];
9
9
  setStopRows: (stopRows: IStopRow[]) => void;