diff --git a/.eslintrc.js b/.eslintrc.js index 62776d2df..2fe27259e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,7 +22,13 @@ module.exports = { }, }, ], - "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }], + "@typescript-eslint/no-unused-vars": [ + "warn", + { + args: "none", + varsIgnorePattern: "^_$" + } + ], "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-namespace": "off", "@typescript-eslint/ban-ts-comment": "warn", diff --git a/packages/base/src/dialogs/symbology/colorRampUtils.ts b/packages/base/src/dialogs/symbology/colorRampUtils.ts new file mode 100644 index 000000000..19b95908e --- /dev/null +++ b/packages/base/src/dialogs/symbology/colorRampUtils.ts @@ -0,0 +1,136 @@ +import colormap from 'colormap'; +import colorScale from 'colormap/colorScale.js'; +import { useEffect } from 'react'; + +import rawCmocean from '@/src/dialogs/symbology/components/color_ramp/cmocean.json'; + +export interface IColorMap { + name: ColorRampName; + colors: string[]; +} + +const { __license__: _, ...cmocean } = rawCmocean; + +Object.assign(colorScale, cmocean); + +export const COLOR_RAMP_NAMES = [ + 'jet', + // 'hsv', 11 steps min + 'hot', + 'cool', + 'spring', + 'summer', + 'autumn', + 'winter', + 'bone', + 'copper', + 'greys', + 'YiGnBu', + 'greens', + 'YiOrRd', + 'bluered', + 'RdBu', + // 'picnic', 11 steps min + 'rainbow', + 'portland', + 'blackbody', + 'earth', + 'electric', + 'viridis', + 'inferno', + 'magma', + 'plasma', + 'warm', + // 'rainbow-soft', 11 steps min + 'bathymetry', + 'cdom', + 'chlorophyll', + 'density', + 'freesurface-blue', + 'freesurface-red', + 'oxygen', + 'par', + 'phase', + 'salinity', + 'temperature', + 'turbidity', + 'velocity-blue', + 'velocity-green', + // 'cubehelix' 16 steps min + 'ice', + 'oxy', + 'matter', + 'amp', + 'tempo', + 'rain', + 'topo', + 'balance', + 'delta', + 'curl', + 'diff', + 'tarn', +] as const; + +export type ColorRampName = (typeof COLOR_RAMP_NAMES)[number]; + +export const getColorMapList = (): IColorMap[] => { + const colorMapList: IColorMap[] = []; + + COLOR_RAMP_NAMES.forEach(name => { + const colorRamp = colormap({ + colormap: name, + nshades: 255, + format: 'rgbaString', + }); + + colorMapList.push({ name, colors: colorRamp }); + }); + + return colorMapList; +}; + +/** + * Hook that loads and sets color maps. + */ +export const useColorMapList = (setColorMaps: (maps: IColorMap[]) => void) => { + useEffect(() => { + setColorMaps(getColorMapList()); + }, [setColorMaps]); +}; + +/** + * Ensure we always get a valid hex string from either an RGB(A) array or string. + */ +export const ensureHexColorCode = (color: number[] | string): string => { + if (typeof color === 'string') { + return color; + } + + // color must be an RGBA array + const hex = color + .slice(0, -1) // Color input doesn't support hex alpha values so cut that out + .map((val: { toString: (arg0: number) => string }) => { + return val.toString(16).padStart(2, '0'); + }) + .join(''); + + return '#' + hex; +}; + +/** + * Convert hex to [r,g,b,a] array. + */ +export function hexToRgb(hex: string): [number, number, number, number] { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + + if (!result) { + console.warn('Unable to parse hex value, defaulting to black'); + return [0, 0, 0, 255]; + } + return [ + parseInt(result[1], 16), + parseInt(result[2], 16), + parseInt(result[3], 16), + 255, // TODO: Make alpha customizable? + ]; +} diff --git a/packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx index 7021eca85..062c21577 100644 --- a/packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx +++ b/packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx @@ -1,7 +1,7 @@ import { Button } from '@jupyterlab/ui-components'; -import colormap from 'colormap'; import React, { useEffect, useRef, useState } from 'react'; +import { useColorMapList } from '@/src/dialogs/symbology/colorRampUtils'; import ColorRampEntry from './ColorRampEntry'; export interface IColorMap { @@ -18,71 +18,11 @@ const CanvasSelectComponent: React.FC = ({ selectedRamp, setSelected, }) => { - const colorRampNames = [ - 'jet', - // 'hsv', 11 steps min - 'hot', - 'cool', - 'spring', - 'summer', - 'autumn', - 'winter', - 'bone', - 'copper', - 'greys', - 'YiGnBu', - 'greens', - 'YiOrRd', - 'bluered', - 'RdBu', - // 'picnic', 11 steps min - 'rainbow', - 'portland', - 'blackbody', - 'earth', - 'electric', - 'viridis', - 'inferno', - 'magma', - 'plasma', - 'warm', - // 'rainbow-soft', 11 steps min - 'bathymetry', - 'cdom', - 'chlorophyll', - 'density', - 'freesurface-blue', - 'freesurface-red', - 'oxygen', - 'par', - 'phase', - 'salinity', - 'temperature', - 'turbidity', - 'velocity-blue', - 'velocity-green', - // 'cubehelix' 16 steps min - ]; - const containerRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [colorMaps, setColorMaps] = useState([]); - useEffect(() => { - const colorMapList: IColorMap[] = []; - - colorRampNames.forEach(name => { - const colorRamp = colormap({ - colormap: name, - nshades: 255, - format: 'rgbaString', - }); - const colorMap = { name: name, colors: colorRamp }; - colorMapList.push(colorMap); - - setColorMaps(colorMapList); - }); - }, []); + useColorMapList(setColorMaps); useEffect(() => { if (colorMaps.length > 0) { diff --git a/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx index 0c39c7927..70af1d808 100644 --- a/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx +++ b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx @@ -38,10 +38,12 @@ const ColorRamp: React.FC = ({ const [isLoading, setIsLoading] = useState(false); useEffect(() => { - populateOptions(); + if (selectedRamp === '' && selectedMode === '' && numberOfShades === '') { + populateOptions(); + } }, [layerParams]); - const populateOptions = async () => { + const populateOptions = () => { let nClasses, singleBandMode, colorRamp; if (layerParams.symbologyState) { diff --git a/packages/base/src/dialogs/symbology/components/color_ramp/cmocean.json b/packages/base/src/dialogs/symbology/components/color_ramp/cmocean.json new file mode 100644 index 000000000..c074fe2ba --- /dev/null +++ b/packages/base/src/dialogs/symbology/components/color_ramp/cmocean.json @@ -0,0 +1,460 @@ +{ + "__license__": "The MIT License (MIT) Copyright (c) 2015 Kristen M. Thyng Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in alL copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.", + + "ice": [ + { + "index": 0.0, + "rgb": [3, 5, 18] + }, + { + "index": 0.125, + "rgb": [33, 32, 65] + }, + { + "index": 0.25, + "rgb": [56, 56, 116] + }, + { + "index": 0.375, + "rgb": [62, 87, 163] + }, + { + "index": 0.5, + "rgb": [66, 122, 183] + }, + { + "index": 0.625, + "rgb": [88, 157, 195] + }, + { + "index": 0.75, + "rgb": [122, 190, 208] + }, + { + "index": 0.875, + "rgb": [176, 221, 225] + }, + { + "index": 1.0, + "rgb": [234, 252, 253] + } + ], + "oxy": [ + { + "index": 0.0, + "rgb": [63, 5, 5] + }, + { + "index": 0.125, + "rgb": [118, 5, 15] + }, + { + "index": 0.25, + "rgb": [91, 91, 90] + }, + { + "index": 0.375, + "rgb": [122, 121, 121] + }, + { + "index": 0.5, + "rgb": [154, 154, 153] + }, + { + "index": 0.625, + "rgb": [190, 189, 188] + }, + { + "index": 0.75, + "rgb": [229, 229, 228] + }, + { + "index": 0.875, + "rgb": [232, 224, 50] + }, + { + "index": 1.0, + "rgb": [220, 174, 25] + } + ], + "matter": [ + { + "index": 0.0, + "rgb": [253, 237, 176] + }, + { + "index": 0.125, + "rgb": [249, 192, 135] + }, + { + "index": 0.25, + "rgb": [241, 148, 102] + }, + { + "index": 0.375, + "rgb": [229, 105, 83] + }, + { + "index": 0.5, + "rgb": [206, 67, 86] + }, + { + "index": 0.625, + "rgb": [171, 41, 96] + }, + { + "index": 0.75, + "rgb": [130, 27, 98] + }, + { + "index": 0.875, + "rgb": [87, 22, 86] + }, + { + "index": 1.0, + "rgb": [47, 15, 61] + } + ], + "amp": [ + { + "index": 0.0, + "rgb": [241, 236, 236] + }, + { + "index": 0.125, + "rgb": [226, 199, 190] + }, + { + "index": 0.25, + "rgb": [215, 162, 144] + }, + { + "index": 0.375, + "rgb": [204, 125, 99] + }, + { + "index": 0.5, + "rgb": [191, 88, 58] + }, + { + "index": 0.625, + "rgb": [174, 46, 36] + }, + { + "index": 0.75, + "rgb": [142, 16, 40] + }, + { + "index": 0.875, + "rgb": [100, 14, 35] + }, + { + "index": 1.0, + "rgb": [60, 9, 17] + } + ], + "tempo": [ + { + "index": 0.0, + "rgb": [254, 245, 244] + }, + { + "index": 0.125, + "rgb": [210, 217, 198] + }, + { + "index": 0.25, + "rgb": [161, 193, 161] + }, + { + "index": 0.375, + "rgb": [105, 171, 137] + }, + { + "index": 0.5, + "rgb": [42, 147, 127] + }, + { + "index": 0.625, + "rgb": [17, 118, 118] + }, + { + "index": 0.75, + "rgb": [26, 88, 103] + }, + { + "index": 0.875, + "rgb": [26, 59, 84] + }, + { + "index": 1.0, + "rgb": [20, 29, 67] + } + ], + "rain": [ + { + "index": 0.0, + "rgb": [238, 237, 242] + }, + { + "index": 0.125, + "rgb": [218, 203, 187] + }, + { + "index": 0.25, + "rgb": [181, 178, 138] + }, + { + "index": 0.375, + "rgb": [125, 160, 119] + }, + { + "index": 0.5, + "rgb": [60, 142, 109] + }, + { + "index": 0.625, + "rgb": [5, 116, 109] + }, + { + "index": 0.75, + "rgb": [20, 86, 102] + }, + { + "index": 0.875, + "rgb": [36, 56, 79] + }, + { + "index": 1.0, + "rgb": [33, 26, 56] + } + ], + "topo": [ + { + "index": 0.0, + "rgb": [39, 26, 44] + }, + { + "index": 0.125, + "rgb": [64, 77, 139] + }, + { + "index": 0.25, + "rgb": [72, 142, 157] + }, + { + "index": 0.375, + "rgb": [121, 206, 162] + }, + { + "index": 0.5, + "rgb": [13, 37, 19] + }, + { + "index": 0.625, + "rgb": [59, 89, 38] + }, + { + "index": 0.75, + "rgb": [144, 129, 63] + }, + { + "index": 0.875, + "rgb": [210, 182, 118] + }, + { + "index": 1.0, + "rgb": [248, 253, 228] + } + ], + "balance": [ + { + "index": 0.0, + "rgb": [23, 28, 66] + }, + { + "index": 0.125, + "rgb": [36, 72, 175] + }, + { + "index": 0.25, + "rgb": [56, 135, 185] + }, + { + "index": 0.375, + "rgb": [151, 185, 197] + }, + { + "index": 0.5, + "rgb": [240, 236, 235] + }, + { + "index": 0.625, + "rgb": [214, 161, 143] + }, + { + "index": 0.75, + "rgb": [191, 87, 58] + }, + { + "index": 0.875, + "rgb": [141, 15, 40] + }, + { + "index": 1.0, + "rgb": [60, 9, 17] + } + ], + "delta": [ + { + "index": 0.0, + "rgb": [16, 31, 63] + }, + { + "index": 0.125, + "rgb": [28, 81, 156] + }, + { + "index": 0.25, + "rgb": [51, 144, 169] + }, + { + "index": 0.375, + "rgb": [151, 197, 190] + }, + { + "index": 0.5, + "rgb": [254, 252, 203] + }, + { + "index": 0.625, + "rgb": [200, 185, 67] + }, + { + "index": 0.75, + "rgb": [93, 145, 12] + }, + { + "index": 0.875, + "rgb": [11, 94, 44] + }, + { + "index": 1.0, + "rgb": [23, 35, 18] + } + ], + "curl": [ + { + "index": 0.0, + "rgb": [20, 29, 67] + }, + { + "index": 0.125, + "rgb": [26, 89, 103] + }, + { + "index": 0.25, + "rgb": [44, 148, 127] + }, + { + "index": 0.375, + "rgb": [163, 194, 162] + }, + { + "index": 0.5, + "rgb": [253, 245, 243] + }, + { + "index": 0.625, + "rgb": [225, 166, 142] + }, + { + "index": 0.75, + "rgb": [194, 88, 96] + }, + { + "index": 0.875, + "rgb": [131, 31, 95] + }, + { + "index": 1.0, + "rgb": [51, 13, 53] + } + ], + "diff": [ + { + "index": 0.0, + "rgb": [7, 34, 63] + }, + { + "index": 0.125, + "rgb": [49, 87, 113] + }, + { + "index": 0.25, + "rgb": [116, 136, 151] + }, + { + "index": 0.375, + "rgb": [184, 191, 197] + }, + { + "index": 0.5, + "rgb": [245, 241, 239] + }, + { + "index": 0.625, + "rgb": [193, 185, 166] + }, + { + "index": 0.75, + "rgb": [140, 128, 91] + }, + { + "index": 0.875, + "rgb": [86, 79, 32] + }, + { + "index": 1.0, + "rgb": [28, 34, 6] + } + ], + "tarn": [ + { + "index": 0.0, + "rgb": [22, 35, 13] + }, + { + "index": 0.125, + "rgb": [79, 84, 19] + }, + { + "index": 0.25, + "rgb": [169, 118, 49] + }, + { + "index": 0.375, + "rgb": [221, 177, 141] + }, + { + "index": 0.5, + "rgb": [252, 247, 245] + }, + { + "index": 0.625, + "rgb": [180, 193, 161] + }, + { + "index": 0.75, + "rgb": [84, 144, 132] + }, + { + "index": 0.875, + "rgb": [25, 89, 110] + }, + { + "index": 1.0, + "rgb": [15, 30, 79] + } + ] +} diff --git a/packages/base/src/dialogs/symbology/components/color_stops/StopContainer.tsx b/packages/base/src/dialogs/symbology/components/color_stops/StopContainer.tsx index 5e0261289..bdaaa7459 100644 --- a/packages/base/src/dialogs/symbology/components/color_stops/StopContainer.tsx +++ b/packages/base/src/dialogs/symbology/components/color_stops/StopContainer.tsx @@ -43,8 +43,8 @@ const StopContainer: React.FC = ({ deleteStopRow(index)} diff --git a/packages/base/src/dialogs/symbology/components/color_stops/StopRow.tsx b/packages/base/src/dialogs/symbology/components/color_stops/StopRow.tsx index f67700859..c8543d881 100644 --- a/packages/base/src/dialogs/symbology/components/color_stops/StopRow.tsx +++ b/packages/base/src/dialogs/symbology/components/color_stops/StopRow.tsx @@ -3,20 +3,25 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button } from '@jupyterlab/ui-components'; import React, { useEffect, useRef } from 'react'; +import { + ensureHexColorCode, + hexToRgb, +} from '@/src/dialogs/symbology/colorRampUtils'; import { IStopRow } from '@/src/dialogs/symbology/symbologyDialog'; +import { SymbologyValue, SizeValue, ColorValue } from '@/src/types'; const StopRow: React.FC<{ index: number; - value: number; - outputValue: number | number[]; + dataValue: number; + symbologyValue: SymbologyValue; stopRows: IStopRow[]; setStopRows: (stopRows: IStopRow[]) => void; deleteRow: () => void; useNumber?: boolean; }> = ({ index, - value, - outputValue, + dataValue, + symbologyValue, stopRows, setStopRows, deleteRow, @@ -30,39 +35,7 @@ const StopRow: React.FC<{ } }, [stopRows]); - const rgbArrToHex = (rgbArr: number | number[]) => { - if (!Array.isArray(rgbArr)) { - return; - } - - const hex = rgbArr - .slice(0, -1) // Color input doesn't support hex alpha values so cut that out - .map((val: { toString: (arg0: number) => string }) => { - return val.toString(16).padStart(2, '0'); - }) - .join(''); - - return '#' + hex; - }; - - const hexToRgb = (hex: string) => { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - - if (!result) { - console.warn('Unable to parse hex value, defaulting to black'); - return [parseInt('0', 16), parseInt('0', 16), parseInt('0', 16)]; - } - const rgbValues = [ - parseInt(result[1], 16), - parseInt(result[2], 16), - parseInt(result[3], 16), - 1, // TODO: Make alpha customizable? - ]; - - return rgbValues; - }; - - const handleStopChange = (event: { target: { value: string | number } }) => { + const handleStopChange = (event: { target: { value: string } }) => { const newRows = [...stopRows]; newRows[index].stop = +event.target.value; setStopRows(newRows); @@ -95,7 +68,7 @@ const StopRow: React.FC<{ @@ -113,7 +86,7 @@ const StopRow: React.FC<{ = ({ diff --git a/packages/base/src/dialogs/symbology/symbologyUtils.ts b/packages/base/src/dialogs/symbology/symbologyUtils.ts index 0b0b8e289..117d6a672 100644 --- a/packages/base/src/dialogs/symbology/symbologyUtils.ts +++ b/packages/base/src/dialogs/symbology/symbologyUtils.ts @@ -3,6 +3,7 @@ import colormap from 'colormap'; import { IStopRow } from './symbologyDialog'; +const COLOR_EXPR_STOPS_START = 3; export namespace VectorUtils { export const buildColorInfo = (layer: IJGISLayer) => { // This it to parse a color object on the layer @@ -32,7 +33,7 @@ export namespace VectorUtils { // Second element is type of interpolation (ie linear) // Third is input value that stop values are compared with // Fourth and on is value:color pairs - for (let i = 3; i < color[key].length; i += 2) { + for (let i = COLOR_EXPR_STOPS_START; i < color[key].length; i += 2) { const pairKey = `${color[key][i]}-${color[key][i + 1]}`; if (!seenPairs.has(pairKey)) { valueColorPairs.push({ @@ -76,10 +77,19 @@ export namespace VectorUtils { const stopOutputPairs: IStopRow[] = []; - for (let i = 3; i < color['circle-radius'].length; i += 2) { + const circleRadius = color['circle-radius']; + + if ( + !Array.isArray(circleRadius) || + circleRadius.length <= COLOR_EXPR_STOPS_START + ) { + return []; + } + + for (let i = COLOR_EXPR_STOPS_START; i < circleRadius.length; i += 2) { const obj: IStopRow = { - stop: color['circle-radius'][i], - output: color['circle-radius'][i + 1], + stop: circleRadius[i], + output: circleRadius[i + 1], }; stopOutputPairs.push(obj); } diff --git a/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx b/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx index f483ca164..0cc58f61d 100644 --- a/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx +++ b/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx @@ -428,8 +428,8 @@ const SingleBandPseudoColor: React.FC = ({ deleteStopRow(index)} diff --git a/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx index c4f680da5..3c2cecd69 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx @@ -223,7 +223,7 @@ const Graduated: React.FC = ({ selectedMode, }); - let stops; + let stops: number[]; const values = Array.from(selectableAttributesAndValues[selectedAttribute]); diff --git a/packages/base/src/types.ts b/packages/base/src/types.ts index a0c9d872f..6ae72d4b2 100644 --- a/packages/base/src/types.ts +++ b/packages/base/src/types.ts @@ -9,6 +9,18 @@ export type JupyterGISTracker = WidgetTracker; export type SymbologyTab = 'color' | 'radius'; +type RgbColorValue = + | [number, number, number] + | [number, number, number, number]; +type HexColorValue = string; +type InternalRgbArray = number[]; + +export type ColorValue = RgbColorValue | HexColorValue; + +export type SizeValue = number; + +export type SymbologyValue = SizeValue | ColorValue | InternalRgbArray; + export type VectorRenderType = | 'Single Symbol' | 'Canonical' diff --git a/packages/base/src/types/colormap.d.ts b/packages/base/src/types/colormap.d.ts new file mode 100644 index 000000000..3baf17a32 --- /dev/null +++ b/packages/base/src/types/colormap.d.ts @@ -0,0 +1 @@ +declare module "colormap/colorScale.js" {} diff --git a/packages/base/tsconfig.json b/packages/base/tsconfig.json index e320f409d..99cb2d08d 100644 --- a/packages/base/tsconfig.json +++ b/packages/base/tsconfig.json @@ -16,6 +16,7 @@ "src/schema/*.json", "src/_interface/*.json", "src/*.json", - "src/types/*" + "src/types/*", + "src/dialogs/symbology/components/color_ramp/cmocean.json" ] }