diff --git a/examples/earthquakes.jGIS b/examples/earthquakes.jGIS
index 091a8a9d..48aea8e1 100644
--- a/examples/earthquakes.jGIS
+++ b/examples/earthquakes.jGIS
@@ -1,7 +1,7 @@
{
"layerTree": [
"0959c04f-a841-4fa2-8b44-d262e89e4c9a",
- "6dc9af9d-206d-42b5-9889-09758e9934b9"
+ "b116b76f-e040-4908-9098-a6fbea7ca5bc"
],
"layers": {
"0959c04f-a841-4fa2-8b44-d262e89e4c9a": {
@@ -12,113 +12,45 @@
"type": "RasterLayer",
"visible": true
},
- "6dc9af9d-206d-42b5-9889-09758e9934b9": {
- "filters": {
- "appliedFilters": [],
- "logicalOp": "any"
- },
+ "b116b76f-e040-4908-9098-a6fbea7ca5bc": {
"name": "earthquakes",
"parameters": {
"color": {
"circle-fill-color": [
- "interpolate",
- [
- "linear"
- ],
- [
- "get",
- "mag"
- ],
- 0.95,
- [
- 8.0,
- 29.0,
- 88.0,
- 1.0
- ],
- 1.159230769230769,
+ "case",
[
- 23.0,
- 41.0,
- 118.0,
- 1.0
- ],
- 1.3684615384615386,
- [
- 37.0,
- 52.0,
- 148.0,
- 1.0
- ],
- 1.577692307692308,
- [
- 34.0,
- 94.0,
- 168.0,
- 1.0
- ],
- 1.7869230769230768,
- [
- 32.0,
- 120.0,
- 180.0,
- 1.0
- ],
- 1.9961538461538462,
- [
- 29.0,
- 145.0,
- 192.0,
- 1.0
- ],
- 2.2053846153846157,
- [
- 65.0,
- 182.0,
- 196.0,
- 1.0
- ],
- 2.4146153846153844,
- [
- 96.0,
- 194.0,
- 192.0,
- 1.0
+ "==",
+ [
+ "get",
+ "tsunami"
+ ],
+ 0.0
],
- 2.623846153846154,
[
- 127.0,
- 205.0,
- 187.0,
+ 125.0,
+ 0.0,
+ 179.0,
1.0
],
- 2.863076923076923,
[
- 199.0,
- 233.0,
- 180.0,
+ "==",
+ [
+ "get",
+ "tsunami"
+ ],
1.0
],
- 3.1246153846153852,
[
- 218.0,
- 241.0,
- 199.0,
- 1.0
- ],
- 3.491538461538462,
- [
- 237.0,
- 248.0,
- 217.0,
+ 147.0,
+ 255.0,
+ 0.0,
1.0
],
- 4.326153846153848,
[
- 255.0,
- 255.0,
- 217.0,
- 1.0
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
]
],
"circle-radius": [
@@ -130,20 +62,33 @@
"get",
"mag"
],
+ 1.0,
+ 1.0,
2.0,
- 5.0,
+ 2.0,
+ 3.0,
+ 3.0,
+ 4.0,
4.0,
- 10.0,
+ 5.0,
+ 5.0,
6.0,
- 15.0
+ 6.0
],
- "circle-stroke-color": "#986a44",
+ "circle-stroke-color": "#3399CC",
"circle-stroke-line-cap": "round",
"circle-stroke-line-join": "round",
"circle-stroke-width": 1.25
},
"opacity": 1.0,
- "source": "4a74edbc-1939-40e3-a0ac-28b2e1d87846",
+ "source": "dc048820-75cd-4b8d-a1fb-91642901cd82",
+ "symbologyState": {
+ "colorRamp": "cool",
+ "mode": "",
+ "nClasses": "",
+ "renderType": "Categorized",
+ "value": "tsunami"
+ },
"type": "circle"
},
"type": "VectorLayer",
@@ -153,10 +98,10 @@
"options": {
"bearing": 0.0,
"extent": [
- -14115404.754324596,
- -3578744.7791191125,
- -9601917.6529872,
- 9131405.218514971
+ -14291047.530673811,
+ -3536164.7121253638,
+ -9426274.876637986,
+ 9088825.15152122
],
"latitude": 24.187972965810673,
"longitude": -106.52816608439294,
@@ -165,20 +110,6 @@
"zoom": 3.8783091860507373
},
"sources": {
- "4a74edbc-1939-40e3-a0ac-28b2e1d87846": {
- "name": "Custom GeoJSON Layer Source",
- "parameters": {
- "path": "eq.json"
- },
- "type": "GeoJSONSource"
- },
- "4aab05ec-5a57-454b-9ba5-31bcf272feda": {
- "name": "Custom GeoJSON Layer Source",
- "parameters": {
- "path": "france_regions.json"
- },
- "type": "GeoJSONSource"
- },
"a7ed9785-8797-4d6d-a6a9-062ce78ba7ba": {
"name": "OpenStreetMap.Mapnik",
"parameters": {
@@ -190,6 +121,13 @@
"urlParameters": {}
},
"type": "RasterSource"
+ },
+ "dc048820-75cd-4b8d-a1fb-91642901cd82": {
+ "name": "Custom GeoJSON Layer Source",
+ "parameters": {
+ "path": "eq.json"
+ },
+ "type": "GeoJSONSource"
}
}
}
diff --git a/examples/geotiff.jGIS b/examples/geotiff.jGIS
index 4fdb4c21..7570f37c 100644
--- a/examples/geotiff.jGIS
+++ b/examples/geotiff.jGIS
@@ -31,44 +31,122 @@
0.0,
0.0
],
- 0.1,
+ 0.0,
+ [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ],
+ 0.07142857142857144,
+ [
+ 17.0,
+ 17.0,
+ 23.0,
+ 1.0
+ ],
+ 0.14285714285714288,
+ [
+ 34.0,
+ 34.0,
+ 46.0,
+ 1.0
+ ],
+ 0.21428571428571427,
+ [
+ 50.0,
+ 50.0,
+ 70.0,
+ 1.0
+ ],
+ 0.28571428571428575,
+ [
+ 67.0,
+ 67.0,
+ 93.0,
+ 1.0
+ ],
+ 0.3571428571428571,
[
- 53.0,
- 132.0,
- 228.0,
+ 84.0,
+ 84.0,
+ 116.0,
1.0
],
- 0.25,
+ 0.42857142857142855,
[
- 248.0,
- 228.0,
- 92.0,
+ 98.0,
+ 103.0,
+ 130.0,
1.0
],
0.5,
[
- 255.0,
- 190.0,
- 111.0,
+ 112.0,
+ 123.0,
+ 144.0,
+ 1.0
+ ],
+ 0.5714285714285714,
+ [
+ 127.0,
+ 142.0,
+ 158.0,
+ 1.0
+ ],
+ 0.6428571428571429,
+ [
+ 141.0,
+ 161.0,
+ 172.0,
+ 1.0
+ ],
+ 0.7142857142857142,
+ [
+ 155.0,
+ 181.0,
+ 186.0,
1.0
],
- 0.75,
+ 0.7857142857142858,
[
- 143.0,
- 240.0,
- 164.0,
+ 169.0,
+ 200.0,
+ 200.0,
+ 1.0
+ ],
+ 0.8571428571428571,
+ [
+ 198.0,
+ 218.0,
+ 218.0,
+ 1.0
+ ],
+ 0.9285714285714286,
+ [
+ 226.0,
+ 237.0,
+ 237.0,
1.0
],
1.0,
[
- 153.0,
- 193.0,
- 241.0,
+ 255.0,
+ 255.0,
+ 255.0,
1.0
]
],
"opacity": 1.0,
- "source": "8b1d4258-5d46-48da-b466-496d376b593d"
+ "source": "8b1d4258-5d46-48da-b466-496d376b593d",
+ "symbologyState": {
+ "band": 1.0,
+ "colorRamp": "bone",
+ "interpolation": "linear",
+ "mode": "equal interval",
+ "nClasses": "15",
+ "renderType": "Singleband Pseudocolor"
+ }
},
"type": "WebGlLayer",
"visible": true
@@ -77,16 +155,16 @@
"options": {
"bearing": 0.0,
"extent": [
- -14740045.41309709,
- 2843576.998577497,
- -8156951.805400478,
- 5992856.553536485
+ -13920582.07909406,
+ -1339375.3727731649,
+ -9144100.770363271,
+ 11083665.925811738
],
- "latitude": 36.849981896612846,
- "longitude": -102.84361280909266,
+ "latitude": 40.042672545275906,
+ "longitude": -103.59678563518457,
"pitch": 0.0,
"projection": "EPSG:3857",
- "zoom": 4.597387349849267
+ "zoom": 3.901573011026123
},
"sources": {
"8b1d4258-5d46-48da-b466-496d376b593d": {
@@ -95,8 +173,8 @@
"normalize": true,
"urls": [
{
- "max": 3000.0,
- "min": 1000.0,
+ "max": 25000.0,
+ "min": 2000.0,
"url": "https://s2downloads.eox.at/demo/EOxCloudless/2020/rgbnir/s2cloudless2020-16bits_sinlge-file_z0-4.tif"
}
],
diff --git a/packages/base/src/classificationModes.ts b/packages/base/src/classificationModes.ts
index 66b375d7..81f35d9a 100644
--- a/packages/base/src/classificationModes.ts
+++ b/packages/base/src/classificationModes.ts
@@ -1,7 +1,7 @@
// Adapted from https://github.com/qgis/QGIS/blob/master/src/core/classification/
import { Pool, fromUrl, TypedArray } from 'geotiff';
-import { InterpolationType } from './dialogs/components/symbology/SingleBandPseudoColor';
+import { InterpolationType } from './dialogs/symbology/tiff_layer/types/SingleBandPseudoColor';
export namespace VectorClassifications {
export const calculateQuantileBreaks = (
diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts
index d676d8a5..46a54dea 100644
--- a/packages/base/src/commands.ts
+++ b/packages/base/src/commands.ts
@@ -17,7 +17,7 @@ import { ITranslator } from '@jupyterlab/translation';
import { CommandIDs, icons } from './constants';
import { CreationFormDialog } from './dialogs/formdialog';
import { LayerBrowserWidget } from './dialogs/layerBrowserDialog';
-import { SymbologyWidget } from './dialogs/symbologyDialog';
+import { SymbologyWidget } from './dialogs/symbology/symbologyDialog';
import { JupyterGISWidget } from './widget';
interface ICreateEntry {
diff --git a/packages/base/src/dialogs/symbology/classificationModes.ts b/packages/base/src/dialogs/symbology/classificationModes.ts
new file mode 100644
index 00000000..dea63d92
--- /dev/null
+++ b/packages/base/src/dialogs/symbology/classificationModes.ts
@@ -0,0 +1,436 @@
+// Adapted from https://github.com/qgis/QGIS/blob/master/src/core/classification/
+
+import { Pool, fromUrl, TypedArray } from 'geotiff';
+import { InterpolationType } from './tiff_layer/types/SingleBandPseudoColor';
+
+export namespace VectorClassifications {
+ export const calculateQuantileBreaks = (
+ values: number[],
+ nClasses: number
+ ) => {
+ // q-th quantile of a data set:
+ // value where q fraction of data is below and (1-q) fraction is above this value
+ // Xq = (1 - r) * X_NI1 + r * X_NI2
+ // NI1 = (int) (q * (n+1))
+ // NI2 = NI1 + 1
+ // r = q * (n+1) - (int) (q * (n+1))
+ // (indices of X: 1...n)
+
+ const sortedValues = [...values].sort((a, b) => a - b);
+
+ const breaks = [];
+
+ if (!sortedValues) {
+ return [];
+ }
+
+ const n = sortedValues.length;
+
+ let xq: number = n > 0 ? sortedValues[0] : 0;
+
+ for (let i = 1; i < nClasses; i++) {
+ if (n > 1) {
+ const q = i / nClasses;
+ const a = q * (n - 1);
+ const aa = Math.floor(a);
+
+ const r = a - aa;
+ xq = (1 - r) * sortedValues[aa] + r * sortedValues[aa + 1];
+ }
+ breaks.push(xq);
+ }
+
+ breaks.push(sortedValues[n - 1]);
+
+ return breaks;
+ };
+
+ export const calculateEqualIntervalBreaks = (
+ values: number[],
+ nClasses: number
+ ) => {
+ const minimum = Math.min(...values);
+ const maximum = Math.max(...values);
+
+ const breaks: number[] = [];
+ const step = (maximum - minimum) / nClasses;
+
+ let value = minimum;
+
+ for (let i = 0; i < nClasses; i++) {
+ value += step;
+ breaks.push(value);
+ }
+
+ breaks[nClasses - 1] = maximum;
+
+ return breaks;
+ };
+
+ export const calculateJenksBreaks = (values: number[], nClasses: number) => {
+ const maximum = Math.max(...values);
+
+ if (values.length === 0) {
+ return [];
+ }
+
+ if (nClasses <= 1) {
+ return [maximum];
+ }
+
+ if (nClasses >= values.length) {
+ return values;
+ }
+
+ const sample = [...values].sort((a, b) => a - b);
+ const n = sample.length;
+
+ const matrixOne = Array.from({ length: n + 1 }, () =>
+ Array(nClasses + 1).fill(0)
+ );
+ const matrixTwo = Array.from({ length: n + 1 }, () =>
+ Array(nClasses + 1).fill(Number.MAX_VALUE)
+ );
+
+ for (let i = 1; i <= nClasses; i++) {
+ matrixOne[0][i] = 1;
+ matrixOne[1][i] = 1;
+ matrixTwo[0][i] = 0.0;
+
+ for (let j = 2; j <= n; j++) {
+ matrixTwo[j][i] = Number.MAX_VALUE;
+ }
+ }
+
+ for (let l = 2; l <= n; l++) {
+ let s1 = 0.0;
+ let s2 = 0.0;
+ let w = 0;
+ let v = 0.0;
+
+ for (let m = 1; m <= l; m++) {
+ const i3 = l - m + 1;
+
+ const val = sample[i3 - 1];
+
+ s2 += val * val;
+ s1 += val;
+ w++;
+
+ v = s2 - (s1 * s1) / w;
+ const i4 = i3 - 1;
+ if (i4 !== 0) {
+ for (let j = 2; j <= nClasses; j++) {
+ if (matrixTwo[l][j] >= v + matrixTwo[i4][j - 1]) {
+ matrixOne[l][j] = i4;
+ matrixTwo[l][j] = v + matrixTwo[i4][j - 1];
+ }
+ }
+ }
+ }
+ matrixOne[l][1] = 1;
+ matrixTwo[l][1] = v;
+ }
+
+ const breaks = Array(nClasses);
+ breaks[nClasses - 1] = sample[n - 1];
+
+ for (let j = nClasses, k = n; j >= 2; j--) {
+ const id = matrixOne[k][j] - 1;
+ breaks[j - 2] = sample[id];
+ k = matrixOne[k][j] - 1;
+ }
+
+ return breaks;
+ };
+
+ export const calculatePrettyBreaks = (values: number[], nClasses: number) => {
+ const minimum = Math.min(...values);
+ const maximum = Math.max(...values);
+
+ const breaks = [];
+
+ if (nClasses < 1) {
+ breaks.push(maximum);
+ return breaks;
+ }
+
+ const minimumCount = Math.floor(nClasses / 3);
+ const shrink = 0.75;
+ const highBias = 1.5;
+ const adjustBias = 0.5 + 1.5 * highBias;
+ const divisions = nClasses;
+ const h = highBias;
+ let cell;
+ let small = false;
+ const dx = maximum - minimum;
+
+ let U;
+ cell = Math.max(Math.abs(minimum), Math.abs(maximum));
+ if (adjustBias >= 1.5 * h + 0.5) {
+ U = 1 + 1.0 / (1 + h);
+ } else {
+ U = 1 + 1.5 / (1 + adjustBias);
+ }
+ small = dx < cell * U * Math.max(1, divisions) * 1e-7 * 3.0;
+
+ if (small) {
+ if (cell > 10) {
+ cell = 9 + cell / 10;
+ cell = cell * shrink;
+ }
+ if (minimumCount > 1) {
+ cell = cell / minimumCount;
+ }
+ } else {
+ cell = dx;
+ if (divisions > 1) {
+ cell = cell / divisions;
+ }
+ }
+ if (cell < 20 * 1e-7) {
+ cell = 20 * 1e-7;
+ }
+
+ const base = Math.pow(10.0, Math.floor(Math.log10(cell)));
+ let unit = base;
+ if (2 * base - cell < h * (cell - unit)) {
+ unit = 2.0 * base;
+ if (5 * base - cell < adjustBias * (cell - unit)) {
+ unit = 5.0 * base;
+ if (10.0 * base - cell < h * (cell - unit)) {
+ unit = 10.0 * base;
+ }
+ }
+ }
+
+ let start = Math.floor(minimum / unit + 1e-7);
+ let end = Math.ceil(maximum / unit - 1e-7);
+
+ while (start * unit > minimum + 1e-7 * unit) {
+ start = start - 1;
+ }
+ while (end * unit < maximum - 1e-7 * unit) {
+ end = end + 1;
+ }
+
+ let k = Math.floor(0.5 + end - start);
+ if (k < minimumCount) {
+ k = minimumCount - k;
+ if (start >= 0) {
+ end = end + k / 2;
+ start = start - k / 2 + (k % 2);
+ } else {
+ start = start - k / 2;
+ end = end + k / 2 + (k % 2);
+ }
+ }
+
+ const minimumBreak = start * unit;
+ const count = end - start;
+
+ for (let i = 1; i < count + 1; i++) {
+ breaks.push(minimumBreak + i * unit);
+ }
+
+ if (breaks.length === 0) {
+ return breaks;
+ }
+
+ if (breaks[0] < minimum) {
+ breaks[0] = minimum;
+ }
+ if (breaks[breaks.length - 1] > maximum) {
+ breaks[breaks.length - 1] = maximum;
+ }
+
+ if (minimum < 0.0 && maximum > 0.0) {
+ const breaksMinusZero = breaks.map(b => b - 0.0);
+
+ let posOfMin = 0;
+ for (let i = 1; i < breaks.length; i++) {
+ if (
+ Math.abs(breaksMinusZero[i]) < Math.abs(breaksMinusZero[posOfMin])
+ ) {
+ posOfMin = i;
+ }
+ }
+
+ breaks[posOfMin] = 0.0; // Set the closest break to zero
+ }
+
+ return breaks;
+ };
+
+ export const calculateLogarithmicBreaks = (
+ values: number[],
+ nClasses: number
+ ) => {
+ const minimum = Math.min(...values);
+ const maximum = Math.max(...values);
+
+ let positiveMinimum = Number.MAX_VALUE;
+
+ let breaks = [];
+
+ positiveMinimum = minimum;
+
+ const actualLogMin = Math.log10(positiveMinimum);
+ let logMin = Math.floor(actualLogMin);
+ const logMax = Math.ceil(Math.log10(maximum));
+
+ let prettyBreaks = calculatePrettyBreaks([logMin, logMax], nClasses);
+
+ while (prettyBreaks.length > 0 && prettyBreaks[0] < actualLogMin) {
+ logMin += 1.0;
+ prettyBreaks = calculatePrettyBreaks([logMin, logMax], nClasses);
+ }
+
+ breaks = prettyBreaks;
+
+ for (let i = 0; i < breaks.length; i++) {
+ breaks[i] = Math.pow(10, breaks[i]);
+ }
+
+ return breaks;
+ };
+}
+
+export namespace GeoTiffClassifications {
+ export const classifyQuantileBreaks = async (
+ nClasses: number,
+ bandNumber: number,
+ url: string,
+ colorRampType: string
+ ) => {
+ const breaks: number[] = [];
+ const isDiscrete = colorRampType === 'discrete';
+
+ const pool = new Pool();
+ const tiff = await fromUrl(url);
+ const image = await tiff.getImage();
+ const values = await image.readRasters({ pool });
+
+ // Band numbers are 1 indexed
+ const bandValues = values[bandNumber - 1] as TypedArray;
+
+ const bandSortedValues = bandValues
+ .filter(value => value !== 0)
+ .sort((a, b) => a - b);
+
+ pool.destroy();
+
+ if (!bandSortedValues) {
+ return [];
+ }
+
+ // Adapted from https://github.com/GeoTIFF/geoblaze/blob/master/src/histogram/histogram.core.js#L64
+ // iterate through values and use a counter to
+ // decide when to set up the next bin.
+ let numValuesInCurrentBin;
+ let valuesPerBin;
+ let startIndex;
+
+ if (isDiscrete) {
+ valuesPerBin = bandSortedValues.length / nClasses;
+ numValuesInCurrentBin = 0;
+ startIndex = 0;
+ } else {
+ valuesPerBin = bandSortedValues.length / (nClasses - 1);
+ breaks.push(1);
+ numValuesInCurrentBin = 1;
+ startIndex = 1;
+ }
+
+ for (let i = startIndex; i < bandSortedValues.length; i++) {
+ if (numValuesInCurrentBin + 1 < valuesPerBin) {
+ numValuesInCurrentBin++;
+ } else {
+ breaks.push(bandSortedValues[i] as number);
+ numValuesInCurrentBin = 0;
+ }
+ }
+
+ if (breaks.length !== nClasses) {
+ //TODO: This should be set based on the type of bandSortedValues I think
+ breaks.push(65535);
+ }
+
+ return breaks;
+ };
+
+ export const classifyContinuousBreaks = (
+ nClasses: number,
+ minimumValue: number,
+ maximumValue: number,
+ colorRampType: InterpolationType
+ ) => {
+ const min = minimumValue;
+ const max = maximumValue;
+
+ if (min > max) {
+ return [];
+ }
+
+ const isDiscrete = colorRampType === 'discrete';
+
+ const breaks: number[] = [];
+
+ const numberOfEntries = nClasses;
+ if (isDiscrete) {
+ const intervalDiff =
+ ((max - min) * (numberOfEntries - 1)) / numberOfEntries;
+
+ for (let i = 1; i < numberOfEntries; i++) {
+ const val = i / numberOfEntries;
+ breaks.push(min + val * intervalDiff);
+ }
+ breaks.push(max);
+ } else {
+ for (let i = 0; i <= numberOfEntries; i++) {
+ if (i === 26) {
+ continue;
+ }
+ const val = i / numberOfEntries;
+ breaks.push(min + val * (max - min));
+ }
+ }
+
+ return breaks;
+ };
+
+ export const classifyEqualIntervalBreaks = (
+ nClasses: number,
+ minimumValue: number,
+ maximumValue: number,
+ colorRampType: InterpolationType
+ ) => {
+ const min = minimumValue;
+ const max = maximumValue;
+
+ if (min > max) {
+ return [];
+ }
+
+ const isDiscrete = colorRampType === 'discrete';
+
+ const breaks: number[] = [];
+
+ if (isDiscrete) {
+ const intervalDiff = (max - min) / nClasses;
+
+ for (let i = 1; i < nClasses; i++) {
+ breaks.push(min + i * intervalDiff);
+ }
+ breaks.push(max);
+ } else {
+ const intervalDiff = (max - min) / (nClasses - 1);
+
+ for (let i = 0; i < nClasses; i++) {
+ breaks.push(min + i * intervalDiff);
+ }
+ }
+
+ return breaks;
+ };
+}
diff --git a/packages/base/src/dialogs/components/symbology/CanvasSelectComponent.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx
similarity index 100%
rename from packages/base/src/dialogs/components/symbology/CanvasSelectComponent.tsx
rename to packages/base/src/dialogs/symbology/components/color_ramp/CanvasSelectComponent.tsx
diff --git a/packages/base/src/dialogs/components/symbology/ColorRamp.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx
similarity index 70%
rename from packages/base/src/dialogs/components/symbology/ColorRamp.tsx
rename to packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx
index ddd82cfa..6b2e65a6 100644
--- a/packages/base/src/dialogs/components/symbology/ColorRamp.tsx
+++ b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRamp.tsx
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
import CanvasSelectComponent from './CanvasSelectComponent';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import ModeSelectRow from './ModeSelectRow';
import { IDict } from '@jupytergis/schema';
interface IColorRampProps {
@@ -14,6 +15,7 @@ interface IColorRampProps {
selectedRamp: string,
setIsLoading: (isLoading: boolean) => void
) => void;
+ showModeRow: boolean;
}
export type ColorRampOptions = {
@@ -25,7 +27,8 @@ export type ColorRampOptions = {
const ColorRamp = ({
layerParams,
modeOptions,
- classifyFunc
+ classifyFunc,
+ showModeRow
}: IColorRampProps) => {
const [selectedRamp, setSelectedRamp] = useState('');
const [selectedMode, setSelectedMode] = useState('');
@@ -44,7 +47,6 @@ const ColorRamp = ({
singleBandMode = layerParams.symbologyState.mode;
colorRamp = layerParams.symbologyState.colorRamp;
}
-
setNumberOfShades(nClasses ? nClasses : '9');
setSelectedMode(singleBandMode ? singleBandMode : 'equal interval');
setSelectedRamp(colorRamp ? colorRamp : 'cool');
@@ -59,36 +61,15 @@ const ColorRamp = ({
setSelected={setSelectedRamp}
/>
-
-
-
- setNumberOfShades(event.target.value)}
- />
-
-
-
-
-
-
+ {showModeRow && (
+
+ )}
{isLoading ? (
) : (
diff --git a/packages/base/src/dialogs/components/symbology/ColorRampEntry.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/ColorRampEntry.tsx
similarity index 100%
rename from packages/base/src/dialogs/components/symbology/ColorRampEntry.tsx
rename to packages/base/src/dialogs/symbology/components/color_ramp/ColorRampEntry.tsx
diff --git a/packages/base/src/dialogs/symbology/components/color_ramp/ModeSelectRow.tsx b/packages/base/src/dialogs/symbology/components/color_ramp/ModeSelectRow.tsx
new file mode 100644
index 00000000..a1926368
--- /dev/null
+++ b/packages/base/src/dialogs/symbology/components/color_ramp/ModeSelectRow.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+interface IModeSelectRowProps {
+ numberOfShades: string;
+ setNumberOfShades: (value: string) => void;
+ selectedMode: string;
+ setSelectedMode: (value: string) => void;
+ modeOptions: string[];
+}
+const ModeSelectRow = ({
+ numberOfShades,
+ setNumberOfShades,
+ selectedMode,
+ setSelectedMode,
+ modeOptions
+}: IModeSelectRowProps) => {
+ return (
+
+
+
+ setNumberOfShades(event.target.value)}
+ disabled={selectedMode === 'continuous'}
+ />
+
+
+
+
+
+
+ );
+};
+
+export default ModeSelectRow;
diff --git a/packages/base/src/dialogs/symbology/components/color_stops/StopContainer.tsx b/packages/base/src/dialogs/symbology/components/color_stops/StopContainer.tsx
new file mode 100644
index 00000000..660a7d9e
--- /dev/null
+++ b/packages/base/src/dialogs/symbology/components/color_stops/StopContainer.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { Button } from '@jupyterlab/ui-components';
+import { IStopRow } from '../../symbologyDialog';
+import StopRow from './StopRow';
+
+interface IStopContainerProps {
+ selectedMethod: string;
+ stopRows: IStopRow[];
+ setStopRows: (stops: IStopRow[]) => void;
+}
+
+const StopContainer = ({
+ selectedMethod,
+ stopRows,
+ setStopRows
+}: IStopContainerProps) => {
+ const addStopRow = () => {
+ setStopRows([
+ {
+ stop: 0,
+ output: [0, 0, 0, 1]
+ },
+ ...stopRows
+ ]);
+ };
+
+ const deleteStopRow = (index: number) => {
+ const newFilters = [...stopRows];
+ newFilters.splice(index, 1);
+
+ setStopRows(newFilters);
+ };
+
+ return (
+ <>
+
+
+ Value
+ Output Value
+
+ {stopRows.map((stop, index) => (
+
deleteStopRow(index)}
+ useNumber={selectedMethod === 'radius' ? true : false}
+ />
+ ))}
+
+
+
+
+ >
+ );
+};
+
+export default StopContainer;
diff --git a/packages/base/src/dialogs/components/symbology/StopRow.tsx b/packages/base/src/dialogs/symbology/components/color_stops/StopRow.tsx
similarity index 100%
rename from packages/base/src/dialogs/components/symbology/StopRow.tsx
rename to packages/base/src/dialogs/symbology/components/color_stops/StopRow.tsx
diff --git a/packages/base/src/dialogs/symbology/hooks/useGetProperties.ts b/packages/base/src/dialogs/symbology/hooks/useGetProperties.ts
new file mode 100644
index 00000000..8bf21d78
--- /dev/null
+++ b/packages/base/src/dialogs/symbology/hooks/useGetProperties.ts
@@ -0,0 +1,72 @@
+// import { GeoJSONFeature } from 'geojson';
+
+import { GeoJSONFeature1, IJupyterGISModel } from '@jupytergis/schema';
+import { useEffect, useState } from 'react';
+
+interface IUseGetPropertiesProps {
+ layerId?: string;
+ model: IJupyterGISModel;
+}
+
+interface IUseGetPropertiesResult {
+ featureProps: Record>;
+ isLoading: boolean;
+ error?: Error;
+}
+
+export const useGetProperties = ({
+ layerId,
+ model
+}: IUseGetPropertiesProps): IUseGetPropertiesResult => {
+ const [featureProps, setFeatureProps] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(undefined);
+
+ const getProperties = async () => {
+ if (!layerId) {
+ return;
+ }
+
+ try {
+ const layer = model.getLayer(layerId);
+ const source = model.getSource(layer?.parameters?.source);
+
+ if (!source) {
+ throw new Error('Source not found');
+ }
+
+ const data = await model.readGeoJSON(source.parameters?.path);
+
+ if (!data) {
+ throw new Error('Failed to read GeoJSON data');
+ }
+
+ const result: Record> = {};
+
+ data.features.forEach((feature: GeoJSONFeature1) => {
+ if (feature.properties) {
+ Object.entries(feature.properties).forEach(([key, value]) => {
+ if (typeof value !== 'string') {
+ if (!(key in result)) {
+ result[key] = new Set();
+ }
+ result[key].add(value);
+ }
+ });
+ }
+ });
+
+ setFeatureProps(result);
+ setIsLoading(false);
+ } catch (err) {
+ setError(err as Error);
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ getProperties();
+ }, [model, layerId]);
+
+ return { featureProps, isLoading, error };
+};
diff --git a/packages/base/src/dialogs/symbologyDialog.tsx b/packages/base/src/dialogs/symbology/symbologyDialog.tsx
similarity index 95%
rename from packages/base/src/dialogs/symbologyDialog.tsx
rename to packages/base/src/dialogs/symbology/symbologyDialog.tsx
index 7bf54dba..0dac696f 100644
--- a/packages/base/src/dialogs/symbologyDialog.tsx
+++ b/packages/base/src/dialogs/symbology/symbologyDialog.tsx
@@ -5,8 +5,8 @@ import { IStateDB } from '@jupyterlab/statedb';
import { PromiseDelegate } from '@lumino/coreutils';
import { Signal } from '@lumino/signaling';
import React, { useEffect, useState } from 'react';
-import BandRendering from './components/symbology/BandRendering';
-import VectorRendering from './components/symbology/VectorRendering';
+import TiffRendering from './tiff_layer/TiffRendering';
+import VectorRendering from './vector_layer/VectorRendering';
export interface ISymbologyDialogProps {
context: DocumentRegistry.IContext;
@@ -87,7 +87,7 @@ const SymbologyDialog = ({
break;
case 'WebGlLayer':
LayerSymbology = (
- {
+ // This it to parse a color object on the layer
+ if (!layer.parameters?.color) {
+ return [];
+ }
+
+ const color = layer.parameters.color;
+
+ // If color is a string we don't need to parse
+ if (typeof color === 'string') {
+ return [];
+ }
+
+ const prefix = layer.parameters.type === 'circle' ? 'circle-' : '';
+
+ if (!color[`${prefix}fill-color`]) {
+ return [];
+ }
+
+ const valueColorPairs: IStopRow[] = [];
+
+ // So if it's not a string then it's an array and we parse
+ // Color[0] is the operator used for the color expression
+ switch (color[`${prefix}fill-color`][0]) {
+ case 'interpolate':
+ // First element is interpolate for linear selection
+ // 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[`${prefix}fill-color`].length; i += 2) {
+ const obj: IStopRow = {
+ stop: color[`${prefix}fill-color`][i],
+ output: color[`${prefix}fill-color`][i + 1]
+ };
+ valueColorPairs.push(obj);
+ }
+ break;
+ case 'case':
+ for (let i = 1; i < color[`${prefix}fill-color`].length - 1; i += 2) {
+ const obj: IStopRow = {
+ stop: color[`${prefix}fill-color`][i][2],
+ output: color[`${prefix}fill-color`][i + 1]
+ };
+ valueColorPairs.push(obj);
+ }
+ break;
+ }
+
+ return valueColorPairs;
+ };
+
+ export const buildRadiusInfo = (layer: IJGISLayer) => {
+ if (!layer.parameters?.color) {
+ return [];
+ }
+
+ const color = layer.parameters.color;
+
+ // If color is a string we don't need to parse
+ if (typeof color === 'string') {
+ return [];
+ }
+
+ const stopOutputPairs: IStopRow[] = [];
+
+ for (let i = 3; i < color['circle-radius'].length; i += 2) {
+ const obj: IStopRow = {
+ stop: color['circle-radius'][i],
+ output: color['circle-radius'][i + 1]
+ };
+ stopOutputPairs.push(obj);
+ }
+
+ return stopOutputPairs;
+ };
+}
+
+export namespace Utils {
+ export const getValueColorPairs = (
+ stops: number[],
+ selectedRamp: string,
+ nClasses: number
+ ) => {
+ let colorMap = colormap({
+ colormap: selectedRamp,
+ nshades: nClasses > 9 ? nClasses : 9,
+ format: 'rgba'
+ });
+
+ const valueColorPairs: IStopRow[] = [];
+
+ // colormap requires 9 classes to generate the ramp
+ // so we do some tomfoolery to make it work with less than 9 stops
+ if (nClasses < 9) {
+ const midIndex = Math.floor(nClasses / 2);
+
+ // Get the first n/2 elements from the second array
+ const firstPart = colorMap.slice(0, midIndex);
+
+ // Get the last n/2 elements from the second array
+ const secondPart = colorMap.slice(
+ colorMap.length - (stops.length - firstPart.length)
+ );
+
+ // Create the new array by combining the first and last parts
+ colorMap = firstPart.concat(secondPart);
+ }
+
+ for (let i = 0; i < nClasses; i++) {
+ valueColorPairs.push({ stop: stops[i], output: colorMap[i] });
+ }
+
+ return valueColorPairs;
+ };
+}
diff --git a/packages/base/src/dialogs/components/symbology/BandRendering.tsx b/packages/base/src/dialogs/symbology/tiff_layer/TiffRendering.tsx
similarity index 89%
rename from packages/base/src/dialogs/components/symbology/BandRendering.tsx
rename to packages/base/src/dialogs/symbology/tiff_layer/TiffRendering.tsx
index 4bf0ff81..9b3be7cf 100644
--- a/packages/base/src/dialogs/components/symbology/BandRendering.tsx
+++ b/packages/base/src/dialogs/symbology/tiff_layer/TiffRendering.tsx
@@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react';
-import { ISymbologyDialogProps } from '../../symbologyDialog';
-import SingleBandPseudoColor from './SingleBandPseudoColor';
+import { ISymbologyDialogProps } from '../symbologyDialog';
+import SingleBandPseudoColor from './types/SingleBandPseudoColor';
-const BandRendering = ({
+const TiffRendering = ({
context,
state,
okSignalPromise,
@@ -64,4 +64,4 @@ const BandRendering = ({
);
};
-export default BandRendering;
+export default TiffRendering;
diff --git a/packages/base/src/dialogs/components/symbology/BandRow.tsx b/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx
similarity index 97%
rename from packages/base/src/dialogs/components/symbology/BandRow.tsx
rename to packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx
index c887c58a..50aeef8b 100644
--- a/packages/base/src/dialogs/components/symbology/BandRow.tsx
+++ b/packages/base/src/dialogs/symbology/tiff_layer/components/BandRow.tsx
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { IBandRow } from './SingleBandPseudoColor';
+import { IBandRow } from '../types/SingleBandPseudoColor';
const BandRow = ({
index,
diff --git a/packages/base/src/dialogs/components/symbology/SingleBandPseudoColor.tsx b/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx
similarity index 94%
rename from packages/base/src/dialogs/components/symbology/SingleBandPseudoColor.tsx
rename to packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx
index 80fbd6a6..73b58c19 100644
--- a/packages/base/src/dialogs/components/symbology/SingleBandPseudoColor.tsx
+++ b/packages/base/src/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.tsx
@@ -1,17 +1,19 @@
import { IDict, IWebGlLayer } from '@jupytergis/schema';
import { Button } from '@jupyterlab/ui-components';
import { ReadonlyJSONObject } from '@lumino/coreutils';
-import colormap from 'colormap';
import { ExpressionValue } from 'ol/expr/expression';
import React, { useEffect, useRef, useState } from 'react';
-import { GeoTiffClassifications } from '../../../classificationModes';
-import { GlobalStateDbManager } from '../../../store';
+import { GeoTiffClassifications } from '../../classificationModes';
+import { GlobalStateDbManager } from '../../../../store';
import { IStopRow, ISymbologyDialogProps } from '../../symbologyDialog';
-import BandRow from './BandRow';
-import ColorRamp, { ColorRampOptions } from './ColorRamp';
-import StopRow from './StopRow';
-import { getGdal } from '../../../gdal';
-import { Spinner } from '../../../mainview/spinner';
+import BandRow from '../components/BandRow';
+import ColorRamp, {
+ ColorRampOptions
+} from '../../components/color_ramp/ColorRamp';
+import StopRow from '../../components/color_stops/StopRow';
+import { Utils } from '../../symbologyUtils';
+import { getGdal } from '../../../../gdal';
+import { Spinner } from '../../../../mainview/spinner';
export interface IBandRow {
band: number;
@@ -120,13 +122,8 @@ const SingleBandPseudoColor = ({
setLayerState(layerState);
const layerParams = layer.parameters as IWebGlLayer;
- const band = layerParams.symbologyState?.band
- ? layerParams.symbologyState.band
- : 1;
-
- const interpolation = layerParams.symbologyState?.interpolation
- ? layerParams.symbologyState.interpolation
- : 'linear';
+ const band = layerParams.symbologyState?.band ?? 1;
+ const interpolation = layerParams.symbologyState?.interpolation ?? 'linear';
setSelectedBand(band);
setSelectedFunction(interpolation);
@@ -235,7 +232,7 @@ const SingleBandPseudoColor = ({
setStopRows(valueColorPairs);
};
- const handleOk = async () => {
+ const handleOk = () => {
// Update source
const bandRow = bandRowsRef.current[selectedBand - 1];
if (!bandRow) {
@@ -377,17 +374,6 @@ const SingleBandPseudoColor = ({
const source = context.model.getSource(layer?.parameters?.source);
const sourceInfo = source?.parameters?.urls[0];
const nClasses = selectedMode === 'continuous' ? 52 : +numberOfShades;
- const colorMap = colormap({
- colormap: selectedRamp,
- nshades: nClasses,
- format: 'rgba'
- });
-
- if (!sourceInfo.url) {
- return;
- }
-
- const valueColorPairs: IStopRow[] = [];
setIsLoading(true);
switch (selectedMode) {
@@ -421,9 +407,11 @@ const SingleBandPseudoColor = ({
}
setIsLoading(false);
- for (let i = 0; i < stops.length; i++) {
- valueColorPairs.push({ stop: stops[i], output: colorMap[i] });
- }
+ const valueColorPairs = Utils.getValueColorPairs(
+ stops,
+ selectedRamp,
+ nClasses
+ );
setStopRows(valueColorPairs);
};
@@ -496,6 +484,7 @@ const SingleBandPseudoColor = ({
layerParams={layer.parameters}
modeOptions={modeOptions}
classifyFunc={buildColorInfoFromClassification}
+ showModeRow={true}
/>
)}
diff --git a/packages/base/src/dialogs/components/symbology/VectorRendering.tsx b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx
similarity index 77%
rename from packages/base/src/dialogs/components/symbology/VectorRendering.tsx
rename to packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx
index 21936307..af544197 100644
--- a/packages/base/src/dialogs/components/symbology/VectorRendering.tsx
+++ b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx
@@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react';
-import { ISymbologyDialogProps } from '../../symbologyDialog';
-import Graduated from './Graduated';
-import SimpleSymbol from './SimpleSymbol';
+import { ISymbologyDialogProps } from '../symbologyDialog';
+import Graduated from './types/Graduated';
+import SimpleSymbol from './types/SimpleSymbol';
+import Categorized from './types/Categorized';
const VectorRendering = ({
context,
@@ -27,11 +28,11 @@ const VectorRendering = ({
}
useEffect(() => {
- const renderType = layer.parameters?.symbologyState.renderType;
+ const renderType = layer.parameters?.symbologyState?.renderType;
setSelectedRenderType(renderType ?? 'Single Symbol');
if (layer.type === 'VectorLayer') {
- const options = ['Single Symbol', 'Graduated'];
+ const options = ['Single Symbol', 'Graduated', 'Categorized'];
setRenderTypeOptions(options);
}
}, []);
@@ -60,6 +61,17 @@ const VectorRendering = ({
/>
);
break;
+ case 'Categorized':
+ RenderComponent = (
+
+ );
+ break;
default:
RenderComponent =
Render Type Not Implemented (yet)
;
}
diff --git a/packages/base/src/dialogs/symbology/vector_layer/components/ValueSelect.tsx b/packages/base/src/dialogs/symbology/vector_layer/components/ValueSelect.tsx
new file mode 100644
index 00000000..abd0c437
--- /dev/null
+++ b/packages/base/src/dialogs/symbology/vector_layer/components/ValueSelect.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+
+interface IValueSelectProps {
+ featureProperties: any;
+ selectedValue: string;
+ setSelectedValue: (value: string) => void;
+}
+
+const ValueSelect = ({
+ featureProperties,
+ selectedValue,
+ setSelectedValue
+}: IValueSelectProps) => {
+ return (
+
+
+
+
+ );
+};
+
+export default ValueSelect;
diff --git a/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx
new file mode 100644
index 00000000..2da45411
--- /dev/null
+++ b/packages/base/src/dialogs/symbology/vector_layer/types/Categorized.tsx
@@ -0,0 +1,156 @@
+import React, { useEffect, useRef, useState } from 'react';
+import ValueSelect from '../components/ValueSelect';
+import { IStopRow, ISymbologyDialogProps } from '../../symbologyDialog';
+import { useGetProperties } from '../../hooks/useGetProperties';
+import StopContainer from '../../components/color_stops/StopContainer';
+import { Utils, VectorUtils } from '../../symbologyUtils';
+import ColorRamp from '../../components/color_ramp/ColorRamp';
+import { ReadonlyJSONObject } from '@lumino/coreutils';
+import { ExpressionValue } from 'ol/expr/expression';
+import { IVectorLayer } from '@jupytergis/schema';
+
+const Categorized = ({
+ context,
+ state,
+ okSignalPromise,
+ cancel,
+ layerId
+}: ISymbologyDialogProps) => {
+ const selectedValueRef = useRef
();
+ const stopRowsRef = useRef();
+ const colorRampOptionsRef = useRef();
+
+ const [selectedValue, setSelectedValue] = useState('');
+ const [stopRows, setStopRows] = useState([]);
+ const [colorRampOptions, setColorRampOptions] = useState<
+ ReadonlyJSONObject | undefined
+ >();
+
+ if (!layerId) {
+ return;
+ }
+ const layer = context.model.getLayer(layerId);
+ if (!layer?.parameters) {
+ return;
+ }
+ const { featureProps } = useGetProperties({
+ layerId,
+ model: context.model
+ });
+
+ useEffect(() => {
+ const valueColorPairs = VectorUtils.buildColorInfo(layer);
+
+ setStopRows(valueColorPairs);
+
+ okSignalPromise.promise.then(okSignal => {
+ okSignal.connect(handleOk, this);
+ });
+
+ return () => {
+ okSignalPromise.promise.then(okSignal => {
+ okSignal.disconnect(handleOk, this);
+ });
+ };
+ }, []);
+
+ useEffect(() => {
+ populateOptions();
+ }, [featureProps]);
+
+ useEffect(() => {
+ selectedValueRef.current = selectedValue;
+ stopRowsRef.current = stopRows;
+ colorRampOptionsRef.current = colorRampOptions;
+ }, [selectedValue, stopRows, colorRampOptions]);
+
+ const populateOptions = async () => {
+ const layerParams = layer.parameters as IVectorLayer;
+ const value =
+ layerParams.symbologyState?.value ?? Object.keys(featureProps)[0];
+
+ setSelectedValue(value);
+ };
+
+ const buildColorInfoFromClassification = (
+ selectedMode: string,
+ numberOfShades: string,
+ selectedRamp: string,
+ setIsLoading: (isLoading: boolean) => void
+ ) => {
+ setColorRampOptions({
+ selectedFunction: '',
+ selectedRamp,
+ numberOfShades: '',
+ selectedMode: ''
+ });
+
+ const stops = Array.from(featureProps[selectedValue]).sort((a, b) => a - b);
+
+ const valueColorPairs = Utils.getValueColorPairs(
+ stops,
+ selectedRamp,
+ stops.length
+ );
+
+ setStopRows(valueColorPairs);
+ };
+
+ const handleOk = () => {
+ if (!layer.parameters) {
+ return;
+ }
+
+ const colorExpr: ExpressionValue[] = [];
+ colorExpr.push('case');
+
+ stopRowsRef.current?.map(stop => {
+ colorExpr.push(['==', ['get', selectedValueRef.current], stop.stop]);
+ colorExpr.push(stop.output);
+ });
+
+ // fallback value
+ colorExpr.push([0, 0, 0, 0.0]);
+
+ const newStyle = { ...layer.parameters.color };
+ newStyle['circle-fill-color'] = colorExpr;
+
+ const symbologyState = {
+ renderType: 'Categorized',
+ value: selectedValueRef.current,
+ colorRamp: colorRampOptionsRef.current?.selectedRamp,
+ nClasses: colorRampOptionsRef.current?.numberOfShades,
+ mode: colorRampOptionsRef.current?.selectedMode
+ };
+
+ layer.parameters.symbologyState = symbologyState;
+ layer.parameters.color = newStyle;
+
+ context.model.sharedModel.updateLayer(layerId, layer);
+ cancel();
+ };
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default Categorized;
diff --git a/packages/base/src/dialogs/components/symbology/Graduated.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx
similarity index 52%
rename from packages/base/src/dialogs/components/symbology/Graduated.tsx
rename to packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx
index 86eac81d..6d38728b 100644
--- a/packages/base/src/dialogs/components/symbology/Graduated.tsx
+++ b/packages/base/src/dialogs/symbology/vector_layer/types/Graduated.tsx
@@ -1,12 +1,15 @@
-import { GeoJSONFeature1, IVectorLayer } from '@jupytergis/schema';
-import { Button } from '@jupyterlab/ui-components';
-import colormap from 'colormap';
import { ExpressionValue } from 'ol/expr/expression';
import React, { useEffect, useRef, useState } from 'react';
-import { VectorClassifications } from '../../../classificationModes';
+import { VectorClassifications } from '../../classificationModes';
import { IStopRow, ISymbologyDialogProps } from '../../symbologyDialog';
-import ColorRamp, { ColorRampOptions } from './ColorRamp';
-import StopRow from './StopRow';
+import ColorRamp, {
+ ColorRampOptions
+} from '../../components/color_ramp/ColorRamp';
+import ValueSelect from '../components/ValueSelect';
+import StopContainer from '../../components/color_stops/StopContainer';
+import { useGetProperties } from '../../hooks/useGetProperties';
+import { Utils, VectorUtils } from '../../symbologyUtils';
+import { IVectorLayer } from '@jupytergis/schema';
const Graduated = ({
context,
@@ -29,10 +32,10 @@ const Graduated = ({
const colorRampOptionsRef = useRef();
const [selectedValue, setSelectedValue] = useState('');
- const [featureProperties, setFeatureProperties] = useState({});
const [selectedMethod, setSelectedMethod] = useState('color');
const [stopRows, setStopRows] = useState([]);
const [methodOptions, setMethodOptions] = useState(['color']);
+
const [colorRampOptions, setColorRampOptions] = useState<
ColorRampOptions | undefined
>();
@@ -45,38 +48,25 @@ const Graduated = ({
return;
}
- useEffect(() => {
- const getProperties = async () => {
- if (!layerId) {
- return;
- }
- const model = context.model;
- const layer = model.getLayer(layerId);
- const source = model.getSource(layer?.parameters?.source);
-
- if (!source) {
- return;
- }
-
- const data = await model.readGeoJSON(source.parameters?.path);
- const featureProps: any = {};
+ const { featureProps } = useGetProperties({
+ layerId,
+ model: context.model
+ });
- data?.features.forEach((feature: GeoJSONFeature1) => {
- feature.properties &&
- Object.entries(feature.properties).forEach(([key, value]) => {
- if (!(key in featureProps)) {
- featureProps[key] = new Set();
- }
+ useEffect(() => {
+ let stopOutputPairs: IStopRow[] = [];
+ const layerParams = layer.parameters as IVectorLayer;
+ const method = layerParams.symbologyState?.method ?? 'color';
- featureProps[key].add(value);
- });
+ if (method === 'color') {
+ stopOutputPairs = VectorUtils.buildColorInfo(layer);
+ }
- setFeatureProperties(featureProps);
- });
- };
+ if (method === 'radius') {
+ stopOutputPairs = VectorUtils.buildRadiusInfo(layer);
+ }
- getProperties();
- buildColorInfo();
+ setStopRows(stopOutputPairs);
okSignalPromise.promise.then(okSignal => {
okSignal.connect(handleOk, this);
@@ -98,49 +88,7 @@ const Graduated = ({
useEffect(() => {
populateOptions();
- }, [featureProperties]);
-
- const buildColorInfo = () => {
- // This it to parse a color object on the layer
- if (!layer.parameters?.color) {
- return;
- }
-
- const color = layer.parameters.color;
-
- // If color is a string we don't need to parse
- if (typeof color === 'string') {
- return;
- }
-
- const prefix = layer.parameters.type === 'circle' ? 'circle-' : '';
- if (!color[`${prefix}fill-color`]) {
- return;
- }
-
- const valueColorPairs: IStopRow[] = [];
-
- // So if it's not a string then it's an array and we parse
- // Color[0] is the operator used for the color expression
- switch (color[`${prefix}fill-color`][0]) {
- case 'interpolate': {
- // First element is interpolate for linear selection
- // 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[`${prefix}fill-color`].length; i += 2) {
- const obj: IStopRow = {
- stop: color[`${prefix}fill-color`][i],
- output: color[`${prefix}fill-color`][i + 1]
- };
- valueColorPairs.push(obj);
- }
- break;
- }
- }
-
- setStopRows(valueColorPairs);
- };
+ }, [featureProps]);
const populateOptions = async () => {
// Set up method options
@@ -150,13 +98,9 @@ const Graduated = ({
}
const layerParams = layer.parameters as IVectorLayer;
- const value = layerParams.symbologyState?.value
- ? layerParams.symbologyState.value
- : Object.keys(featureProperties)[0];
-
- const method = layerParams.symbologyState?.method
- ? layerParams.symbologyState.method
- : 'color';
+ const value =
+ layerParams.symbologyState?.value ?? Object.keys(featureProps)[0];
+ const method = layerParams.symbologyState?.method ?? 'color';
setSelectedValue(value);
setSelectedMethod(method);
@@ -215,23 +159,6 @@ const Graduated = ({
cancel();
};
- const addStopRow = () => {
- setStopRows([
- {
- stop: 0,
- output: [0, 0, 0, 1]
- },
- ...stopRows
- ]);
- };
-
- const deleteStopRow = (index: number) => {
- const newFilters = [...stopRows];
- newFilters.splice(index, 1);
-
- setStopRows(newFilters);
- };
-
const buildColorInfoFromClassification = (
selectedMode: string,
numberOfShades: string,
@@ -245,7 +172,7 @@ const Graduated = ({
let stops;
- const values = featureProperties[selectedValue];
+ const values = Array.from(featureProps[selectedValue]);
switch (selectedMode) {
case 'quantile':
@@ -283,42 +210,29 @@ const Graduated = ({
return;
}
- const colorMap = colormap({
- colormap: selectedRamp,
- nshades: +numberOfShades,
- format: 'rgba'
- });
-
- const valueColorPairs: IStopRow[] = [];
-
- for (let i = 0; i < +numberOfShades; i++) {
- valueColorPairs.push({ stop: stops[i], output: colorMap[i] });
+ let stopOutputPairs = [];
+ if (selectedMethod === 'radius') {
+ for (let i = 0; i < +numberOfShades; i++) {
+ stopOutputPairs.push({ stop: stops[i], output: stops[i] });
+ }
+ } else {
+ stopOutputPairs = Utils.getValueColorPairs(
+ stops,
+ selectedRamp,
+ +numberOfShades
+ );
}
- setStopRows(valueColorPairs);
+ setStopRows(stopOutputPairs);
};
return (
-
-
-
-
+
+
-
-
- Value
- Output Value
-
- {stopRows.map((stop, index) => (
-
deleteStopRow(index)}
- useNumber={selectedMethod === 'radius' ? true : false}
- />
- ))}
-
-
-
-
);
};
diff --git a/packages/base/src/dialogs/components/symbology/SimpleSymbol.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/SimpleSymbol.tsx
similarity index 99%
rename from packages/base/src/dialogs/components/symbology/SimpleSymbol.tsx
rename to packages/base/src/dialogs/symbology/vector_layer/types/SimpleSymbol.tsx
index b89cfbf4..25b09aaf 100644
--- a/packages/base/src/dialogs/components/symbology/SimpleSymbol.tsx
+++ b/packages/base/src/dialogs/symbology/vector_layer/types/SimpleSymbol.tsx
@@ -1,6 +1,6 @@
import { FlatStyle } from 'ol/style/flat';
import React, { useEffect, useRef, useState } from 'react';
-import { IParsedStyle, parseColor } from '../../../tools';
+import { IParsedStyle, parseColor } from '../../../../tools';
import { ISymbologyDialogProps } from '../../symbologyDialog';
const SimpleSymbol = ({