Skip to content

Commit 3c2f5ea

Browse files
Floating side panels
1 parent 830d5e3 commit 3c2f5ea

File tree

17 files changed

+295
-375
lines changed

17 files changed

+295
-375
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,8 @@
7272
"rimraf": "^3.0.2",
7373
"typescript": "^5",
7474
"webpack": "^5.76.3"
75+
},
76+
"dependencies": {
77+
"react-rnd": "^10.5.2"
7578
}
7679
}

packages/base/src/Lumino.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Widget } from '@lumino/widgets';
2+
import * as React from 'react';
3+
4+
type LuminoProps = {
5+
id?: string;
6+
height?: string | number;
7+
children: Widget;
8+
};
9+
10+
export const Lumino = (props: LuminoProps) => {
11+
const ref = React.useRef<HTMLDivElement>(null);
12+
const { children, id, height } = props;
13+
React.useEffect(() => {
14+
if (ref && ref.current) {
15+
try {
16+
Widget.attach(children, ref.current);
17+
} catch (e) {
18+
console.warn('Exception while attaching Lumino widget.', e);
19+
}
20+
return () => {
21+
try {
22+
if (children.isAttached || children.node.isConnected) {
23+
children.dispose();
24+
Widget.detach(children);
25+
}
26+
} catch (e) {
27+
// no-op.
28+
// console.debug('Exception while detaching Lumino widget.', e);
29+
}
30+
};
31+
}
32+
}, [ref, children]);
33+
return (
34+
<div id={id} ref={ref} style={{ height: height, minHeight: height }} />
35+
);
36+
};
37+
38+
Lumino.defaultProps = {
39+
id: 'lumino-id',
40+
height: '100%',
41+
};
42+
43+
export default Lumino;

packages/base/src/annotations/components/Annotation.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@ import { showDialog, Dialog } from '@jupyterlab/apputils';
99
import { Button } from '@jupyterlab/ui-components';
1010
import React, { useMemo, useState } from 'react';
1111

12-
import { IControlPanelModel } from '@/src/types';
1312
import { Message } from './Message';
1413

1514
export interface IAnnotationProps {
1615
itemId: string;
1716
annotationModel: IAnnotationModel;
18-
rightPanelModel?: IControlPanelModel;
17+
rightPanelModel?: IJupyterGISModel;
1918
children?: JSX.Element[] | JSX.Element;
2019
}
2120

@@ -26,20 +25,10 @@ const Annotation = ({
2625
children,
2726
}: IAnnotationProps) => {
2827
const [messageContent, setMessageContent] = useState('');
29-
const [jgisModel, setJgisModel] = useState<IJupyterGISModel | undefined>(
30-
rightPanelModel?.jGISModel,
31-
);
32-
28+
const jgisModel = rightPanelModel;
3329
const annotation = annotationModel.getAnnotation(itemId);
3430
const contents = useMemo(() => annotation?.contents ?? [], [annotation]);
3531

36-
/**
37-
* Update the model when it changes.
38-
*/
39-
rightPanelModel?.documentChanged.connect((_, widget) => {
40-
setJgisModel(widget?.model);
41-
});
42-
4332
const handleSubmit = () => {
4433
annotationModel.addContent(itemId, messageContent);
4534
setMessageContent('');

packages/base/src/mainview/mainView.tsx

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { IObservableMap, ObservableMap } from '@jupyterlab/observables';
3434
import { User } from '@jupyterlab/services';
3535
import { CommandRegistry } from '@lumino/commands';
3636
import { JSONValue, UUID } from '@lumino/coreutils';
37-
import { ContextMenu } from '@lumino/widgets';
37+
import { ContextMenu, Widget } from '@lumino/widgets';
3838
import { Collection, MapBrowserEvent, Map as OlMap, View, getUid } from 'ol';
3939
import Feature, { FeatureLike } from 'ol/Feature';
4040
import { FullScreen, ScaleLine } from 'ol/control';
@@ -77,19 +77,23 @@ import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
7777
import proj4 from 'proj4';
7878
import proj4list from 'proj4-list';
7979
import * as React from 'react';
80+
import { Rnd } from 'react-rnd';
8081

8182
import AnnotationFloater from '@/src/annotations/components/AnnotationFloater';
8283
import { CommandIDs } from '@/src/constants';
8384
import { LoadingOverlay } from '@/src/shared/components/loading';
8485
import StatusBar from '@/src/statusbar/StatusBar';
8586
import { isLightTheme, loadFile, throttle } from '@/src/tools';
87+
import Lumino from '../Lumino';
8688
import CollaboratorPointers, { ClientPointer } from './CollaboratorPointers';
8789
import { FollowIndicator } from './FollowIndicator';
8890
import TemporalSlider from './TemporalSlider';
8991
import { MainViewModel } from './mainviewmodel';
9092

9193
interface IProps {
9294
viewModel: MainViewModel;
95+
leftPanel?: Widget;
96+
rightPanel?: Widget;
9397
}
9498

9599
interface IStates {
@@ -111,7 +115,8 @@ interface IStates {
111115
export class MainView extends React.Component<IProps, IStates> {
112116
constructor(props: IProps) {
113117
super(props);
114-
118+
this._leftPanel = props.leftPanel;
119+
this._rightPanel = props.rightPanel;
115120
this._mainViewModel = this.props.viewModel;
116121
this._mainViewModel.viewSettingChanged.connect(this._onViewChanged, this);
117122

@@ -173,7 +178,9 @@ export class MainView extends React.Component<IProps, IStates> {
173178
this._sources = [];
174179
this._loadingLayers = new Set();
175180
this._commands = new CommandRegistry();
176-
this._contextMenu = new ContextMenu({ commands: this._commands });
181+
this._contextMenu = new ContextMenu({
182+
commands: this._commands,
183+
});
177184
}
178185

179186
async componentDidMount(): Promise<void> {
@@ -285,7 +292,13 @@ export class MainView extends React.Component<IProps, IStates> {
285292
return;
286293
}
287294
this._model.syncViewport(
288-
{ coordinates: { x: center[0], y: center[1] }, zoom },
295+
{
296+
coordinates: {
297+
x: center[0],
298+
y: center[1],
299+
},
300+
zoom,
301+
},
289302
this._mainViewModel.id,
290303
);
291304
}),
@@ -458,7 +471,10 @@ export class MainView extends React.Component<IProps, IStates> {
458471
}
459472

460473
this._mainViewModel.addAnnotation({
461-
position: { x: this._clickCoords[0], y: this._clickCoords[1] },
474+
position: {
475+
x: this._clickCoords[0],
476+
y: this._clickCoords[1],
477+
},
462478
zoom: this._Map.getView().getZoom() ?? 0,
463479
label: 'New annotation',
464480
contents: [],
@@ -538,7 +554,9 @@ export class MainView extends React.Component<IProps, IStates> {
538554
minZoom: sourceParameters.minZoom,
539555
maxZoom: sourceParameters.maxZoom,
540556
url: url,
541-
format: new MVT({ featureClass: RenderFeature }),
557+
format: new MVT({
558+
featureClass: RenderFeature,
559+
}),
542560
});
543561
} else {
544562
newSource = new PMTilesVectorSource({
@@ -916,7 +934,9 @@ export class MainView extends React.Component<IProps, IStates> {
916934
};
917935

918936
if (layerParameters.color) {
919-
layerOptions['style'] = { color: layerParameters.color };
937+
layerOptions['style'] = {
938+
color: layerParameters.color,
939+
};
920940
}
921941

922942
newMapLayer = new WebGlTileLayer(layerOptions);
@@ -1339,8 +1359,13 @@ export class MainView extends React.Component<IProps, IStates> {
13391359
return new Style({
13401360
image: new Circle({
13411361
radius: 6,
1342-
fill: new Fill({ color: 'rgba(255, 255, 0, 0.8)' }),
1343-
stroke: new Stroke({ color: '#ff0', width: 2 }),
1362+
fill: new Fill({
1363+
color: 'rgba(255, 255, 0, 0.8)',
1364+
}),
1365+
stroke: new Stroke({
1366+
color: '#ff0',
1367+
width: 2,
1368+
}),
13441369
}),
13451370
});
13461371
case 'LineString':
@@ -1388,7 +1413,10 @@ export class MainView extends React.Component<IProps, IStates> {
13881413
return new Promise(resolve => {
13891414
const checkReady = () => {
13901415
if (this._loadingLayers.size === 0) {
1391-
this.setState(old => ({ ...old, loadingLayer: false }));
1416+
this.setState(old => ({
1417+
...old,
1418+
loadingLayer: false,
1419+
}));
13921420
resolve();
13931421
} else {
13941422
setTimeout(checkReady, 50);
@@ -1454,7 +1482,10 @@ export class MainView extends React.Component<IProps, IStates> {
14541482
}
14551483

14561484
if (remoteState.user?.username !== this.state.remoteUser?.username) {
1457-
this.setState(old => ({ ...old, remoteUser: remoteState.user }));
1485+
this.setState(old => ({
1486+
...old,
1487+
remoteUser: remoteState.user,
1488+
}));
14581489
}
14591490

14601491
const remoteViewport = remoteState.viewportState;
@@ -1468,7 +1499,10 @@ export class MainView extends React.Component<IProps, IStates> {
14681499
} else {
14691500
// If we are unfollowing a remote user, we reset our center and zoom to their previous values
14701501
if (this.state.remoteUser !== null) {
1471-
this.setState(old => ({ ...old, remoteUser: null }));
1502+
this.setState(old => ({
1503+
...old,
1504+
remoteUser: null,
1505+
}));
14721506
const viewportState = localState.viewportState?.value;
14731507

14741508
if (viewportState) {
@@ -1506,14 +1540,26 @@ export class MainView extends React.Component<IProps, IStates> {
15061540
username: client.user.username,
15071541
displayName: client.user.display_name,
15081542
color: client.user.color,
1509-
coordinates: { x: pixel[0], y: pixel[1] },
1510-
lonLat: { longitude: lonLat[0], latitude: lonLat[1] },
1543+
coordinates: {
1544+
x: pixel[0],
1545+
y: pixel[1],
1546+
},
1547+
lonLat: {
1548+
longitude: lonLat[0],
1549+
latitude: lonLat[1],
1550+
},
15111551
};
15121552
} else {
15131553
currentClientPointer = {
15141554
...currentClientPointer,
1515-
coordinates: { x: pixel[0], y: pixel[1] },
1516-
lonLat: { longitude: lonLat[0], latitude: lonLat[1] },
1555+
coordinates: {
1556+
x: pixel[0],
1557+
y: pixel[1],
1558+
},
1559+
lonLat: {
1560+
longitude: lonLat[0],
1561+
latitude: lonLat[1],
1562+
},
15171563
};
15181564
}
15191565

@@ -1875,7 +1921,10 @@ export class MainView extends React.Component<IProps, IStates> {
18751921
// Zoom needs to be set before changing center
18761922
if (!view.animate === undefined) {
18771923
view.animate({ zoom, duration });
1878-
view.animate({ center: [center.x, center.y], duration });
1924+
view.animate({
1925+
center: [center.x, center.y],
1926+
duration,
1927+
});
18791928
}
18801929
}
18811930

@@ -2072,6 +2121,37 @@ export class MainView extends React.Component<IProps, IStates> {
20722121
scale={this.state.scale}
20732122
/>
20742123
</div>
2124+
{this._leftPanel && (
2125+
<Rnd
2126+
default={{
2127+
x: 0,
2128+
y: 0,
2129+
width: 200,
2130+
height: 600,
2131+
}}
2132+
bounds="window"
2133+
minWidth={100}
2134+
minHeight={100}
2135+
>
2136+
<Lumino>{this._leftPanel}</Lumino>
2137+
</Rnd>
2138+
)}
2139+
2140+
{this._rightPanel && (
2141+
<Rnd
2142+
default={{
2143+
x: 400,
2144+
y: 0,
2145+
width: 200,
2146+
height: 600,
2147+
}}
2148+
bounds="window"
2149+
minWidth={100}
2150+
minHeight={100}
2151+
>
2152+
<Lumino>{this._rightPanel}</Lumino>
2153+
</Rnd>
2154+
)}
20752155
</>
20762156
);
20772157
}
@@ -2091,4 +2171,6 @@ export class MainView extends React.Component<IProps, IStates> {
20912171
private _loadingLayers: Set<string>;
20922172
private _originalFeatures: IDict<Feature<Geometry>[]> = {};
20932173
private _highlightLayer: VectorLayer<VectorSource>;
2174+
private _leftPanel?: Widget;
2175+
private _rightPanel?: Widget;
20942176
}
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ReactWidget } from '@jupyterlab/apputils';
2+
import { Widget } from '@lumino/widgets';
23
import * as React from 'react';
34

45
import { MainView } from './mainView';
@@ -8,15 +9,29 @@ export class JupyterGISMainViewPanel extends ReactWidget {
89
/**
910
* Construct a `JupyterGISPanel`.
1011
*/
11-
constructor(options: { mainViewModel: MainViewModel }) {
12+
constructor(
13+
options: { mainViewModel: MainViewModel },
14+
leftPanel?: Widget,
15+
rightPanel?: Widget,
16+
) {
1217
super();
1318
this._mainViewModel = options.mainViewModel;
1419
this.addClass('jp-jupytergis-panel');
20+
this._leftPanel = leftPanel;
21+
this._rightPanel = rightPanel;
1522
}
1623

1724
render(): JSX.Element {
18-
return <MainView viewModel={this._mainViewModel} />;
25+
return (
26+
<MainView
27+
leftPanel={this._leftPanel}
28+
rightPanel={this._rightPanel}
29+
viewModel={this._mainViewModel}
30+
/>
31+
);
1932
}
2033

2134
private _mainViewModel: MainViewModel;
35+
private _leftPanel?: Widget;
36+
private _rightPanel?: Widget;
2237
}

0 commit comments

Comments
 (0)