@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.
Files changed (42) hide show
  1. package/lib/commands/index.js +2 -6
  2. package/lib/dialogs/symbology/hooks/useEffectiveSymbologyParams.d.ts +16 -0
  3. package/lib/dialogs/symbology/hooks/useEffectiveSymbologyParams.js +24 -0
  4. package/lib/dialogs/symbology/hooks/useOkSignal.d.ts +6 -0
  5. package/lib/dialogs/symbology/hooks/useOkSignal.js +25 -0
  6. package/lib/dialogs/symbology/symbologyDialog.d.ts +4 -2
  7. package/lib/dialogs/symbology/symbologyDialog.js +6 -10
  8. package/lib/dialogs/symbology/symbologyUtils.d.ts +25 -2
  9. package/lib/dialogs/symbology/symbologyUtils.js +74 -4
  10. package/lib/dialogs/symbology/tiff_layer/TiffRendering.js +3 -3
  11. package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +31 -34
  12. package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +68 -62
  13. package/lib/dialogs/symbology/vector_layer/VectorRendering.js +33 -21
  14. package/lib/dialogs/symbology/vector_layer/types/Canonical.js +23 -24
  15. package/lib/dialogs/symbology/vector_layer/types/Categorized.js +49 -50
  16. package/lib/dialogs/symbology/vector_layer/types/Graduated.js +53 -62
  17. package/lib/dialogs/symbology/vector_layer/types/Heatmap.js +35 -34
  18. package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.js +45 -47
  19. package/lib/formbuilder/objectform/StoryEditorForm.js +0 -17
  20. package/lib/formbuilder/objectform/baseform.d.ts +11 -0
  21. package/lib/formbuilder/objectform/baseform.js +72 -38
  22. package/lib/formbuilder/objectform/components/LayerSelect.d.ts +7 -0
  23. package/lib/formbuilder/objectform/components/LayerSelect.js +43 -0
  24. package/lib/formbuilder/objectform/components/OpacitySlider.d.ts +4 -0
  25. package/lib/formbuilder/objectform/components/OpacitySlider.js +40 -0
  26. package/lib/formbuilder/objectform/components/SegmentFormSymbology.d.ts +3 -0
  27. package/lib/formbuilder/objectform/components/SegmentFormSymbology.js +59 -0
  28. package/lib/formbuilder/objectform/layer/storySegmentLayerForm.d.ts +2 -2
  29. package/lib/formbuilder/objectform/layer/storySegmentLayerForm.js +19 -0
  30. package/lib/formbuilder/objectform/source/geojsonsource.js +1 -3
  31. package/lib/mainview/mainView.js +6 -1
  32. package/lib/panelview/rightpanel.d.ts +3 -1
  33. package/lib/panelview/rightpanel.js +2 -2
  34. package/lib/panelview/story-maps/StoryViewerPanel.d.ts +3 -1
  35. package/lib/panelview/story-maps/StoryViewerPanel.js +127 -19
  36. package/lib/shared/hooks/useLatest.d.ts +1 -0
  37. package/lib/shared/hooks/useLatest.js +8 -0
  38. package/lib/types.d.ts +1 -0
  39. package/lib/types.js +6 -1
  40. package/package.json +2 -2
  41. package/style/base.css +8 -0
  42. 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,4 @@
1
+ import { FieldProps } from '@rjsf/utils';
2
+ import React from 'react';
3
+ declare function OpacitySlider({ formData, onChange }: FieldProps<number>): React.JSX.Element;
4
+ export default OpacitySlider;
@@ -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,3 @@
1
+ import { ArrayFieldTemplateProps } from '@rjsf/core';
2
+ import React from 'react';
3
+ export declare function ArrayFieldTemplate(props: ArrayFieldTemplateProps): React.JSX.Element;
@@ -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: IDict<any> | undefined, schema: IDict, uiSchema: IDict): void;
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
- if ((data === null || data === void 0 ? void 0 : data.path) !== '') {
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.)';
@@ -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 (currentIndexDisplayed > 0) {
267
+ if (hasPrev) {
160
268
  setIndex(currentIndexDisplayed - 1);
161
269
  }
162
270
  }, [currentIndexDisplayed, setIndex]);
163
271
  const handleNext = useCallback(() => {
164
- if (currentIndexDisplayed < storySegments.length - 1) {
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 navProps = {
280
+ const storyNavBarProps = {
179
281
  onPrev: handlePrev,
180
282
  onNext: handleNext,
181
- hasPrev: currentIndexDisplayed > 0,
182
- hasNext: currentIndexDisplayed < storySegments.length - 1,
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 }, navProps))) : null;
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>;
@@ -0,0 +1,8 @@
1
+ import { useEffect, useRef } from 'react';
2
+ export function useLatest(value) {
3
+ const ref = useRef(value);
4
+ useEffect(() => {
5
+ ref.current = value;
6
+ }, [value]);
7
+ return ref;
8
+ }
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
@@ -6,4 +6,9 @@ const classificationModes = [
6
6
  'logarithmic',
7
7
  'continuous',
8
8
  ];
9
- export {};
9
+ export const SYMBOLOGY_VALID_LAYER_TYPES = [
10
+ 'VectorLayer',
11
+ 'VectorTileLayer',
12
+ 'WebGlLayer',
13
+ 'HeatmapLayer',
14
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupytergis/base",
3
- "version": "0.12.2",
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.12.2",
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;
@@ -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-inverse-font-color1));
193
+ color: var(--jgis-specta-text-color, var(--jp-ui-font-color1));
194
194
  overflow-y: auto;
195
195
  max-height: 100%;
196
196
  }