@jupytergis/base 0.16.0-alpha.0 → 0.16.0-alpha.1
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.
- package/lib/commands/index.js +1 -2
- package/lib/constants.js +4 -0
- package/lib/features/layers/forms/layer/index.d.ts +0 -1
- package/lib/features/layers/forms/layer/index.js +0 -1
- package/lib/features/layers/symbology/Grammar.js +101 -20
- package/lib/features/layers/symbology/components/MappingRow.js +32 -28
- package/lib/features/layers/symbology/components/ScaleEditor.js +3 -3
- package/lib/features/layers/symbology/components/color_ramp/ColorRampControls.js +1 -1
- package/lib/features/layers/symbology/components/color_stops/StopContainer.js +1 -1
- package/lib/features/layers/symbology/components/color_stops/StopRow.js +13 -1
- package/lib/features/layers/symbology/grammarToOLStyle.js +22 -2
- package/lib/features/layers/symbology/styleBuilder.d.ts +3 -0
- package/lib/features/layers/symbology/styleBuilder.js +15 -2
- package/lib/features/layers/symbology/symbologyDialog.js +3 -5
- package/lib/features/story/SpectaPanel.js +1 -1
- package/lib/features/story/components/ListStoryStageOverlay.d.ts +0 -1
- package/lib/features/story/components/ListStoryStageOverlay.js +52 -34
- package/lib/features/story/components/ListStoryTitleBar.d.ts +7 -0
- package/lib/features/story/components/ListStoryTitleBar.js +20 -0
- package/lib/features/story/components/ListStoryTitleBarDesktop.d.ts +2 -0
- package/lib/features/story/components/ListStoryTitleBarDesktop.js +55 -0
- package/lib/features/story/components/ListStoryTitleBarMobile.d.ts +2 -0
- package/lib/features/story/components/ListStoryTitleBarMobile.js +41 -0
- package/lib/features/story/components/SpectaMobileListModeContent.d.ts +13 -0
- package/lib/features/story/components/SpectaMobileListModeContent.js +36 -0
- package/lib/features/story/components/SpectaMobileSingleModeContent.d.ts +14 -0
- package/lib/features/story/components/SpectaMobileSingleModeContent.js +98 -0
- package/lib/features/story/components/SpectaMobileView.d.ts +7 -3
- package/lib/features/story/components/SpectaMobileView.js +11 -99
- package/lib/features/story/context/ListStoryScrollTrackContext.d.ts +4 -0
- package/lib/features/story/context/ListStoryScrollTrackContext.js +50 -6
- package/lib/features/story/hooks/useStoryMap.js +1 -16
- package/lib/features/story/types/types.d.ts +5 -0
- package/lib/features/story/utils/computeListStoryScrollState.d.ts +2 -0
- package/lib/features/story/utils/computeListStoryScrollState.js +34 -25
- package/lib/mainview/components/MainViewMapSurface.d.ts +15 -0
- package/lib/mainview/components/MainViewMapSurface.js +13 -0
- package/lib/mainview/components/MainViewOverlayLayer.d.ts +9 -0
- package/lib/mainview/components/MainViewOverlayLayer.js +11 -0
- package/lib/mainview/components/MainViewSidePanels.d.ts +17 -0
- package/lib/mainview/components/MainViewSidePanels.js +10 -0
- package/lib/mainview/components/MainViewSpectaPanel.d.ts +17 -0
- package/lib/mainview/components/MainViewSpectaPanel.js +8 -0
- package/lib/mainview/components/MainViewStoryStage.d.ts +13 -0
- package/lib/mainview/components/MainViewStoryStage.js +17 -0
- package/lib/mainview/components/PositionedFloater.d.ts +10 -0
- package/lib/mainview/components/PositionedFloater.js +7 -0
- package/lib/mainview/mainView.d.ts +3 -7
- package/lib/mainview/mainView.js +84 -164
- package/lib/shared/formbuilder/formselectors.js +1 -4
- package/lib/types.js +0 -1
- package/package.json +2 -2
- package/style/base.css +18 -4
- package/style/layerBrowser.css +3 -3
- package/style/storyPanel.css +192 -2
- package/style/symbologyDialog.css +269 -32
- package/lib/features/layers/forms/layer/heatmapLayerForm.d.ts +0 -3
- package/lib/features/layers/forms/layer/heatmapLayerForm.js +0 -96
- package/lib/features/layers/symbology/Heatmap.d.ts +0 -4
- package/lib/features/layers/symbology/Heatmap.js +0 -109
package/lib/commands/index.js
CHANGED
|
@@ -230,10 +230,9 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
|
|
|
230
230
|
if (!layerType) {
|
|
231
231
|
return false;
|
|
232
232
|
}
|
|
233
|
-
// Selection should only be one vector or heatmap layer
|
|
234
233
|
return (Object.keys(selectedLayers).length === 1 &&
|
|
235
234
|
!model.getSource(layerId) &&
|
|
236
|
-
|
|
235
|
+
layerType === 'VectorLayer');
|
|
237
236
|
}, execute: (args) => {
|
|
238
237
|
const filePath = args === null || args === void 0 ? void 0 : args.filePath;
|
|
239
238
|
const current = filePath
|
package/lib/constants.js
CHANGED
|
@@ -17,9 +17,13 @@ const iconObject = {
|
|
|
17
17
|
RasterLayer: { icon: rasterIcon },
|
|
18
18
|
OpenEOTileLayer: { icon: rasterIcon },
|
|
19
19
|
VectorLayer: { iconClass: 'fa fa-vector-square' },
|
|
20
|
+
VectorTileLayer: { iconClass: 'fa fa-vector-square' },
|
|
20
21
|
HillshadeLayer: { icon: moundIcon },
|
|
22
|
+
GeoTiffLayer: { iconClass: 'fa fa-image' },
|
|
23
|
+
StacLayer: { icon: rasterIcon },
|
|
21
24
|
ImageLayer: { iconClass: 'fa fa-image' },
|
|
22
25
|
VideoLayer: { iconClass: 'fa fa-video' },
|
|
26
|
+
StorySegmentLayer: { iconClass: 'fa fa-link' },
|
|
23
27
|
[CommandIDs.redo]: { icon: redoIcon },
|
|
24
28
|
[CommandIDs.undo]: { icon: undoIcon },
|
|
25
29
|
[CommandIDs.openLayerBrowser]: { icon: bookOpenIcon },
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* transforms (KDE, cluster) followed by (field → scale → channels) mapping rows.
|
|
6
6
|
* Multiple layers allow independent rendering pipelines on the same source.
|
|
7
7
|
*/
|
|
8
|
-
import { faPlus, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
|
|
8
|
+
import { faArrowDown, faArrowUp, faGripVertical, faPlus, faTrash, faXmark, } from '@fortawesome/free-solid-svg-icons';
|
|
9
9
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
10
10
|
import { UUID } from '@lumino/coreutils';
|
|
11
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
|
11
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
12
12
|
import MappingRow, { WhenAddForm, formatPredicate, } from "./components/MappingRow";
|
|
13
13
|
import { NumericInput } from "./components/NumericInput";
|
|
14
14
|
import { useEffectiveSymbologyParams } from "./hooks/useEffectiveSymbologyParams";
|
|
@@ -57,12 +57,24 @@ const TransformRow = ({ transform, availableFields, onChange, onDelete, }) => {
|
|
|
57
57
|
transform.type === 'cluster' && (React.createElement(React.Fragment, null,
|
|
58
58
|
React.createElement("label", null, "radius"),
|
|
59
59
|
React.createElement(NumericInput, { style: { width: 52 }, value: transform.radius, onChange: v => onChange(Object.assign(Object.assign({}, transform), { radius: v })) }))),
|
|
60
|
-
React.createElement(Button, { type: "button",
|
|
60
|
+
React.createElement(Button, { type: "button", variant: "ghost", onClick: onDelete, title: "Remove transform", style: { marginLeft: 'auto' } },
|
|
61
61
|
React.createElement(FontAwesomeIcon, { icon: faTrash }))));
|
|
62
62
|
};
|
|
63
|
-
const LayerSection = ({ layer, layerIndex, totalLayers, availableFields, featureValues, isRasterLayer = false, onChange, onDelete, }) => {
|
|
63
|
+
const LayerSection = ({ layer, layerIndex, totalLayers, availableFields, featureValues, isRasterLayer = false, onChange, onDelete, onMoveUp, onMoveDown, }) => {
|
|
64
64
|
var _a, _b, _c, _d;
|
|
65
65
|
const [addingLayerWhen, setAddingLayerWhen] = useState(false);
|
|
66
|
+
const dragIndexRef = useRef(null);
|
|
67
|
+
const dragOverRef = useRef(null);
|
|
68
|
+
const [dragOverIndex, setDragOverIndex] = useState(null);
|
|
69
|
+
const moveRow = useCallback((fromIndex, toIndex) => {
|
|
70
|
+
if (fromIndex === toIndex) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const next = [...layer.rows];
|
|
74
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
75
|
+
next.splice(toIndex, 0, moved);
|
|
76
|
+
onChange(Object.assign(Object.assign({}, layer), { rows: next }));
|
|
77
|
+
}, [layer, onChange]);
|
|
66
78
|
const addLayerPredicate = useCallback((pred) => {
|
|
67
79
|
var _a;
|
|
68
80
|
onChange(Object.assign(Object.assign({}, layer), { when: [...((_a = layer.when) !== null && _a !== void 0 ? _a : []), pred] }));
|
|
@@ -73,16 +85,17 @@ const LayerSection = ({ layer, layerIndex, totalLayers, availableFields, feature
|
|
|
73
85
|
const next = ((_a = layer.when) !== null && _a !== void 0 ? _a : []).filter((_, i) => i !== index);
|
|
74
86
|
onChange(Object.assign(Object.assign({}, layer), { when: next.length > 0 ? next : undefined }));
|
|
75
87
|
}, [layer, onChange]);
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
const addTransform = useCallback(() => {
|
|
89
|
+
if (layer.transforms.length > 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
onChange(Object.assign(Object.assign({}, layer), { transforms: [defaultTransform('kde')] }));
|
|
80
93
|
}, [layer, onChange]);
|
|
81
|
-
const
|
|
82
|
-
onChange(Object.assign(Object.assign({}, layer), { transforms:
|
|
94
|
+
const updateTransform = useCallback((t) => {
|
|
95
|
+
onChange(Object.assign(Object.assign({}, layer), { transforms: [t] }));
|
|
83
96
|
}, [layer, onChange]);
|
|
84
|
-
const
|
|
85
|
-
onChange(Object.assign(Object.assign({}, layer), { transforms: [
|
|
97
|
+
const removeTransform = useCallback(() => {
|
|
98
|
+
onChange(Object.assign(Object.assign({}, layer), { transforms: [] }));
|
|
86
99
|
}, [layer, onChange]);
|
|
87
100
|
const updateRow = useCallback((index, row) => {
|
|
88
101
|
const next = [...layer.rows];
|
|
@@ -115,10 +128,14 @@ const LayerSection = ({ layer, layerIndex, totalLayers, availableFields, feature
|
|
|
115
128
|
React.createElement("span", { className: "jp-gis-grammar-layer-label" },
|
|
116
129
|
"Layer ",
|
|
117
130
|
layerIndex + 1),
|
|
118
|
-
React.createElement(Button, { type: "button", variant: "
|
|
131
|
+
layer.transforms.length === 0 && (React.createElement(Button, { type: "button", variant: "ghost", onClick: addTransform, title: "Add transform" },
|
|
119
132
|
React.createElement(FontAwesomeIcon, { "data-icon": "inline-start", icon: faPlus }),
|
|
120
|
-
"Transform"),
|
|
121
|
-
totalLayers > 1 && (React.createElement(Button, { type: "button", variant: "
|
|
133
|
+
"Transform")),
|
|
134
|
+
totalLayers > 1 && onMoveUp && (React.createElement(Button, { type: "button", variant: "ghost", style: { height: 32, width: 32 }, onClick: onMoveUp, title: "Move layer up" },
|
|
135
|
+
React.createElement(FontAwesomeIcon, { icon: faArrowUp }))),
|
|
136
|
+
totalLayers > 1 && onMoveDown && (React.createElement(Button, { type: "button", variant: "ghost", style: { height: 32, width: 32 }, onClick: onMoveDown, title: "Move layer down" },
|
|
137
|
+
React.createElement(FontAwesomeIcon, { icon: faArrowDown }))),
|
|
138
|
+
totalLayers > 1 && (React.createElement(Button, { type: "button", variant: "ghost", style: { height: 32, width: 32 }, onClick: onDelete, title: "Remove layer" },
|
|
122
139
|
React.createElement(FontAwesomeIcon, { icon: faTrash })))),
|
|
123
140
|
React.createElement("div", { className: "jp-gis-grammar-when-row" },
|
|
124
141
|
React.createElement("span", { className: "jp-gis-grammar-when-label" }, "when"),
|
|
@@ -132,10 +149,63 @@ const LayerSection = ({ layer, layerIndex, totalLayers, availableFields, feature
|
|
|
132
149
|
React.createElement(FontAwesomeIcon, { icon: faXmark }))))),
|
|
133
150
|
addingLayerWhen ? (React.createElement(WhenAddForm, { availableFields: availableFields, onAdd: addLayerPredicate, onCancel: () => setAddingLayerWhen(false) })) : (React.createElement(Button, { type: "button", className: "jp-gis-grammar-when-add-btn", onClick: () => setAddingLayerWhen(true) },
|
|
134
151
|
React.createElement(FontAwesomeIcon, { icon: faPlus })))),
|
|
135
|
-
layer.transforms
|
|
136
|
-
|
|
152
|
+
layer.transforms[0] && (React.createElement(TransformRow, { transform: layer.transforms[0], availableFields: availableFields, onChange: updateTransform, onDelete: removeTransform })),
|
|
153
|
+
React.createElement("div", { className: "jp-gis-grammar-rules-container", onDragOver: e => {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
// Find which wrapper the cursor is closest to
|
|
156
|
+
const wrappers = Array.from(e.currentTarget.querySelectorAll(':scope > .jp-gis-grammar-drag-wrapper'));
|
|
157
|
+
let idx = layer.rows.length;
|
|
158
|
+
for (let j = 0; j < wrappers.length; j++) {
|
|
159
|
+
const rect = wrappers[j].getBoundingClientRect();
|
|
160
|
+
const midY = rect.top + rect.height / 2;
|
|
161
|
+
if (e.clientY < midY) {
|
|
162
|
+
idx = j;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
dragOverRef.current = idx;
|
|
167
|
+
setDragOverIndex(idx);
|
|
168
|
+
}, onDrop: () => {
|
|
169
|
+
const over = dragOverRef.current;
|
|
170
|
+
if (dragIndexRef.current !== null && over !== null) {
|
|
171
|
+
const to = over > dragIndexRef.current ? over - 1 : over;
|
|
172
|
+
moveRow(dragIndexRef.current, to);
|
|
173
|
+
}
|
|
174
|
+
dragIndexRef.current = null;
|
|
175
|
+
dragOverRef.current = null;
|
|
176
|
+
setDragOverIndex(null);
|
|
177
|
+
}, onDragEnd: () => {
|
|
178
|
+
dragIndexRef.current = null;
|
|
179
|
+
dragOverRef.current = null;
|
|
180
|
+
setDragOverIndex(null);
|
|
181
|
+
} }, layer.rows.map((row, i) => (React.createElement("div", { key: row.id, className: "jp-gis-grammar-drag-wrapper", style: {
|
|
182
|
+
borderTop: dragOverIndex === i && dragIndexRef.current !== i
|
|
183
|
+
? '2px solid var(--jp-brand-color1)'
|
|
184
|
+
: '2px solid transparent',
|
|
185
|
+
borderBottom: dragOverIndex === layer.rows.length &&
|
|
186
|
+
i === layer.rows.length - 1 &&
|
|
187
|
+
dragIndexRef.current !== i
|
|
188
|
+
? '2px solid var(--jp-brand-color1)'
|
|
189
|
+
: '2px solid transparent',
|
|
190
|
+
} },
|
|
191
|
+
layer.rows.length > 1 && (React.createElement("div", { className: "jp-gis-grammar-reorder-bar" },
|
|
192
|
+
React.createElement(Button, { type: "button", disabled: i === 0, onClick: () => moveRow(i, i - 1), title: "Move up" },
|
|
193
|
+
React.createElement(FontAwesomeIcon, { icon: faArrowUp })),
|
|
194
|
+
React.createElement("div", { className: "jp-gis-grammar-drag-handle", draggable: true, onDragStart: e => {
|
|
195
|
+
dragIndexRef.current = i;
|
|
196
|
+
const wrapper = e.currentTarget.closest('.jp-gis-grammar-drag-wrapper');
|
|
197
|
+
if (wrapper) {
|
|
198
|
+
e.dataTransfer.setDragImage(wrapper, 0, 0);
|
|
199
|
+
}
|
|
200
|
+
}, title: "Drag to reorder" },
|
|
201
|
+
React.createElement(FontAwesomeIcon, { icon: faGripVertical })),
|
|
202
|
+
React.createElement(Button, { type: "button", disabled: i === layer.rows.length - 1, onClick: () => moveRow(i, i + 1), title: "Move down" },
|
|
203
|
+
React.createElement(FontAwesomeIcon, { icon: faArrowDown })))),
|
|
204
|
+
React.createElement(MappingRow, { row: row, availableFields: encodingFields, featureValues: featureValues, isRaster: isRaster, onChange: updated => updateRow(i, updated), onDelete: () => removeRow(i) }))))),
|
|
137
205
|
React.createElement("div", { className: "jp-gis-symbology-button-container" },
|
|
138
|
-
React.createElement(Button, {
|
|
206
|
+
React.createElement(Button, { variant: "ghost", style: { margin: '0 0 0.5rem 1rem' }, onClick: addRow },
|
|
207
|
+
React.createElement(FontAwesomeIcon, { icon: faPlus }),
|
|
208
|
+
"Add Mapping"))));
|
|
139
209
|
};
|
|
140
210
|
// ---------------------------------------------------------------------------
|
|
141
211
|
// Grammar panel
|
|
@@ -224,12 +294,23 @@ const Grammar = ({ model, okSignalPromise, layerId, isStorySegmentOverride, segm
|
|
|
224
294
|
{ id: UUID.uuid4(), transforms: [], rows: [] },
|
|
225
295
|
]);
|
|
226
296
|
};
|
|
297
|
+
const moveLayer = useCallback((from, to) => {
|
|
298
|
+
if (from === to) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
setLayers(prev => {
|
|
302
|
+
const next = [...prev];
|
|
303
|
+
const [moved] = next.splice(from, 1);
|
|
304
|
+
next.splice(to, 0, moved);
|
|
305
|
+
return next;
|
|
306
|
+
});
|
|
307
|
+
}, [setLayers]);
|
|
227
308
|
const availableFields = isRasterLayer
|
|
228
309
|
? bandRows.map(b => `$band-${b.band}`)
|
|
229
310
|
: Object.keys(selectableAttributesAndValues);
|
|
230
311
|
return (React.createElement("div", { className: "jp-gis-layer-symbology-container" },
|
|
231
|
-
layers.map((uiLayer, i) => (React.createElement(LayerSection, { key: uiLayer.id, layer: uiLayer, layerIndex: i, totalLayers: layers.length, availableFields: availableFields, featureValues: selectableAttributesAndValues, isRasterLayer: isRasterLayer, onChange: updated => setLayers(prev => prev.map((l, j) => (j === i ? updated : l))), onDelete: () => setLayers(prev => prev.filter((_, j) => j !== i)) }))),
|
|
312
|
+
layers.map((uiLayer, i) => (React.createElement(LayerSection, { key: uiLayer.id, layer: uiLayer, layerIndex: i, totalLayers: layers.length, availableFields: availableFields, featureValues: selectableAttributesAndValues, isRasterLayer: isRasterLayer, onChange: updated => setLayers(prev => prev.map((l, j) => (j === i ? updated : l))), onDelete: () => setLayers(prev => prev.filter((_, j) => j !== i)), onMoveUp: i > 0 ? () => moveLayer(i, i - 1) : undefined, onMoveDown: i < layers.length - 1 ? () => moveLayer(i, i + 1) : undefined }))),
|
|
232
313
|
React.createElement("div", { className: "jp-gis-symbology-button-container" },
|
|
233
|
-
React.createElement(Button, { className: "jp-
|
|
314
|
+
React.createElement(Button, { className: "jp-gis-grammar-action-btn", onClick: addLayer }, "Add Layer"))));
|
|
234
315
|
};
|
|
235
316
|
export default Grammar;
|
|
@@ -96,7 +96,7 @@ function defaultScaleForScheme(scheme, _currentChannels) {
|
|
|
96
96
|
return {
|
|
97
97
|
scheme: 'categorical',
|
|
98
98
|
params: {
|
|
99
|
-
colorRamp: '
|
|
99
|
+
colorRamp: 'schemeCategory10',
|
|
100
100
|
reverse: false,
|
|
101
101
|
fallback: [0, 0, 0, 0],
|
|
102
102
|
},
|
|
@@ -455,36 +455,40 @@ const MappingRow = ({ row, availableFields, featureValues, isRaster = false, onC
|
|
|
455
455
|
}, [row, onChange]);
|
|
456
456
|
const compat = compatibleChannels(row.scale, isRaster);
|
|
457
457
|
const availableToAdd = compat.filter(ch => !row.channels.includes(ch));
|
|
458
|
-
const previewRowSpan = row.channels.length + (availableToAdd.length > 0 ? 1 : 0);
|
|
459
458
|
return (React.createElement("div", { className: "jp-gis-grammar-rule" },
|
|
460
459
|
React.createElement("div", { className: "jp-gis-grammar-rule-grid" },
|
|
461
|
-
React.createElement(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
React.createElement("
|
|
465
|
-
React.createElement(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
React.createElement(FontAwesomeIcon, { icon: faTrash }))))),
|
|
475
|
-
availableToAdd.length > 0 && (React.createElement(React.Fragment, null,
|
|
476
|
-
React.createElement("span", { className: "jp-gis-grammar-arrow", style: { gridRow: row.channels.length + 1, gridColumn: 4 } }, "+"),
|
|
477
|
-
React.createElement("div", { className: "jp-gis-grammar-channel-select", style: { gridRow: row.channels.length + 1, gridColumn: 5 } },
|
|
478
|
-
React.createElement(NativeSelect, { value: "", onChange: e => {
|
|
479
|
-
if (e.target.value) {
|
|
480
|
-
addChannel(e.target.value);
|
|
481
|
-
}
|
|
482
|
-
} },
|
|
483
|
-
React.createElement(NativeSelectOption, { value: "" }, "(add channel)"),
|
|
484
|
-
availableToAdd.map(ch => {
|
|
460
|
+
React.createElement("div", { className: "jp-gis-grammar-section jp-gis-grammar-input-section" },
|
|
461
|
+
React.createElement(FieldSelector, { fieldCount: fieldCountForScale(row.scale.scheme), fields: (_a = row.fields) !== null && _a !== void 0 ? _a : [], availableFields: availableFields, onFieldChange: handleFieldChange, onAddField: addField })),
|
|
462
|
+
React.createElement("span", { className: "jp-gis-grammar-arrow jp-gis-grammar-arrow-input" }, "\u2192"),
|
|
463
|
+
React.createElement("div", { className: "jp-gis-grammar-section jp-gis-grammar-scale-section" },
|
|
464
|
+
React.createElement(NativeSelect, { value: row.scale.scheme, onChange: e => handleSchemeChange(e.target.value) }, SCHEME_OPTIONS.filter(({ disabled }) => !disabled).map(({ value, label }) => (React.createElement(NativeSelectOption, { key: value, value: value }, label)))),
|
|
465
|
+
React.createElement("button", { type: "button", className: "jp-gis-grammar-preview-btn", onClick: () => setExpanded(v => !v), title: expanded ? 'Collapse editor' : 'Edit scale' },
|
|
466
|
+
React.createElement(ScalePreview, { scale: row.scale }),
|
|
467
|
+
React.createElement("span", { className: "jp-gis-grammar-preview-chevron", "aria-hidden": "true" }, expanded ? '▾' : '▸'))),
|
|
468
|
+
React.createElement("span", { className: "jp-gis-grammar-arrow jp-gis-grammar-arrow-output" }, "\u2192"),
|
|
469
|
+
React.createElement("div", { className: "jp-gis-grammar-section jp-gis-grammar-output-section" },
|
|
470
|
+
row.channels.map((ch, i) => (React.createElement("div", { key: `${ch}-${i}`, className: "jp-gis-grammar-channel-row" },
|
|
471
|
+
React.createElement("div", { className: "jp-gis-grammar-channel-select" },
|
|
472
|
+
React.createElement(NativeSelect, { value: ch, onChange: e => handleChannelChange(i, e.target.value) }, compat.map(c => {
|
|
485
473
|
var _a;
|
|
486
|
-
return (React.createElement(NativeSelectOption, { key:
|
|
487
|
-
})))
|
|
474
|
+
return (React.createElement(NativeSelectOption, { key: c, value: c }, (_a = CHANNEL_LABELS[c]) !== null && _a !== void 0 ? _a : c));
|
|
475
|
+
}))),
|
|
476
|
+
React.createElement(Button, { type: "button", variant: "ghost", size: "icon-md", className: "jp-mod-styled", onClick: () => removeChannel(ch), title: row.channels.length === 1
|
|
477
|
+
? 'Remove mapping'
|
|
478
|
+
: 'Remove channel' },
|
|
479
|
+
React.createElement(FontAwesomeIcon, { icon: faTrash }))))),
|
|
480
|
+
availableToAdd.length > 0 && (React.createElement("div", { className: "jp-gis-grammar-channel-row" },
|
|
481
|
+
React.createElement("div", { className: "jp-gis-grammar-channel-select" },
|
|
482
|
+
React.createElement(NativeSelect, { value: "", onChange: e => {
|
|
483
|
+
if (e.target.value) {
|
|
484
|
+
addChannel(e.target.value);
|
|
485
|
+
}
|
|
486
|
+
} },
|
|
487
|
+
React.createElement(NativeSelectOption, { value: "" }, "(add channel)"),
|
|
488
|
+
availableToAdd.map(ch => {
|
|
489
|
+
var _a;
|
|
490
|
+
return (React.createElement(NativeSelectOption, { key: ch, value: ch }, (_a = CHANNEL_LABELS[ch]) !== null && _a !== void 0 ? _a : ch));
|
|
491
|
+
}))))))),
|
|
488
492
|
React.createElement("div", { className: "jp-gis-grammar-when-row" },
|
|
489
493
|
React.createElement("span", { className: "jp-gis-grammar-when-label" }, "when"),
|
|
490
494
|
((_c = (_b = row.when) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 1 && (React.createElement(Button, { type: "button", className: "jp-gis-grammar-when-op", onClick: () => {
|
|
@@ -112,7 +112,7 @@ export const ColorRampEditor = ({ scale, field, featureValues, onChange, }) => {
|
|
|
112
112
|
React.createElement("div", { className: "jp-gis-symbology-row" },
|
|
113
113
|
React.createElement("label", null, "Fallback"),
|
|
114
114
|
React.createElement(RgbaColorPicker, { color: params.fallback, onChange: color => update({ fallback: color }) })),
|
|
115
|
-
React.createElement("button", { className: "jp-
|
|
115
|
+
React.createElement("button", { className: "jp-gis-grammar-action-btn", disabled: !field, onClick: classify }, "Classify"),
|
|
116
116
|
stopRows.length > 0 && (React.createElement(StopContainer, { selectedMethod: "color", stopRows: stopRows, setStopRows: handleStopRowsChange }))));
|
|
117
117
|
};
|
|
118
118
|
export const CategoricalEditor = ({ scale, field, featureValues, onChange, }) => {
|
|
@@ -163,7 +163,7 @@ export const CategoricalEditor = ({ scale, field, featureValues, onChange, }) =>
|
|
|
163
163
|
React.createElement("div", { className: "jp-gis-symbology-row" },
|
|
164
164
|
React.createElement("label", null, "Fallback"),
|
|
165
165
|
React.createElement(RgbaColorPicker, { color: params.fallback, onChange: color => update({ fallback: color }) })),
|
|
166
|
-
React.createElement("button", { className: "jp-
|
|
166
|
+
React.createElement("button", { className: "jp-gis-grammar-action-btn", disabled: !field, onClick: classify }, "Classify"),
|
|
167
167
|
stopRows.length > 0 && (React.createElement(StopContainer, { selectedMethod: "color", stopRows: stopRows, setStopRows: handleStopRowsChange }))));
|
|
168
168
|
};
|
|
169
169
|
export const ScalarEditor = ({ scale, field, featureValues, onChange, }) => {
|
|
@@ -216,6 +216,6 @@ export const ScalarEditor = ({ scale, field, featureValues, onChange, }) => {
|
|
|
216
216
|
React.createElement("div", { className: "jp-gis-symbology-row" },
|
|
217
217
|
React.createElement("label", null, "Fallback"),
|
|
218
218
|
React.createElement(NumericInput, { className: "jp-mod-styled", value: params.fallback, onChange: v => update({ fallback: v }) })),
|
|
219
|
-
React.createElement("button", { className: "jp-
|
|
219
|
+
React.createElement("button", { className: "jp-gis-grammar-action-btn", onClick: classify }, "Set stops"),
|
|
220
220
|
stopRows.length > 0 && (React.createElement(StopContainer, { selectedMethod: "radius", stopRows: stopRows, setStopRows: handleStopRowsChange }))));
|
|
221
221
|
};
|
|
@@ -80,6 +80,6 @@ const ColorRampControls = ({ layerParams, modeOptions, classifyFunc, showModeRow
|
|
|
80
80
|
React.createElement(ColorRampSelector, { selectedRamp: selectedRamp, setSelected: handleRampChange, reverse: reverseRamp, setReverse: setReverseRamp }))),
|
|
81
81
|
showModeRow && (React.createElement(ModeSelectRow, { modeOptions: modeOptions, numberOfShades: numberOfShades, setNumberOfShades: setNumberOfShades, selectedMode: selectedMode, setSelectedMode: setSelectedMode })),
|
|
82
82
|
warning && (React.createElement("div", { className: "jp-gis-warning", style: { color: 'orange', marginTop: 4 } }, warning)),
|
|
83
|
-
isLoading ? (React.createElement(LoadingIcon, null)) : (React.createElement(Button, { className: "jp-
|
|
83
|
+
isLoading ? (React.createElement(LoadingIcon, null)) : (React.createElement(Button, { className: "jp-gis-grammar-action-btn", disabled: !isValidNumberOfShades(numberOfShades) || !selectedMode || !!warning, onClick: () => classifyFunc(selectedMode, numberOfShades, selectedRamp, reverseRamp, setIsLoading) }, "Classify"))));
|
|
84
84
|
};
|
|
85
85
|
export default ColorRampControls;
|
|
@@ -25,6 +25,6 @@ const StopContainer = ({ selectedMethod, stopRows, setStopRows, }) => {
|
|
|
25
25
|
React.createElement("span", null, "Output Value")),
|
|
26
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 })))),
|
|
27
27
|
React.createElement("div", { className: "jp-gis-symbology-button-container" },
|
|
28
|
-
React.createElement(Button, { className: "jp-
|
|
28
|
+
React.createElement(Button, { className: "jp-gis-grammar-action-btn", onClick: addStopRow }, "Add Stop"))));
|
|
29
29
|
};
|
|
30
30
|
export default StopContainer;
|
|
@@ -4,6 +4,8 @@ import { Button } from '@jupyterlab/ui-components';
|
|
|
4
4
|
import React, { useEffect, useRef } from 'react';
|
|
5
5
|
import { colorToRgba, } from "../../colorRampUtils";
|
|
6
6
|
import RgbaColorPicker from "../color_ramp/RgbaColorPicker";
|
|
7
|
+
import { STOP_NULL, STOP_UNDEFINED, } from "../../styleBuilder";
|
|
8
|
+
const SENTINELS = new Set([STOP_NULL, STOP_UNDEFINED]);
|
|
7
9
|
const StopRow = ({ index, dataValue, symbologyValue, stopRows, setStopRows, deleteRow, useNumber, }) => {
|
|
8
10
|
const inputRef = useRef(null);
|
|
9
11
|
useEffect(() => {
|
|
@@ -21,6 +23,14 @@ const StopRow = ({ index, dataValue, symbologyValue, stopRows, setStopRows, dele
|
|
|
21
23
|
const handleBlur = () => {
|
|
22
24
|
const newRows = [...stopRows];
|
|
23
25
|
newRows.sort((a, b) => {
|
|
26
|
+
const aIsSentinel = SENTINELS.has(a.stop);
|
|
27
|
+
const bIsSentinel = SENTINELS.has(b.stop);
|
|
28
|
+
if (aIsSentinel && !bIsSentinel) {
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
if (!aIsSentinel && bIsSentinel) {
|
|
32
|
+
return -1;
|
|
33
|
+
}
|
|
24
34
|
if (a.stop < b.stop) {
|
|
25
35
|
return -1;
|
|
26
36
|
}
|
|
@@ -42,7 +52,9 @@ const StopRow = ({ index, dataValue, symbologyValue, stopRows, setStopRows, dele
|
|
|
42
52
|
setStopRows(newRows);
|
|
43
53
|
};
|
|
44
54
|
return (React.createElement("div", { className: "jp-gis-color-row" },
|
|
45
|
-
React.createElement("input", { id: `jp-gis-color-value-${index}`, type: useNumber ? 'number' : 'text', value: dataValue, onChange: handleStopChange, onBlur: handleBlur, className: "jp-mod-styled jp-gis-color-row-value-input"
|
|
55
|
+
React.createElement("input", { id: `jp-gis-color-value-${index}`, type: useNumber ? 'number' : 'text', value: dataValue, onChange: handleStopChange, onBlur: handleBlur, className: "jp-mod-styled jp-gis-color-row-value-input", style: SENTINELS.has(dataValue)
|
|
56
|
+
? { fontStyle: 'italic' }
|
|
57
|
+
: undefined }),
|
|
46
58
|
useNumber ? (React.createElement("input", { type: "number", ref: inputRef, value: symbologyValue, onChange: handleOutputChange, className: "jp-mod-styled jp-gis-color-row-output-input" })) : (React.createElement(RgbaColorPicker, { color: colorToRgba(symbologyValue), onChange: handleColorOutputChange })),
|
|
47
59
|
React.createElement(Button, { id: `jp-gis-remove-color-${index}`, className: "jp-Button jp-gis-filter-icon" },
|
|
48
60
|
React.createElement(FontAwesomeIcon, { icon: faTrash, onClick: deleteRow }))));
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 3. Assemble sub-channels (fill-red/green/blue/alpha) into a composite
|
|
12
12
|
* fill-color ['array', r, g, b, a] expression.
|
|
13
13
|
*/
|
|
14
|
-
import { computeCategorizedColorStops, computeGraduatedColorStops, } from './styleBuilder';
|
|
14
|
+
import { computeCategorizedColorStops, computeGraduatedColorStops, STOP_NULL, STOP_UNDEFINED, } from './styleBuilder';
|
|
15
15
|
// '$density' is the pseudo-field produced by a kde transform (KDE density raster).
|
|
16
16
|
// Encoding rules referencing it are compiled only when a kde transform is present;
|
|
17
17
|
// the actual OL HeatmapLayer instantiation happens outside this compiler.
|
|
@@ -433,7 +433,27 @@ function compileCategorical(field, scale, featureValues) {
|
|
|
433
433
|
}
|
|
434
434
|
const caseExpr = ['case'];
|
|
435
435
|
for (const stop of stops) {
|
|
436
|
-
|
|
436
|
+
let condition;
|
|
437
|
+
if (stop.value === STOP_UNDEFINED) {
|
|
438
|
+
// Property missing entirely
|
|
439
|
+
condition = ['!', ['has', field]];
|
|
440
|
+
}
|
|
441
|
+
else if (stop.value === STOP_NULL) {
|
|
442
|
+
// Property exists but value is null
|
|
443
|
+
condition = [
|
|
444
|
+
'all',
|
|
445
|
+
['has', field],
|
|
446
|
+
['==', ['coalesce', fieldExpr(field), '__jgis_ns__'], '__jgis_ns__'],
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
condition = [
|
|
451
|
+
'==',
|
|
452
|
+
fieldExpr(field),
|
|
453
|
+
stop.value,
|
|
454
|
+
];
|
|
455
|
+
}
|
|
456
|
+
caseExpr.push(condition, stop.color);
|
|
437
457
|
}
|
|
438
458
|
caseExpr.push(scale.params.fallback);
|
|
439
459
|
return caseExpr;
|
|
@@ -6,6 +6,9 @@ export type SymbologyState = NonNullable<IVectorLayer['symbologyState']>;
|
|
|
6
6
|
export type GeometryType = 'fill' | 'circle' | 'line';
|
|
7
7
|
/** Default OL flat style used when no Grammar rules produce output. */
|
|
8
8
|
export declare const DEFAULT_FLAT_STYLE: FlatStyle;
|
|
9
|
+
/** Sentinel stop values for missing data. */
|
|
10
|
+
export declare const STOP_NULL = "__null__";
|
|
11
|
+
export declare const STOP_UNDEFINED = "__undefined__";
|
|
9
12
|
/** A computed stop: value → RGBA color. */
|
|
10
13
|
export interface IComputedStop {
|
|
11
14
|
value: number | string | boolean;
|
|
@@ -12,6 +12,9 @@ export const DEFAULT_FLAT_STYLE = {
|
|
|
12
12
|
'circle-stroke-width': 1.25,
|
|
13
13
|
'circle-stroke-color': '#3399CC',
|
|
14
14
|
};
|
|
15
|
+
/** Sentinel stop values for missing data. */
|
|
16
|
+
export const STOP_NULL = '__null__';
|
|
17
|
+
export const STOP_UNDEFINED = '__undefined__';
|
|
15
18
|
// Stop computation helpers — used by the Grammar compiler
|
|
16
19
|
/**
|
|
17
20
|
* Compute color stops for Graduated symbology from the config + feature values.
|
|
@@ -80,8 +83,18 @@ export function computeCategorizedColorStops(state, featureValues) {
|
|
|
80
83
|
const rampName = (_a = state.colorRamp) !== null && _a !== void 0 ? _a : 'viridis';
|
|
81
84
|
const reverse = (_b = state.reverseRamp) !== null && _b !== void 0 ? _b : false;
|
|
82
85
|
const uniqueValues = [
|
|
83
|
-
...new Set(featureValues.
|
|
84
|
-
].sort((a, b) =>
|
|
86
|
+
...new Set(featureValues.map(v => v === null ? STOP_NULL : v === undefined ? STOP_UNDEFINED : v)),
|
|
87
|
+
].sort((a, b) => {
|
|
88
|
+
const aIsSentinel = a === STOP_NULL || a === STOP_UNDEFINED;
|
|
89
|
+
const bIsSentinel = b === STOP_NULL || b === STOP_UNDEFINED;
|
|
90
|
+
if (aIsSentinel && !bIsSentinel) {
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
if (!aIsSentinel && bIsSentinel) {
|
|
94
|
+
return -1;
|
|
95
|
+
}
|
|
96
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
97
|
+
});
|
|
85
98
|
if (uniqueValues.length === 0) {
|
|
86
99
|
return [];
|
|
87
100
|
}
|
|
@@ -3,7 +3,6 @@ import { PromiseDelegate } from '@lumino/coreutils';
|
|
|
3
3
|
import { Signal } from '@lumino/signaling';
|
|
4
4
|
import React, { useEffect, useState } from 'react';
|
|
5
5
|
import Grammar from './Grammar';
|
|
6
|
-
import Heatmap from './Heatmap';
|
|
7
6
|
const SymbologyDialog = ({ model, okSignalPromise, isStorySegmentOverride, segmentId, }) => {
|
|
8
7
|
const [selectedLayer, setSelectedLayer] = useState(null);
|
|
9
8
|
const [componentToRender, setComponentToRender] = useState(null);
|
|
@@ -34,9 +33,6 @@ const SymbologyDialog = ({ model, okSignalPromise, isStorySegmentOverride, segme
|
|
|
34
33
|
}
|
|
35
34
|
// TODO GeoTiffLayers can also be used for other layers, need a better way to determine source + layer combo
|
|
36
35
|
switch (layer.type) {
|
|
37
|
-
case 'HeatmapLayer':
|
|
38
|
-
LayerSymbology = (React.createElement(Heatmap, { model: model, okSignalPromise: okSignalPromise, layerId: selectedLayer, isStorySegmentOverride: isStorySegmentOverride, segmentId: segmentId }));
|
|
39
|
-
break;
|
|
40
36
|
case 'VectorLayer':
|
|
41
37
|
case 'VectorTileLayer':
|
|
42
38
|
case 'GeoTiffLayer':
|
|
@@ -53,7 +49,9 @@ export class SymbologyWidget extends Dialog {
|
|
|
53
49
|
constructor(options) {
|
|
54
50
|
const okSignalPromise = new PromiseDelegate();
|
|
55
51
|
const body = (React.createElement(SymbologyDialog, { model: options.model, okSignalPromise: okSignalPromise, isStorySegmentOverride: options.isStorySegmentOverride, segmentId: options.segmentId }));
|
|
56
|
-
|
|
52
|
+
const layerId = Object.keys(options.model.localState.selected.value)[0];
|
|
53
|
+
const layerName = options.model.getLayer(layerId).name;
|
|
54
|
+
super({ title: `Symbology — ${layerName}`, body });
|
|
57
55
|
this.id = 'jupytergis::symbologyWidget';
|
|
58
56
|
this.okSignal = new Signal(this);
|
|
59
57
|
okSignalPromise.resolve(this.okSignal);
|
|
@@ -28,7 +28,7 @@ export function SpectaPanel({ model, isSpecta, isMobile, onSegmentTransitionEnd,
|
|
|
28
28
|
return () => el.removeEventListener('animationend', handleAnimationEnd);
|
|
29
29
|
}, [currentIndex, onSegmentTransitionEnd]);
|
|
30
30
|
if (isMobile) {
|
|
31
|
-
return (React.createElement(SpectaMobileView, { segmentContainerRef: segmentContainerRef, storyData: storyData, currentIndex: currentIndex, activeSlide: activeSlide, layerName: layerName, handlePrev: handlePrev, handleNext: handleNext, hasPrev: hasPrev, hasNext: hasNext }));
|
|
31
|
+
return (React.createElement(SpectaMobileView, { model: model, segmentContainerRef: segmentContainerRef, storyData: storyData, currentIndex: currentIndex, setIndex: setIndex, activeSlide: activeSlide, layerName: layerName, handlePrev: handlePrev, handleNext: handleNext, hasPrev: hasPrev, hasNext: hasNext, onSegmentTransitionChange: onSegmentTransitionChange }));
|
|
32
32
|
}
|
|
33
33
|
return (React.createElement(SpectaDesktopView, { model: model, isSpecta: isSpecta, containerRef: containerRef, storyViewerPanelRef: storyViewerPanelRef, segmentContainerRef: segmentContainerRef, storyData: storyData, currentIndex: currentIndex, activeSlide: activeSlide, layerName: layerName, handlePrev: handlePrev, handleNext: handleNext, hasPrev: hasPrev, hasNext: hasNext, showGradient: showGradient, setIndex: setIndex, onSegmentTransitionChange: onSegmentTransitionChange }));
|
|
34
34
|
}
|
|
@@ -6,7 +6,6 @@ interface IListStoryStageOverlayProps {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* List-story stage overlay: map + markdown segments on the map stage.
|
|
9
|
-
* The story column scrolls only the virtual track; this is the visible UI.
|
|
10
9
|
*/
|
|
11
10
|
export declare function ListStoryStageOverlay({ model, segmentTransition, }: IListStoryStageOverlayProps): JSX.Element | null;
|
|
12
11
|
export {};
|