@jupytergis/base 0.13.1 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/BaseCommandIDs.d.ts +2 -4
- package/lib/commands/BaseCommandIDs.js +2 -4
- package/lib/commands/index.js +63 -62
- package/lib/constants.js +2 -0
- package/lib/dialogs/symbology/components/color_ramp/ColorRampSelector.js +1 -1
- package/lib/keybindings.json +4 -14
- package/lib/mainview/mainView.d.ts +2 -0
- package/lib/mainview/mainView.js +58 -54
- package/lib/panelview/components/layers.js +6 -1
- package/lib/panelview/leftpanel.js +20 -18
- package/lib/panelview/rightpanel.js +21 -19
- package/lib/panelview/story-maps/StoryViewerPanel.d.ts +9 -1
- package/lib/panelview/story-maps/StoryViewerPanel.js +54 -5
- package/lib/widget.js +14 -2
- package/package.json +2 -2
- package/style/shared/tabs.css +6 -0
|
@@ -16,10 +16,8 @@ export declare const newImageEntry = "jupytergis:newImageEntry";
|
|
|
16
16
|
export declare const newVideoEntry = "jupytergis:newVideoEntry";
|
|
17
17
|
export declare const newGeoTiffEntry = "jupytergis:newGeoTiffEntry";
|
|
18
18
|
export declare const newGeoParquetEntry = "jupytergis:newGeoParquetEntry";
|
|
19
|
-
export declare const
|
|
20
|
-
export declare const
|
|
21
|
-
export declare const renameGroup = "jupytergis:renameGroup";
|
|
22
|
-
export declare const removeGroup = "jupytergis:removeGroup";
|
|
19
|
+
export declare const renameSelected = "jupytergis:renameSelected";
|
|
20
|
+
export declare const removeSelected = "jupytergis:removeSelected";
|
|
23
21
|
export declare const moveLayersToGroup = "jupytergis:moveLayersToGroup";
|
|
24
22
|
export declare const moveLayerToNewGroup = "jupytergis:moveLayerToNewGroup";
|
|
25
23
|
export declare const renameSource = "jupytergis:renameSource";
|
|
@@ -25,10 +25,8 @@ export const newVideoEntry = 'jupytergis:newVideoEntry';
|
|
|
25
25
|
export const newGeoTiffEntry = 'jupytergis:newGeoTiffEntry';
|
|
26
26
|
export const newGeoParquetEntry = 'jupytergis:newGeoParquetEntry';
|
|
27
27
|
// Layer and group actions
|
|
28
|
-
export const
|
|
29
|
-
export const
|
|
30
|
-
export const renameGroup = 'jupytergis:renameGroup';
|
|
31
|
-
export const removeGroup = 'jupytergis:removeGroup';
|
|
28
|
+
export const renameSelected = 'jupytergis:renameSelected';
|
|
29
|
+
export const removeSelected = 'jupytergis:removeSelected';
|
|
32
30
|
export const moveLayersToGroup = 'jupytergis:moveLayersToGroup';
|
|
33
31
|
export const moveLayerToNewGroup = 'jupytergis:moveLayerToNewGroup';
|
|
34
32
|
// Source actions
|
package/lib/commands/index.js
CHANGED
|
@@ -361,43 +361,34 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
|
|
|
361
361
|
/**
|
|
362
362
|
* LAYERS and LAYER GROUP actions.
|
|
363
363
|
*/
|
|
364
|
-
commands.addCommand(CommandIDs.
|
|
365
|
-
|
|
366
|
-
execute: async () => {
|
|
367
|
-
var _a;
|
|
364
|
+
commands.addCommand(CommandIDs.renameSelected, Object.assign({ label: trans.__('Rename'), isEnabled: () => {
|
|
365
|
+
var _a, _b, _c;
|
|
368
366
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
label: trans.__('Remove Layer'),
|
|
374
|
-
execute: () => {
|
|
375
|
-
var _a;
|
|
367
|
+
const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
|
|
368
|
+
return !!selected && Object.keys(selected).length === 1;
|
|
369
|
+
}, execute: async () => {
|
|
370
|
+
var _a, _b, _c;
|
|
376
371
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
commands.addCommand(CommandIDs.
|
|
384
|
-
|
|
385
|
-
execute: async () => {
|
|
386
|
-
var _a;
|
|
372
|
+
const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
|
|
373
|
+
if (!model || !selected) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
await Private.renameSelectedItem(model);
|
|
377
|
+
} }, icons.get(CommandIDs.renameSelected)));
|
|
378
|
+
commands.addCommand(CommandIDs.removeSelected, Object.assign({ label: trans.__('Remove'), isEnabled: () => {
|
|
379
|
+
var _a, _b, _c;
|
|
387
380
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
label: trans.__('Remove Group'),
|
|
393
|
-
execute: async () => {
|
|
394
|
-
var _a;
|
|
381
|
+
const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
|
|
382
|
+
return !!selected && Object.keys(selected).length > 0;
|
|
383
|
+
}, execute: async () => {
|
|
384
|
+
var _a, _b, _c;
|
|
395
385
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
386
|
+
const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
|
|
387
|
+
if (!model || !selected) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
await Private.removeSelectedItems(model);
|
|
391
|
+
} }, icons.get(CommandIDs.removeSelected)));
|
|
401
392
|
commands.addCommand(CommandIDs.moveLayersToGroup, {
|
|
402
393
|
label: args => args['label'] ? args['label'] : trans.__('Move to Root'),
|
|
403
394
|
execute: args => {
|
|
@@ -473,7 +464,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
|
|
|
473
464
|
execute: async () => {
|
|
474
465
|
var _a;
|
|
475
466
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
476
|
-
await Private.renameSelectedItem(model
|
|
467
|
+
await Private.renameSelectedItem(model);
|
|
477
468
|
},
|
|
478
469
|
});
|
|
479
470
|
commands.addCommand(CommandIDs.removeSource, {
|
|
@@ -481,15 +472,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
|
|
|
481
472
|
execute: () => {
|
|
482
473
|
var _a;
|
|
483
474
|
const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
|
|
484
|
-
Private.
|
|
485
|
-
var _a;
|
|
486
|
-
if (!((_a = model === null || model === void 0 ? void 0 : model.getLayersBySource(selection).length) !== null && _a !== void 0 ? _a : true)) {
|
|
487
|
-
model === null || model === void 0 ? void 0 : model.sharedModel.removeSource(selection);
|
|
488
|
-
}
|
|
489
|
-
else {
|
|
490
|
-
showErrorMessage('Remove source error', 'The source is used by a layer.');
|
|
491
|
-
}
|
|
492
|
-
});
|
|
475
|
+
Private.removeSelectedSources(model);
|
|
493
476
|
},
|
|
494
477
|
});
|
|
495
478
|
// Console commands
|
|
@@ -934,42 +917,60 @@ var Private;
|
|
|
934
917
|
};
|
|
935
918
|
}
|
|
936
919
|
Private.createEntry = createEntry;
|
|
937
|
-
function removeSelectedItems(model
|
|
920
|
+
function removeSelectedItems(model) {
|
|
938
921
|
var _a, _b;
|
|
939
922
|
const selected = (_b = (_a = model === null || model === void 0 ? void 0 : model.localState) === null || _a === void 0 ? void 0 : _a.selected) === null || _b === void 0 ? void 0 : _b.value;
|
|
940
|
-
if (!selected) {
|
|
923
|
+
if (!selected || !model) {
|
|
941
924
|
console.error('Failed to remove selected item -- nothing selected');
|
|
942
925
|
return;
|
|
943
926
|
}
|
|
944
|
-
for (const
|
|
945
|
-
|
|
946
|
-
|
|
927
|
+
for (const id of Object.keys(selected)) {
|
|
928
|
+
const item = selected[id];
|
|
929
|
+
switch (item.type) {
|
|
930
|
+
case 'layer':
|
|
931
|
+
model.removeLayer(id);
|
|
932
|
+
break;
|
|
933
|
+
case 'group':
|
|
934
|
+
model.removeLayerGroup(id);
|
|
935
|
+
break;
|
|
947
936
|
}
|
|
948
937
|
}
|
|
949
938
|
}
|
|
950
939
|
Private.removeSelectedItems = removeSelectedItems;
|
|
951
|
-
async function renameSelectedItem(model
|
|
952
|
-
var _a;
|
|
953
|
-
const selectedItems = (_a = model === null || model === void 0 ? void 0 : model.localState) === null || _a === void 0 ? void 0 : _a.selected.value;
|
|
940
|
+
async function renameSelectedItem(model) {
|
|
941
|
+
var _a, _b;
|
|
942
|
+
const selectedItems = (_b = (_a = model === null || model === void 0 ? void 0 : model.localState) === null || _a === void 0 ? void 0 : _a.selected) === null || _b === void 0 ? void 0 : _b.value;
|
|
954
943
|
if (!selectedItems || !model) {
|
|
955
|
-
console.error(
|
|
944
|
+
console.error('No item selected');
|
|
956
945
|
return;
|
|
957
946
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
if (selectedItems[id].type === itemType) {
|
|
962
|
-
itemId = id;
|
|
963
|
-
break;
|
|
964
|
-
}
|
|
947
|
+
const ids = Object.keys(selectedItems);
|
|
948
|
+
if (ids.length === 0) {
|
|
949
|
+
return;
|
|
965
950
|
}
|
|
966
|
-
|
|
951
|
+
const itemId = ids[0];
|
|
952
|
+
const item = selectedItems[itemId];
|
|
953
|
+
if (!item.type) {
|
|
967
954
|
return;
|
|
968
955
|
}
|
|
969
|
-
|
|
970
|
-
model.setEditingItem(itemType, itemId);
|
|
956
|
+
model.setEditingItem(item.type, itemId);
|
|
971
957
|
}
|
|
972
958
|
Private.renameSelectedItem = renameSelectedItem;
|
|
959
|
+
function removeSelectedSources(model) {
|
|
960
|
+
var _a, _b;
|
|
961
|
+
const selected = (_b = (_a = model === null || model === void 0 ? void 0 : model.localState) === null || _a === void 0 ? void 0 : _a.selected) === null || _b === void 0 ? void 0 : _b.value;
|
|
962
|
+
if (!selected || !model) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
for (const id of Object.keys(selected)) {
|
|
966
|
+
if (model.getLayersBySource(id).length > 0) {
|
|
967
|
+
showErrorMessage('Remove source error', 'The source is used by a layer.');
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
model.sharedModel.removeSource(id);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
Private.removeSelectedSources = removeSelectedSources;
|
|
973
974
|
function executeConsole(tracker) {
|
|
974
975
|
const current = tracker.currentWidget;
|
|
975
976
|
if (!current || !(current instanceof JupyterGISDocumentWidget)) {
|
package/lib/constants.js
CHANGED
|
@@ -39,6 +39,8 @@ const iconObject = {
|
|
|
39
39
|
[CommandIDs.toggleStoryPresentationMode]: {
|
|
40
40
|
iconClass: 'fa fa-book jgis-icon-adjust',
|
|
41
41
|
},
|
|
42
|
+
[CommandIDs.renameSelected]: { iconClass: 'fa fa-pen' },
|
|
43
|
+
[CommandIDs.removeSelected]: { iconClass: 'fa fa-trash' },
|
|
42
44
|
};
|
|
43
45
|
/**
|
|
44
46
|
* The registered icons
|
package/lib/keybindings.json
CHANGED
|
@@ -30,24 +30,14 @@
|
|
|
30
30
|
"selector": ".data-jgis-keybinding .jp-gis-source"
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
|
-
"command": "jupytergis:
|
|
33
|
+
"command": "jupytergis:removeSelected",
|
|
34
34
|
"keys": ["Delete"],
|
|
35
|
-
"selector": ".data-jgis-keybinding
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"command": "jupytergis:renameLayer",
|
|
39
|
-
"keys": ["F2"],
|
|
40
|
-
"selector": ".jp-gis-layerItem"
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"command": "jupytergis:removeGroup",
|
|
44
|
-
"keys": ["Delete"],
|
|
45
|
-
"selector": ".data-jgis-keybinding .jp-gis-layerGroupHeader"
|
|
35
|
+
"selector": ".data-jgis-keybinding"
|
|
46
36
|
},
|
|
47
37
|
{
|
|
48
|
-
"command": "jupytergis:
|
|
38
|
+
"command": "jupytergis:renameSelected",
|
|
49
39
|
"keys": ["F2"],
|
|
50
|
-
"selector": ".
|
|
40
|
+
"selector": ".data-jgis-keybinding"
|
|
51
41
|
},
|
|
52
42
|
{
|
|
53
43
|
"command": "jupytergis:executeConsole",
|
|
@@ -225,6 +225,8 @@ export declare class MainView extends React.Component<IProps, IStates> {
|
|
|
225
225
|
private _featurePropertyCache;
|
|
226
226
|
private _isSpectaPresentationInitialized;
|
|
227
227
|
private _storyScrollHandler;
|
|
228
|
+
private _clearStoryScrollGuard;
|
|
229
|
+
private _pendingStoryScrollRafId;
|
|
228
230
|
}
|
|
229
231
|
/** Thin wrapper that injects isMobile from useMediaQuery so MainView can use it in JSX. */
|
|
230
232
|
declare function MainViewWithMediaQuery(props: IProps): React.JSX.Element;
|
package/lib/mainview/mainView.js
CHANGED
|
@@ -447,72 +447,75 @@ export class MainView extends React.Component {
|
|
|
447
447
|
});
|
|
448
448
|
};
|
|
449
449
|
this._setupStoryScrollListener = () => {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const handleScroll = (e) => {
|
|
450
|
+
var _a, _b;
|
|
451
|
+
// Guard: block wheel-driven segment change until transition has ended
|
|
452
|
+
let segmentChangeInProgress = false;
|
|
453
|
+
const clearGuard = () => {
|
|
454
|
+
segmentChangeInProgress = false;
|
|
455
|
+
};
|
|
456
|
+
this._clearStoryScrollGuard = clearGuard;
|
|
457
|
+
let accumulatedDeltaY = 0;
|
|
458
|
+
let scrollContainer = (_b = (_a = this.storyViewerPanelRef.current) === null || _a === void 0 ? void 0 : _a.getScrollContainer()) !== null && _b !== void 0 ? _b : null;
|
|
459
|
+
const processStoryScrollFrame = () => {
|
|
460
|
+
this._pendingStoryScrollRafId = null;
|
|
462
461
|
const currentPanelHandle = this.storyViewerPanelRef.current;
|
|
463
|
-
if (!currentPanelHandle || !
|
|
462
|
+
if (!currentPanelHandle || !scrollContainer) {
|
|
463
|
+
accumulatedDeltaY = 0;
|
|
464
464
|
return;
|
|
465
465
|
}
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
466
|
+
const deltaY = accumulatedDeltaY;
|
|
467
|
+
accumulatedDeltaY = 0;
|
|
468
|
+
const isScrollingUp = deltaY < 0;
|
|
469
|
+
const isScrollingDown = deltaY > 0;
|
|
470
|
+
const isAtTop = currentPanelHandle.getAtTop();
|
|
471
|
+
const isAtBottom = currentPanelHandle.getAtBottom();
|
|
472
|
+
const hasOverflow = !(isAtTop && isAtBottom);
|
|
473
|
+
const canGoInDirection = (isScrollingDown && currentPanelHandle.hasNext) ||
|
|
474
|
+
(isScrollingUp && currentPanelHandle.hasPrev);
|
|
475
|
+
const atEdge = (isScrollingDown && isAtBottom) || (isScrollingUp && isAtTop);
|
|
476
|
+
const wantSegmentChange = canGoInDirection && (!hasOverflow || atEdge);
|
|
477
|
+
if (wantSegmentChange) {
|
|
478
|
+
if (segmentChangeInProgress) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
segmentChangeInProgress = true;
|
|
482
|
+
isScrollingDown
|
|
483
|
+
? currentPanelHandle.handleNext()
|
|
484
|
+
: currentPanelHandle.handlePrev();
|
|
474
485
|
return;
|
|
475
486
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
487
|
+
scrollContainer.scrollBy({ top: deltaY });
|
|
488
|
+
};
|
|
489
|
+
const handleScroll = (event) => {
|
|
490
|
+
var _a, _b;
|
|
491
|
+
event.preventDefault();
|
|
492
|
+
if (!scrollContainer || !document.contains(scrollContainer)) {
|
|
493
|
+
scrollContainer =
|
|
494
|
+
(_b = (_a = this.storyViewerPanelRef.current) === null || _a === void 0 ? void 0 : _a.getScrollContainer()) !== null && _b !== void 0 ? _b : null;
|
|
482
495
|
}
|
|
483
|
-
|
|
484
|
-
const scrollTop = storyViewerPanel.scrollTop;
|
|
485
|
-
const scrollHeight = storyViewerPanel.scrollHeight;
|
|
486
|
-
const clientHeight = storyViewerPanel.clientHeight;
|
|
487
|
-
const isAtBottom = scrollTop + clientHeight >= scrollHeight - SCROLL_EDGE_THRESHOLD;
|
|
488
|
-
const isAtTop = scrollTop <= SCROLL_EDGE_THRESHOLD;
|
|
489
|
-
const isScrollingDown = wheelEvent.deltaY > 0;
|
|
490
|
-
const isScrollingUp = wheelEvent.deltaY < 0;
|
|
491
|
-
// At edges: change segments
|
|
492
|
-
if ((isScrollingDown && isAtBottom) || (isScrollingUp && isAtTop)) {
|
|
493
|
-
wheelEvent.preventDefault();
|
|
494
|
-
isScrollingDown ? throttledHandleNext() : throttledHandlePrev();
|
|
496
|
+
if (!scrollContainer) {
|
|
495
497
|
return;
|
|
496
498
|
}
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
499
|
+
// One physical scroll tick often fires ~4 wheel events (sometimes across
|
|
500
|
+
// frames on slow hardware). We accumulate deltaY and run flush once per
|
|
501
|
+
// frame via rAF—the frame boundary batches events without adding delay.
|
|
502
|
+
// So one scroll means one segment/scroll decision.
|
|
503
|
+
accumulatedDeltaY += event.deltaY;
|
|
504
|
+
if (this._pendingStoryScrollRafId === null) {
|
|
505
|
+
this._pendingStoryScrollRafId = requestAnimationFrame(processStoryScrollFrame);
|
|
500
506
|
}
|
|
501
|
-
// Scrolling outside the panel: forward scroll to panel (no throttling for smooth scrolling)
|
|
502
|
-
wheelEvent.preventDefault();
|
|
503
|
-
const newScrollTop = Math.max(0, Math.min(scrollHeight - clientHeight, scrollTop + wheelEvent.deltaY));
|
|
504
|
-
storyViewerPanel.scrollTop = newScrollTop;
|
|
505
507
|
};
|
|
506
508
|
this._storyScrollHandler = handleScroll;
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
containerElement.addEventListener('wheel', handleScroll, {
|
|
511
|
-
passive: false,
|
|
512
|
-
});
|
|
509
|
+
const container = document.querySelector('.jGIS-Mainview-Container');
|
|
510
|
+
if (container) {
|
|
511
|
+
container.addEventListener('wheel', handleScroll, { passive: false });
|
|
513
512
|
}
|
|
514
513
|
};
|
|
515
514
|
this._cleanupStoryScrollListener = () => {
|
|
515
|
+
if (this._pendingStoryScrollRafId !== null) {
|
|
516
|
+
cancelAnimationFrame(this._pendingStoryScrollRafId);
|
|
517
|
+
this._pendingStoryScrollRafId = null;
|
|
518
|
+
}
|
|
516
519
|
if (this._storyScrollHandler) {
|
|
517
520
|
const containerElement = document.querySelector('.jGIS-Mainview-Container');
|
|
518
521
|
if (containerElement) {
|
|
@@ -596,6 +599,7 @@ export class MainView extends React.Component {
|
|
|
596
599
|
this._featurePropertyCache = new Map();
|
|
597
600
|
this._isSpectaPresentationInitialized = false;
|
|
598
601
|
this._storyScrollHandler = null;
|
|
602
|
+
this._pendingStoryScrollRafId = null;
|
|
599
603
|
this._state = props.state;
|
|
600
604
|
this._formSchemaRegistry = props.formSchemaRegistry;
|
|
601
605
|
this._annotationModel = props.annotationModel;
|
|
@@ -659,7 +663,7 @@ export class MainView extends React.Component {
|
|
|
659
663
|
displayTemporalController: false,
|
|
660
664
|
filterStates: {},
|
|
661
665
|
jgisSettings: this._model.jgisSettings,
|
|
662
|
-
isSpectaPresentation:
|
|
666
|
+
isSpectaPresentation: this._model.isSpectaMode(),
|
|
663
667
|
};
|
|
664
668
|
this._sources = [];
|
|
665
669
|
this._loadingLayers = new Set();
|
|
@@ -2094,7 +2098,7 @@ export class MainView extends React.Component {
|
|
|
2094
2098
|
this._state && (React.createElement(LeftPanel, { model: this._model, commands: this._mainViewModel.commands, state: this._state, settings: this.state.jgisSettings })),
|
|
2095
2099
|
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" },
|
|
2096
2100
|
React.createElement("div", { ref: this.spectaContainerRef, className: "jgis-specta-story-panel-container" },
|
|
2097
|
-
React.createElement(StoryViewerPanel, { ref: this.storyViewerPanelRef, model: this._model, isSpecta: this.state.isSpectaPresentation, className: "jgis-story-viewer-panel-specta-mod" }))))),
|
|
2101
|
+
React.createElement(StoryViewerPanel, { ref: this.storyViewerPanelRef, model: this._model, isSpecta: this.state.isSpectaPresentation, className: "jgis-story-viewer-panel-specta-mod", onSegmentTransitionEnd: () => this._clearStoryScrollGuard() }))))),
|
|
2098
2102
|
React.createElement("div", { ref: this.controlsToolbarRef, className: "jgis-controls-toolbar" }))),
|
|
2099
2103
|
!this.state.isSpectaPresentation && (React.createElement(StatusBar, { jgisModel: this._model, loading: this.state.loadingLayer, projection: this.state.viewProjection, scale: this.state.scale })))));
|
|
2100
2104
|
}
|
|
@@ -345,6 +345,11 @@ const LayerComponent = props => {
|
|
|
345
345
|
const moveToExtent = () => {
|
|
346
346
|
gisModel === null || gisModel === void 0 ? void 0 : gisModel.centerOnPosition(layerId);
|
|
347
347
|
};
|
|
348
|
+
const handleDoubleClick = (e) => {
|
|
349
|
+
e.preventDefault();
|
|
350
|
+
e.stopPropagation();
|
|
351
|
+
moveToExtent();
|
|
352
|
+
};
|
|
348
353
|
const getSlideNumber = () => {
|
|
349
354
|
if (!gisModel) {
|
|
350
355
|
return;
|
|
@@ -373,7 +378,7 @@ const LayerComponent = props => {
|
|
|
373
378
|
padding: '2px 4px',
|
|
374
379
|
fontSize: 'inherit',
|
|
375
380
|
fontFamily: 'inherit',
|
|
376
|
-
}, autoFocus: true })) : (React.createElement("span", { id: id, className: LAYER_TEXT_CLASS, tabIndex: -2 }, name)),
|
|
381
|
+
}, autoFocus: true })) : (React.createElement("span", { id: id, className: LAYER_TEXT_CLASS, tabIndex: -2, onDoubleClick: handleDoubleClick, title: "Double-click to zoom to layer" }, name)),
|
|
377
382
|
React.createElement(Button, { title: 'Move map to the extent of the layer', onClick: moveToExtent, minimal: true },
|
|
378
383
|
React.createElement(LabIcon.resolveReact, { icon: targetWithCenterIcon, className: LAYER_ICON_CLASS, tag: "span" }))),
|
|
379
384
|
expanded && gisModel && hasSupportedSymbology && (React.createElement("div", { style: { marginTop: 6, width: '100%' } },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import Draggable from 'react-draggable';
|
|
2
3
|
import { CommandIDs } from '../constants';
|
|
3
4
|
import { LayersBodyComponent } from './components/layers';
|
|
4
5
|
import FilterComponent from './filter-panel/Filter';
|
|
@@ -123,22 +124,23 @@ export const LeftPanel = (props) => {
|
|
|
123
124
|
props.settings.filtersDisabled &&
|
|
124
125
|
props.settings.storyMapsDisabled;
|
|
125
126
|
const leftPanelVisible = !props.settings.leftPanelDisabled && !allLeftTabsDisabled;
|
|
126
|
-
return (React.createElement(
|
|
127
|
-
React.createElement(
|
|
128
|
-
React.createElement(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
React.createElement(
|
|
138
|
-
|
|
139
|
-
React.createElement(
|
|
140
|
-
|
|
141
|
-
React.createElement(
|
|
142
|
-
|
|
143
|
-
React.createElement(
|
|
127
|
+
return (React.createElement(Draggable, { handle: ".jgis-tabs-list", cancel: ".jgis-tabs-trigger", bounds: ".jGIS-Mainview-Container" },
|
|
128
|
+
React.createElement("div", { className: "jgis-left-panel-container", style: { display: leftPanelVisible ? 'block' : 'none' } },
|
|
129
|
+
React.createElement(PanelTabs, { curTab: curTab, className: "jgis-panel-tabs" },
|
|
130
|
+
React.createElement(TabsList, null, tabInfo.map(tab => (React.createElement(TabsTrigger, { className: "jGIS-layer-browser-category", key: tab.name, value: tab.name, onClick: () => {
|
|
131
|
+
if (curTab !== tab.name) {
|
|
132
|
+
setCurTab(tab.name);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
setCurTab('');
|
|
136
|
+
}
|
|
137
|
+
} }, tab.title)))),
|
|
138
|
+
!props.settings.layersDisabled && (React.createElement(TabsContent, { value: "layers", className: "jgis-panel-tab-content jp-gis-layerPanel" },
|
|
139
|
+
React.createElement(LayersBodyComponent, { model: props.model, commands: props.commands, state: props.state, layerTree: filteredLayerTree }))),
|
|
140
|
+
!props.settings.stacBrowserDisabled && (React.createElement(TabsContent, { value: "stac", className: "jgis-panel-tab-content jgis-panel-tab-content-stac-panel" },
|
|
141
|
+
React.createElement(StacPanel, { model: props.model }))),
|
|
142
|
+
!props.settings.filtersDisabled && (React.createElement(TabsContent, { value: "filters", className: "jgis-panel-tab-content" },
|
|
143
|
+
React.createElement(FilterComponent, { model: props.model }))),
|
|
144
|
+
!props.settings.storyMapsDisabled && (React.createElement(TabsContent, { value: "segments", className: "jgis-panel-tab-content" },
|
|
145
|
+
React.createElement(LayersBodyComponent, { model: props.model, commands: props.commands, state: props.state, layerTree: storySegmentLayerTree })))))));
|
|
144
146
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import Draggable from 'react-draggable';
|
|
2
3
|
import { AnnotationsPanel } from './annotationPanel';
|
|
3
4
|
import { IdentifyPanelComponent } from './identify-panel/IdentifyPanel';
|
|
4
5
|
import { ObjectPropertiesReact } from './objectproperties';
|
|
@@ -74,23 +75,24 @@ export const RightPanel = props => {
|
|
|
74
75
|
const toggleEditor = () => {
|
|
75
76
|
setEditorMode(!editorMode);
|
|
76
77
|
};
|
|
77
|
-
return (React.createElement(
|
|
78
|
-
React.createElement(
|
|
79
|
-
React.createElement(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
React.createElement(
|
|
89
|
-
|
|
90
|
-
!
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
React.createElement(
|
|
94
|
-
|
|
95
|
-
React.createElement(
|
|
78
|
+
return (React.createElement(Draggable, { handle: ".jgis-tabs-list", cancel: ".jgis-tabs-trigger", bounds: ".jGIS-Mainview-Container" },
|
|
79
|
+
React.createElement("div", { className: "jgis-right-panel-container", style: { display: rightPanelVisible ? 'block' : 'none' } },
|
|
80
|
+
React.createElement(PanelTabs, { className: "jgis-panel-tabs", curTab: curTab },
|
|
81
|
+
React.createElement(TabsList, null, tabInfo.map(tab => (React.createElement(TabsTrigger, { className: "jGIS-layer-browser-category", key: `${tab.name}-${tab.title}`, value: tab.name, onClick: () => {
|
|
82
|
+
if (curTab !== tab.name) {
|
|
83
|
+
setCurTab(tab.name);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
setCurTab('');
|
|
87
|
+
}
|
|
88
|
+
} }, tab.title)))),
|
|
89
|
+
!props.settings.objectPropertiesDisabled && (React.createElement(TabsContent, { value: "objectProperties", className: "jgis-panel-tab-content" },
|
|
90
|
+
React.createElement(ObjectPropertiesReact, { setSelectedObject: setSelectedObjectProperties, selectedObject: selectedObjectProperties, formSchemaRegistry: props.formSchemaRegistry, model: props.model }))),
|
|
91
|
+
!props.settings.storyMapsDisabled && (React.createElement(TabsContent, { value: "storyPanel", className: "jgis-panel-tab-content", style: { paddingTop: 0 } },
|
|
92
|
+
!storyMapPresentationMode && (React.createElement(PreviewModeSwitch, { checked: !editorMode, onCheckedChange: toggleEditor })),
|
|
93
|
+
showEditor ? (React.createElement(StoryEditorPanel, { model: props.model, commands: props.commands })) : (React.createElement(StoryViewerPanel, { model: props.model, isSpecta: false, addLayer: props.addLayer, removeLayer: props.removeLayer })))),
|
|
94
|
+
!props.settings.annotationsDisabled && (React.createElement(TabsContent, { value: "annotations", className: "jgis-panel-tab-content" },
|
|
95
|
+
React.createElement(AnnotationsPanel, { annotationModel: props.annotationModel, jgisModel: props.model }))),
|
|
96
|
+
!props.settings.identifyDisabled && (React.createElement(TabsContent, { value: "identifyPanel", className: "jgis-panel-tab-content" },
|
|
97
|
+
React.createElement(IdentifyPanelComponent, { model: props.model })))))));
|
|
96
98
|
};
|
|
@@ -7,11 +7,19 @@ interface IStoryViewerPanelProps {
|
|
|
7
7
|
className?: string;
|
|
8
8
|
addLayer?: (id: string, layer: IJGISLayer, index: number) => Promise<void>;
|
|
9
9
|
removeLayer?: (id: string) => void;
|
|
10
|
+
/** Called when the segment transition animation has finished (e.g. for scroll-guard cleanup). */
|
|
11
|
+
onSegmentTransitionEnd?: () => void;
|
|
10
12
|
}
|
|
11
13
|
export interface IStoryViewerPanelHandle {
|
|
12
14
|
handlePrev: () => void;
|
|
13
15
|
handleNext: () => void;
|
|
14
|
-
|
|
16
|
+
spectaMode: boolean;
|
|
17
|
+
hasPrev: boolean;
|
|
18
|
+
hasNext: boolean;
|
|
19
|
+
getAtTop: () => boolean;
|
|
20
|
+
getAtBottom: () => boolean;
|
|
21
|
+
/** The scrollable panel DOM element (same instance for all segments). */
|
|
22
|
+
getScrollContainer: () => HTMLDivElement | null;
|
|
15
23
|
}
|
|
16
24
|
/**
|
|
17
25
|
* Where the story nav bar should be rendered in the viewer layout.
|
|
@@ -18,12 +18,17 @@ function getStoryNavPlacement(isSpecta, hasImage, storyType, isMobile) {
|
|
|
18
18
|
}
|
|
19
19
|
return hasImage ? 'over-image' : 'below-title';
|
|
20
20
|
}
|
|
21
|
-
const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, className, addLayer, removeLayer }, ref) => {
|
|
21
|
+
const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, className, addLayer, removeLayer, onSegmentTransitionEnd, }, ref) => {
|
|
22
22
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
23
23
|
const [currentIndexDisplayed, setCurrentIndexDisplayed] = useState(() => model.getCurrentSegmentIndex());
|
|
24
24
|
const [storyData, setStoryData] = useState((_a = model.getSelectedStory().story) !== null && _a !== void 0 ? _a : null);
|
|
25
25
|
const [imageLoaded, setImageLoaded] = useState(false);
|
|
26
26
|
const panelRef = useRef(null);
|
|
27
|
+
const segmentContainerRef = useRef(null);
|
|
28
|
+
const topSentinelRef = useRef(null);
|
|
29
|
+
const bottomSentinelRef = useRef(null);
|
|
30
|
+
const atTopRef = useRef(false);
|
|
31
|
+
const atBottomRef = useRef(false);
|
|
27
32
|
const setIndex = useCallback((index) => {
|
|
28
33
|
model.setCurrentSegmentIndex(index);
|
|
29
34
|
setCurrentIndexDisplayed(index);
|
|
@@ -283,20 +288,63 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
|
|
|
283
288
|
hasPrev,
|
|
284
289
|
hasNext,
|
|
285
290
|
};
|
|
291
|
+
// IntersectionObserver for at-top/at-bottom (avoids layout reads in scroll path)
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
const root = panelRef.current;
|
|
294
|
+
const topEl = topSentinelRef.current;
|
|
295
|
+
const bottomEl = bottomSentinelRef.current;
|
|
296
|
+
if (!root || !topEl || !bottomEl) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const observer = new IntersectionObserver((entries) => {
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
if (entry.target === topEl) {
|
|
302
|
+
atTopRef.current = entry.isIntersecting;
|
|
303
|
+
}
|
|
304
|
+
else if (entry.target === bottomEl) {
|
|
305
|
+
atBottomRef.current = entry.isIntersecting;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}, { root, threshold: 0, rootMargin: '0px' });
|
|
309
|
+
observer.observe(topEl);
|
|
310
|
+
observer.observe(bottomEl);
|
|
311
|
+
return () => observer.disconnect();
|
|
312
|
+
}, [currentIndexDisplayed]);
|
|
286
313
|
// Expose methods via ref for parent component to use
|
|
287
314
|
useImperativeHandle(ref, () => ({
|
|
288
315
|
handlePrev,
|
|
289
316
|
handleNext,
|
|
290
|
-
|
|
291
|
-
|
|
317
|
+
spectaMode: isSpecta,
|
|
318
|
+
hasPrev,
|
|
319
|
+
hasNext,
|
|
320
|
+
getAtTop: () => atTopRef.current,
|
|
321
|
+
getAtBottom: () => atBottomRef.current,
|
|
322
|
+
getScrollContainer: () => panelRef.current,
|
|
323
|
+
}), [handlePrev, handleNext, storyData, isSpecta, hasPrev, hasNext]);
|
|
292
324
|
const hasImage = !!(((_d = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _d === void 0 ? void 0 : _d.image) && imageLoaded);
|
|
293
325
|
const storyType = (_e = storyData.storyType) !== null && _e !== void 0 ? _e : 'guided';
|
|
294
326
|
const navPlacement = getStoryNavPlacement(isSpecta, hasImage, storyType, isMobile);
|
|
295
327
|
const navSlot = navPlacement !== null ? (React.createElement(StoryNavBar, Object.assign({ placement: navPlacement }, storyNavBarProps))) : null;
|
|
296
328
|
// Get transition time from current segment, default to 0.3s
|
|
297
329
|
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;
|
|
330
|
+
// Notify parent when segment transition animation ends (e.g. for scroll-guard cleanup)
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
const el = segmentContainerRef.current;
|
|
333
|
+
if (!el || !onSegmentTransitionEnd) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const handleAnimationEnd = (e) => {
|
|
337
|
+
if (e.animationName === 'fadeIn') {
|
|
338
|
+
el.removeEventListener('animationend', handleAnimationEnd);
|
|
339
|
+
onSegmentTransitionEnd();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
el.addEventListener('animationend', handleAnimationEnd);
|
|
343
|
+
return () => el.removeEventListener('animationend', handleAnimationEnd);
|
|
344
|
+
}, [currentIndexDisplayed, onSegmentTransitionEnd]);
|
|
298
345
|
return (React.createElement("div", { ref: panelRef, className: cn('jgis-story-viewer-panel', className), id: "jgis-story-segment-panel" },
|
|
299
|
-
React.createElement("div", {
|
|
346
|
+
React.createElement("div", { ref: topSentinelRef, "aria-hidden": true, "data-story-scroll-sentinel": "top", style: { height: 1, minHeight: 1, pointerEvents: 'none' } }),
|
|
347
|
+
React.createElement("div", { ref: segmentContainerRef, key: currentIndexDisplayed, className: "jgis-story-segment-container", style: {
|
|
300
348
|
animationDuration: `${transitionTime}s`,
|
|
301
349
|
} },
|
|
302
350
|
React.createElement("div", { id: "jgis-story-segment-header" },
|
|
@@ -307,7 +355,8 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
|
|
|
307
355
|
? navSlot
|
|
308
356
|
: null })),
|
|
309
357
|
React.createElement("div", { id: "jgis-story-segment-content" },
|
|
310
|
-
React.createElement(StoryContentSection, { markdown: (_o = (_m = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _m === void 0 ? void 0 : _m.markdown) !== null && _o !== void 0 ? _o : '' })))
|
|
358
|
+
React.createElement(StoryContentSection, { markdown: (_o = (_m = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _m === void 0 ? void 0 : _m.markdown) !== null && _o !== void 0 ? _o : '' }))),
|
|
359
|
+
React.createElement("div", { ref: bottomSentinelRef, "aria-hidden": true, "data-story-scroll-sentinel": "bottom", style: { height: 1, minHeight: 1, pointerEvents: 'none' } })));
|
|
311
360
|
});
|
|
312
361
|
StoryViewerPanel.displayName = 'StoryViewerPanel';
|
|
313
362
|
export default StoryViewerPanel;
|
package/lib/widget.js
CHANGED
|
@@ -70,10 +70,13 @@ export class JupyterGISPanel extends SplitPanel {
|
|
|
70
70
|
super({ orientation: 'vertical', spacing: 0 });
|
|
71
71
|
this._consoleOpened = false;
|
|
72
72
|
this._state = state;
|
|
73
|
-
this._initModel({ model, commandRegistry });
|
|
74
|
-
this._initView(formSchemaRegistry, annotationModel);
|
|
75
73
|
this._consoleOption = Object.assign({ commandRegistry }, consoleOption);
|
|
76
74
|
this._consoleTracker = consoleTracker;
|
|
75
|
+
const readyPromise = model.sharedModel.initialSyncReady;
|
|
76
|
+
readyPromise.then(() => {
|
|
77
|
+
this._initModel({ model, commandRegistry });
|
|
78
|
+
this._initView(formSchemaRegistry, annotationModel);
|
|
79
|
+
});
|
|
77
80
|
}
|
|
78
81
|
_initModel(options) {
|
|
79
82
|
this._view = new ObservableMap();
|
|
@@ -94,9 +97,15 @@ export class JupyterGISPanel extends SplitPanel {
|
|
|
94
97
|
SplitPanel.setStretch(this._jupyterGISMainViewPanel, 1);
|
|
95
98
|
}
|
|
96
99
|
get jupyterGISMainViewPanel() {
|
|
100
|
+
if (!this._jupyterGISMainViewPanel) {
|
|
101
|
+
console.warn('JupyterGISPanel not ready (initialSyncReady not resolved)');
|
|
102
|
+
}
|
|
97
103
|
return this._jupyterGISMainViewPanel;
|
|
98
104
|
}
|
|
99
105
|
get viewChanged() {
|
|
106
|
+
if (!this._view) {
|
|
107
|
+
console.warn('JupyterGISPanel not ready (initialSyncReady not resolved)');
|
|
108
|
+
}
|
|
100
109
|
return this._view.changed;
|
|
101
110
|
}
|
|
102
111
|
/**
|
|
@@ -114,6 +123,9 @@ export class JupyterGISPanel extends SplitPanel {
|
|
|
114
123
|
super.dispose();
|
|
115
124
|
}
|
|
116
125
|
get currentViewModel() {
|
|
126
|
+
if (!this._mainViewModel) {
|
|
127
|
+
console.warn('JupyterGISPanel not ready (initialSyncReady not resolved)');
|
|
128
|
+
}
|
|
117
129
|
return this._mainViewModel;
|
|
118
130
|
}
|
|
119
131
|
get consolePanel() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupytergis/base",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.3",
|
|
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.13.
|
|
47
|
+
"@jupytergis/schema": "^0.13.3",
|
|
48
48
|
"@jupyterlab/application": "^4.3.0",
|
|
49
49
|
"@jupyterlab/apputils": "^4.3.0",
|
|
50
50
|
"@jupyterlab/completer": "^4.3.0",
|
package/style/shared/tabs.css
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
align-items: center;
|
|
6
6
|
background-color: var(--jp-layout-color0);
|
|
7
7
|
}
|
|
8
|
+
|
|
8
9
|
.jgis-tabs-list {
|
|
9
10
|
display: inline-flex;
|
|
10
11
|
height: 2.5rem;
|
|
@@ -12,12 +13,17 @@
|
|
|
12
13
|
justify-content: space-evenly;
|
|
13
14
|
background-color: var(--jp-layout-color2);
|
|
14
15
|
color: var(--jp-ui-font-color0);
|
|
16
|
+
cursor: grab;
|
|
15
17
|
gap: 1rem;
|
|
16
18
|
width: 100%;
|
|
17
19
|
font-size: 9px;
|
|
18
20
|
overflow-x: scroll;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
.jgis-tabs-list:active {
|
|
24
|
+
cursor: grabbing;
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
.jgis-tabs-trigger {
|
|
22
28
|
display: inline-flex;
|
|
23
29
|
align-items: center;
|