@jupytergis/base 0.12.1 → 0.13.0

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 (84) hide show
  1. package/lib/commands/index.js +2 -6
  2. package/lib/dialogs/layerBrowserDialog.d.ts +3 -3
  3. package/lib/dialogs/layerBrowserDialog.js +9 -10
  4. package/lib/dialogs/symbology/hooks/useEffectiveSymbologyParams.d.ts +16 -0
  5. package/lib/dialogs/symbology/hooks/useEffectiveSymbologyParams.js +24 -0
  6. package/lib/dialogs/symbology/hooks/useOkSignal.d.ts +6 -0
  7. package/lib/dialogs/symbology/hooks/useOkSignal.js +25 -0
  8. package/lib/dialogs/symbology/symbologyDialog.d.ts +4 -2
  9. package/lib/dialogs/symbology/symbologyDialog.js +6 -10
  10. package/lib/dialogs/symbology/symbologyUtils.d.ts +25 -2
  11. package/lib/dialogs/symbology/symbologyUtils.js +74 -4
  12. package/lib/dialogs/symbology/tiff_layer/TiffRendering.js +3 -3
  13. package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +31 -34
  14. package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +68 -62
  15. package/lib/dialogs/symbology/vector_layer/VectorRendering.js +33 -21
  16. package/lib/dialogs/symbology/vector_layer/types/Canonical.js +23 -24
  17. package/lib/dialogs/symbology/vector_layer/types/Categorized.js +48 -49
  18. package/lib/dialogs/symbology/vector_layer/types/Graduated.js +53 -62
  19. package/lib/dialogs/symbology/vector_layer/types/Heatmap.js +35 -34
  20. package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.js +45 -47
  21. package/lib/formbuilder/objectform/StoryEditorForm.js +1 -18
  22. package/lib/formbuilder/objectform/baseform.d.ts +6 -0
  23. package/lib/formbuilder/objectform/baseform.js +21 -38
  24. package/lib/formbuilder/objectform/components/LayerSelect.d.ts +7 -0
  25. package/lib/formbuilder/objectform/components/LayerSelect.js +43 -0
  26. package/lib/formbuilder/objectform/components/OpacitySlider.d.ts +4 -0
  27. package/lib/formbuilder/objectform/components/OpacitySlider.js +40 -0
  28. package/lib/formbuilder/objectform/components/SegmentFormSymbology.d.ts +3 -0
  29. package/lib/formbuilder/objectform/components/SegmentFormSymbology.js +59 -0
  30. package/lib/formbuilder/objectform/layer/storySegmentLayerForm.d.ts +2 -2
  31. package/lib/formbuilder/objectform/layer/storySegmentLayerForm.js +19 -0
  32. package/lib/mainview/mainView.d.ts +8 -2
  33. package/lib/mainview/mainView.js +41 -7
  34. package/lib/mainview/mainviewwidget.js +2 -2
  35. package/lib/panelview/leftpanel.d.ts +2 -1
  36. package/lib/panelview/leftpanel.js +28 -20
  37. package/lib/panelview/rightpanel.d.ts +4 -1
  38. package/lib/panelview/rightpanel.js +14 -20
  39. package/lib/panelview/story-maps/MobileSpectaPanel.d.ts +7 -0
  40. package/lib/panelview/story-maps/MobileSpectaPanel.js +114 -0
  41. package/lib/panelview/story-maps/StoryNavBar.d.ts +3 -2
  42. package/lib/panelview/story-maps/StoryNavBar.js +18 -6
  43. package/lib/panelview/story-maps/StoryViewerPanel.d.ts +13 -1
  44. package/lib/panelview/story-maps/StoryViewerPanel.js +168 -34
  45. package/lib/panelview/story-maps/components/StoryImageSection.d.ts +2 -7
  46. package/lib/panelview/story-maps/components/StoryImageSection.js +2 -4
  47. package/lib/panelview/story-maps/components/StorySubtitleSection.d.ts +2 -6
  48. package/lib/panelview/story-maps/components/StorySubtitleSection.js +2 -4
  49. package/lib/panelview/story-maps/components/StoryTitleSection.d.ts +2 -7
  50. package/lib/panelview/story-maps/components/StoryTitleSection.js +2 -3
  51. package/lib/shared/components/Button.js +2 -2
  52. package/lib/shared/components/Calendar.js +0 -1
  53. package/lib/shared/components/Checkbox.d.ts +1 -1
  54. package/lib/shared/components/Checkbox.js +1 -1
  55. package/lib/shared/components/Dialog.d.ts +1 -1
  56. package/lib/shared/components/Dialog.js +1 -1
  57. package/lib/shared/components/Drawer.d.ts +13 -0
  58. package/lib/shared/components/Drawer.js +59 -0
  59. package/lib/shared/components/DropdownMenu.d.ts +1 -1
  60. package/lib/shared/components/DropdownMenu.js +1 -1
  61. package/lib/shared/components/Popover.d.ts +1 -1
  62. package/lib/shared/components/Popover.js +1 -1
  63. package/lib/shared/components/RadioGroup.d.ts +1 -1
  64. package/lib/shared/components/RadioGroup.js +1 -1
  65. package/lib/shared/components/Sheet.d.ts +15 -0
  66. package/lib/shared/components/Sheet.js +64 -0
  67. package/lib/shared/components/Switch.d.ts +1 -1
  68. package/lib/shared/components/Switch.js +1 -1
  69. package/lib/shared/components/Tabs.d.ts +1 -1
  70. package/lib/shared/components/Tabs.js +1 -1
  71. package/lib/shared/components/ToggleGroup.d.ts +1 -1
  72. package/lib/shared/components/ToggleGroup.js +1 -1
  73. package/lib/shared/hooks/useLatest.d.ts +1 -0
  74. package/lib/shared/hooks/useLatest.js +8 -0
  75. package/lib/shared/hooks/useMediaQuery.d.ts +9 -0
  76. package/lib/shared/hooks/useMediaQuery.js +32 -0
  77. package/lib/tools.js +21 -34
  78. package/lib/types.d.ts +1 -0
  79. package/lib/types.js +6 -1
  80. package/package.json +5 -12
  81. package/style/base.css +15 -0
  82. package/style/shared/drawer.css +154 -0
  83. package/style/shared/sheet.css +258 -0
  84. package/style/storyPanel.css +36 -8
@@ -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 symbology 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 Symbology 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
  }
@@ -1,4 +1,4 @@
1
- import { IAnnotation, IAnnotationModel, IDict, IJGISFilterItem, IJGISFormSchemaRegistry, IJGISLayer, IJGISSource } from '@jupytergis/schema';
1
+ import { IAnnotation, IAnnotationModel, IDict, IJGISFilterItem, IJGISFormSchemaRegistry, IJGISLayer, IJGISSource, IJupyterGISSettings } from '@jupytergis/schema';
2
2
  import { User } from '@jupyterlab/services';
3
3
  import { IStateDB } from '@jupyterlab/statedb';
4
4
  import { Layer } from 'ol/layer';
@@ -10,6 +10,8 @@ interface IProps {
10
10
  state?: IStateDB;
11
11
  formSchemaRegistry?: IJGISFormSchemaRegistry;
12
12
  annotationModel?: IAnnotationModel;
13
+ /** True when viewport matches (max-width: 768px). Injected by MainViewWithMediaQuery. */
14
+ isMobile?: boolean;
13
15
  }
14
16
  interface IStates {
15
17
  id: string;
@@ -31,6 +33,7 @@ interface IStates {
31
33
  }>;
32
34
  displayTemporalController: boolean;
33
35
  filterStates: IDict<IJGISFilterItem | undefined>;
36
+ jgisSettings: IJupyterGISSettings;
34
37
  isSpectaPresentation: boolean;
35
38
  }
36
39
  export declare class MainView extends React.Component<IProps, IStates> {
@@ -136,6 +139,7 @@ export declare class MainView extends React.Component<IProps, IStates> {
136
139
  removeLayer(id: string): void;
137
140
  private _onClientSharedStateChanged;
138
141
  private _onSharedOptionsChanged;
142
+ private _syncSettingsFromRegistry;
139
143
  private _onSettingsChanged;
140
144
  private updateOptions;
141
145
  private _onViewChanged;
@@ -222,4 +226,6 @@ export declare class MainView extends React.Component<IProps, IStates> {
222
226
  private _isSpectaPresentationInitialized;
223
227
  private _storyScrollHandler;
224
228
  }
225
- export {};
229
+ /** Thin wrapper that injects isMobile from useMediaQuery so MainView can use it in JSX. */
230
+ declare function MainViewWithMediaQuery(props: IProps): React.JSX.Element;
231
+ export { MainViewWithMediaQuery };
@@ -39,6 +39,7 @@ import * as React from 'react';
39
39
  import AnnotationFloater from "../annotations/components/AnnotationFloater";
40
40
  import { CommandIDs } from "../constants";
41
41
  import { LoadingOverlay } from "../shared/components/loading";
42
+ import useMediaQuery from "../shared/hooks/useMediaQuery";
42
43
  import StatusBar from "../statusbar/StatusBar";
43
44
  import { debounce, isLightTheme, loadFile, throttle } from "../tools";
44
45
  import CollaboratorPointers from './CollaboratorPointers';
@@ -47,6 +48,7 @@ import TemporalSlider from './TemporalSlider';
47
48
  import { hexToRgb } from '../dialogs/symbology/colorRampUtils';
48
49
  import { markerIcon } from '../icons';
49
50
  import { LeftPanel, RightPanel } from '../panelview';
51
+ import { MobileSpectaPanel } from '../panelview/story-maps/MobileSpectaPanel';
50
52
  import StoryViewerPanel from '../panelview/story-maps/StoryViewerPanel';
51
53
  export class MainView extends React.Component {
52
54
  constructor(props) {
@@ -529,7 +531,7 @@ export class MainView extends React.Component {
529
531
  }
530
532
  const story = this._model.getSelectedStory().story;
531
533
  const bgColor = story === null || story === void 0 ? void 0 : story.presentationBgColor;
532
- const textColor = story === null || story === void 0 ? void 0 : story.presentaionTextColor;
534
+ const textColor = story === null || story === void 0 ? void 0 : story.presentationTextColor;
533
535
  // Set background color
534
536
  if (bgColor) {
535
537
  const rgb = hexToRgb(bgColor);
@@ -634,6 +636,9 @@ export class MainView extends React.Component {
634
636
  this._model.geolocationChanged.connect(this._handleGeolocationChanged, this);
635
637
  this._model.flyToGeometrySignal.connect(this.flyToGeometry, this);
636
638
  this._model.highlightFeatureSignal.connect(this.highlightFeatureOnMap, this);
639
+ Promise.resolve().then(() => {
640
+ this._syncSettingsFromRegistry();
641
+ });
637
642
  // Watch isIdentifying and clear the highlight when Identify Tool is turned off
638
643
  this._model.sharedModel.awareness.on('change', () => {
639
644
  var _a;
@@ -653,6 +658,7 @@ export class MainView extends React.Component {
653
658
  loadingErrors: [],
654
659
  displayTemporalController: false,
655
660
  filterStates: {},
661
+ jgisSettings: this._model.jgisSettings,
656
662
  isSpectaPresentation: false,
657
663
  };
658
664
  this._sources = [];
@@ -1203,6 +1209,7 @@ export class MainView extends React.Component {
1203
1209
  layerParameters = layer.parameters;
1204
1210
  newMapLayer = new VectorTileLayer({
1205
1211
  opacity: layerParameters.opacity,
1212
+ visible: layer.visible,
1206
1213
  source: this._sources[layerParameters.source],
1207
1214
  style: this.vectorLayerStyleRuleBuilder(layer),
1208
1215
  });
@@ -1212,6 +1219,7 @@ export class MainView extends React.Component {
1212
1219
  layerParameters = layer.parameters;
1213
1220
  newMapLayer = new WebGlTileLayer({
1214
1221
  opacity: 0.3,
1222
+ visible: layer.visible,
1215
1223
  source: this._sources[layerParameters.source],
1216
1224
  style: {
1217
1225
  color: ['color', this.hillshadeMath()],
@@ -1223,6 +1231,7 @@ export class MainView extends React.Component {
1223
1231
  layerParameters = layer.parameters;
1224
1232
  newMapLayer = new ImageLayer({
1225
1233
  opacity: layerParameters.opacity,
1234
+ visible: layer.visible,
1226
1235
  source: this._sources[layerParameters.source],
1227
1236
  });
1228
1237
  break;
@@ -1232,6 +1241,7 @@ export class MainView extends React.Component {
1232
1241
  // This is to handle python sending a None for the color
1233
1242
  const layerOptions = {
1234
1243
  opacity: layerParameters.opacity,
1244
+ visible: layer.visible,
1235
1245
  source: this._sources[layerParameters.source],
1236
1246
  };
1237
1247
  if (layerParameters.color) {
@@ -1246,6 +1256,7 @@ export class MainView extends React.Component {
1246
1256
  layerParameters = layer.parameters;
1247
1257
  newMapLayer = new HeatmapLayer({
1248
1258
  opacity: layerParameters.opacity,
1259
+ visible: layer.visible,
1249
1260
  source: this._sources[layerParameters.source],
1250
1261
  blur: (_b = layerParameters.blur) !== null && _b !== void 0 ? _b : 15,
1251
1262
  radius: (_c = layerParameters.radius) !== null && _c !== void 0 ? _c : 8,
@@ -1564,8 +1575,16 @@ export class MainView extends React.Component {
1564
1575
  this._isPositionInitialized = true;
1565
1576
  }
1566
1577
  }
1567
- _onSettingsChanged(sender, key) {
1568
- if (key !== 'zoomButtonsEnabled' || !this._Map) {
1578
+ async _syncSettingsFromRegistry() {
1579
+ const composite = this._model.jgisSettings;
1580
+ if (composite) {
1581
+ this.setState({ jgisSettings: composite });
1582
+ this._onSettingsChanged();
1583
+ }
1584
+ }
1585
+ _onSettingsChanged() {
1586
+ this.setState({ jgisSettings: this._model.jgisSettings });
1587
+ if (!this._Map) {
1569
1588
  return;
1570
1589
  }
1571
1590
  const enabled = this._model.jgisSettings.zoomButtonsEnabled;
@@ -1795,6 +1814,14 @@ export class MainView extends React.Component {
1795
1814
  if ((jgisLayer === null || jgisLayer === void 0 ? void 0 : jgisLayer.type) === 'StorySegmentLayer') {
1796
1815
  const layerParams = jgisLayer.parameters;
1797
1816
  const coords = getCenter(layerParams.extent);
1817
+ // Don't move map if we're already centered on the segment
1818
+ const viewCenter = this._Map.getView().getCenter();
1819
+ const centersEqual = viewCenter !== undefined &&
1820
+ Math.abs(viewCenter[0] - coords[0]) < 1e-9 &&
1821
+ Math.abs(viewCenter[1] - coords[1]) < 1e-9;
1822
+ if (centersEqual) {
1823
+ return;
1824
+ }
1798
1825
  this._flyToPosition({ x: coords[0], y: coords[1] }, layerParams.zoom, ((_c = layerParams.transition.time) !== null && _c !== void 0 ? _c : 1) * 1000, // seconds -> ms
1799
1826
  layerParams.transition.type);
1800
1827
  return;
@@ -2064,11 +2091,18 @@ export class MainView extends React.Component {
2064
2091
  height: '100%',
2065
2092
  } },
2066
2093
  React.createElement("div", { className: "jgis-panels-wrapper" }, !this.state.isSpectaPresentation ? (React.createElement(React.Fragment, null,
2067
- this._state && (React.createElement(LeftPanel, { model: this._model, commands: this._mainViewModel.commands, state: this._state })),
2068
- this._formSchemaRegistry && this._annotationModel && (React.createElement(RightPanel, { model: this._model, commands: this._mainViewModel.commands, formSchemaRegistry: this._formSchemaRegistry, annotationModel: this._annotationModel })))) : (React.createElement("div", { className: "jgis-specta-right-panel-container-mod jgis-right-panel-container" },
2094
+ this._state && (React.createElement(LeftPanel, { model: this._model, commands: this._mainViewModel.commands, state: this._state, settings: this.state.jgisSettings })),
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" },
2069
2096
  React.createElement("div", { ref: this.spectaContainerRef, className: "jgis-specta-story-panel-container" },
2070
- React.createElement(StoryViewerPanel, { ref: this.storyViewerPanelRef, model: this._model, isSpecta: this.state.isSpectaPresentation }))))),
2097
+ React.createElement(StoryViewerPanel, { ref: this.storyViewerPanelRef, model: this._model, isSpecta: this.state.isSpectaPresentation, className: "jgis-story-viewer-panel-specta-mod" }))))),
2071
2098
  React.createElement("div", { ref: this.controlsToolbarRef, className: "jgis-controls-toolbar" }))),
2072
- React.createElement(StatusBar, { jgisModel: this._model, loading: this.state.loadingLayer, projection: this.state.viewProjection, scale: this.state.scale }))));
2099
+ !this.state.isSpectaPresentation && (React.createElement(StatusBar, { jgisModel: this._model, loading: this.state.loadingLayer, projection: this.state.viewProjection, scale: this.state.scale })))));
2073
2100
  }
2074
2101
  }
2102
+ // ! TODO make mainview a modern react component instead of a class
2103
+ /** Thin wrapper that injects isMobile from useMediaQuery so MainView can use it in JSX. */
2104
+ function MainViewWithMediaQuery(props) {
2105
+ const isMobile = useMediaQuery('(max-width: 768px)');
2106
+ return React.createElement(MainView, Object.assign({}, props, { isMobile: isMobile }));
2107
+ }
2108
+ export { MainViewWithMediaQuery };
@@ -1,6 +1,6 @@
1
1
  import { ReactWidget } from '@jupyterlab/apputils';
2
2
  import * as React from 'react';
3
- import { MainView } from './mainView';
3
+ import { MainViewWithMediaQuery } from './mainView';
4
4
  export class JupyterGISMainViewPanel extends ReactWidget {
5
5
  /**
6
6
  * Construct a `JupyterGISPanel`.
@@ -12,6 +12,6 @@ export class JupyterGISMainViewPanel extends ReactWidget {
12
12
  this._options = options;
13
13
  }
14
14
  render() {
15
- return (React.createElement(MainView, { state: this._state, viewModel: this._options.mainViewModel, formSchemaRegistry: this._options.formSchemaRegistry, annotationModel: this._options.annotationModel }));
15
+ return (React.createElement(MainViewWithMediaQuery, { state: this._state, viewModel: this._options.mainViewModel, formSchemaRegistry: this._options.formSchemaRegistry, annotationModel: this._options.annotationModel }));
16
16
  }
17
17
  }
@@ -1,4 +1,4 @@
1
- import { IJupyterGISModel, SelectionType } from '@jupytergis/schema';
1
+ import { IJupyterGISModel, SelectionType, IJupyterGISSettings } from '@jupytergis/schema';
2
2
  import { IStateDB } from '@jupyterlab/statedb';
3
3
  import { CommandRegistry } from '@lumino/commands';
4
4
  import { MouseEvent as ReactMouseEvent } from 'react';
@@ -12,6 +12,7 @@ interface ILeftPanelProps {
12
12
  model: IJupyterGISModel;
13
13
  state: IStateDB;
14
14
  commands: CommandRegistry;
15
+ settings: IJupyterGISSettings;
15
16
  }
16
17
  export declare const LeftPanel: React.FC<ILeftPanelProps>;
17
18
  export {};
@@ -6,32 +6,42 @@ import { PanelTabs, TabsContent, TabsList, TabsTrigger, } from '../shared/compon
6
6
  import StacPanel from '../stacBrowser/components/StacPanel';
7
7
  export const LeftPanel = (props) => {
8
8
  var _a;
9
- const [settings, setSettings] = React.useState(props.model.jgisSettings);
10
9
  const [options, setOptions] = React.useState(props.model.getOptions());
11
10
  const storyMapPresentationMode = (_a = options.storyMapPresentationMode) !== null && _a !== void 0 ? _a : false;
12
11
  const [layerTree, setLayerTree] = React.useState(props.model.getLayerTree());
12
+ const hasSyncedInitialSelectionRef = React.useRef(false);
13
13
  const tabInfo = [
14
- !settings.layersDisabled ? { name: 'layers', title: 'Layers' } : false,
15
- !settings.stacBrowserDisabled && !storyMapPresentationMode
14
+ !props.settings.layersDisabled
15
+ ? { name: 'layers', title: 'Layers' }
16
+ : false,
17
+ !props.settings.stacBrowserDisabled && !storyMapPresentationMode
16
18
  ? { name: 'stac', title: 'Stac Browser' }
17
19
  : false,
18
- !settings.filtersDisabled && !storyMapPresentationMode
20
+ !props.settings.filtersDisabled && !storyMapPresentationMode
19
21
  ? { name: 'filters', title: 'Filters' }
20
22
  : false,
21
- !settings.storyMapsDisabled
23
+ !props.settings.storyMapsDisabled
22
24
  ? { name: 'segments', title: 'Segments' }
23
25
  : false,
24
26
  ].filter(Boolean);
25
27
  const [curTab, setCurTab] = React.useState(tabInfo.length > 0 ? tabInfo[0].name : undefined);
26
28
  React.useEffect(() => {
27
- const onSettingsChanged = () => {
28
- setSettings(Object.assign({}, props.model.jgisSettings));
29
- };
30
29
  const onOptionsChanged = () => {
31
30
  setOptions(Object.assign({}, props.model.getOptions()));
32
31
  };
33
32
  const updateLayerTree = () => {
34
- setLayerTree(props.model.getLayerTree() || []);
33
+ const freshTree = props.model.getLayerTree() || [];
34
+ setLayerTree(freshTree);
35
+ // Sync selected to top layer/group only the first time the tree has items
36
+ if (!hasSyncedInitialSelectionRef.current && freshTree.length > 0) {
37
+ hasSyncedInitialSelectionRef.current = true;
38
+ const lastItem = freshTree[freshTree.length - 1];
39
+ const lastId = typeof lastItem === 'string' ? lastItem : lastItem === null || lastItem === void 0 ? void 0 : lastItem.name;
40
+ const lastType = typeof lastItem === 'string' ? 'layer' : 'group';
41
+ if (lastId) {
42
+ props.model.syncSelected({ [lastId]: { type: lastType } }, props.model.getClientId().toString());
43
+ }
44
+ }
35
45
  // Need to let command know when segments get populated
36
46
  props.commands.notifyCommandChanged(CommandIDs.toggleStoryPresentationMode);
37
47
  };
@@ -39,14 +49,12 @@ export const LeftPanel = (props) => {
39
49
  props.model.syncSelected({ [payload.storySegmentId]: { type: 'layer' } }, props.model.getClientId().toString());
40
50
  setCurTab('segments');
41
51
  };
42
- props.model.settingsChanged.connect(onSettingsChanged);
43
52
  props.model.sharedOptionsChanged.connect(onOptionsChanged);
44
53
  props.model.sharedModel.layersChanged.connect(updateLayerTree);
45
54
  props.model.sharedModel.layerTreeChanged.connect(updateLayerTree);
46
55
  props.model.segmentAdded.connect(onSegmentAdded);
47
56
  updateLayerTree();
48
57
  return () => {
49
- props.model.settingsChanged.disconnect(onSettingsChanged);
50
58
  props.model.sharedOptionsChanged.disconnect(onOptionsChanged);
51
59
  props.model.sharedModel.layersChanged.disconnect(updateLayerTree);
52
60
  props.model.sharedModel.layerTreeChanged.disconnect(updateLayerTree);
@@ -110,11 +118,11 @@ export const LeftPanel = (props) => {
110
118
  }
111
119
  props.model.sharedModel.updateStoryMap(storyId, Object.assign(Object.assign({}, story), { storySegments: storySegmentLayerTree }));
112
120
  }, [storySegmentLayerTree]);
113
- const allLeftTabsDisabled = settings.layersDisabled &&
114
- settings.stacBrowserDisabled &&
115
- settings.filtersDisabled &&
116
- settings.storyMapsDisabled;
117
- const leftPanelVisible = !settings.leftPanelDisabled && !allLeftTabsDisabled;
121
+ const allLeftTabsDisabled = props.settings.layersDisabled &&
122
+ props.settings.stacBrowserDisabled &&
123
+ props.settings.filtersDisabled &&
124
+ props.settings.storyMapsDisabled;
125
+ const leftPanelVisible = !props.settings.leftPanelDisabled && !allLeftTabsDisabled;
118
126
  return (React.createElement("div", { className: "jgis-left-panel-container", style: { display: leftPanelVisible ? 'block' : 'none' } },
119
127
  React.createElement(PanelTabs, { curTab: curTab, className: "jgis-panel-tabs" },
120
128
  React.createElement(TabsList, null, tabInfo.map(tab => (React.createElement(TabsTrigger, { className: "jGIS-layer-browser-category", key: tab.name, value: tab.name, onClick: () => {
@@ -125,12 +133,12 @@ export const LeftPanel = (props) => {
125
133
  setCurTab('');
126
134
  }
127
135
  } }, tab.title)))),
128
- !settings.layersDisabled && (React.createElement(TabsContent, { value: "layers", className: "jgis-panel-tab-content jp-gis-layerPanel" },
136
+ !props.settings.layersDisabled && (React.createElement(TabsContent, { value: "layers", className: "jgis-panel-tab-content jp-gis-layerPanel" },
129
137
  React.createElement(LayersBodyComponent, { model: props.model, commands: props.commands, state: props.state, layerTree: filteredLayerTree }))),
130
- !settings.stacBrowserDisabled && (React.createElement(TabsContent, { value: "stac", className: "jgis-panel-tab-content jgis-panel-tab-content-stac-panel" },
138
+ !props.settings.stacBrowserDisabled && (React.createElement(TabsContent, { value: "stac", className: "jgis-panel-tab-content jgis-panel-tab-content-stac-panel" },
131
139
  React.createElement(StacPanel, { model: props.model }))),
132
- !settings.filtersDisabled && (React.createElement(TabsContent, { value: "filters", className: "jgis-panel-tab-content" },
140
+ !props.settings.filtersDisabled && (React.createElement(TabsContent, { value: "filters", className: "jgis-panel-tab-content" },
133
141
  React.createElement(FilterComponent, { model: props.model }))),
134
- !settings.storyMapsDisabled && (React.createElement(TabsContent, { value: "segments", className: "jgis-panel-tab-content" },
142
+ !props.settings.storyMapsDisabled && (React.createElement(TabsContent, { value: "segments", className: "jgis-panel-tab-content" },
135
143
  React.createElement(LayersBodyComponent, { model: props.model, commands: props.commands, state: props.state, layerTree: storySegmentLayerTree }))))));
136
144
  };
@@ -1,4 +1,4 @@
1
- import { IAnnotationModel, IJGISFormSchemaRegistry, IJupyterGISModel } 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 {
@@ -6,6 +6,9 @@ interface IRightPanelProps {
6
6
  annotationModel: IAnnotationModel;
7
7
  model: IJupyterGISModel;
8
8
  commands: CommandRegistry;
9
+ settings: IJupyterGISSettings;
10
+ addLayer?: (id: string, layer: IJGISLayer, index: number) => Promise<void>;
11
+ removeLayer?: (id: string) => void;
9
12
  }
10
13
  export declare const RightPanel: React.FC<IRightPanelProps>;
11
14
  export {};
@@ -9,8 +9,8 @@ import { PanelTabs, TabsContent, TabsList, TabsTrigger, } from '../shared/compon
9
9
  export const RightPanel = props => {
10
10
  var _a;
11
11
  const [editorMode, setEditorMode] = React.useState(true);
12
- const [settings, setSettings] = React.useState(props.model.jgisSettings);
13
12
  const [storyMapPresentationMode, setStoryMapPresentationMode] = React.useState((_a = props.model.getOptions().storyMapPresentationMode) !== null && _a !== void 0 ? _a : false);
13
+ const [selectedObjectProperties, setSelectedObjectProperties] = React.useState(undefined);
14
14
  // Only show editor when not in presentation mode and editorMode is true
15
15
  const showEditor = !storyMapPresentationMode && editorMode;
16
16
  // Tab title: "Story Map" in presentation mode, otherwise based on editorMode
@@ -20,19 +20,19 @@ export const RightPanel = props => {
20
20
  ? 'Story Editor'
21
21
  : 'Story Map';
22
22
  const tabInfo = [
23
- !settings.objectPropertiesDisabled && !storyMapPresentationMode
23
+ !props.settings.objectPropertiesDisabled && !storyMapPresentationMode
24
24
  ? { name: 'objectProperties', title: 'Object Properties' }
25
25
  : false,
26
- !settings.storyMapsDisabled
26
+ !props.settings.storyMapsDisabled
27
27
  ? {
28
28
  name: 'storyPanel',
29
29
  title: storyPanelTitle,
30
30
  }
31
31
  : false,
32
- !settings.annotationsDisabled
32
+ !props.settings.annotationsDisabled
33
33
  ? { name: 'annotations', title: 'Annotations' }
34
34
  : false,
35
- !settings.identifyDisabled
35
+ !props.settings.identifyDisabled
36
36
  ? { name: 'identifyPanel', title: 'Identified Features' }
37
37
  : false,
38
38
  ].filter(Boolean);
@@ -43,9 +43,6 @@ export const RightPanel = props => {
43
43
  return tabInfo.length > 0 ? tabInfo[0].name : '';
44
44
  });
45
45
  React.useEffect(() => {
46
- const onSettingsChanged = () => {
47
- setSettings(Object.assign({}, props.model.jgisSettings));
48
- };
49
46
  const onOptionsChanged = () => {
50
47
  const { storyMapPresentationMode } = props.model.getOptions();
51
48
  setStoryMapPresentationMode(storyMapPresentationMode !== null && storyMapPresentationMode !== void 0 ? storyMapPresentationMode : false);
@@ -63,20 +60,17 @@ export const RightPanel = props => {
63
60
  setCurTab('identifyPanel');
64
61
  }
65
62
  };
66
- props.model.settingsChanged.connect(onSettingsChanged);
67
63
  props.model.sharedOptionsChanged.connect(onOptionsChanged);
68
64
  props.model.clientStateChanged.connect(onAwerenessChanged);
69
65
  return () => {
70
- props.model.settingsChanged.disconnect(onSettingsChanged);
71
66
  props.model.sharedOptionsChanged.disconnect(onOptionsChanged);
72
67
  props.model.clientStateChanged.disconnect(onAwerenessChanged);
73
68
  };
74
69
  }, [props.model]);
75
- const allRightTabsDisabled = settings.objectPropertiesDisabled &&
76
- settings.annotationsDisabled &&
77
- settings.identifyDisabled;
78
- const rightPanelVisible = !settings.rightPanelDisabled && !allRightTabsDisabled;
79
- const [selectedObjectProperties, setSelectedObjectProperties] = React.useState(undefined);
70
+ const allRightTabsDisabled = props.settings.objectPropertiesDisabled &&
71
+ props.settings.annotationsDisabled &&
72
+ props.settings.identifyDisabled;
73
+ const rightPanelVisible = !props.settings.rightPanelDisabled && !allRightTabsDisabled;
80
74
  const toggleEditor = () => {
81
75
  setEditorMode(!editorMode);
82
76
  };
@@ -90,13 +84,13 @@ export const RightPanel = props => {
90
84
  setCurTab('');
91
85
  }
92
86
  } }, tab.title)))),
93
- !settings.objectPropertiesDisabled && (React.createElement(TabsContent, { value: "objectProperties", className: "jgis-panel-tab-content" },
87
+ !props.settings.objectPropertiesDisabled && (React.createElement(TabsContent, { value: "objectProperties", className: "jgis-panel-tab-content" },
94
88
  React.createElement(ObjectPropertiesReact, { setSelectedObject: setSelectedObjectProperties, selectedObject: selectedObjectProperties, formSchemaRegistry: props.formSchemaRegistry, model: props.model }))),
95
- !settings.storyMapsDisabled && (React.createElement(TabsContent, { value: "storyPanel", className: "jgis-panel-tab-content", style: { paddingTop: 0 } },
89
+ !props.settings.storyMapsDisabled && (React.createElement(TabsContent, { value: "storyPanel", className: "jgis-panel-tab-content", style: { paddingTop: 0 } },
96
90
  !storyMapPresentationMode && (React.createElement(PreviewModeSwitch, { checked: !editorMode, onCheckedChange: toggleEditor })),
97
- showEditor ? (React.createElement(StoryEditorPanel, { model: props.model, commands: props.commands })) : (React.createElement(StoryViewerPanel, { model: props.model, isSpecta: false })))),
98
- !settings.annotationsDisabled && (React.createElement(TabsContent, { value: "annotations", className: "jgis-panel-tab-content" },
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
+ !props.settings.annotationsDisabled && (React.createElement(TabsContent, { value: "annotations", className: "jgis-panel-tab-content" },
99
93
  React.createElement(AnnotationsPanel, { annotationModel: props.annotationModel, jgisModel: props.model }))),
100
- !settings.identifyDisabled && (React.createElement(TabsContent, { value: "identifyPanel", className: "jgis-panel-tab-content" },
94
+ !props.settings.identifyDisabled && (React.createElement(TabsContent, { value: "identifyPanel", className: "jgis-panel-tab-content" },
101
95
  React.createElement(IdentifyPanelComponent, { model: props.model }))))));
102
96
  };
@@ -0,0 +1,7 @@
1
+ import { IJupyterGISModel } from '@jupytergis/schema';
2
+ import React from 'react';
3
+ interface IMobileSpectaPanelProps {
4
+ model: IJupyterGISModel;
5
+ }
6
+ export declare function MobileSpectaPanel({ model }: IMobileSpectaPanelProps): React.JSX.Element;
7
+ export {};