@jupytergis/base 0.12.2 → 0.13.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 +2 -6
- package/lib/dialogs/symbology/hooks/useEffectiveSymbologyParams.d.ts +16 -0
- package/lib/dialogs/symbology/hooks/useEffectiveSymbologyParams.js +24 -0
- package/lib/dialogs/symbology/hooks/useOkSignal.d.ts +6 -0
- package/lib/dialogs/symbology/hooks/useOkSignal.js +25 -0
- package/lib/dialogs/symbology/symbologyDialog.d.ts +4 -2
- package/lib/dialogs/symbology/symbologyDialog.js +6 -10
- package/lib/dialogs/symbology/symbologyUtils.d.ts +25 -2
- package/lib/dialogs/symbology/symbologyUtils.js +74 -4
- package/lib/dialogs/symbology/tiff_layer/TiffRendering.js +3 -3
- package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +31 -34
- package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +68 -62
- package/lib/dialogs/symbology/vector_layer/VectorRendering.js +33 -21
- package/lib/dialogs/symbology/vector_layer/types/Canonical.js +23 -24
- package/lib/dialogs/symbology/vector_layer/types/Categorized.js +49 -50
- package/lib/dialogs/symbology/vector_layer/types/Graduated.js +53 -62
- package/lib/dialogs/symbology/vector_layer/types/Heatmap.js +35 -34
- package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.js +45 -47
- package/lib/formbuilder/objectform/StoryEditorForm.js +0 -17
- package/lib/formbuilder/objectform/baseform.d.ts +11 -0
- package/lib/formbuilder/objectform/baseform.js +72 -38
- package/lib/formbuilder/objectform/components/LayerSelect.d.ts +7 -0
- package/lib/formbuilder/objectform/components/LayerSelect.js +43 -0
- package/lib/formbuilder/objectform/components/OpacitySlider.d.ts +4 -0
- package/lib/formbuilder/objectform/components/OpacitySlider.js +40 -0
- package/lib/formbuilder/objectform/components/SegmentFormSymbology.d.ts +3 -0
- package/lib/formbuilder/objectform/components/SegmentFormSymbology.js +59 -0
- package/lib/formbuilder/objectform/layer/storySegmentLayerForm.d.ts +2 -2
- package/lib/formbuilder/objectform/layer/storySegmentLayerForm.js +19 -0
- package/lib/formbuilder/objectform/source/geojsonsource.js +1 -3
- package/lib/mainview/mainView.js +6 -1
- package/lib/panelview/rightpanel.d.ts +3 -1
- package/lib/panelview/rightpanel.js +2 -2
- package/lib/panelview/story-maps/StoryViewerPanel.d.ts +3 -1
- package/lib/panelview/story-maps/StoryViewerPanel.js +127 -19
- package/lib/shared/hooks/useLatest.d.ts +1 -0
- package/lib/shared/hooks/useLatest.js +8 -0
- package/lib/types.d.ts +1 -0
- package/lib/types.js +6 -1
- package/package.json +2 -2
- package/style/base.css +8 -0
- package/style/storyPanel.css +4 -4
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
function extractlayerOverrideIndex(idSchema) {
|
|
3
|
+
var _a;
|
|
4
|
+
const id = (_a = idSchema === null || idSchema === void 0 ? void 0 : idSchema.$id) !== null && _a !== void 0 ? _a : '';
|
|
5
|
+
const match = id.match(/layerOverride_(\d+)/);
|
|
6
|
+
return match ? parseInt(match[1], 10) : undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Simple select populated with layers (valid types only).
|
|
10
|
+
* Used as the targetLayer field inside layerOverride array items.
|
|
11
|
+
*/
|
|
12
|
+
export function LayerSelect(props) {
|
|
13
|
+
var _a, _b, _c, _d, _e, _f;
|
|
14
|
+
const { idSchema, formContext, formData, onChange } = props;
|
|
15
|
+
const context = formContext;
|
|
16
|
+
const model = context === null || context === void 0 ? void 0 : context.model;
|
|
17
|
+
const fullFormData = (_a = context === null || context === void 0 ? void 0 : context.formData) !== null && _a !== void 0 ? _a : formData;
|
|
18
|
+
const arrayIndex = extractlayerOverrideIndex(idSchema !== null && idSchema !== void 0 ? idSchema : {});
|
|
19
|
+
const value = arrayIndex !== undefined && ((_b = fullFormData === null || fullFormData === void 0 ? void 0 : fullFormData.layerOverride) === null || _b === void 0 ? void 0 : _b[arrayIndex])
|
|
20
|
+
? ((_c = fullFormData.layerOverride[arrayIndex].targetLayer) !== null && _c !== void 0 ? _c : '')
|
|
21
|
+
: '';
|
|
22
|
+
if (!model) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const layerOverride = (_d = fullFormData === null || fullFormData === void 0 ? void 0 : fullFormData.layerOverride) !== null && _d !== void 0 ? _d : [];
|
|
26
|
+
const currentTargetLayer = arrayIndex !== undefined
|
|
27
|
+
? (_f = (_e = fullFormData === null || fullFormData === void 0 ? void 0 : fullFormData.layerOverride) === null || _e === void 0 ? void 0 : _e[arrayIndex]) === null || _f === void 0 ? void 0 : _f.targetLayer
|
|
28
|
+
: undefined;
|
|
29
|
+
const usedTargetLayerIds = new Set(layerOverride
|
|
30
|
+
.filter((_, i) => i !== arrayIndex)
|
|
31
|
+
.map(override => override.targetLayer)
|
|
32
|
+
.filter(id => id !== undefined && id !== '')
|
|
33
|
+
.filter(id => id !== currentTargetLayer));
|
|
34
|
+
const availableLayers = model.getLayers();
|
|
35
|
+
const optionsList = Object.entries(availableLayers).filter(([layerId]) => !usedTargetLayerIds.has(layerId));
|
|
36
|
+
const handleChange = (e) => {
|
|
37
|
+
const newValue = e.target.value;
|
|
38
|
+
onChange(newValue === '' ? undefined : newValue);
|
|
39
|
+
};
|
|
40
|
+
return (React.createElement("select", { value: value !== null && value !== void 0 ? value : '', onChange: handleChange, style: { width: '100%' } },
|
|
41
|
+
React.createElement("option", { value: "" }, "Select a layer"),
|
|
42
|
+
optionsList.map(([layerId, layer]) => (React.createElement("option", { key: layerId, value: layerId }, layer.name.charAt(0).toUpperCase() + layer.name.slice(1))))));
|
|
43
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Slider } from '@jupyter/react-components';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
function OpacitySlider({ formData, onChange }) {
|
|
4
|
+
var _a;
|
|
5
|
+
const [inputValue, setInputValue] = React.useState((_a = formData === null || formData === void 0 ? void 0 : formData.toFixed(1)) !== null && _a !== void 0 ? _a : '1');
|
|
6
|
+
React.useEffect(() => {
|
|
7
|
+
var _a;
|
|
8
|
+
const newValue = (_a = formData === null || formData === void 0 ? void 0 : formData.toFixed(1)) !== null && _a !== void 0 ? _a : '1';
|
|
9
|
+
if (newValue !== inputValue) {
|
|
10
|
+
setInputValue(newValue);
|
|
11
|
+
}
|
|
12
|
+
}, [formData]);
|
|
13
|
+
const handleSliderChange = (event) => {
|
|
14
|
+
const target = event.target;
|
|
15
|
+
if (target && '_value' in target) {
|
|
16
|
+
const sliderValue = parseFloat(target._value); // Slider value is in 0–10 range
|
|
17
|
+
const normalizedValue = sliderValue / 10; // Normalize to 0.1–1 range
|
|
18
|
+
onChange(normalizedValue);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const handleInputChange = (event) => {
|
|
22
|
+
const value = event.target.value;
|
|
23
|
+
setInputValue(value);
|
|
24
|
+
const parsedValue = parseFloat(value);
|
|
25
|
+
if (!isNaN(parsedValue) && parsedValue >= 0.1 && parsedValue <= 1) {
|
|
26
|
+
onChange(parsedValue);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
return (React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
30
|
+
React.createElement(Slider, { min: 1, max: 10, step: 1, value: formData ? formData * 10 : 10, onChange: handleSliderChange }),
|
|
31
|
+
React.createElement("input", { type: "number", value: inputValue, step: 0.1, min: 0.1, onChange: handleInputChange, style: {
|
|
32
|
+
width: '50px',
|
|
33
|
+
textAlign: 'center',
|
|
34
|
+
border: '1px solid #ccc',
|
|
35
|
+
borderRadius: '4px',
|
|
36
|
+
padding: '4px',
|
|
37
|
+
marginBottom: '5px',
|
|
38
|
+
} })));
|
|
39
|
+
}
|
|
40
|
+
export default OpacitySlider;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SymbologyWidget } from "../../../dialogs/symbology/symbologyDialog";
|
|
3
|
+
import { Button } from "../../../shared/components/Button";
|
|
4
|
+
import { GlobalStateDbManager } from "../../../store";
|
|
5
|
+
import { SYMBOLOGY_VALID_LAYER_TYPES } from "../../../types";
|
|
6
|
+
const SELECTION_SETTLE_MS = 100;
|
|
7
|
+
function LayerOverrideItem({ item, formContext }) {
|
|
8
|
+
var _a, _b;
|
|
9
|
+
const model = formContext === null || formContext === void 0 ? void 0 : formContext.model;
|
|
10
|
+
if (!model) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const state = GlobalStateDbManager.getInstance().getStateDb();
|
|
14
|
+
const currentItem = (_b = (_a = formContext === null || formContext === void 0 ? void 0 : formContext.formData) === null || _a === void 0 ? void 0 : _a.layerOverride) === null || _b === void 0 ? void 0 : _b[item.index];
|
|
15
|
+
const targetLayerId = currentItem === null || currentItem === void 0 ? void 0 : currentItem.targetLayer;
|
|
16
|
+
const selectedLayer = targetLayerId
|
|
17
|
+
? model.getLayer(targetLayerId)
|
|
18
|
+
: undefined;
|
|
19
|
+
const canOpenSymbology = Boolean(targetLayerId &&
|
|
20
|
+
selectedLayer &&
|
|
21
|
+
SYMBOLOGY_VALID_LAYER_TYPES.includes(selectedLayer.type));
|
|
22
|
+
const handleOpenSymbology = async () => {
|
|
23
|
+
if (!targetLayerId || !state || !selectedLayer) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const previousSelection = model.selected;
|
|
27
|
+
const segmentId = Object.keys(previousSelection !== null && previousSelection !== void 0 ? previousSelection : {}).find(key => { var _a; return ((_a = model.getLayer(key)) === null || _a === void 0 ? void 0 : _a.type) === 'StorySegmentLayer'; });
|
|
28
|
+
// Temporarily set the selected layer to the target layer
|
|
29
|
+
model.syncSelected({ [targetLayerId]: { type: 'layer' } });
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, SELECTION_SETTLE_MS));
|
|
31
|
+
const dialog = new SymbologyWidget({
|
|
32
|
+
model,
|
|
33
|
+
state,
|
|
34
|
+
isStorySegmentOverride: true,
|
|
35
|
+
segmentId,
|
|
36
|
+
});
|
|
37
|
+
await dialog.launch();
|
|
38
|
+
model.syncSelected(previousSelection !== null && previousSelection !== void 0 ? previousSelection : {});
|
|
39
|
+
};
|
|
40
|
+
return (React.createElement("div", { className: "jGIS-symbology-override-item" },
|
|
41
|
+
React.createElement("div", { style: { flex: 1 } }, item.children),
|
|
42
|
+
React.createElement("div", { style: { display: 'flex', gap: '1rem' } },
|
|
43
|
+
React.createElement(Button, { title: "Edit layer override for the target layer", onClick: handleOpenSymbology, style: { width: '100%' }, disabled: !canOpenSymbology },
|
|
44
|
+
React.createElement("span", { className: "fa fa-brush", style: { marginRight: '4px' } }),
|
|
45
|
+
"Edit Symbology"),
|
|
46
|
+
item.hasRemove && (React.createElement(Button, { variant: "destructive", onClick: item.onDropIndexClick(item.index), title: "Remove item" }, "Remove")))));
|
|
47
|
+
}
|
|
48
|
+
export function ArrayFieldTemplate(props) {
|
|
49
|
+
return (React.createElement(React.Fragment, null,
|
|
50
|
+
React.createElement("div", null, props.title),
|
|
51
|
+
React.createElement("div", { style: {
|
|
52
|
+
display: 'flex',
|
|
53
|
+
flexDirection: 'column',
|
|
54
|
+
gap: '1rem',
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
} },
|
|
57
|
+
props.items.map(item => (React.createElement(LayerOverrideItem, { key: item.key, item: item, formContext: props.formContext }))),
|
|
58
|
+
props.canAdd && (React.createElement(Button, { onClick: props.onAddClick }, "Add Layer Override")))));
|
|
59
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { IDict } from '@jupytergis/schema';
|
|
1
|
+
import { IDict, IStorySegmentLayer } from '@jupytergis/schema';
|
|
2
2
|
import { LayerPropertiesForm } from './layerform';
|
|
3
3
|
export declare class StorySegmentLayerPropertiesForm extends LayerPropertiesForm {
|
|
4
|
-
protected processSchema(data:
|
|
4
|
+
protected processSchema(data: IStorySegmentLayer | undefined, schema: IDict, uiSchema: IDict): void;
|
|
5
5
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { LayerPropertiesForm } from './layerform';
|
|
3
|
+
import { ArrayFieldTemplate } from '../components/SegmentFormSymbology';
|
|
3
4
|
import StorySegmentReset from '../components/StorySegmentReset';
|
|
4
5
|
export class StorySegmentLayerPropertiesForm extends LayerPropertiesForm {
|
|
5
6
|
processSchema(data, schema, uiSchema) {
|
|
7
|
+
var _a, _b, _c;
|
|
6
8
|
super.processSchema(data, schema, uiSchema);
|
|
7
9
|
if (!this.props.model.selected) {
|
|
8
10
|
return;
|
|
@@ -27,6 +29,23 @@ export class StorySegmentLayerPropertiesForm extends LayerPropertiesForm {
|
|
|
27
29
|
rows: 10,
|
|
28
30
|
},
|
|
29
31
|
} });
|
|
32
|
+
uiSchema['layerOverride'] = Object.assign(Object.assign({}, uiSchema['layerOverride']), { items: {
|
|
33
|
+
'ui:title': '',
|
|
34
|
+
targetLayer: {
|
|
35
|
+
'ui:field': 'layerSelect',
|
|
36
|
+
},
|
|
37
|
+
opacity: {
|
|
38
|
+
'ui:field': 'opacity',
|
|
39
|
+
},
|
|
40
|
+
}, 'ui:options': {
|
|
41
|
+
orderable: false,
|
|
42
|
+
}, 'ui:ArrayFieldTemplate': ArrayFieldTemplate });
|
|
43
|
+
// Remove properties that should not be displayed in the form
|
|
44
|
+
const layerOverrideItems = (_c = (_b = (_a = schema.properties) === null || _a === void 0 ? void 0 : _a.layerOverride) === null || _b === void 0 ? void 0 : _b.items) === null || _c === void 0 ? void 0 : _c.properties;
|
|
45
|
+
if (layerOverrideItems) {
|
|
46
|
+
delete layerOverrideItems.color;
|
|
47
|
+
delete layerOverrideItems.symbologyState;
|
|
48
|
+
}
|
|
30
49
|
this.removeFormEntry('zoom', data, schema, uiSchema);
|
|
31
50
|
}
|
|
32
51
|
}
|
|
@@ -15,9 +15,7 @@ export class GeoJSONSourcePropertiesForm extends PathBasedSourcePropertiesForm {
|
|
|
15
15
|
this._validatePath((_b = (_a = props.sourceData) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : '');
|
|
16
16
|
}
|
|
17
17
|
processSchema(data, schema, uiSchema) {
|
|
18
|
-
|
|
19
|
-
this.removeFormEntry('data', data, schema, uiSchema);
|
|
20
|
-
}
|
|
18
|
+
this.removeFormEntry('data', data, schema, uiSchema);
|
|
21
19
|
if (this.props.formContext === 'create') {
|
|
22
20
|
schema.properties.path.description =
|
|
23
21
|
'The local path to a GeoJSON file. (If no path/url is provided, an empty GeoJSON is created.)';
|
package/lib/mainview/mainView.js
CHANGED
|
@@ -1209,6 +1209,7 @@ export class MainView extends React.Component {
|
|
|
1209
1209
|
layerParameters = layer.parameters;
|
|
1210
1210
|
newMapLayer = new VectorTileLayer({
|
|
1211
1211
|
opacity: layerParameters.opacity,
|
|
1212
|
+
visible: layer.visible,
|
|
1212
1213
|
source: this._sources[layerParameters.source],
|
|
1213
1214
|
style: this.vectorLayerStyleRuleBuilder(layer),
|
|
1214
1215
|
});
|
|
@@ -1218,6 +1219,7 @@ export class MainView extends React.Component {
|
|
|
1218
1219
|
layerParameters = layer.parameters;
|
|
1219
1220
|
newMapLayer = new WebGlTileLayer({
|
|
1220
1221
|
opacity: 0.3,
|
|
1222
|
+
visible: layer.visible,
|
|
1221
1223
|
source: this._sources[layerParameters.source],
|
|
1222
1224
|
style: {
|
|
1223
1225
|
color: ['color', this.hillshadeMath()],
|
|
@@ -1229,6 +1231,7 @@ export class MainView extends React.Component {
|
|
|
1229
1231
|
layerParameters = layer.parameters;
|
|
1230
1232
|
newMapLayer = new ImageLayer({
|
|
1231
1233
|
opacity: layerParameters.opacity,
|
|
1234
|
+
visible: layer.visible,
|
|
1232
1235
|
source: this._sources[layerParameters.source],
|
|
1233
1236
|
});
|
|
1234
1237
|
break;
|
|
@@ -1238,6 +1241,7 @@ export class MainView extends React.Component {
|
|
|
1238
1241
|
// This is to handle python sending a None for the color
|
|
1239
1242
|
const layerOptions = {
|
|
1240
1243
|
opacity: layerParameters.opacity,
|
|
1244
|
+
visible: layer.visible,
|
|
1241
1245
|
source: this._sources[layerParameters.source],
|
|
1242
1246
|
};
|
|
1243
1247
|
if (layerParameters.color) {
|
|
@@ -1252,6 +1256,7 @@ export class MainView extends React.Component {
|
|
|
1252
1256
|
layerParameters = layer.parameters;
|
|
1253
1257
|
newMapLayer = new HeatmapLayer({
|
|
1254
1258
|
opacity: layerParameters.opacity,
|
|
1259
|
+
visible: layer.visible,
|
|
1255
1260
|
source: this._sources[layerParameters.source],
|
|
1256
1261
|
blur: (_b = layerParameters.blur) !== null && _b !== void 0 ? _b : 15,
|
|
1257
1262
|
radius: (_c = layerParameters.radius) !== null && _c !== void 0 ? _c : 8,
|
|
@@ -2087,7 +2092,7 @@ export class MainView extends React.Component {
|
|
|
2087
2092
|
} },
|
|
2088
2093
|
React.createElement("div", { className: "jgis-panels-wrapper" }, !this.state.isSpectaPresentation ? (React.createElement(React.Fragment, null,
|
|
2089
2094
|
this._state && (React.createElement(LeftPanel, { model: this._model, commands: this._mainViewModel.commands, state: this._state, settings: this.state.jgisSettings })),
|
|
2090
|
-
this._formSchemaRegistry && this._annotationModel && (React.createElement(RightPanel, { model: this._model, commands: this._mainViewModel.commands, formSchemaRegistry: this._formSchemaRegistry, annotationModel: this._annotationModel, settings: this.state.jgisSettings })))) : this.props.isMobile ? (React.createElement(MobileSpectaPanel, { model: this._model })) : (React.createElement("div", { className: "jgis-specta-right-panel-container-mod jgis-right-panel-container" },
|
|
2095
|
+
this._formSchemaRegistry && this._annotationModel && (React.createElement(RightPanel, { model: this._model, commands: this._mainViewModel.commands, formSchemaRegistry: this._formSchemaRegistry, annotationModel: this._annotationModel, addLayer: this.addLayer.bind(this), removeLayer: this.removeLayer.bind(this), settings: this.state.jgisSettings })))) : this.props.isMobile ? (React.createElement(MobileSpectaPanel, { model: this._model })) : (React.createElement("div", { className: "jgis-specta-right-panel-container-mod jgis-right-panel-container" },
|
|
2091
2096
|
React.createElement("div", { ref: this.spectaContainerRef, className: "jgis-specta-story-panel-container" },
|
|
2092
2097
|
React.createElement(StoryViewerPanel, { ref: this.storyViewerPanelRef, model: this._model, isSpecta: this.state.isSpectaPresentation, className: "jgis-story-viewer-panel-specta-mod" }))))),
|
|
2093
2098
|
React.createElement("div", { ref: this.controlsToolbarRef, className: "jgis-controls-toolbar" }))),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IAnnotationModel, IJGISFormSchemaRegistry, IJupyterGISModel, IJupyterGISSettings } from '@jupytergis/schema';
|
|
1
|
+
import { IAnnotationModel, IJGISFormSchemaRegistry, IJGISLayer, IJupyterGISModel, IJupyterGISSettings } from '@jupytergis/schema';
|
|
2
2
|
import { CommandRegistry } from '@lumino/commands';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
interface IRightPanelProps {
|
|
@@ -7,6 +7,8 @@ interface IRightPanelProps {
|
|
|
7
7
|
model: IJupyterGISModel;
|
|
8
8
|
commands: CommandRegistry;
|
|
9
9
|
settings: IJupyterGISSettings;
|
|
10
|
+
addLayer?: (id: string, layer: IJGISLayer, index: number) => Promise<void>;
|
|
11
|
+
removeLayer?: (id: string) => void;
|
|
10
12
|
}
|
|
11
13
|
export declare const RightPanel: React.FC<IRightPanelProps>;
|
|
12
14
|
export {};
|
|
@@ -10,6 +10,7 @@ export const RightPanel = props => {
|
|
|
10
10
|
var _a;
|
|
11
11
|
const [editorMode, setEditorMode] = React.useState(true);
|
|
12
12
|
const [storyMapPresentationMode, setStoryMapPresentationMode] = React.useState((_a = props.model.getOptions().storyMapPresentationMode) !== null && _a !== void 0 ? _a : false);
|
|
13
|
+
const [selectedObjectProperties, setSelectedObjectProperties] = React.useState(undefined);
|
|
13
14
|
// Only show editor when not in presentation mode and editorMode is true
|
|
14
15
|
const showEditor = !storyMapPresentationMode && editorMode;
|
|
15
16
|
// Tab title: "Story Map" in presentation mode, otherwise based on editorMode
|
|
@@ -70,7 +71,6 @@ export const RightPanel = props => {
|
|
|
70
71
|
props.settings.annotationsDisabled &&
|
|
71
72
|
props.settings.identifyDisabled;
|
|
72
73
|
const rightPanelVisible = !props.settings.rightPanelDisabled && !allRightTabsDisabled;
|
|
73
|
-
const [selectedObjectProperties, setSelectedObjectProperties] = React.useState(undefined);
|
|
74
74
|
const toggleEditor = () => {
|
|
75
75
|
setEditorMode(!editorMode);
|
|
76
76
|
};
|
|
@@ -88,7 +88,7 @@ export const RightPanel = props => {
|
|
|
88
88
|
React.createElement(ObjectPropertiesReact, { setSelectedObject: setSelectedObjectProperties, selectedObject: selectedObjectProperties, formSchemaRegistry: props.formSchemaRegistry, model: props.model }))),
|
|
89
89
|
!props.settings.storyMapsDisabled && (React.createElement(TabsContent, { value: "storyPanel", className: "jgis-panel-tab-content", style: { paddingTop: 0 } },
|
|
90
90
|
!storyMapPresentationMode && (React.createElement(PreviewModeSwitch, { checked: !editorMode, onCheckedChange: toggleEditor })),
|
|
91
|
-
showEditor ? (React.createElement(StoryEditorPanel, { model: props.model, commands: props.commands })) : (React.createElement(StoryViewerPanel, { model: props.model, isSpecta: false })))),
|
|
91
|
+
showEditor ? (React.createElement(StoryEditorPanel, { model: props.model, commands: props.commands })) : (React.createElement(StoryViewerPanel, { model: props.model, isSpecta: false, addLayer: props.addLayer, removeLayer: props.removeLayer })))),
|
|
92
92
|
!props.settings.annotationsDisabled && (React.createElement(TabsContent, { value: "annotations", className: "jgis-panel-tab-content" },
|
|
93
93
|
React.createElement(AnnotationsPanel, { annotationModel: props.annotationModel, jgisModel: props.model }))),
|
|
94
94
|
!props.settings.identifyDisabled && (React.createElement(TabsContent, { value: "identifyPanel", className: "jgis-panel-tab-content" },
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { IJupyterGISModel } from '@jupytergis/schema';
|
|
1
|
+
import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
interface IStoryViewerPanelProps {
|
|
4
4
|
model: IJupyterGISModel;
|
|
5
5
|
isSpecta: boolean;
|
|
6
6
|
isMobile?: boolean;
|
|
7
7
|
className?: string;
|
|
8
|
+
addLayer?: (id: string, layer: IJGISLayer, index: number) => Promise<void>;
|
|
9
|
+
removeLayer?: (id: string) => void;
|
|
8
10
|
}
|
|
9
11
|
export interface IStoryViewerPanelHandle {
|
|
10
12
|
handlePrev: () => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UUID } from '@lumino/coreutils';
|
|
1
2
|
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
2
3
|
import { cn } from "../../shared/components/utils";
|
|
3
4
|
import StoryNavBar from './StoryNavBar';
|
|
@@ -17,7 +18,7 @@ function getStoryNavPlacement(isSpecta, hasImage, storyType, isMobile) {
|
|
|
17
18
|
}
|
|
18
19
|
return hasImage ? 'over-image' : 'below-title';
|
|
19
20
|
}
|
|
20
|
-
const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, className }, ref) => {
|
|
21
|
+
const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, className, addLayer, removeLayer }, ref) => {
|
|
21
22
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
22
23
|
const [currentIndexDisplayed, setCurrentIndexDisplayed] = useState(() => model.getCurrentSegmentIndex());
|
|
23
24
|
const [storyData, setStoryData] = useState((_a = model.getSelectedStory().story) !== null && _a !== void 0 ? _a : null);
|
|
@@ -27,6 +28,25 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
|
|
|
27
28
|
model.setCurrentSegmentIndex(index);
|
|
28
29
|
setCurrentIndexDisplayed(index);
|
|
29
30
|
}, [model]);
|
|
31
|
+
/** Layers affected by layer override
|
|
32
|
+
* We want to remove added layers (ie Heatmap)
|
|
33
|
+
* and Restore the original symbology for modified layers
|
|
34
|
+
*/
|
|
35
|
+
const overrideLayerEntriesRef = useRef([]);
|
|
36
|
+
const clearOverrideLayers = useCallback(() => {
|
|
37
|
+
overrideLayerEntriesRef.current.forEach(({ layerId, action }) => {
|
|
38
|
+
if (action === 'remove') {
|
|
39
|
+
removeLayer === null || removeLayer === void 0 ? void 0 : removeLayer(layerId);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const layer = model.getLayer(layerId);
|
|
43
|
+
if (layer) {
|
|
44
|
+
model.triggerLayerUpdate(layerId, layer);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
overrideLayerEntriesRef.current = [];
|
|
49
|
+
}, [model]);
|
|
30
50
|
// Derive story segments from story data
|
|
31
51
|
const storySegments = useMemo(() => {
|
|
32
52
|
if (!(storyData === null || storyData === void 0 ? void 0 : storyData.storySegments)) {
|
|
@@ -44,15 +64,14 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
|
|
|
44
64
|
const activeSlide = useMemo(() => {
|
|
45
65
|
return currentStorySegment === null || currentStorySegment === void 0 ? void 0 : currentStorySegment.parameters;
|
|
46
66
|
}, [currentStorySegment]);
|
|
47
|
-
const layerName = useMemo(() => {
|
|
48
|
-
var _a;
|
|
49
|
-
return (_a = currentStorySegment === null || currentStorySegment === void 0 ? void 0 : currentStorySegment.name) !== null && _a !== void 0 ? _a : '';
|
|
50
|
-
}, [currentStorySegment]);
|
|
67
|
+
const layerName = useMemo(() => { var _a; return (_a = currentStorySegment === null || currentStorySegment === void 0 ? void 0 : currentStorySegment.name) !== null && _a !== void 0 ? _a : ''; }, [currentStorySegment]);
|
|
51
68
|
// Derive story segment ID for zooming
|
|
52
69
|
const currentStorySegmentId = useMemo(() => {
|
|
53
70
|
var _a;
|
|
54
71
|
return (_a = storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) === null || _a === void 0 ? void 0 : _a[currentIndexDisplayed];
|
|
55
72
|
}, [storyData, currentIndexDisplayed]);
|
|
73
|
+
const hasPrev = currentIndexDisplayed > 0;
|
|
74
|
+
const hasNext = currentIndexDisplayed < storySegments.length - 1;
|
|
56
75
|
const zoomToCurrentLayer = () => {
|
|
57
76
|
if (currentStorySegmentId) {
|
|
58
77
|
model.centerOnPosition(currentStorySegmentId);
|
|
@@ -69,12 +88,32 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
|
|
|
69
88
|
};
|
|
70
89
|
}
|
|
71
90
|
}, [storyData, model]);
|
|
91
|
+
// On unmount: remove override layers and restore layer symbology
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
return () => {
|
|
94
|
+
var _a;
|
|
95
|
+
clearOverrideLayers();
|
|
96
|
+
(_a = storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) === null || _a === void 0 ? void 0 : _a.forEach(segmentId => {
|
|
97
|
+
var _a;
|
|
98
|
+
const segment = model.getLayer(segmentId);
|
|
99
|
+
const overrides = (_a = segment === null || segment === void 0 ? void 0 : segment.parameters) === null || _a === void 0 ? void 0 : _a.layerOverride;
|
|
100
|
+
if (Array.isArray(overrides)) {
|
|
101
|
+
overrides.forEach((override) => {
|
|
102
|
+
const targetLayerId = override.targetLayer;
|
|
103
|
+
const targetLayer = model.getLayer(targetLayerId);
|
|
104
|
+
targetLayer &&
|
|
105
|
+
model.triggerLayerUpdate(targetLayerId, targetLayer);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
}, [storyData, model, clearOverrideLayers]);
|
|
72
111
|
useEffect(() => {
|
|
73
112
|
const updateStory = () => {
|
|
74
113
|
var _a;
|
|
114
|
+
clearOverrideLayers();
|
|
75
115
|
const { story } = model.getSelectedStory();
|
|
76
116
|
setStoryData(story !== null && story !== void 0 ? story : null);
|
|
77
|
-
// Reset to first slide when story changes
|
|
78
117
|
setIndex((_a = model.getCurrentSegmentIndex()) !== null && _a !== void 0 ? _a : 0);
|
|
79
118
|
};
|
|
80
119
|
updateStory();
|
|
@@ -82,7 +121,7 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
|
|
|
82
121
|
return () => {
|
|
83
122
|
model.sharedModel.storyMapsChanged.disconnect(updateStory);
|
|
84
123
|
};
|
|
85
|
-
}, [model, setIndex]);
|
|
124
|
+
}, [model, setIndex, clearOverrideLayers]);
|
|
86
125
|
// Prefetch image when slide changes
|
|
87
126
|
useEffect(() => {
|
|
88
127
|
var _a;
|
|
@@ -114,6 +153,20 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
|
|
|
114
153
|
zoomToCurrentLayer();
|
|
115
154
|
}
|
|
116
155
|
}, [currentStorySegmentId, model]);
|
|
156
|
+
// Set selected layer and apply symbology when segment changes; remove previous segment's override layers first.
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!(storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) || currentIndexDisplayed < 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
clearOverrideLayers();
|
|
162
|
+
setSelectedLayerByIndex(currentIndexDisplayed);
|
|
163
|
+
overrideSymbology(currentIndexDisplayed);
|
|
164
|
+
}, [
|
|
165
|
+
storyData,
|
|
166
|
+
currentIndexDisplayed,
|
|
167
|
+
setSelectedLayerByIndex,
|
|
168
|
+
clearOverrideLayers,
|
|
169
|
+
]);
|
|
117
170
|
// Set selected layer on initial render and when story data changes
|
|
118
171
|
useEffect(() => {
|
|
119
172
|
if ((storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) && currentIndexDisplayed >= 0) {
|
|
@@ -150,41 +203,96 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
|
|
|
150
203
|
}
|
|
151
204
|
setIndex(index);
|
|
152
205
|
};
|
|
206
|
+
// ! TODO really only want to connect this un unguided mode
|
|
153
207
|
model.sharedModel.awareness.on('change', handleSelectedStorySegmentChange);
|
|
154
208
|
return () => {
|
|
155
209
|
model.sharedModel.awareness.off('change', handleSelectedStorySegmentChange);
|
|
156
210
|
};
|
|
157
211
|
}, [model, storyData, setIndex]);
|
|
212
|
+
// Apply layer overrides for the segment at the given index
|
|
213
|
+
const overrideSymbology = (index) => {
|
|
214
|
+
var _a;
|
|
215
|
+
if (index < 0 || !storySegments[index]) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const segment = storySegments[index];
|
|
219
|
+
const layerOverrides = (_a = segment.parameters) === null || _a === void 0 ? void 0 : _a.layerOverride;
|
|
220
|
+
if (!Array.isArray(layerOverrides)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Apply all layer overrides for this segment
|
|
224
|
+
layerOverrides.forEach(override => {
|
|
225
|
+
const { color, opacity, symbologyState, targetLayer: targetLayerId, visible, } = override;
|
|
226
|
+
if (!targetLayerId) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
overrideLayerEntriesRef.current.push({
|
|
230
|
+
layerId: targetLayerId,
|
|
231
|
+
action: 'restore',
|
|
232
|
+
});
|
|
233
|
+
const targetLayer = model.getLayer(targetLayerId);
|
|
234
|
+
if (targetLayer === null || targetLayer === void 0 ? void 0 : targetLayer.parameters) {
|
|
235
|
+
if (symbologyState !== undefined) {
|
|
236
|
+
targetLayer.parameters.symbologyState = symbologyState;
|
|
237
|
+
}
|
|
238
|
+
if (color !== undefined) {
|
|
239
|
+
targetLayer.parameters.color = color;
|
|
240
|
+
}
|
|
241
|
+
if (opacity !== undefined) {
|
|
242
|
+
targetLayer.parameters.opacity = opacity;
|
|
243
|
+
}
|
|
244
|
+
if (visible !== undefined) {
|
|
245
|
+
targetLayer.visible = visible;
|
|
246
|
+
}
|
|
247
|
+
// Heatmaps are actually a different layer, not just symbology
|
|
248
|
+
// so they need special handling
|
|
249
|
+
if ((symbologyState === null || symbologyState === void 0 ? void 0 : symbologyState.renderType) === 'Heatmap') {
|
|
250
|
+
targetLayer.type = 'HeatmapLayer';
|
|
251
|
+
if (addLayer) {
|
|
252
|
+
const newId = UUID.uuid4();
|
|
253
|
+
addLayer(newId, targetLayer, 100);
|
|
254
|
+
overrideLayerEntriesRef.current.push({
|
|
255
|
+
layerId: newId,
|
|
256
|
+
action: 'remove',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
model.triggerLayerUpdate(targetLayerId, targetLayer);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
};
|
|
158
266
|
const handlePrev = useCallback(() => {
|
|
159
|
-
if (
|
|
267
|
+
if (hasPrev) {
|
|
160
268
|
setIndex(currentIndexDisplayed - 1);
|
|
161
269
|
}
|
|
162
270
|
}, [currentIndexDisplayed, setIndex]);
|
|
163
271
|
const handleNext = useCallback(() => {
|
|
164
|
-
if (
|
|
272
|
+
if (hasNext) {
|
|
165
273
|
setIndex(currentIndexDisplayed + 1);
|
|
166
274
|
}
|
|
167
275
|
}, [currentIndexDisplayed, storySegments.length, setIndex]);
|
|
168
|
-
// Expose methods via ref for parent component to use
|
|
169
|
-
useImperativeHandle(ref, () => ({
|
|
170
|
-
handlePrev,
|
|
171
|
-
handleNext,
|
|
172
|
-
canNavigate: isSpecta,
|
|
173
|
-
}), [handlePrev, handleNext, storyData, isSpecta]);
|
|
174
276
|
if (!storyData || ((_c = storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) === null || _c === void 0 ? void 0 : _c.length) === 0) {
|
|
175
277
|
return (React.createElement("div", { style: { padding: '1rem' } },
|
|
176
278
|
React.createElement("p", null, "No Segments available. Add one using the Add Layer menu.")));
|
|
177
279
|
}
|
|
178
|
-
const
|
|
280
|
+
const storyNavBarProps = {
|
|
179
281
|
onPrev: handlePrev,
|
|
180
282
|
onNext: handleNext,
|
|
181
|
-
hasPrev
|
|
182
|
-
hasNext
|
|
283
|
+
hasPrev,
|
|
284
|
+
hasNext,
|
|
183
285
|
};
|
|
286
|
+
// Expose methods via ref for parent component to use
|
|
287
|
+
useImperativeHandle(ref, () => ({
|
|
288
|
+
handlePrev,
|
|
289
|
+
handleNext,
|
|
290
|
+
canNavigate: isSpecta,
|
|
291
|
+
}), [handlePrev, handleNext, storyData, isSpecta]);
|
|
184
292
|
const hasImage = !!(((_d = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _d === void 0 ? void 0 : _d.image) && imageLoaded);
|
|
185
293
|
const storyType = (_e = storyData.storyType) !== null && _e !== void 0 ? _e : 'guided';
|
|
186
294
|
const navPlacement = getStoryNavPlacement(isSpecta, hasImage, storyType, isMobile);
|
|
187
|
-
const navSlot = navPlacement !== null ? (React.createElement(StoryNavBar, Object.assign({ placement: navPlacement },
|
|
295
|
+
const navSlot = navPlacement !== null ? (React.createElement(StoryNavBar, Object.assign({ placement: navPlacement }, storyNavBarProps))) : null;
|
|
188
296
|
// Get transition time from current segment, default to 0.3s
|
|
189
297
|
const transitionTime = (_g = (_f = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.transition) === null || _f === void 0 ? void 0 : _f.time) !== null && _g !== void 0 ? _g : 0.3;
|
|
190
298
|
return (React.createElement("div", { ref: panelRef, className: cn('jgis-story-viewer-panel', className), id: "jgis-story-segment-panel" },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useLatest<T>(value: T): React.MutableRefObject<T>;
|
package/lib/types.d.ts
CHANGED
|
@@ -27,3 +27,4 @@ declare global {
|
|
|
27
27
|
}
|
|
28
28
|
declare const classificationModes: readonly ["quantile", "equal interval", "jenks", "pretty", "logarithmic", "continuous"];
|
|
29
29
|
export type ClassificationMode = (typeof classificationModes)[number];
|
|
30
|
+
export declare const SYMBOLOGY_VALID_LAYER_TYPES: string[];
|
package/lib/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupytergis/base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "A JupyterLab extension for 3D modelling.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@jupyter/collaboration": "^4",
|
|
45
45
|
"@jupyter/react-components": "^0.16.6",
|
|
46
46
|
"@jupyter/ydoc": "^2.0.0 || ^3.0.0",
|
|
47
|
-
"@jupytergis/schema": "^0.
|
|
47
|
+
"@jupytergis/schema": "^0.13.1",
|
|
48
48
|
"@jupyterlab/application": "^4.3.0",
|
|
49
49
|
"@jupyterlab/apputils": "^4.3.0",
|
|
50
50
|
"@jupyterlab/completer": "^4.3.0",
|
package/style/base.css
CHANGED
|
@@ -68,6 +68,14 @@
|
|
|
68
68
|
flex-direction: column;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
.jGIS-symbology-override-item {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
align-items: center;
|
|
75
|
+
padding-bottom: 1rem;
|
|
76
|
+
border-bottom: solid 1px var(--jp-border-color0);
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
.jp-gis-text-label {
|
|
72
80
|
margin: 0;
|
|
73
81
|
padding: 0;
|
package/style/storyPanel.css
CHANGED
|
@@ -169,16 +169,16 @@
|
|
|
169
169
|
overflow: auto;
|
|
170
170
|
background: linear-gradient(
|
|
171
171
|
to left,
|
|
172
|
-
var(--jgis-specta-bg-color, --jp-layout-color0) 49%,
|
|
172
|
+
var(--jgis-specta-bg-color, var(--jp-layout-color0)) 49%,
|
|
173
173
|
color-mix(
|
|
174
174
|
in srgb,
|
|
175
|
-
var(--jgis-specta-bg-color, --jp-layout-color0) 60%,
|
|
175
|
+
var(--jgis-specta-bg-color, var(--jp-layout-color0)) 60%,
|
|
176
176
|
transparent
|
|
177
177
|
)
|
|
178
178
|
65%,
|
|
179
179
|
color-mix(
|
|
180
180
|
in srgb,
|
|
181
|
-
var(--jgis-specta-bg-color, --jp-layout-color0) 50%,
|
|
181
|
+
var(--jgis-specta-bg-color, var(--jp-layout-color0)) 50%,
|
|
182
182
|
transparent
|
|
183
183
|
)
|
|
184
184
|
70%,
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
width: 45%;
|
|
191
191
|
font-size: var(--jp-ui-font-size3);
|
|
192
192
|
padding-right: 1.7rem;
|
|
193
|
-
color: var(--jgis-specta-text-color, var(--jp-ui-
|
|
193
|
+
color: var(--jgis-specta-text-color, var(--jp-ui-font-color1));
|
|
194
194
|
overflow-y: auto;
|
|
195
195
|
max-height: 100%;
|
|
196
196
|
}
|