@jupytergis/base 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands.d.ts +11 -0
- package/lib/commands.js +809 -0
- package/lib/console/consoleview.d.ts +24 -0
- package/lib/console/consoleview.js +55 -0
- package/lib/console/index.d.ts +1 -0
- package/lib/console/index.js +1 -0
- package/lib/constants.d.ts +57 -0
- package/lib/constants.js +89 -0
- package/lib/dialogs/components/symbology/BandRendering.d.ts +4 -0
- package/lib/dialogs/components/symbology/BandRendering.js +29 -0
- package/lib/dialogs/components/symbology/BandRow.d.ts +10 -0
- package/lib/dialogs/components/symbology/BandRow.js +43 -0
- package/lib/dialogs/components/symbology/SingleBandPseudoColor.d.ts +20 -0
- package/lib/dialogs/components/symbology/SingleBandPseudoColor.js +281 -0
- package/lib/dialogs/components/symbology/StopRow.d.ts +11 -0
- package/lib/dialogs/components/symbology/StopRow.js +58 -0
- package/lib/dialogs/formdialog.d.ts +31 -0
- package/lib/dialogs/formdialog.js +68 -0
- package/lib/dialogs/layerBrowserDialog.d.ts +25 -0
- package/lib/dialogs/layerBrowserDialog.js +141 -0
- package/lib/dialogs/symbologyDialog.d.ts +23 -0
- package/lib/dialogs/symbologyDialog.js +68 -0
- package/lib/dialogs/terrainDialog.d.ts +21 -0
- package/lib/dialogs/terrainDialog.js +60 -0
- package/lib/formbuilder/creationform.d.ts +56 -0
- package/lib/formbuilder/creationform.js +117 -0
- package/lib/formbuilder/editform.d.ts +24 -0
- package/lib/formbuilder/editform.js +60 -0
- package/lib/formbuilder/formselectors.d.ts +5 -0
- package/lib/formbuilder/formselectors.js +38 -0
- package/lib/formbuilder/index.d.ts +6 -0
- package/lib/formbuilder/index.js +6 -0
- package/lib/formbuilder/objectform/baseform.d.ts +79 -0
- package/lib/formbuilder/objectform/baseform.js +167 -0
- package/lib/formbuilder/objectform/geojsonsource.d.ts +19 -0
- package/lib/formbuilder/objectform/geojsonsource.js +80 -0
- package/lib/formbuilder/objectform/hillshadeLayerForm.d.ts +8 -0
- package/lib/formbuilder/objectform/hillshadeLayerForm.js +12 -0
- package/lib/formbuilder/objectform/layerform.d.ts +19 -0
- package/lib/formbuilder/objectform/layerform.js +17 -0
- package/lib/formbuilder/objectform/tilesourceform.d.ts +7 -0
- package/lib/formbuilder/objectform/tilesourceform.js +60 -0
- package/lib/formbuilder/objectform/vectorlayerform.d.ts +15 -0
- package/lib/formbuilder/objectform/vectorlayerform.js +88 -0
- package/lib/formbuilder/objectform/webGlLayerForm.d.ts +8 -0
- package/lib/formbuilder/objectform/webGlLayerForm.js +10 -0
- package/lib/icons.d.ts +6 -0
- package/lib/icons.js +31 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +10 -0
- package/lib/mainview/index.d.ts +3 -0
- package/lib/mainview/index.js +3 -0
- package/lib/mainview/mainView.d.ts +113 -0
- package/lib/mainview/mainView.js +743 -0
- package/lib/mainview/mainviewmodel.d.ts +24 -0
- package/lib/mainview/mainviewmodel.js +34 -0
- package/lib/mainview/mainviewwidget.d.ts +14 -0
- package/lib/mainview/mainviewwidget.js +18 -0
- package/lib/mainview/spinner.d.ts +6 -0
- package/lib/mainview/spinner.js +5 -0
- package/lib/panelview/components/filter-panel/Filter.d.ts +19 -0
- package/lib/panelview/components/filter-panel/Filter.js +223 -0
- package/lib/panelview/components/filter-panel/FilterRow.d.ts +9 -0
- package/lib/panelview/components/filter-panel/FilterRow.js +61 -0
- package/lib/panelview/components/layers.d.ts +13 -0
- package/lib/panelview/components/layers.js +275 -0
- package/lib/panelview/components/sources.d.ts +10 -0
- package/lib/panelview/components/sources.js +147 -0
- package/lib/panelview/header.d.ts +11 -0
- package/lib/panelview/header.js +20 -0
- package/lib/panelview/index.d.ts +5 -0
- package/lib/panelview/index.js +5 -0
- package/lib/panelview/leftpanel.d.ts +51 -0
- package/lib/panelview/leftpanel.js +125 -0
- package/lib/panelview/model.d.ts +19 -0
- package/lib/panelview/model.js +30 -0
- package/lib/panelview/objectproperties.d.ts +17 -0
- package/lib/panelview/objectproperties.js +92 -0
- package/lib/panelview/rightpanel.d.ts +19 -0
- package/lib/panelview/rightpanel.js +45 -0
- package/lib/toolbar/index.d.ts +2 -0
- package/lib/toolbar/index.js +2 -0
- package/lib/toolbar/usertoolbaritem.d.ts +19 -0
- package/lib/toolbar/usertoolbaritem.js +57 -0
- package/lib/toolbar/widget.d.ts +22 -0
- package/lib/toolbar/widget.js +104 -0
- package/lib/tools.d.ts +25 -0
- package/lib/tools.js +215 -0
- package/lib/types.d.ts +11 -0
- package/lib/types.js +1 -0
- package/lib/widget.d.ts +49 -0
- package/lib/widget.js +144 -0
- package/package.json +95 -0
- package/style/base.css +55 -0
- package/style/colorExpression.css +36 -0
- package/style/dialog.css +8 -0
- package/style/filterPanel.css +70 -0
- package/style/icons/geojson.svg +12 -0
- package/style/icons/mound.svg +9 -0
- package/style/icons/nonvisibility.svg +8 -0
- package/style/icons/raster.svg +5 -0
- package/style/icons/visibility.svg +7 -0
- package/style/index.css +6 -0
- package/style/index.js +6 -0
- package/style/layerBrowser.css +256 -0
- package/style/leftPanel.css +150 -0
- package/style/symbologyDialog.css +86 -0
- package/style/terrainDialog.css +14 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { DOMUtils } from '@jupyterlab/apputils';
|
|
2
|
+
import { Button, LabIcon, ReactWidget, caretDownIcon } from '@jupyterlab/ui-components';
|
|
3
|
+
import { Panel } from '@lumino/widgets';
|
|
4
|
+
import React, { useEffect, useState } from 'react';
|
|
5
|
+
import { icons } from '../../constants';
|
|
6
|
+
import { nonVisibilityIcon, visibilityIcon } from '../../icons';
|
|
7
|
+
const LAYERS_PANEL_CLASS = 'jp-gis-layerPanel';
|
|
8
|
+
const LAYER_GROUP_CLASS = 'jp-gis-layerGroup';
|
|
9
|
+
const LAYER_GROUP_HEADER_CLASS = 'jp-gis-layerGroupHeader';
|
|
10
|
+
const LAYER_GROUP_COLLAPSER_CLASS = 'jp-gis-layerGroupCollapser';
|
|
11
|
+
const LAYER_ITEM_CLASS = 'jp-gis-layerItem';
|
|
12
|
+
const LAYER_CLASS = 'jp-gis-layer';
|
|
13
|
+
const LAYER_TITLE_CLASS = 'jp-gis-layerTitle';
|
|
14
|
+
const LAYER_ICON_CLASS = 'jp-gis-layerIcon';
|
|
15
|
+
const LAYER_TEXT_CLASS = 'jp-gis-layerText';
|
|
16
|
+
/**
|
|
17
|
+
* The layers panel widget.
|
|
18
|
+
*/
|
|
19
|
+
export class LayersPanel extends Panel {
|
|
20
|
+
constructor(options) {
|
|
21
|
+
super();
|
|
22
|
+
// This happens when dragging over empty space below the tree.
|
|
23
|
+
this._onDragOver = (e) => {
|
|
24
|
+
e.stopPropagation();
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
this.node.appendChild(Private.dragIndicator);
|
|
27
|
+
Private.dragInfo.dragOverElement = null;
|
|
28
|
+
Private.dragInfo.dragOverPosition = null;
|
|
29
|
+
};
|
|
30
|
+
this._onDrop = (e) => {
|
|
31
|
+
Private.dragIndicator.style.display = 'none';
|
|
32
|
+
if (this._model === undefined) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const { jGISModel: model } = this._model;
|
|
36
|
+
const { draggedElement, dragOverElement, dragOverPosition } = Private.dragInfo;
|
|
37
|
+
if (dragOverElement === 'error') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!draggedElement) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const draggedId = draggedElement.dataset.id;
|
|
44
|
+
if (!draggedId) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Element has been dropped in the empty zone below the tree.
|
|
48
|
+
if (dragOverElement === null) {
|
|
49
|
+
model === null || model === void 0 ? void 0 : model.moveItemsToGroup([draggedId], '', 0);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const dragOverId = dragOverElement.dataset.id;
|
|
53
|
+
if (!dragOverId) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Handle the special case where we want to drop the element on top of the first
|
|
57
|
+
// element of a group.
|
|
58
|
+
if (dragOverElement.classList.contains(LAYER_GROUP_HEADER_CLASS) &&
|
|
59
|
+
dragOverPosition === 'below') {
|
|
60
|
+
model === null || model === void 0 ? void 0 : model.moveItemsToGroup([draggedId], dragOverId);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
model === null || model === void 0 ? void 0 : model.moveItemRelatedTo(draggedId, dragOverId, dragOverPosition === 'above');
|
|
64
|
+
};
|
|
65
|
+
this._model = options.model;
|
|
66
|
+
this._onSelect = options.onSelect;
|
|
67
|
+
this._state = options.state;
|
|
68
|
+
this.id = 'jupytergis::layerTree';
|
|
69
|
+
this.addClass(LAYERS_PANEL_CLASS);
|
|
70
|
+
this.addWidget(ReactWidget.create(React.createElement(LayersBodyComponent, { model: this._model, onSelect: this._onSelect, state: this._state })));
|
|
71
|
+
this.node.ondragover = this._onDragOver;
|
|
72
|
+
this.node.ondrop = this._onDrop;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The body component of the panel.
|
|
77
|
+
*/
|
|
78
|
+
function LayersBodyComponent(props) {
|
|
79
|
+
var _a, _b;
|
|
80
|
+
const [model, setModel] = useState((_a = props.model) === null || _a === void 0 ? void 0 : _a.jGISModel);
|
|
81
|
+
const [layerTree, setLayerTree] = useState((model === null || model === void 0 ? void 0 : model.getLayerTree()) || []);
|
|
82
|
+
/**
|
|
83
|
+
* Propagate the layer selection.
|
|
84
|
+
*/
|
|
85
|
+
const onItemClick = ({ type, item, nodeId, event }) => {
|
|
86
|
+
props.onSelect({ type, item, nodeId, event });
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Listen to the layers and layer tree changes.
|
|
90
|
+
*/
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const updateLayers = () => {
|
|
93
|
+
setLayerTree((model === null || model === void 0 ? void 0 : model.getLayerTree()) || []);
|
|
94
|
+
};
|
|
95
|
+
model === null || model === void 0 ? void 0 : model.sharedModel.layersChanged.connect(updateLayers);
|
|
96
|
+
model === null || model === void 0 ? void 0 : model.sharedModel.layerTreeChanged.connect(updateLayers);
|
|
97
|
+
return () => {
|
|
98
|
+
model === null || model === void 0 ? void 0 : model.sharedModel.layersChanged.disconnect(updateLayers);
|
|
99
|
+
model === null || model === void 0 ? void 0 : model.sharedModel.layerTreeChanged.disconnect(updateLayers);
|
|
100
|
+
};
|
|
101
|
+
}, [model]);
|
|
102
|
+
/**
|
|
103
|
+
* Update the model when it changes.
|
|
104
|
+
*/
|
|
105
|
+
(_b = props.model) === null || _b === void 0 ? void 0 : _b.documentChanged.connect((_, widget) => {
|
|
106
|
+
var _a;
|
|
107
|
+
setModel(widget === null || widget === void 0 ? void 0 : widget.context.model);
|
|
108
|
+
setLayerTree(((_a = widget === null || widget === void 0 ? void 0 : widget.context.model) === null || _a === void 0 ? void 0 : _a.getLayerTree()) || []);
|
|
109
|
+
});
|
|
110
|
+
return (React.createElement("div", { id: "jp-gis-layer-tree" }, layerTree
|
|
111
|
+
.slice()
|
|
112
|
+
.reverse()
|
|
113
|
+
.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 })))));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The component to handle group of layers.
|
|
117
|
+
*/
|
|
118
|
+
function LayerGroupComponent(props) {
|
|
119
|
+
var _a, _b;
|
|
120
|
+
const { group, gisModel, onClick, state } = props;
|
|
121
|
+
if (group === undefined) {
|
|
122
|
+
return React.createElement(React.Fragment, null);
|
|
123
|
+
}
|
|
124
|
+
const [id, setId] = useState('');
|
|
125
|
+
const [open, setOpen] = useState(false);
|
|
126
|
+
const name = (_a = group === null || group === void 0 ? void 0 : group.name) !== null && _a !== void 0 ? _a : 'Undefined group';
|
|
127
|
+
const layers = (_b = group === null || group === void 0 ? void 0 : group.layers) !== null && _b !== void 0 ? _b : [];
|
|
128
|
+
const [selected, setSelected] = useState(
|
|
129
|
+
// TODO Support multi-selection as `model?.jGISModel?.localState?.selected.value` does
|
|
130
|
+
isSelected(group.name, gisModel));
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
setId(DOMUtils.createDomID());
|
|
133
|
+
const getExpandedState = async () => {
|
|
134
|
+
var _a;
|
|
135
|
+
const groupState = await state.fetch(group.name);
|
|
136
|
+
setOpen(groupState ? ((_a = groupState['expanded']) !== null && _a !== void 0 ? _a : false) : false);
|
|
137
|
+
};
|
|
138
|
+
getExpandedState();
|
|
139
|
+
}, []);
|
|
140
|
+
/**
|
|
141
|
+
* Listen to the changes on the current layer.
|
|
142
|
+
*/
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const onClientSharedStateChanged = () => {
|
|
145
|
+
// TODO Support follow mode and remoteUser state
|
|
146
|
+
setSelected(isSelected(group.name, gisModel));
|
|
147
|
+
};
|
|
148
|
+
gisModel === null || gisModel === void 0 ? void 0 : gisModel.clientStateChanged.connect(onClientSharedStateChanged);
|
|
149
|
+
return () => {
|
|
150
|
+
gisModel === null || gisModel === void 0 ? void 0 : gisModel.clientStateChanged.disconnect(onClientSharedStateChanged);
|
|
151
|
+
};
|
|
152
|
+
}, [gisModel]);
|
|
153
|
+
const handleRightClick = (event) => {
|
|
154
|
+
var _a;
|
|
155
|
+
const childId = (_a = event.currentTarget.children.namedItem(id)) === null || _a === void 0 ? void 0 : _a.id;
|
|
156
|
+
onClick({ type: 'group', item: name, nodeId: childId, event });
|
|
157
|
+
};
|
|
158
|
+
const handleExpand = async () => {
|
|
159
|
+
state.save(group.name, { expanded: !open });
|
|
160
|
+
setOpen(!open);
|
|
161
|
+
};
|
|
162
|
+
return (React.createElement("div", { className: `${LAYER_ITEM_CLASS} ${LAYER_GROUP_CLASS}`, draggable: true, onDragStart: Private.onDragStart, onDragEnd: Private.onDragEnd, "data-id": name },
|
|
163
|
+
React.createElement("div", { onClick: handleExpand, onContextMenu: handleRightClick, className: `${LAYER_GROUP_HEADER_CLASS} ${selected ? ' jp-mod-selected' : ''}`, onDragOver: Private.onDragOver, "data-id": name },
|
|
164
|
+
React.createElement(LabIcon.resolveReact, { icon: caretDownIcon, className: LAYER_GROUP_COLLAPSER_CLASS + (open ? ' jp-mod-expanded' : ''), tag: 'span' }),
|
|
165
|
+
React.createElement("span", { id: id, className: LAYER_TEXT_CLASS }, name)),
|
|
166
|
+
open && (React.createElement("div", null, layers
|
|
167
|
+
.slice()
|
|
168
|
+
.reverse()
|
|
169
|
+
.map(layer => typeof layer === 'string' ? (React.createElement(LayerComponent, { key: layer, gisModel: gisModel, layerId: layer, onClick: onClick })) : (React.createElement(LayerGroupComponent, { key: layer.name, gisModel: gisModel, group: layer, onClick: onClick, state: props.state })))))));
|
|
170
|
+
}
|
|
171
|
+
function isSelected(layerId, model) {
|
|
172
|
+
var _a, _b, _c, _d;
|
|
173
|
+
return ((((_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) &&
|
|
174
|
+
Object.keys((_d = (_c = model === null || model === void 0 ? void 0 : model.localState) === null || _c === void 0 ? void 0 : _c.selected) === null || _d === void 0 ? void 0 : _d.value).includes(layerId)) ||
|
|
175
|
+
false);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The component to display a single layer.
|
|
179
|
+
*/
|
|
180
|
+
function LayerComponent(props) {
|
|
181
|
+
const { layerId, gisModel, onClick } = props;
|
|
182
|
+
const layer = gisModel === null || gisModel === void 0 ? void 0 : gisModel.getLayer(layerId);
|
|
183
|
+
if (layer === undefined) {
|
|
184
|
+
return React.createElement(React.Fragment, null);
|
|
185
|
+
}
|
|
186
|
+
const [id, setId] = useState('');
|
|
187
|
+
const [selected, setSelected] = useState(
|
|
188
|
+
// TODO Support multi-selection as `model?.jGISModel?.localState?.selected.value` does
|
|
189
|
+
isSelected(layerId, gisModel));
|
|
190
|
+
const name = layer.name;
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
setId(DOMUtils.createDomID());
|
|
193
|
+
}, []);
|
|
194
|
+
/**
|
|
195
|
+
* Listen to the changes on the current layer.
|
|
196
|
+
*/
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
const onClientSharedStateChanged = (sender, clients) => {
|
|
199
|
+
// TODO Support follow mode and remoteUser state
|
|
200
|
+
setSelected(isSelected(layerId, gisModel));
|
|
201
|
+
};
|
|
202
|
+
gisModel === null || gisModel === void 0 ? void 0 : gisModel.clientStateChanged.connect(onClientSharedStateChanged);
|
|
203
|
+
return () => {
|
|
204
|
+
gisModel === null || gisModel === void 0 ? void 0 : gisModel.clientStateChanged.disconnect(onClientSharedStateChanged);
|
|
205
|
+
};
|
|
206
|
+
}, [gisModel]);
|
|
207
|
+
/**
|
|
208
|
+
* Toggle layer visibility.
|
|
209
|
+
*/
|
|
210
|
+
const toggleVisibility = () => {
|
|
211
|
+
var _a;
|
|
212
|
+
layer.visible = !layer.visible;
|
|
213
|
+
(_a = gisModel === null || gisModel === void 0 ? void 0 : gisModel.sharedModel) === null || _a === void 0 ? void 0 : _a.updateLayer(layerId, layer);
|
|
214
|
+
};
|
|
215
|
+
const setSelection = (event) => {
|
|
216
|
+
var _a;
|
|
217
|
+
const childId = (_a = event.currentTarget.children.namedItem(id)) === null || _a === void 0 ? void 0 : _a.id;
|
|
218
|
+
onClick({ type: 'layer', item: layerId, nodeId: childId, event });
|
|
219
|
+
};
|
|
220
|
+
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 },
|
|
221
|
+
React.createElement("div", { className: LAYER_TITLE_CLASS, onClick: setSelection, onContextMenu: setSelection },
|
|
222
|
+
icons.has(layer.type) && (React.createElement(LabIcon.resolveReact, Object.assign({}, icons.get(layer.type), { className: LAYER_ICON_CLASS }))),
|
|
223
|
+
React.createElement("span", { id: id, className: LAYER_TEXT_CLASS }, name)),
|
|
224
|
+
React.createElement(Button, { title: layer.visible ? 'Hide layer' : 'Show layer', onClick: toggleVisibility, minimal: true },
|
|
225
|
+
React.createElement(LabIcon.resolveReact, { icon: layer.visible ? visibilityIcon : nonVisibilityIcon, className: LAYER_ICON_CLASS, tag: "span" }))));
|
|
226
|
+
}
|
|
227
|
+
var Private;
|
|
228
|
+
(function (Private) {
|
|
229
|
+
Private.dragIndicator = document.createElement('div');
|
|
230
|
+
Private.dragIndicator.id = 'jp-drag-indicator';
|
|
231
|
+
Private.dragInfo = {
|
|
232
|
+
draggedElement: null,
|
|
233
|
+
dragOverElement: null,
|
|
234
|
+
dragOverPosition: null
|
|
235
|
+
};
|
|
236
|
+
Private.onDragStart = (e) => {
|
|
237
|
+
Private.dragInfo.draggedElement = e.target;
|
|
238
|
+
};
|
|
239
|
+
Private.onDragOver = (e) => {
|
|
240
|
+
var _a;
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
e.stopPropagation();
|
|
243
|
+
const { clientY } = e;
|
|
244
|
+
let target = e.target.closest(`.${LAYER_GROUP_HEADER_CLASS}, .${LAYER_ITEM_CLASS}`);
|
|
245
|
+
if (!target) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Do not allow move a group into itself.
|
|
249
|
+
if ((_a = Private.dragInfo.draggedElement) === null || _a === void 0 ? void 0 : _a.contains(target)) {
|
|
250
|
+
Private.dragInfo.dragOverElement = 'error';
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
Private.dragInfo.dragOverElement = target;
|
|
254
|
+
const boundingBox = target.getBoundingClientRect();
|
|
255
|
+
if (clientY - boundingBox.top < boundingBox.bottom - clientY) {
|
|
256
|
+
Private.dragInfo.dragOverPosition = 'above';
|
|
257
|
+
if (target.classList.contains(LAYER_GROUP_HEADER_CLASS)) {
|
|
258
|
+
target = target.parentNode;
|
|
259
|
+
}
|
|
260
|
+
target.insertAdjacentElement('beforebegin', Private.dragIndicator);
|
|
261
|
+
Private.dragIndicator.style.display = 'block';
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
Private.dragInfo.dragOverPosition = 'below';
|
|
265
|
+
target.insertAdjacentElement('afterend', Private.dragIndicator);
|
|
266
|
+
Private.dragIndicator.style.display = 'block';
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
Private.onDragEnd = () => {
|
|
270
|
+
Private.dragIndicator.style.display = 'none';
|
|
271
|
+
Private.dragInfo.draggedElement = null;
|
|
272
|
+
Private.dragInfo.dragOverElement = null;
|
|
273
|
+
Private.dragInfo.dragOverPosition = null;
|
|
274
|
+
};
|
|
275
|
+
})(Private || (Private = {}));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Panel } from '@lumino/widgets';
|
|
2
|
+
import { ILeftPanelOptions } from '../leftpanel';
|
|
3
|
+
/**
|
|
4
|
+
* The sources panel widget.
|
|
5
|
+
*/
|
|
6
|
+
export declare class SourcesPanel extends Panel {
|
|
7
|
+
constructor(options: ILeftPanelOptions);
|
|
8
|
+
private _model;
|
|
9
|
+
private _onSelect;
|
|
10
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { DOMUtils } from '@jupyterlab/apputils';
|
|
2
|
+
import { LabIcon, ReactWidget } from '@jupyterlab/ui-components';
|
|
3
|
+
import { Panel } from '@lumino/widgets';
|
|
4
|
+
import React, { useEffect, useState } from 'react';
|
|
5
|
+
import { icons } from '../../constants';
|
|
6
|
+
const SOURCES_PANEL_CLASS = 'jp-gis-sourcePanel';
|
|
7
|
+
const SOURCE_CLASS = 'jp-gis-source';
|
|
8
|
+
const SOURCE_TITLE_CLASS = 'jp-gis-sourceTitle';
|
|
9
|
+
const SOURCE_ICON_CLASS = 'jp-gis-sourceIcon';
|
|
10
|
+
const SOURCE_TEXT_CLASS = 'jp-gis-sourceText';
|
|
11
|
+
const SOURCE_UNUSED = 'jp-gis-sourceUnused';
|
|
12
|
+
const SOURCE_INFO = 'jp-gis-sourceInfo';
|
|
13
|
+
/**
|
|
14
|
+
* The sources panel widget.
|
|
15
|
+
*/
|
|
16
|
+
export class SourcesPanel extends Panel {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
super();
|
|
19
|
+
this._model = options.model;
|
|
20
|
+
this._onSelect = options.onSelect;
|
|
21
|
+
this.id = 'jupytergis::sourcesPanel';
|
|
22
|
+
this.addClass(SOURCES_PANEL_CLASS);
|
|
23
|
+
this.addWidget(ReactWidget.create(React.createElement(SourcesBodyComponent, { model: this._model, onSelect: this._onSelect })));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The body component of the panel.
|
|
28
|
+
*/
|
|
29
|
+
function SourcesBodyComponent(props) {
|
|
30
|
+
var _a, _b;
|
|
31
|
+
const [model, setModel] = useState((_a = props.model) === null || _a === void 0 ? void 0 : _a.jGISModel);
|
|
32
|
+
const [sourceIds, setSourceIds] = useState(Private.sortedSourceIds(model));
|
|
33
|
+
/**
|
|
34
|
+
* Propagate the source selection.
|
|
35
|
+
*/
|
|
36
|
+
const onItemClick = ({ type, item, nodeId, event }) => {
|
|
37
|
+
props.onSelect({ type, item, nodeId, event });
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Listen to the sources and changes.
|
|
41
|
+
*/
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const updateSources = () => {
|
|
44
|
+
setSourceIds([...Private.sortedSourceIds(model)]);
|
|
45
|
+
};
|
|
46
|
+
model === null || model === void 0 ? void 0 : model.sharedModel.sourcesChanged.connect(updateSources);
|
|
47
|
+
model === null || model === void 0 ? void 0 : model.clientStateChanged.connect(updateSources);
|
|
48
|
+
updateSources();
|
|
49
|
+
return () => {
|
|
50
|
+
model === null || model === void 0 ? void 0 : model.sharedModel.sourcesChanged.disconnect(updateSources);
|
|
51
|
+
model === null || model === void 0 ? void 0 : model.clientStateChanged.disconnect(updateSources);
|
|
52
|
+
};
|
|
53
|
+
}, [model]);
|
|
54
|
+
/**
|
|
55
|
+
* Update the model when it changes.
|
|
56
|
+
*/
|
|
57
|
+
(_b = props.model) === null || _b === void 0 ? void 0 : _b.documentChanged.connect((_, widget) => {
|
|
58
|
+
setModel(widget === null || widget === void 0 ? void 0 : widget.context.model);
|
|
59
|
+
});
|
|
60
|
+
return (React.createElement("div", { id: "jp-gis-sources" }, sourceIds.map(sourceId => {
|
|
61
|
+
return (React.createElement(SourceComponent, { key: `source-${sourceId}`, gisModel: model, sourceId: sourceId, onClick: onItemClick }));
|
|
62
|
+
})));
|
|
63
|
+
}
|
|
64
|
+
function isSelected(sourceId, model) {
|
|
65
|
+
var _a, _b, _c, _d;
|
|
66
|
+
return ((((_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) &&
|
|
67
|
+
Object.keys((_d = (_c = model === null || model === void 0 ? void 0 : model.localState) === null || _c === void 0 ? void 0 : _c.selected) === null || _d === void 0 ? void 0 : _d.value).includes(sourceId)) ||
|
|
68
|
+
false);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The component to display a single source.
|
|
72
|
+
*/
|
|
73
|
+
function SourceComponent(props) {
|
|
74
|
+
const { sourceId, gisModel, onClick } = props;
|
|
75
|
+
const source = gisModel === null || gisModel === void 0 ? void 0 : gisModel.getSource(sourceId);
|
|
76
|
+
if (source === undefined) {
|
|
77
|
+
return React.createElement(React.Fragment, null);
|
|
78
|
+
}
|
|
79
|
+
const [id, setId] = useState('');
|
|
80
|
+
const [selected, setSelected] = useState(
|
|
81
|
+
// TODO Support multi-selection as `model?.jGISModel?.localState?.selected.value` does
|
|
82
|
+
isSelected(sourceId, gisModel));
|
|
83
|
+
const [unused, setUnused] = useState(false);
|
|
84
|
+
const name = source.name;
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
setId(DOMUtils.createDomID());
|
|
87
|
+
}, []);
|
|
88
|
+
/**
|
|
89
|
+
* Check if the source is used by a layer.
|
|
90
|
+
*/
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const checkUsage = () => {
|
|
93
|
+
const sources = gisModel === null || gisModel === void 0 ? void 0 : gisModel.getLayersBySource(sourceId);
|
|
94
|
+
setUnused(sources ? sources.length === 0 : true);
|
|
95
|
+
};
|
|
96
|
+
gisModel === null || gisModel === void 0 ? void 0 : gisModel.sharedLayersChanged.connect(checkUsage);
|
|
97
|
+
checkUsage();
|
|
98
|
+
return () => {
|
|
99
|
+
gisModel === null || gisModel === void 0 ? void 0 : gisModel.sharedLayersChanged.disconnect(checkUsage);
|
|
100
|
+
};
|
|
101
|
+
}, [gisModel]);
|
|
102
|
+
/**
|
|
103
|
+
* Listen to the changes on the current source.
|
|
104
|
+
*/
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const onClientSharedStateChanged = (sender, clients) => {
|
|
107
|
+
// TODO Support follow mode and remoteUser state
|
|
108
|
+
setSelected(isSelected(sourceId, gisModel));
|
|
109
|
+
};
|
|
110
|
+
gisModel === null || gisModel === void 0 ? void 0 : gisModel.clientStateChanged.connect(onClientSharedStateChanged);
|
|
111
|
+
return () => {
|
|
112
|
+
gisModel === null || gisModel === void 0 ? void 0 : gisModel.clientStateChanged.disconnect(onClientSharedStateChanged);
|
|
113
|
+
};
|
|
114
|
+
}, [gisModel]);
|
|
115
|
+
const setSelection = (event) => {
|
|
116
|
+
var _a;
|
|
117
|
+
const childId = (_a = event.currentTarget.children.namedItem(id)) === null || _a === void 0 ? void 0 : _a.id;
|
|
118
|
+
onClick({ type: 'source', item: sourceId, nodeId: childId, event });
|
|
119
|
+
};
|
|
120
|
+
let mainClasses = SOURCE_CLASS;
|
|
121
|
+
if (selected) {
|
|
122
|
+
mainClasses = mainClasses.concat(' jp-mod-selected');
|
|
123
|
+
}
|
|
124
|
+
if (unused) {
|
|
125
|
+
mainClasses = mainClasses.concat(` ${SOURCE_UNUSED}`);
|
|
126
|
+
}
|
|
127
|
+
return (React.createElement("div", { className: mainClasses },
|
|
128
|
+
React.createElement("div", { className: SOURCE_TITLE_CLASS, onClick: setSelection, onContextMenu: setSelection },
|
|
129
|
+
icons.has(source.type) && (React.createElement(LabIcon.resolveReact, Object.assign({}, icons.get(source.type), { className: SOURCE_ICON_CLASS }))),
|
|
130
|
+
React.createElement("span", { id: id, className: SOURCE_TEXT_CLASS }, name),
|
|
131
|
+
unused && React.createElement("span", { className: SOURCE_INFO }, "(unused)"))));
|
|
132
|
+
}
|
|
133
|
+
var Private;
|
|
134
|
+
(function (Private) {
|
|
135
|
+
function sortedSourceIds(model) {
|
|
136
|
+
const sources = model === null || model === void 0 ? void 0 : model.getSources();
|
|
137
|
+
if (sources === undefined) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
return Object.keys(sources).sort((id1, id2) => {
|
|
141
|
+
const name1 = sources[id1].name.toLowerCase();
|
|
142
|
+
const name2 = sources[id2].name.toLowerCase();
|
|
143
|
+
return name1 < name2 ? -1 : name1 > name2 ? 1 : 0;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
Private.sortedSourceIds = sortedSourceIds;
|
|
147
|
+
})(Private || (Private = {}));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Widget } from '@lumino/widgets';
|
|
2
|
+
export declare class ControlPanelHeader extends Widget {
|
|
3
|
+
/**
|
|
4
|
+
* Instantiate a new sidebar header.
|
|
5
|
+
*/
|
|
6
|
+
constructor();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Create a sidebar header node.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createHeader(): HTMLElement;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Widget } from '@lumino/widgets';
|
|
2
|
+
export class ControlPanelHeader extends Widget {
|
|
3
|
+
/**
|
|
4
|
+
* Instantiate a new sidebar header.
|
|
5
|
+
*/
|
|
6
|
+
constructor() {
|
|
7
|
+
super({ node: createHeader() });
|
|
8
|
+
this.title.changed.connect(_ => {
|
|
9
|
+
this.node.textContent = this.title.label;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create a sidebar header node.
|
|
15
|
+
*/
|
|
16
|
+
export function createHeader() {
|
|
17
|
+
const title = document.createElement('h2');
|
|
18
|
+
title.textContent = '-';
|
|
19
|
+
return title;
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { IJupyterGISTracker, JupyterGISDoc, SelectionType } from '@jupytergis/schema';
|
|
2
|
+
import { IStateDB } from '@jupyterlab/statedb';
|
|
3
|
+
import { SidePanel } from '@jupyterlab/ui-components';
|
|
4
|
+
import { Message } from '@lumino/messaging';
|
|
5
|
+
import { MouseEvent as ReactMouseEvent } from 'react';
|
|
6
|
+
import { IControlPanelModel } from '../types';
|
|
7
|
+
/**
|
|
8
|
+
* Options of the left panel widget.
|
|
9
|
+
*/
|
|
10
|
+
export interface ILeftPanelOptions {
|
|
11
|
+
model: IControlPanelModel;
|
|
12
|
+
onSelect: ({ type, item, nodeId }: ILeftPanelClickHandlerParams) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface ILayerPanelOptions extends ILeftPanelOptions {
|
|
15
|
+
state: IStateDB;
|
|
16
|
+
}
|
|
17
|
+
export interface ILeftPanelClickHandlerParams {
|
|
18
|
+
type: SelectionType;
|
|
19
|
+
item: string;
|
|
20
|
+
nodeId?: string;
|
|
21
|
+
event: ReactMouseEvent;
|
|
22
|
+
}
|
|
23
|
+
export declare class LeftPanelWidget extends SidePanel {
|
|
24
|
+
constructor(options: LeftPanelWidget.IOptions);
|
|
25
|
+
dispose(): void;
|
|
26
|
+
protected onAfterAttach(msg: Message): void;
|
|
27
|
+
protected onBeforeDetach(msg: Message): void;
|
|
28
|
+
handleEvent(event: Event): void;
|
|
29
|
+
private _mouseUpEvent;
|
|
30
|
+
/**
|
|
31
|
+
* Function to call when a layer is selected from a component of the panel.
|
|
32
|
+
*
|
|
33
|
+
* @param item - the selected layer or group.
|
|
34
|
+
*/
|
|
35
|
+
private _onSelect;
|
|
36
|
+
resetSelected(type: SelectionType, nodeId?: string, item?: string): void;
|
|
37
|
+
private _lastSelectedNodeId;
|
|
38
|
+
private _model;
|
|
39
|
+
private _state;
|
|
40
|
+
}
|
|
41
|
+
export declare namespace LeftPanelWidget {
|
|
42
|
+
interface IOptions {
|
|
43
|
+
model: IControlPanelModel;
|
|
44
|
+
tracker: IJupyterGISTracker;
|
|
45
|
+
state: IStateDB;
|
|
46
|
+
}
|
|
47
|
+
interface IProps {
|
|
48
|
+
filePath?: string;
|
|
49
|
+
sharedModel?: JupyterGISDoc;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { SidePanel } from '@jupyterlab/ui-components';
|
|
2
|
+
import { LayersPanel } from './components/layers';
|
|
3
|
+
import { SourcesPanel } from './components/sources';
|
|
4
|
+
import { ControlPanelHeader } from './header';
|
|
5
|
+
export class LeftPanelWidget extends SidePanel {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
super();
|
|
8
|
+
/**
|
|
9
|
+
* Function to call when a layer is selected from a component of the panel.
|
|
10
|
+
*
|
|
11
|
+
* @param item - the selected layer or group.
|
|
12
|
+
*/
|
|
13
|
+
this._onSelect = ({ type, item, nodeId, event }) => {
|
|
14
|
+
var _a, _b;
|
|
15
|
+
if (!this._model || !nodeId) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const { jGISModel } = this._model;
|
|
19
|
+
const selectedValue = (_b = (_a = jGISModel === null || jGISModel === void 0 ? void 0 : jGISModel.localState) === null || _a === void 0 ? void 0 : _a.selected) === null || _b === void 0 ? void 0 : _b.value;
|
|
20
|
+
const node = document.getElementById(nodeId);
|
|
21
|
+
if (!node) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
node.tabIndex = 0;
|
|
25
|
+
node.focus();
|
|
26
|
+
// Early return if no selection exists
|
|
27
|
+
if (!selectedValue) {
|
|
28
|
+
this.resetSelected(type, nodeId, item);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Don't want to reset selected if right clicking a selected item
|
|
32
|
+
if (!event.ctrlKey && event.button === 2 && item in selectedValue) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Reset selection for normal left click
|
|
36
|
+
if (!event.ctrlKey) {
|
|
37
|
+
this.resetSelected(type, nodeId, item);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (nodeId) {
|
|
41
|
+
// Check if new selection is the same type as previous selections
|
|
42
|
+
const isSelectedSameType = Object.values(selectedValue).some(selection => selection.type === type);
|
|
43
|
+
if (!isSelectedSameType) {
|
|
44
|
+
// Selecting a new type, so reset selected
|
|
45
|
+
this.resetSelected(type, nodeId, item);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// If types are the same add the selection
|
|
49
|
+
const updatedSelectedValue = Object.assign(Object.assign({}, selectedValue), { [item]: { type, selectedNodeId: nodeId } });
|
|
50
|
+
this._lastSelectedNodeId = nodeId;
|
|
51
|
+
jGISModel.syncSelected(updatedSelectedValue, this.id);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
this.addClass('jGIS-sidepanel-widget');
|
|
55
|
+
this._model = options.model;
|
|
56
|
+
this._state = options.state;
|
|
57
|
+
const header = new ControlPanelHeader();
|
|
58
|
+
this.header.addWidget(header);
|
|
59
|
+
const sourcesPanel = new SourcesPanel({
|
|
60
|
+
model: this._model,
|
|
61
|
+
onSelect: this._onSelect
|
|
62
|
+
});
|
|
63
|
+
sourcesPanel.title.caption = 'Sources';
|
|
64
|
+
sourcesPanel.title.label = 'Sources';
|
|
65
|
+
this.addWidget(sourcesPanel);
|
|
66
|
+
const layerTree = new LayersPanel({
|
|
67
|
+
model: this._model,
|
|
68
|
+
state: this._state,
|
|
69
|
+
onSelect: this._onSelect
|
|
70
|
+
});
|
|
71
|
+
layerTree.title.caption = 'Layer tree';
|
|
72
|
+
layerTree.title.label = 'Layers';
|
|
73
|
+
this.addWidget(layerTree);
|
|
74
|
+
options.tracker.currentChanged.connect((_, changed) => {
|
|
75
|
+
if (changed) {
|
|
76
|
+
header.title.label = changed.context.localPath;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
header.title.label = '-';
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
dispose() {
|
|
84
|
+
super.dispose();
|
|
85
|
+
}
|
|
86
|
+
onAfterAttach(msg) {
|
|
87
|
+
super.onAfterAttach(msg);
|
|
88
|
+
const node = this.node;
|
|
89
|
+
node.addEventListener('mouseup', this);
|
|
90
|
+
}
|
|
91
|
+
onBeforeDetach(msg) {
|
|
92
|
+
super.onBeforeDetach(msg);
|
|
93
|
+
const node = this.node;
|
|
94
|
+
node.removeEventListener('mouseup', this);
|
|
95
|
+
}
|
|
96
|
+
handleEvent(event) {
|
|
97
|
+
switch (event.type) {
|
|
98
|
+
case 'mouseup':
|
|
99
|
+
this._mouseUpEvent(event);
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
_mouseUpEvent(event) {
|
|
106
|
+
// If we click on empty space in the layer panel, keep the focus on the last selected element
|
|
107
|
+
const node = document.getElementById(this._lastSelectedNodeId);
|
|
108
|
+
if (!node) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
node.focus();
|
|
112
|
+
}
|
|
113
|
+
resetSelected(type, nodeId, item) {
|
|
114
|
+
var _a, _b;
|
|
115
|
+
const selection = {};
|
|
116
|
+
if (item && nodeId) {
|
|
117
|
+
selection[item] = {
|
|
118
|
+
type,
|
|
119
|
+
selectedNodeId: nodeId
|
|
120
|
+
};
|
|
121
|
+
this._lastSelectedNodeId = nodeId;
|
|
122
|
+
}
|
|
123
|
+
(_b = (_a = this._model) === null || _a === void 0 ? void 0 : _a.jGISModel) === null || _b === void 0 ? void 0 : _b.syncSelected(selection, this.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { IJupyterGISDoc, IJupyterGISModel, IJupyterGISTracker, IJupyterGISWidget } from '@jupytergis/schema';
|
|
2
|
+
import { ISignal } from '@lumino/signaling';
|
|
3
|
+
import { IControlPanelModel } from '../types';
|
|
4
|
+
export declare class ControlPanelModel implements IControlPanelModel {
|
|
5
|
+
constructor(options: ControlPanelModel.IOptions);
|
|
6
|
+
get documentChanged(): ISignal<IJupyterGISTracker, IJupyterGISWidget | null>;
|
|
7
|
+
get filePath(): string | undefined;
|
|
8
|
+
get jGISModel(): IJupyterGISModel | undefined;
|
|
9
|
+
get sharedModel(): IJupyterGISDoc | undefined;
|
|
10
|
+
disconnect(f: any): void;
|
|
11
|
+
private readonly _tracker;
|
|
12
|
+
private _documentChanged;
|
|
13
|
+
}
|
|
14
|
+
declare namespace ControlPanelModel {
|
|
15
|
+
interface IOptions {
|
|
16
|
+
tracker: IJupyterGISTracker;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export {};
|