@jupytergis/base 0.10.1 → 0.12.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 (120) hide show
  1. package/lib/commands/BaseCommandIDs.d.ts +2 -0
  2. package/lib/commands/BaseCommandIDs.js +3 -0
  3. package/lib/commands/index.js +66 -0
  4. package/lib/constants.js +4 -0
  5. package/lib/dialogs/symbology/hooks/useGetBandInfo.d.ts +0 -6
  6. package/lib/dialogs/symbology/hooks/useGetBandInfo.js +2 -2
  7. package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +4 -4
  8. package/lib/dialogs/symbology/vector_layer/types/Categorized.js +1 -5
  9. package/lib/formbuilder/formselectors.js +5 -1
  10. package/lib/formbuilder/objectform/StoryEditorForm.d.ts +9 -0
  11. package/lib/formbuilder/objectform/StoryEditorForm.js +16 -0
  12. package/lib/formbuilder/objectform/components/StorySegmentReset.d.ts +8 -0
  13. package/lib/formbuilder/objectform/components/StorySegmentReset.js +24 -0
  14. package/lib/formbuilder/objectform/layer/index.d.ts +1 -0
  15. package/lib/formbuilder/objectform/layer/index.js +1 -0
  16. package/lib/formbuilder/objectform/layer/storySegmentLayerForm.d.ts +5 -0
  17. package/lib/formbuilder/objectform/layer/storySegmentLayerForm.js +32 -0
  18. package/lib/mainview/mainView.d.ts +18 -0
  19. package/lib/mainview/mainView.js +293 -14
  20. package/lib/panelview/components/layers.d.ts +2 -1
  21. package/lib/panelview/components/layers.js +31 -23
  22. package/lib/panelview/{components/filter-panel → filter-panel}/Filter.js +1 -1
  23. package/lib/panelview/leftpanel.js +89 -7
  24. package/lib/panelview/rightpanel.d.ts +2 -0
  25. package/lib/panelview/rightpanel.js +41 -4
  26. package/lib/panelview/story-maps/PreviewModeSwitch.d.ts +7 -0
  27. package/lib/panelview/story-maps/PreviewModeSwitch.js +13 -0
  28. package/lib/panelview/story-maps/StoryEditorPanel.d.ts +9 -0
  29. package/lib/panelview/story-maps/StoryEditorPanel.js +34 -0
  30. package/lib/panelview/story-maps/StoryNavBar.d.ts +10 -0
  31. package/lib/panelview/story-maps/StoryNavBar.js +11 -0
  32. package/lib/panelview/story-maps/StoryViewerPanel.d.ts +13 -0
  33. package/lib/panelview/story-maps/StoryViewerPanel.js +179 -0
  34. package/lib/panelview/story-maps/components/StoryContentSection.d.ts +6 -0
  35. package/lib/panelview/story-maps/components/StoryContentSection.js +10 -0
  36. package/lib/panelview/story-maps/components/StoryImageSection.d.ts +15 -0
  37. package/lib/panelview/story-maps/components/StoryImageSection.js +13 -0
  38. package/lib/panelview/story-maps/components/StorySubtitleSection.d.ts +11 -0
  39. package/lib/panelview/story-maps/components/StorySubtitleSection.js +9 -0
  40. package/lib/panelview/story-maps/components/StoryTitleSection.d.ts +12 -0
  41. package/lib/panelview/story-maps/components/StoryTitleSection.js +8 -0
  42. package/lib/shared/components/Calendar.d.ts +1 -1
  43. package/lib/shared/components/Combobox.d.ts +21 -0
  44. package/lib/shared/components/Combobox.js +32 -0
  45. package/lib/shared/components/Command.d.ts +18 -0
  46. package/lib/shared/components/Command.js +60 -0
  47. package/lib/shared/components/Dialog.d.ts +15 -0
  48. package/lib/shared/components/Dialog.js +62 -0
  49. package/lib/shared/components/Input.d.ts +3 -0
  50. package/lib/shared/components/Input.js +18 -0
  51. package/lib/shared/components/Pagination.js +3 -2
  52. package/lib/shared/components/RadioGroup.d.ts +5 -0
  53. package/lib/shared/components/RadioGroup.js +26 -0
  54. package/lib/shared/components/Select.d.ts +19 -0
  55. package/lib/shared/components/Select.js +28 -0
  56. package/lib/shared/components/SingleDatePicker.d.ts +11 -0
  57. package/lib/shared/components/SingleDatePicker.js +16 -0
  58. package/lib/shared/components/Switch.d.ts +4 -0
  59. package/lib/shared/components/Switch.js +20 -0
  60. package/lib/stacBrowser/components/StacPanel.d.ts +9 -1
  61. package/lib/stacBrowser/components/StacPanel.js +53 -9
  62. package/lib/stacBrowser/components/filter-extension/QueryableComboBox.d.ts +9 -0
  63. package/lib/stacBrowser/components/filter-extension/QueryableComboBox.js +179 -0
  64. package/lib/stacBrowser/components/filter-extension/QueryableRow.d.ts +16 -0
  65. package/lib/stacBrowser/components/filter-extension/QueryableRow.js +16 -0
  66. package/lib/stacBrowser/components/filter-extension/StacFilterExtensionPanel.d.ts +7 -0
  67. package/lib/stacBrowser/components/filter-extension/StacFilterExtensionPanel.js +49 -0
  68. package/lib/stacBrowser/components/filter-extension/StacQueryableFilters.d.ts +11 -0
  69. package/lib/stacBrowser/components/filter-extension/StacQueryableFilters.js +19 -0
  70. package/lib/stacBrowser/components/{StacFilterSection.d.ts → geodes/StacFilterSection.d.ts} +1 -1
  71. package/lib/stacBrowser/components/{StacFilterSection.js → geodes/StacFilterSection.js} +3 -3
  72. package/lib/stacBrowser/components/geodes/StacGeodesFilterPanel.d.ts +7 -0
  73. package/lib/stacBrowser/components/geodes/StacGeodesFilterPanel.js +69 -0
  74. package/lib/stacBrowser/components/shared/StacPanelResults.d.ts +3 -0
  75. package/lib/stacBrowser/components/shared/StacPanelResults.js +68 -0
  76. package/lib/stacBrowser/components/shared/StacSpatialExtent.d.ts +8 -0
  77. package/lib/stacBrowser/components/shared/StacSpatialExtent.js +10 -0
  78. package/lib/stacBrowser/components/shared/StacTemporalExtent.d.ts +9 -0
  79. package/lib/stacBrowser/components/shared/StacTemporalExtent.js +9 -0
  80. package/lib/stacBrowser/context/StacResultsContext.d.ts +33 -0
  81. package/lib/stacBrowser/context/StacResultsContext.js +269 -0
  82. package/lib/stacBrowser/hooks/useGeodesSearch.d.ts +24 -0
  83. package/lib/stacBrowser/hooks/useGeodesSearch.js +178 -0
  84. package/lib/stacBrowser/hooks/useStacFilterExtension.d.ts +30 -0
  85. package/lib/stacBrowser/hooks/useStacFilterExtension.js +262 -0
  86. package/lib/stacBrowser/hooks/useStacSearch.d.ts +5 -16
  87. package/lib/stacBrowser/hooks/useStacSearch.js +30 -184
  88. package/lib/stacBrowser/types/types.d.ts +86 -3
  89. package/lib/toolbar/widget.d.ts +15 -0
  90. package/lib/toolbar/widget.js +70 -0
  91. package/lib/tools.d.ts +0 -7
  92. package/lib/tools.js +56 -15
  93. package/package.json +8 -3
  94. package/style/base.css +42 -3
  95. package/style/leftPanel.css +18 -0
  96. package/style/shared/button.css +6 -5
  97. package/style/shared/calendar.css +7 -1
  98. package/style/shared/combobox.css +75 -0
  99. package/style/shared/command.css +178 -0
  100. package/style/shared/dialog.css +177 -0
  101. package/style/shared/input.css +59 -0
  102. package/style/shared/pagination.css +1 -1
  103. package/style/shared/popover.css +1 -0
  104. package/style/shared/radioGroup.css +55 -0
  105. package/style/shared/switch.css +63 -0
  106. package/style/shared/tabs.css +4 -3
  107. package/style/shared/toggle.css +1 -1
  108. package/style/stacBrowser.css +169 -16
  109. package/style/statusBar.css +1 -0
  110. package/style/storyPanel.css +185 -0
  111. package/style/tabPanel.css +1 -88
  112. package/lib/stacBrowser/components/StacPanelFilters.d.ts +0 -14
  113. package/lib/stacBrowser/components/StacPanelFilters.js +0 -81
  114. package/lib/stacBrowser/components/StacPanelResults.d.ts +0 -13
  115. package/lib/stacBrowser/components/StacPanelResults.js +0 -48
  116. /package/lib/panelview/{components/filter-panel → filter-panel}/Filter.d.ts +0 -0
  117. /package/lib/panelview/{components/filter-panel → filter-panel}/FilterRow.d.ts +0 -0
  118. /package/lib/panelview/{components/filter-panel → filter-panel}/FilterRow.js +0 -0
  119. /package/lib/panelview/{components/identify-panel → identify-panel}/IdentifyPanel.d.ts +0 -0
  120. /package/lib/panelview/{components/identify-panel → identify-panel}/IdentifyPanel.js +0 -0
@@ -16,11 +16,12 @@ import { UUID } from '@lumino/coreutils';
16
16
  import { ContextMenu } from '@lumino/widgets';
17
17
  import { Collection, Map as OlMap, View, getUid, } from 'ol';
18
18
  import Feature from 'ol/Feature';
19
- import { FullScreen, ScaleLine } from 'ol/control';
19
+ import { FullScreen, ScaleLine, Zoom } from 'ol/control';
20
20
  import { singleClick } from 'ol/events/condition';
21
+ import { getCenter } from 'ol/extent';
21
22
  import { GeoJSON, MVT } from 'ol/format';
22
23
  import { Point } from 'ol/geom';
23
- import { DragAndDrop, Select } from 'ol/interaction';
24
+ import { DragAndDrop, DragPan, DragRotate, DragZoom, KeyboardPan, KeyboardZoom, MouseWheelZoom, PinchRotate, PinchZoom, DoubleClickZoom, Select, } from 'ol/interaction';
24
25
  import { Heatmap as HeatmapLayer, Image as ImageLayer, Layer, Vector as VectorLayer, VectorTile as VectorTileLayer, WebGLTile as WebGlTileLayer, } from 'ol/layer';
25
26
  import TileLayer from 'ol/layer/Tile';
26
27
  import { fromLonLat, get as getProjection, toLonLat, transformExtent, } from 'ol/proj';
@@ -43,8 +44,10 @@ import { debounce, isLightTheme, loadFile, throttle } from "../tools";
43
44
  import CollaboratorPointers from './CollaboratorPointers';
44
45
  import { FollowIndicator } from './FollowIndicator';
45
46
  import TemporalSlider from './TemporalSlider';
47
+ import { hexToRgb } from '../dialogs/symbology/colorRampUtils';
46
48
  import { markerIcon } from '../icons';
47
49
  import { LeftPanel, RightPanel } from '../panelview';
50
+ import StoryViewerPanel from '../panelview/story-maps/StoryViewerPanel';
48
51
  export class MainView extends React.Component {
49
52
  constructor(props) {
50
53
  super(props);
@@ -405,6 +408,138 @@ export class MainView extends React.Component {
405
408
  }
406
409
  }
407
410
  };
411
+ /**
412
+ * Handler for when story maps change in the model.
413
+ * Updates specta state and presentation colors when story data becomes available.
414
+ */
415
+ this._setupSpectaMode = () => {
416
+ this._removeAllInteractions();
417
+ this._setupStoryScrollListener();
418
+ // Update colors CSS variables with colors from story
419
+ this._updateSpectaPresentationColors();
420
+ };
421
+ this._removeAllInteractions = () => {
422
+ // Remove all default interactions
423
+ const interactions = this._Map.getInteractions();
424
+ const interactionArray = interactions.getArray();
425
+ // Remove each interaction type
426
+ const interactionsToRemove = [
427
+ DragPan,
428
+ DragRotate,
429
+ DragZoom,
430
+ KeyboardPan,
431
+ KeyboardZoom,
432
+ MouseWheelZoom,
433
+ PinchRotate,
434
+ PinchZoom,
435
+ DoubleClickZoom,
436
+ DragAndDrop,
437
+ Select,
438
+ ];
439
+ this._zoomControl && this._Map.removeControl(this._zoomControl);
440
+ interactionsToRemove.forEach(InteractionClass => {
441
+ const interaction = interactionArray.find(interaction => interaction instanceof InteractionClass);
442
+ if (interaction) {
443
+ this._Map.removeInteraction(interaction);
444
+ }
445
+ });
446
+ };
447
+ this._setupStoryScrollListener = () => {
448
+ const segmentNavigationThrottle = 750; // Minimum time between segment changes (ms)
449
+ const SCROLL_EDGE_THRESHOLD = 0; // Pixels from top/bottom to trigger segment change
450
+ // Create throttled functions that call the current panel handle dynamically
451
+ const throttledHandleNext = throttle(() => {
452
+ const panelHandle = this.storyViewerPanelRef.current;
453
+ panelHandle === null || panelHandle === void 0 ? void 0 : panelHandle.handleNext();
454
+ }, segmentNavigationThrottle);
455
+ const throttledHandlePrev = throttle(() => {
456
+ const panelHandle = this.storyViewerPanelRef.current;
457
+ panelHandle === null || panelHandle === void 0 ? void 0 : panelHandle.handlePrev();
458
+ }, segmentNavigationThrottle);
459
+ const handleScroll = (e) => {
460
+ const currentPanelHandle = this.storyViewerPanelRef.current;
461
+ if (!currentPanelHandle || !currentPanelHandle.canNavigate) {
462
+ return;
463
+ }
464
+ const wheelEvent = e;
465
+ const target = wheelEvent.target;
466
+ // Find the story viewer panel
467
+ const storyViewerPanel = document.querySelector('.jgis-story-viewer-panel');
468
+ // If no panel found, change segments normally
469
+ if (!storyViewerPanel) {
470
+ wheelEvent.preventDefault();
471
+ wheelEvent.deltaY > 0 ? throttledHandleNext() : throttledHandlePrev();
472
+ return;
473
+ }
474
+ const hasOverflow = storyViewerPanel.scrollHeight > storyViewerPanel.clientHeight;
475
+ // If panel has no overflow, change segments normally
476
+ if (!hasOverflow) {
477
+ wheelEvent.preventDefault();
478
+ wheelEvent.deltaY > 0 ? throttledHandleNext() : throttledHandlePrev();
479
+ return;
480
+ }
481
+ // Panel has overflow - handle scroll forwarding and edge detection
482
+ const scrollTop = storyViewerPanel.scrollTop;
483
+ const scrollHeight = storyViewerPanel.scrollHeight;
484
+ const clientHeight = storyViewerPanel.clientHeight;
485
+ const isAtBottom = scrollTop + clientHeight >= scrollHeight - SCROLL_EDGE_THRESHOLD;
486
+ const isAtTop = scrollTop <= SCROLL_EDGE_THRESHOLD;
487
+ const isScrollingDown = wheelEvent.deltaY > 0;
488
+ const isScrollingUp = wheelEvent.deltaY < 0;
489
+ // At edges: change segments
490
+ if ((isScrollingDown && isAtBottom) || (isScrollingUp && isAtTop)) {
491
+ wheelEvent.preventDefault();
492
+ isScrollingDown ? throttledHandleNext() : throttledHandlePrev();
493
+ return;
494
+ }
495
+ // If scrolling inside the panel, let it scroll naturally
496
+ if (target.closest('.jgis-story-viewer-panel')) {
497
+ return;
498
+ }
499
+ // Scrolling outside the panel: forward scroll to panel (no throttling for smooth scrolling)
500
+ wheelEvent.preventDefault();
501
+ const newScrollTop = Math.max(0, Math.min(scrollHeight - clientHeight, scrollTop + wheelEvent.deltaY));
502
+ storyViewerPanel.scrollTop = newScrollTop;
503
+ };
504
+ this._storyScrollHandler = handleScroll;
505
+ // Attach wheel event listener to the main container
506
+ const containerElement = document.querySelector('.jGIS-Mainview-Container');
507
+ if (containerElement) {
508
+ containerElement.addEventListener('wheel', handleScroll, {
509
+ passive: false,
510
+ });
511
+ }
512
+ };
513
+ this._cleanupStoryScrollListener = () => {
514
+ if (this._storyScrollHandler) {
515
+ const containerElement = document.querySelector('.jGIS-Mainview-Container');
516
+ if (containerElement) {
517
+ containerElement.removeEventListener('wheel', this._storyScrollHandler);
518
+ }
519
+ this._storyScrollHandler = null;
520
+ }
521
+ };
522
+ this._updateSpectaPresentationColors = () => {
523
+ var _a;
524
+ // Try ref first, fallback to querySelector if ref not available yet
525
+ const container = this.spectaContainerRef.current ||
526
+ ((_a = this.divRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('.jgis-specta-story-panel-container'));
527
+ if (!container) {
528
+ return;
529
+ }
530
+ const story = this._model.getSelectedStory().story;
531
+ const bgColor = story === null || story === void 0 ? void 0 : story.presentaionBgColor;
532
+ const textColor = story === null || story === void 0 ? void 0 : story.presentaionTextColor;
533
+ // Set background color
534
+ if (bgColor) {
535
+ const rgb = hexToRgb(bgColor);
536
+ container.style.setProperty('--jgis-specta-bg-color', `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`);
537
+ }
538
+ // Set text color
539
+ if (textColor) {
540
+ container.style.setProperty('--jgis-specta-text-color', textColor);
541
+ }
542
+ };
408
543
  this._onSharedMetadataChanged = (_, changes) => {
409
544
  const newState = Object.assign({}, this.state.annotations);
410
545
  changes.forEach((val, key) => {
@@ -450,10 +585,15 @@ export class MainView extends React.Component {
450
585
  };
451
586
  this._isPositionInitialized = false;
452
587
  this.divRef = React.createRef(); // Reference of render div
588
+ this.controlsToolbarRef = React.createRef();
589
+ this.spectaContainerRef = React.createRef();
590
+ this.storyViewerPanelRef = React.createRef();
453
591
  this._ready = false;
454
592
  this._sourceToLayerMap = new Map();
455
593
  this._originalFeatures = {};
456
594
  this._featurePropertyCache = new Map();
595
+ this._isSpectaPresentationInitialized = false;
596
+ this._storyScrollHandler = null;
457
597
  this._state = props.state;
458
598
  this._formSchemaRegistry = props.formSchemaRegistry;
459
599
  this._annotationModel = props.annotationModel;
@@ -488,6 +628,7 @@ export class MainView extends React.Component {
488
628
  this._model.sharedModel.changed.connect(this._onSharedModelStateChange);
489
629
  this._model.sharedMetadataChanged.connect(this._onSharedMetadataChanged, this);
490
630
  this._model.zoomToPositionSignal.connect(this._onZoomToPosition, this);
631
+ this._model.settingsChanged.connect(this._onSettingsChanged, this);
491
632
  this._model.updateLayerSignal.connect(this._triggerLayerUpdate, this);
492
633
  this._model.addFeatureAsMsSignal.connect(this._convertFeatureToMs, this);
493
634
  this._model.geolocationChanged.connect(this._handleGeolocationChanged, this);
@@ -512,6 +653,7 @@ export class MainView extends React.Component {
512
653
  loadingErrors: [],
513
654
  displayTemporalController: false,
514
655
  filterStates: {},
656
+ isSpectaPresentation: false,
515
657
  };
516
658
  this._sources = [];
517
659
  this._loadingLayers = new Set();
@@ -529,12 +671,19 @@ export class MainView extends React.Component {
529
671
  : [0, 0];
530
672
  const zoom = options.zoom !== undefined ? options.zoom : 1;
531
673
  await this.generateMap(center, zoom);
532
- this.addContextMenu();
533
674
  this._mainViewModel.initSignal();
534
675
  if (window.jupytergisMaps !== undefined && this._documentPath) {
535
676
  window.jupytergisMaps[this._documentPath] = this._Map;
536
677
  }
537
678
  }
679
+ componentDidUpdate(prevProps, prevState) {
680
+ // Run setup when isSpectaPresentation changes from false/undefined to true
681
+ if (this.state.isSpectaPresentation &&
682
+ !this._isSpectaPresentationInitialized) {
683
+ this._setupSpectaMode();
684
+ this._isSpectaPresentationInitialized = true;
685
+ }
686
+ }
538
687
  componentWillUnmount() {
539
688
  if (window.jupytergisMaps !== undefined && this._documentPath) {
540
689
  delete window.jupytergisMaps[this._documentPath];
@@ -542,20 +691,37 @@ export class MainView extends React.Component {
542
691
  window.removeEventListener('resize', this._handleWindowResize);
543
692
  this._mainViewModel.viewSettingChanged.disconnect(this._onViewChanged, this);
544
693
  this._model.themeChanged.disconnect(this._handleThemeChange, this);
694
+ this._model.settingsChanged.disconnect(this._onSettingsChanged, this);
545
695
  this._model.sharedOptionsChanged.disconnect(this._onSharedOptionsChanged, this);
546
696
  this._model.clientStateChanged.disconnect(this._onClientSharedStateChanged, this);
697
+ // Clean up story scroll listener
698
+ this._cleanupStoryScrollListener();
547
699
  this._mainViewModel.dispose();
548
700
  }
549
701
  async generateMap(center, zoom) {
702
+ const scaleLine = new ScaleLine({
703
+ target: this.controlsToolbarRef.current || undefined,
704
+ });
705
+ const fullScreen = new FullScreen({
706
+ target: this.controlsToolbarRef.current || undefined,
707
+ });
708
+ this._zoomControl = new Zoom({
709
+ target: this.controlsToolbarRef.current || undefined,
710
+ });
711
+ const controls = [scaleLine, fullScreen];
712
+ if (this._model.jgisSettings.zoomButtonsEnabled) {
713
+ controls.push(this._zoomControl);
714
+ }
550
715
  if (this.divRef.current) {
551
716
  this._Map = new OlMap({
552
717
  target: this.divRef.current,
718
+ keyboardEventTarget: document,
553
719
  layers: [],
554
720
  view: new View({
555
721
  center,
556
722
  zoom,
557
723
  }),
558
- controls: [new ScaleLine(), new FullScreen()],
724
+ controls,
559
725
  });
560
726
  // Add map interactions
561
727
  const dragAndDropInteraction = new DragAndDrop({
@@ -997,7 +1163,8 @@ export class MainView extends React.Component {
997
1163
  let layerParameters;
998
1164
  let sourceId;
999
1165
  let source;
1000
- if (layer.type !== 'StacLayer') {
1166
+ // Sourceless layers
1167
+ if (!['StacLayer', 'StorySegmentLayer'].includes(layer.type)) {
1001
1168
  sourceId = (_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.source;
1002
1169
  if (!sourceId) {
1003
1170
  return;
@@ -1099,6 +1266,10 @@ export class MainView extends React.Component {
1099
1266
  this.setState(old => (Object.assign(Object.assign({}, old), { metadata: layerParameters.data.properties })));
1100
1267
  break;
1101
1268
  }
1269
+ case 'StorySegmentLayer': {
1270
+ // Special layer not for this
1271
+ return;
1272
+ }
1102
1273
  }
1103
1274
  // OpenLayers doesn't have name/id field so add it
1104
1275
  newMapLayer.set('id', id);
@@ -1164,7 +1335,6 @@ export class MainView extends React.Component {
1164
1335
  }
1165
1336
  catch (error) {
1166
1337
  if (this.state.loadingErrors.find(item => item.id === id && item.error === error.message)) {
1167
- this._loadingLayers.delete(id);
1168
1338
  return;
1169
1339
  }
1170
1340
  await showErrorMessage(`Error Adding ${layer.name}`, `Failed to add ${layer.name}: ${error.message || 'invalid file path'}`);
@@ -1174,7 +1344,10 @@ export class MainView extends React.Component {
1174
1344
  error: error.message || 'invalid file path',
1175
1345
  index,
1176
1346
  });
1347
+ }
1348
+ finally {
1177
1349
  this._loadingLayers.delete(id);
1350
+ this.setState(old => (Object.assign(Object.assign({}, old), { loadingLayer: false })));
1178
1351
  }
1179
1352
  }
1180
1353
  /**
@@ -1185,7 +1358,7 @@ export class MainView extends React.Component {
1185
1358
  */
1186
1359
  async updateLayer(id, layer, mapLayer, oldLayer) {
1187
1360
  var _a, _b, _c, _d, _e, _f, _g, _h;
1188
- mapLayer.setVisible(layer.visible);
1361
+ layer.type !== 'StorySegmentLayer' && mapLayer.setVisible(layer.visible);
1189
1362
  switch (layer.type) {
1190
1363
  case 'RasterLayer': {
1191
1364
  mapLayer.setOpacity(((_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.opacity) || 1);
@@ -1371,12 +1544,42 @@ export class MainView extends React.Component {
1371
1544
  }
1372
1545
  }
1373
1546
  _onSharedOptionsChanged() {
1547
+ // ! would prefer a model ready signal or something, this feels hacky
1548
+ const enableSpectaPresentation = this._model.isSpectaMode();
1549
+ // Handle initialization based on specta presentation state
1550
+ if (!this._isSpectaPresentationInitialized) {
1551
+ if (enableSpectaPresentation) {
1552
+ // _setupSpectaMode will be called in componentDidUpdate when state changes
1553
+ this.setState(old => (Object.assign(Object.assign({}, old), { isSpectaPresentation: true })));
1554
+ }
1555
+ else {
1556
+ // Add context menu when not in specta mode
1557
+ this.addContextMenu();
1558
+ this._isSpectaPresentationInitialized = true;
1559
+ }
1560
+ }
1374
1561
  if (!this._isPositionInitialized) {
1375
1562
  const options = this._model.getOptions();
1376
1563
  this.updateOptions(options);
1377
1564
  this._isPositionInitialized = true;
1378
1565
  }
1379
1566
  }
1567
+ _onSettingsChanged(sender, key) {
1568
+ if (key !== 'zoomButtonsEnabled' || !this._Map) {
1569
+ return;
1570
+ }
1571
+ const enabled = this._model.jgisSettings.zoomButtonsEnabled;
1572
+ if (!enabled && this._zoomControl) {
1573
+ this._Map.removeControl(this._zoomControl);
1574
+ this._zoomControl = undefined;
1575
+ }
1576
+ if (enabled && !this._zoomControl) {
1577
+ this._zoomControl = new Zoom({
1578
+ target: this.controlsToolbarRef.current || undefined,
1579
+ });
1580
+ this._Map.addControl(this._zoomControl);
1581
+ }
1582
+ }
1380
1583
  async updateOptions(options) {
1381
1584
  const { projection, extent, useExtent, latitude, longitude, zoom, bearing, } = options;
1382
1585
  let view = this._Map.getView();
@@ -1545,8 +1748,9 @@ export class MainView extends React.Component {
1545
1748
  }
1546
1749
  });
1547
1750
  }
1751
+ // TODO this and flyToPosition need a rework
1548
1752
  _onZoomToPosition(_, id) {
1549
- var _a;
1753
+ var _a, _b, _c;
1550
1754
  // Check if the id is an annotation
1551
1755
  const annotation = (_a = this._model.annotationModel) === null || _a === void 0 ? void 0 : _a.getAnnotation(id);
1552
1756
  if (annotation) {
@@ -1557,6 +1761,45 @@ export class MainView extends React.Component {
1557
1761
  let extent;
1558
1762
  const layer = this.getLayer(id);
1559
1763
  const source = layer === null || layer === void 0 ? void 0 : layer.getSource();
1764
+ const jgisLayer = this._model.getLayer(id);
1765
+ /**
1766
+ * Layer may be undefined in two cases:
1767
+ * 1. StorySegmentLayer: These layers don't have an associated OpenLayers layer
1768
+ * 2. StacLayer: When centerOnPosition is called immediately after adding the layer,
1769
+ * the OpenLayers layer hasn't been created yet, so we use the bbox from the
1770
+ * layer model's STAC data directly.
1771
+ */
1772
+ if (!layer) {
1773
+ // Handle StacLayer that hasn't been added to the map yet
1774
+ if ((jgisLayer === null || jgisLayer === void 0 ? void 0 : jgisLayer.type) === 'StacLayer') {
1775
+ const layerParams = jgisLayer.parameters;
1776
+ const stacBbox = (_b = layerParams.data) === null || _b === void 0 ? void 0 : _b.bbox;
1777
+ if (stacBbox && stacBbox.length === 4) {
1778
+ // STAC bbox format: [west, south, east, north] in EPSG:4326
1779
+ const [west, south, east, north] = stacBbox;
1780
+ const bboxExtent = [west, south, east, north];
1781
+ // Convert from EPSG:4326 to view projection
1782
+ const viewProjection = this._Map.getView().getProjection();
1783
+ const transformedExtent = viewProjection.getCode() !== 'EPSG:4326'
1784
+ ? transformExtent(bboxExtent, 'EPSG:4326', viewProjection)
1785
+ : bboxExtent;
1786
+ this._Map.getView().fit(transformedExtent, {
1787
+ size: this._Map.getSize(),
1788
+ duration: 500,
1789
+ padding: [250, 250, 250, 250],
1790
+ });
1791
+ return;
1792
+ }
1793
+ }
1794
+ // Handle StorySegmentLayer
1795
+ if ((jgisLayer === null || jgisLayer === void 0 ? void 0 : jgisLayer.type) === 'StorySegmentLayer') {
1796
+ const layerParams = jgisLayer.parameters;
1797
+ const coords = getCenter(layerParams.extent);
1798
+ this._flyToPosition({ x: coords[0], y: coords[1] }, layerParams.zoom, ((_c = layerParams.transition.time) !== null && _c !== void 0 ? _c : 1) * 1000, // seconds -> ms
1799
+ layerParams.transition.type);
1800
+ return;
1801
+ }
1802
+ }
1560
1803
  if (source instanceof VectorSource) {
1561
1804
  extent = source.getExtent();
1562
1805
  }
@@ -1596,10 +1839,43 @@ export class MainView extends React.Component {
1596
1839
  });
1597
1840
  }
1598
1841
  }
1599
- _flyToPosition(center, zoom, duration = 1000) {
1842
+ _flyToPosition(center, zoom, duration = 1000, transitionType) {
1600
1843
  const view = this._Map.getView();
1601
- view.animate({ zoom, duration });
1602
- view.animate({ center: [center.x, center.y], duration });
1844
+ // Cancel any in-progress animations before starting new ones
1845
+ view.cancelAnimations();
1846
+ const targetCenter = [center.x, center.y];
1847
+ if (transitionType === 'linear') {
1848
+ // Linear: direct zoom
1849
+ view.animate({
1850
+ center: targetCenter,
1851
+ zoom: zoom,
1852
+ duration,
1853
+ });
1854
+ return;
1855
+ }
1856
+ if (transitionType === 'smooth') {
1857
+ // Smooth: zoom out, center, and zoom in
1858
+ // Centering takes full duration, zoom out completes halfway, zoom in starts halfway
1859
+ // 3 shows most of the map
1860
+ const zoomOutLevel = 3;
1861
+ // Start centering (full duration) and zoom out (50% duration) simultaneously
1862
+ view.animate({
1863
+ center: targetCenter,
1864
+ duration: duration,
1865
+ });
1866
+ // Chain zoom out -> zoom in (zoom in starts when zoom out completes)
1867
+ view.animate({
1868
+ zoom: zoomOutLevel,
1869
+ duration: duration * 0.5,
1870
+ }, {
1871
+ zoom: zoom,
1872
+ duration: duration * 0.5,
1873
+ });
1874
+ return;
1875
+ }
1876
+ // Immediate move
1877
+ view.setCenter(targetCenter);
1878
+ view.setZoom(zoom);
1603
1879
  }
1604
1880
  _onPointerMove(e) {
1605
1881
  const pixel = this._Map.getEventPixel(e);
@@ -1775,7 +2051,7 @@ export class MainView extends React.Component {
1775
2051
  }),
1776
2052
  React.createElement("div", { className: "jGIS-Mainview-Container" },
1777
2053
  this.state.displayTemporalController && (React.createElement(TemporalSlider, { model: this._model, filterStates: this.state.filterStates })),
1778
- React.createElement("div", { className: "jGIS-Mainview data-jgis-keybinding", tabIndex: -2, style: {
2054
+ React.createElement("div", { className: "jGIS-Mainview data-jgis-keybinding", tabIndex: 0, style: {
1779
2055
  border: this.state.remoteUser
1780
2056
  ? `solid 3px ${this.state.remoteUser.color}`
1781
2057
  : 'unset',
@@ -1787,9 +2063,12 @@ export class MainView extends React.Component {
1787
2063
  width: '100%',
1788
2064
  height: '100%',
1789
2065
  } },
1790
- React.createElement("div", { className: "jgis-panels-wrapper" },
2066
+ React.createElement("div", { className: "jgis-panels-wrapper" }, !this.state.isSpectaPresentation ? (React.createElement(React.Fragment, null,
1791
2067
  this._state && (React.createElement(LeftPanel, { model: this._model, commands: this._mainViewModel.commands, state: this._state })),
1792
- this._formSchemaRegistry && this._annotationModel && (React.createElement(RightPanel, { model: this._model, formSchemaRegistry: this._formSchemaRegistry, annotationModel: this._annotationModel }))))),
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" },
2069
+ 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 }))))),
2071
+ React.createElement("div", { ref: this.controlsToolbarRef, className: "jgis-controls-toolbar" }))),
1793
2072
  React.createElement(StatusBar, { jgisModel: this._model, loading: this.state.loadingLayer, projection: this.state.viewProjection, scale: this.state.scale }))));
1794
2073
  }
1795
2074
  }
@@ -1,4 +1,4 @@
1
- import { IJupyterGISModel } from '@jupytergis/schema';
1
+ import { IJGISLayerTree, IJupyterGISModel } from '@jupytergis/schema';
2
2
  import { IStateDB } from '@jupyterlab/statedb';
3
3
  import { CommandRegistry } from '@lumino/commands';
4
4
  import React from 'react';
@@ -6,6 +6,7 @@ interface IBodyProps {
6
6
  model: IJupyterGISModel;
7
7
  commands: CommandRegistry;
8
8
  state: IStateDB;
9
+ layerTree: IJGISLayerTree;
9
10
  }
10
11
  export declare const LayersBodyComponent: React.FC<IBodyProps>;
11
12
  export {};
@@ -3,7 +3,7 @@ import { Button, LabIcon, caretDownIcon, caretRightIcon, } from '@jupyterlab/ui-
3
3
  import React, { useEffect, useState, } from 'react';
4
4
  import { CommandIDs, icons } from "../../constants";
5
5
  import { useGetSymbology } from "../../dialogs/symbology/hooks/useGetSymbology";
6
- import { nonVisibilityIcon, visibilityIcon } from "../../icons";
6
+ import { nonVisibilityIcon, targetWithCenterIcon, visibilityIcon, } from "../../icons";
7
7
  import { LegendItem } from './legendItem';
8
8
  const LAYER_GROUP_CLASS = 'jp-gis-layerGroup';
9
9
  const LAYER_GROUP_HEADER_CLASS = 'jp-gis-layerGroupHeader';
@@ -13,9 +13,10 @@ const LAYER_CLASS = 'jp-gis-layer';
13
13
  const LAYER_TITLE_CLASS = 'jp-gis-layerTitle';
14
14
  const LAYER_ICON_CLASS = 'jp-gis-layerIcon';
15
15
  const LAYER_TEXT_CLASS = 'jp-gis-layerText data-jgis-keybinding';
16
+ const LAYER_SLIDE_NUMBER_CLASS = 'jp-gis-layerSlideNumber';
16
17
  export const LayersBodyComponent = props => {
17
18
  const model = props.model;
18
- const [layerTree, setLayerTree] = useState((model === null || model === void 0 ? void 0 : model.getLayerTree()) || []);
19
+ const [layerTree, setLayerTree] = useState(props.layerTree || []);
19
20
  const notifyCommands = () => {
20
21
  // Notify commands that need updating
21
22
  props.commands.notifyCommandChanged(CommandIDs.identify);
@@ -117,25 +118,13 @@ export const LayersBodyComponent = props => {
117
118
  const onItemClick = ({ type, item, event }) => {
118
119
  onSelect({ type, item, event });
119
120
  };
120
- /**
121
- * Listen to the layers and layer tree changes.
122
- */
121
+ // Update layerTree when prop changes
123
122
  useEffect(() => {
124
- const updateLayers = () => {
125
- setLayerTree((model === null || model === void 0 ? void 0 : model.getLayerTree()) || []);
126
- };
127
- model === null || model === void 0 ? void 0 : model.sharedModel.layersChanged.connect(updateLayers);
128
- model === null || model === void 0 ? void 0 : model.sharedModel.layerTreeChanged.connect(updateLayers);
129
- updateLayers();
130
- return () => {
131
- model === null || model === void 0 ? void 0 : model.sharedModel.layersChanged.disconnect(updateLayers);
132
- model === null || model === void 0 ? void 0 : model.sharedModel.layerTreeChanged.disconnect(updateLayers);
133
- };
134
- }, [model]);
135
- return (React.createElement("div", { id: "jp-gis-layer-tree", onDrop: _onDrop, onDragOver: _onDragOver }, layerTree
136
- .slice()
137
- .reverse()
138
- .map(layer => typeof layer === 'string' ? (React.createElement(LayerComponent, { key: layer, gisModel: model, layerId: layer, onClick: onItemClick })) : (React.createElement(LayerGroupComponent, { key: layer.name, gisModel: model, group: layer, onClick: onItemClick, state: props.state })))));
123
+ if (props.layerTree) {
124
+ setLayerTree(props.layerTree);
125
+ }
126
+ }, [props.layerTree]);
127
+ return (React.createElement("div", { id: "jp-gis-layer-tree", onDrop: _onDrop, onDragOver: _onDragOver }, layerTree.map(layer => typeof layer === 'string' ? (React.createElement(LayerComponent, { key: layer, gisModel: model, layerId: layer, onClick: onItemClick })) : (React.createElement(LayerGroupComponent, { key: layer.name, gisModel: model, group: layer, onClick: onItemClick, state: props.state })))));
139
128
  };
140
129
  /**
141
130
  * The component to handle group of layers.
@@ -350,6 +339,23 @@ const LayerComponent = props => {
350
339
  handleRenameCancel();
351
340
  }
352
341
  };
342
+ /**
343
+ * Set layer to current map view.
344
+ */
345
+ const moveToExtent = () => {
346
+ gisModel === null || gisModel === void 0 ? void 0 : gisModel.centerOnPosition(layerId);
347
+ };
348
+ const getSlideNumber = () => {
349
+ if (!gisModel) {
350
+ return;
351
+ }
352
+ const { story } = gisModel.getSelectedStory();
353
+ if (!(story === null || story === void 0 ? void 0 : story.storySegments)) {
354
+ return;
355
+ }
356
+ const slideNum = story.storySegments.indexOf(layerId) + 1;
357
+ return slideNum;
358
+ };
353
359
  return (React.createElement("div", { className: `${LAYER_ITEM_CLASS} ${LAYER_CLASS}${selected ? ' jp-mod-selected' : ''}`, draggable: true, onDragStart: Private.onDragStart, onDragOver: Private.onDragOver, onDragEnd: Private.onDragEnd, "data-id": layerId, style: { display: 'flex', flexDirection: 'column' } },
354
360
  React.createElement("div", { className: LAYER_TITLE_CLASS, onClick: setSelection, onContextMenu: setSelection, style: { display: 'flex' } },
355
361
  hasSupportedSymbology && (React.createElement(Button, { minimal: true, onClick: e => {
@@ -357,8 +363,8 @@ const LayerComponent = props => {
357
363
  setExpanded(v => !v);
358
364
  }, title: expanded ? 'Hide legend' : 'Show legend' },
359
365
  React.createElement(LabIcon.resolveReact, { icon: expanded ? caretDownIcon : caretRightIcon, tag: "span" }))),
360
- React.createElement(Button, { title: layer.visible ? 'Hide layer' : 'Show layer', onClick: toggleVisibility, minimal: true },
361
- React.createElement(LabIcon.resolveReact, { icon: layer.visible ? visibilityIcon : nonVisibilityIcon, className: `${LAYER_ICON_CLASS}${layer.visible ? '' : ' jp-gis-mod-hidden'}`, tag: "span" })),
366
+ layer.type === 'StorySegmentLayer' ? (React.createElement("span", { className: LAYER_SLIDE_NUMBER_CLASS, title: "Slide number" }, getSlideNumber())) : (React.createElement(Button, { title: layer.visible ? 'Hide layer' : 'Show layer', onClick: toggleVisibility, minimal: true },
367
+ React.createElement(LabIcon.resolveReact, { icon: layer.visible ? visibilityIcon : nonVisibilityIcon, className: `${LAYER_ICON_CLASS}${layer.visible ? '' : ' jp-gis-mod-hidden'}`, tag: "span" }))),
362
368
  icons.has(layer.type) && (React.createElement(LabIcon.resolveReact, Object.assign({}, icons.get(layer.type), { className: LAYER_ICON_CLASS }))),
363
369
  isEditing ? (React.createElement("input", { type: "text", value: editValue, onChange: e => setEditValue(e.target.value), onKeyDown: handleRenameKeyDown, onBlur: handleRenameSave, className: LAYER_TEXT_CLASS, style: {
364
370
  flex: 1,
@@ -367,7 +373,9 @@ const LayerComponent = props => {
367
373
  padding: '2px 4px',
368
374
  fontSize: 'inherit',
369
375
  fontFamily: 'inherit',
370
- }, autoFocus: true })) : (React.createElement("span", { id: id, className: LAYER_TEXT_CLASS, tabIndex: -2 }, name))),
376
+ }, autoFocus: true })) : (React.createElement("span", { id: id, className: LAYER_TEXT_CLASS, tabIndex: -2 }, name)),
377
+ React.createElement(Button, { title: 'Move map to the extent of the layer', onClick: moveToExtent, minimal: true },
378
+ React.createElement(LabIcon.resolveReact, { icon: targetWithCenterIcon, className: LAYER_ICON_CLASS, tag: "span" }))),
371
379
  expanded && gisModel && hasSupportedSymbology && (React.createElement("div", { style: { marginTop: 6, width: '100%' } },
372
380
  React.createElement(LegendItem, { layerId: layerId, model: gisModel })))));
373
381
  };
@@ -1,7 +1,7 @@
1
1
  import { Button } from '@jupyterlab/ui-components';
2
2
  import { cloneDeep } from 'lodash';
3
3
  import React, { useEffect, useRef, useState } from 'react';
4
- import { debounce, loadFile } from "../../../tools";
4
+ import { debounce, loadFile } from "../../tools";
5
5
  import FilterRow from './FilterRow';
6
6
  const FilterComponent = ({ model }) => {
7
7
  const featuresInLayerRef = useRef({});