@jupytergis/base 0.1.7 → 0.2.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.
- package/lib/annotations/components/Annotation.d.ts +11 -0
- package/lib/annotations/components/Annotation.js +61 -0
- package/lib/annotations/components/AnnotationFloater.d.ts +7 -0
- package/lib/annotations/components/AnnotationFloater.js +30 -0
- package/lib/annotations/components/Message.d.ts +8 -0
- package/lib/annotations/components/Message.js +17 -0
- package/lib/annotations/index.d.ts +3 -0
- package/lib/annotations/index.js +3 -0
- package/lib/annotations/model.d.ts +28 -0
- package/lib/annotations/model.js +67 -0
- package/lib/commands.js +51 -6
- package/lib/constants.d.ts +2 -0
- package/lib/constants.js +5 -1
- package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +25 -33
- package/lib/formbuilder/formselectors.js +4 -0
- package/lib/formbuilder/objectform/baseform.d.ts +1 -1
- package/lib/formbuilder/objectform/baseform.js +31 -42
- package/lib/formbuilder/objectform/geojsonsource.js +33 -30
- package/lib/formbuilder/objectform/geotiffsource.d.ts +16 -0
- package/lib/formbuilder/objectform/geotiffsource.js +71 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/mainview/CollaboratorPointers.d.ts +17 -0
- package/lib/mainview/CollaboratorPointers.js +37 -0
- package/lib/mainview/FollowIndicator.d.ts +7 -0
- package/lib/mainview/FollowIndicator.js +9 -0
- package/lib/mainview/mainView.d.ts +36 -2
- package/lib/mainview/mainView.js +389 -27
- package/lib/mainview/mainviewmodel.d.ts +2 -1
- package/lib/mainview/mainviewmodel.js +5 -0
- package/lib/panelview/annotationPanel.d.ts +27 -0
- package/lib/panelview/annotationPanel.js +45 -0
- package/lib/panelview/components/filter-panel/Filter.d.ts +7 -2
- package/lib/panelview/components/filter-panel/Filter.js +1 -1
- package/lib/panelview/components/filter-panel/FilterRow.js +3 -3
- package/lib/panelview/components/identify-panel/IdentifyPanel.d.ts +15 -0
- package/lib/panelview/components/identify-panel/IdentifyPanel.js +108 -0
- package/lib/panelview/components/layers.js +4 -4
- package/lib/panelview/leftpanel.js +8 -0
- package/lib/panelview/rightpanel.d.ts +4 -1
- package/lib/panelview/rightpanel.js +28 -7
- package/lib/toolbar/widget.js +11 -1
- package/lib/tools.d.ts +35 -0
- package/lib/tools.js +86 -0
- package/package.json +5 -6
- package/style/base.css +4 -8
- package/style/dialog.css +1 -1
- package/style/icons/logo_mini.svg +70 -148
- package/style/icons/nonvisibility.svg +2 -7
- package/style/icons/visibility.svg +2 -6
- package/style/leftPanel.css +5 -0
package/lib/mainview/mainView.js
CHANGED
|
@@ -3,7 +3,7 @@ import { UUID } from '@lumino/coreutils';
|
|
|
3
3
|
import { Collection, Map as OlMap, View } from 'ol';
|
|
4
4
|
import { ScaleLine } from 'ol/control';
|
|
5
5
|
import { GeoJSON, MVT } from 'ol/format';
|
|
6
|
-
import DragAndDrop from 'ol/interaction
|
|
6
|
+
import { DragAndDrop, Select } from 'ol/interaction';
|
|
7
7
|
import { Image as ImageLayer, Vector as VectorLayer, VectorTile as VectorTileLayer, WebGLTile as WebGlTileLayer } from 'ol/layer';
|
|
8
8
|
import TileLayer from 'ol/layer/Tile';
|
|
9
9
|
import { fromLonLat, toLonLat } from 'ol/proj';
|
|
@@ -17,13 +17,47 @@ import { get as getProjection } from 'ol/proj.js';
|
|
|
17
17
|
import proj4 from 'proj4';
|
|
18
18
|
import * as React from 'react';
|
|
19
19
|
import shp from 'shpjs';
|
|
20
|
-
import { isLightTheme } from '../tools';
|
|
20
|
+
import { isLightTheme, loadGeoTIFFWithCache, throttle } from '../tools';
|
|
21
21
|
import { Spinner } from './spinner';
|
|
22
22
|
//@ts-expect-error no types for proj4-list
|
|
23
23
|
import proj4list from 'proj4-list';
|
|
24
|
+
import { ContextMenu } from '@lumino/widgets';
|
|
25
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
26
|
+
import AnnotationFloater from '../annotations/components/AnnotationFloater';
|
|
27
|
+
import { CommandIDs } from '../constants';
|
|
28
|
+
import { FollowIndicator } from './FollowIndicator';
|
|
29
|
+
import CollaboratorPointers from './CollaboratorPointers';
|
|
30
|
+
import { Circle, Fill, Stroke, Style } from 'ol/style';
|
|
31
|
+
import { singleClick } from 'ol/events/condition';
|
|
24
32
|
export class MainView extends React.Component {
|
|
25
33
|
constructor(props) {
|
|
26
34
|
super(props);
|
|
35
|
+
this.addContextMenu = () => {
|
|
36
|
+
this._commands.addCommand(CommandIDs.addAnnotation, {
|
|
37
|
+
execute: () => {
|
|
38
|
+
var _a;
|
|
39
|
+
if (!this._Map) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this._mainViewModel.addAnnotation({
|
|
43
|
+
position: { x: this._clickCoords[0], y: this._clickCoords[1] },
|
|
44
|
+
zoom: (_a = this._Map.getView().getZoom()) !== null && _a !== void 0 ? _a : 0,
|
|
45
|
+
label: 'New annotation',
|
|
46
|
+
contents: [],
|
|
47
|
+
parent: this._Map.getViewport().id
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
label: 'Add annotation',
|
|
51
|
+
isEnabled: () => {
|
|
52
|
+
return !!this._Map;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
this._contextMenu.addItem({
|
|
56
|
+
command: CommandIDs.addAnnotation,
|
|
57
|
+
selector: '.ol-viewport',
|
|
58
|
+
rank: 1
|
|
59
|
+
});
|
|
60
|
+
};
|
|
27
61
|
this.vectorLayerStyleRuleBuilder = (layer) => {
|
|
28
62
|
const layerParams = layer.parameters;
|
|
29
63
|
if (!layerParams) {
|
|
@@ -121,7 +155,71 @@ export class MainView extends React.Component {
|
|
|
121
155
|
return scaled;
|
|
122
156
|
};
|
|
123
157
|
this._onClientSharedStateChanged = (sender, clients) => {
|
|
124
|
-
|
|
158
|
+
var _a, _b, _c, _d, _e;
|
|
159
|
+
const remoteUser = (_a = this._model.localState) === null || _a === void 0 ? void 0 : _a.remoteUser;
|
|
160
|
+
// If we are in following mode, we update our position and selection
|
|
161
|
+
if (remoteUser) {
|
|
162
|
+
const remoteState = clients.get(remoteUser);
|
|
163
|
+
if (!remoteState) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (((_b = remoteState.user) === null || _b === void 0 ? void 0 : _b.username) !== ((_c = this.state.remoteUser) === null || _c === void 0 ? void 0 : _c.username)) {
|
|
167
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { remoteUser: remoteState.user })));
|
|
168
|
+
}
|
|
169
|
+
const remoteViewport = remoteState.viewportState;
|
|
170
|
+
if (remoteViewport.value) {
|
|
171
|
+
const { x, y } = remoteViewport.value.coordinates;
|
|
172
|
+
const zoom = remoteViewport.value.zoom;
|
|
173
|
+
this._moveToPosition({ x, y }, zoom, 0);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
// If we are unfollowing a remote user, we reset our center and zoom to their previous values
|
|
178
|
+
if (this.state.remoteUser !== null) {
|
|
179
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { remoteUser: null })));
|
|
180
|
+
const viewportState = (_e = (_d = this._model.localState) === null || _d === void 0 ? void 0 : _d.viewportState) === null || _e === void 0 ? void 0 : _e.value;
|
|
181
|
+
if (viewportState) {
|
|
182
|
+
this._moveToPosition(viewportState.coordinates, viewportState.zoom);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// cursors
|
|
187
|
+
clients.forEach((client, clientId) => {
|
|
188
|
+
var _a;
|
|
189
|
+
if (!(client === null || client === void 0 ? void 0 : client.user)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const pointer = (_a = client.pointer) === null || _a === void 0 ? void 0 : _a.value;
|
|
193
|
+
// We already display our own cursor on mouse move
|
|
194
|
+
if (this._model.getClientId() === clientId) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const clientPointers = this.state.clientPointers;
|
|
198
|
+
let currentClientPointer = clientPointers[clientId];
|
|
199
|
+
if (pointer) {
|
|
200
|
+
const pixel = this._Map.getPixelFromCoordinate([
|
|
201
|
+
pointer.coordinates.x,
|
|
202
|
+
pointer.coordinates.y
|
|
203
|
+
]);
|
|
204
|
+
const lonLat = toLonLat([pointer.coordinates.x, pointer.coordinates.y]);
|
|
205
|
+
if (!currentClientPointer) {
|
|
206
|
+
currentClientPointer = clientPointers[clientId] = {
|
|
207
|
+
username: client.user.username,
|
|
208
|
+
displayName: client.user.display_name,
|
|
209
|
+
color: client.user.color,
|
|
210
|
+
coordinates: { x: pixel[0], y: pixel[1] },
|
|
211
|
+
lonLat: { longitude: lonLat[0], latitude: lonLat[1] }
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
currentClientPointer.coordinates.x = pixel[0];
|
|
215
|
+
currentClientPointer.coordinates.y = pixel[1];
|
|
216
|
+
clientPointers[clientId] = currentClientPointer;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
delete clientPointers[clientId];
|
|
220
|
+
}
|
|
221
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { clientPointers: clientPointers })));
|
|
222
|
+
});
|
|
125
223
|
};
|
|
126
224
|
this._onSharedModelStateChange = (_, change) => {
|
|
127
225
|
var _a;
|
|
@@ -140,6 +238,34 @@ export class MainView extends React.Component {
|
|
|
140
238
|
}
|
|
141
239
|
}
|
|
142
240
|
};
|
|
241
|
+
this._onSharedMetadataChanged = (_, changes) => {
|
|
242
|
+
const newState = Object.assign({}, this.state.annotations);
|
|
243
|
+
changes.forEach((val, key) => {
|
|
244
|
+
if (!key.startsWith('annotation')) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const data = this._model.sharedModel.getMetadata(key);
|
|
248
|
+
let open = true;
|
|
249
|
+
if (this.state.firstLoad) {
|
|
250
|
+
open = false;
|
|
251
|
+
}
|
|
252
|
+
if (data && (val.action === 'add' || val.action === 'update')) {
|
|
253
|
+
const jsonData = JSON.parse(data);
|
|
254
|
+
jsonData['open'] = open;
|
|
255
|
+
newState[key] = jsonData;
|
|
256
|
+
}
|
|
257
|
+
else if (val.action === 'delete') {
|
|
258
|
+
delete newState[key];
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { annotations: newState, firstLoad: false })));
|
|
262
|
+
};
|
|
263
|
+
this._syncPointer = throttle((coordinates) => {
|
|
264
|
+
const pointer = {
|
|
265
|
+
coordinates: { x: coordinates[0], y: coordinates[1] }
|
|
266
|
+
};
|
|
267
|
+
this._model.syncPointer(pointer);
|
|
268
|
+
});
|
|
143
269
|
this._handleThemeChange = () => {
|
|
144
270
|
const lightTheme = isLightTheme();
|
|
145
271
|
// TODO SOMETHING
|
|
@@ -163,18 +289,25 @@ export class MainView extends React.Component {
|
|
|
163
289
|
this._model.sharedLayersChanged.connect(this._onLayersChanged, this);
|
|
164
290
|
this._model.sharedLayerTreeChanged.connect(this._onLayerTreeChange, this);
|
|
165
291
|
this._model.sharedSourcesChanged.connect(this._onSourcesChange, this);
|
|
292
|
+
this._model.sharedModel.changed.connect(this._onSharedModelStateChange);
|
|
293
|
+
this._mainViewModel.jGISModel.sharedMetadataChanged.connect(this._onSharedMetadataChanged, this);
|
|
294
|
+
this._model.zoomToAnnotationSignal.connect(this._onZoomToAnnotation, this);
|
|
166
295
|
this.state = {
|
|
167
296
|
id: this._mainViewModel.id,
|
|
168
297
|
lightTheme: isLightTheme(),
|
|
169
298
|
loading: true,
|
|
170
|
-
firstLoad: true
|
|
299
|
+
firstLoad: true,
|
|
300
|
+
annotations: {},
|
|
301
|
+
clientPointers: {}
|
|
171
302
|
};
|
|
172
303
|
this._sources = [];
|
|
173
|
-
this.
|
|
304
|
+
this._commands = new CommandRegistry();
|
|
305
|
+
this._contextMenu = new ContextMenu({ commands: this._commands });
|
|
174
306
|
}
|
|
175
307
|
async componentDidMount() {
|
|
176
308
|
window.addEventListener('resize', this._handleWindowResize);
|
|
177
309
|
await this.generateScene();
|
|
310
|
+
this.addContextMenu();
|
|
178
311
|
this._mainViewModel.initSignal();
|
|
179
312
|
if (window.jupytergisMaps !== undefined && this._documentPath) {
|
|
180
313
|
window.jupytergisMaps[this._documentPath] = this._Map;
|
|
@@ -202,6 +335,7 @@ export class MainView extends React.Component {
|
|
|
202
335
|
}),
|
|
203
336
|
controls: [new ScaleLine()]
|
|
204
337
|
});
|
|
338
|
+
// Add map interactions
|
|
205
339
|
const dragAndDropInteraction = new DragAndDrop({
|
|
206
340
|
formatConstructors: [GeoJSON]
|
|
207
341
|
});
|
|
@@ -226,10 +360,68 @@ export class MainView extends React.Component {
|
|
|
226
360
|
source: sourceId
|
|
227
361
|
}
|
|
228
362
|
};
|
|
229
|
-
this.addLayer(layerId, layerModel, this.
|
|
363
|
+
this.addLayer(layerId, layerModel, this.getLayerIDs().length);
|
|
230
364
|
this._model.addLayer(layerId, layerModel);
|
|
231
365
|
});
|
|
232
366
|
this._Map.addInteraction(dragAndDropInteraction);
|
|
367
|
+
const selectInteraction = new Select({
|
|
368
|
+
hitTolerance: 5,
|
|
369
|
+
multi: true,
|
|
370
|
+
layers: layer => {
|
|
371
|
+
var _a, _b;
|
|
372
|
+
const localState = (_a = this._model) === null || _a === void 0 ? void 0 : _a.sharedModel.awareness.getLocalState();
|
|
373
|
+
const selectedLayers = (_b = localState === null || localState === void 0 ? void 0 : localState.selected) === null || _b === void 0 ? void 0 : _b.value;
|
|
374
|
+
if (!selectedLayers) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
const selectedLayerId = Object.keys(selectedLayers)[0];
|
|
378
|
+
return layer === this.getLayer(selectedLayerId);
|
|
379
|
+
},
|
|
380
|
+
condition: (event) => {
|
|
381
|
+
return singleClick(event) && this._model.isIdentifying;
|
|
382
|
+
},
|
|
383
|
+
style: new Style({
|
|
384
|
+
image: new Circle({
|
|
385
|
+
radius: 5,
|
|
386
|
+
fill: new Fill({
|
|
387
|
+
color: '#C52707'
|
|
388
|
+
}),
|
|
389
|
+
stroke: new Stroke({
|
|
390
|
+
color: '#171717',
|
|
391
|
+
width: 2
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
})
|
|
395
|
+
});
|
|
396
|
+
selectInteraction.on('select', event => {
|
|
397
|
+
const identifiedFeatures = [];
|
|
398
|
+
selectInteraction.getFeatures().forEach(feature => {
|
|
399
|
+
identifiedFeatures.push(feature.getProperties());
|
|
400
|
+
});
|
|
401
|
+
this._model.syncIdentifiedFeatures(identifiedFeatures, this._mainViewModel.id);
|
|
402
|
+
});
|
|
403
|
+
this._Map.addInteraction(selectInteraction);
|
|
404
|
+
const view = this._Map.getView();
|
|
405
|
+
// TODO: Note for the future, will need to update listeners if view changes
|
|
406
|
+
view.on('change:center', throttle(() => {
|
|
407
|
+
var _a;
|
|
408
|
+
// Not syncing center if following someone else
|
|
409
|
+
if ((_a = this._model.localState) === null || _a === void 0 ? void 0 : _a.remoteUser) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const view = this._Map.getView();
|
|
413
|
+
const center = view.getCenter();
|
|
414
|
+
const zoom = view.getZoom();
|
|
415
|
+
if (!center || !zoom) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
this._model.syncViewport({ coordinates: { x: center[0], y: center[1] }, zoom }, this._mainViewModel.id);
|
|
419
|
+
}));
|
|
420
|
+
this._Map.on('postrender', () => {
|
|
421
|
+
if (this.state.annotations) {
|
|
422
|
+
this._updateAnnotation();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
233
425
|
this._Map.on('moveend', () => {
|
|
234
426
|
if (!this._initializedPosition) {
|
|
235
427
|
return;
|
|
@@ -251,12 +443,23 @@ export class MainView extends React.Component {
|
|
|
251
443
|
updatedOptions.extent = view.calculateExtent();
|
|
252
444
|
this._model.setOptions(Object.assign(Object.assign({}, currentOptions), updatedOptions));
|
|
253
445
|
});
|
|
446
|
+
this._Map.on('click', this._identifyFeature.bind(this));
|
|
447
|
+
this._Map
|
|
448
|
+
.getViewport()
|
|
449
|
+
.addEventListener('pointermove', this._onPointerMove.bind(this));
|
|
254
450
|
if (JupyterGISModel.getOrderedLayerIds(this._model).length !== 0) {
|
|
255
451
|
await this._updateLayersImpl(JupyterGISModel.getOrderedLayerIds(this._model));
|
|
256
452
|
const options = this._model.getOptions();
|
|
257
453
|
this.updateOptions(options);
|
|
258
454
|
this._initializedPosition = true;
|
|
259
455
|
}
|
|
456
|
+
this._Map.getViewport().addEventListener('contextmenu', event => {
|
|
457
|
+
event.preventDefault();
|
|
458
|
+
event.stopPropagation();
|
|
459
|
+
const coordinate = this._Map.getEventCoordinate(event);
|
|
460
|
+
this._clickCoords = coordinate;
|
|
461
|
+
this._contextMenu.open(event);
|
|
462
|
+
});
|
|
260
463
|
this.setState(old => (Object.assign(Object.assign({}, old), { loading: false })));
|
|
261
464
|
}
|
|
262
465
|
}
|
|
@@ -272,6 +475,10 @@ export class MainView extends React.Component {
|
|
|
272
475
|
throw error;
|
|
273
476
|
}
|
|
274
477
|
}
|
|
478
|
+
async _loadGeoTIFFWithCache(sourceInfo) {
|
|
479
|
+
const result = await loadGeoTIFFWithCache(sourceInfo);
|
|
480
|
+
return result === null || result === void 0 ? void 0 : result.file;
|
|
481
|
+
}
|
|
275
482
|
/**
|
|
276
483
|
* Add a source in the map.
|
|
277
484
|
*
|
|
@@ -397,8 +604,12 @@ export class MainView extends React.Component {
|
|
|
397
604
|
const addNoData = (url) => {
|
|
398
605
|
return Object.assign(Object.assign({}, url), { nodata: 0 });
|
|
399
606
|
};
|
|
607
|
+
const sourcesWithBlobs = await Promise.all(sourceParameters.urls.map(async (sourceInfo) => {
|
|
608
|
+
const blob = await this._loadGeoTIFFWithCache(sourceInfo);
|
|
609
|
+
return Object.assign(Object.assign({}, addNoData(sourceInfo)), { blob });
|
|
610
|
+
}));
|
|
400
611
|
newSource = new GeoTIFFSource({
|
|
401
|
-
sources:
|
|
612
|
+
sources: sourcesWithBlobs,
|
|
402
613
|
normalize: sourceParameters.normalize,
|
|
403
614
|
wrapX: sourceParameters.wrapX
|
|
404
615
|
});
|
|
@@ -462,20 +673,45 @@ export class MainView extends React.Component {
|
|
|
462
673
|
updateLayers(layerIds) {
|
|
463
674
|
this._updateLayersImpl(layerIds);
|
|
464
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Updates the position and existence of layers in the OL map based on the layer IDs.
|
|
678
|
+
*
|
|
679
|
+
* @param layerIds - An array of layer IDs that should be present on the map.
|
|
680
|
+
* @returns {} Nothing is returned.
|
|
681
|
+
*/
|
|
465
682
|
async _updateLayersImpl(layerIds) {
|
|
466
|
-
|
|
467
|
-
|
|
683
|
+
// get layers that are currently on the OL map
|
|
684
|
+
const previousLayerIds = this.getLayerIDs();
|
|
685
|
+
// Iterate over the new layer IDs:
|
|
686
|
+
// * Add layers to the map that are present in the list but not the map.
|
|
687
|
+
// * Remove layers from the map that are present in the map but not the list.
|
|
688
|
+
// * Update layer positions to match the list.
|
|
689
|
+
for (let targetLayerPosition = 0; targetLayerPosition < layerIds.length; targetLayerPosition++) {
|
|
690
|
+
const layerId = layerIds[targetLayerPosition];
|
|
468
691
|
const layer = this._model.sharedModel.getLayer(layerId);
|
|
469
692
|
if (!layer) {
|
|
470
|
-
console.
|
|
693
|
+
console.warn(`Layer with ID ${layerId} does not exist in the shared model.`);
|
|
471
694
|
continue;
|
|
472
695
|
}
|
|
473
|
-
const
|
|
474
|
-
if (
|
|
475
|
-
|
|
696
|
+
const mapLayer = this.getLayer(layerId);
|
|
697
|
+
if (mapLayer !== undefined) {
|
|
698
|
+
this.moveLayer(layerId, targetLayerPosition);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
await this.addLayer(layerId, layer, targetLayerPosition);
|
|
702
|
+
}
|
|
703
|
+
const previousIndex = previousLayerIds.indexOf(layerId);
|
|
704
|
+
if (previousIndex > -1) {
|
|
705
|
+
previousLayerIds.splice(previousIndex, 1);
|
|
476
706
|
}
|
|
477
707
|
}
|
|
478
|
-
|
|
708
|
+
// Remove layers that are no longer in the `layerIds` list.
|
|
709
|
+
previousLayerIds.forEach(layerId => {
|
|
710
|
+
const layer = this.getLayer(layerId);
|
|
711
|
+
if (layer !== undefined) {
|
|
712
|
+
this._Map.removeLayer(layer);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
479
715
|
this._ready = true;
|
|
480
716
|
}
|
|
481
717
|
/**
|
|
@@ -675,8 +911,7 @@ export class MainView extends React.Component {
|
|
|
675
911
|
}
|
|
676
912
|
else {
|
|
677
913
|
const centerCoord = fromLonLat([longitude || 0, latitude || 0], view.getProjection());
|
|
678
|
-
|
|
679
|
-
view.setZoom(zoom || 0);
|
|
914
|
+
this._moveToPosition({ x: centerCoord[0], y: centerCoord[1] }, zoom || 0);
|
|
680
915
|
// Save the extent if it does not exists, to allow proper export to qgis.
|
|
681
916
|
if (!options.extent) {
|
|
682
917
|
options.extent = view.calculateExtent();
|
|
@@ -699,15 +934,48 @@ export class MainView extends React.Component {
|
|
|
699
934
|
.getArray()
|
|
700
935
|
.find(layer => layer.get('id') === id);
|
|
701
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Convenience method to get a specific layer index from OpenLayers Map
|
|
939
|
+
* @param id Layer to retrieve
|
|
940
|
+
*/
|
|
941
|
+
getLayerIndex(id) {
|
|
942
|
+
return this._Map
|
|
943
|
+
.getLayers()
|
|
944
|
+
.getArray()
|
|
945
|
+
.findIndex(layer => layer.get('id') === id);
|
|
946
|
+
}
|
|
702
947
|
/**
|
|
703
948
|
* Convenience method to get list layer IDs from the OpenLayers Map
|
|
704
949
|
*/
|
|
705
|
-
|
|
950
|
+
getLayerIDs() {
|
|
706
951
|
return this._Map
|
|
707
952
|
.getLayers()
|
|
708
953
|
.getArray()
|
|
709
954
|
.map(layer => layer.get('id'));
|
|
710
955
|
}
|
|
956
|
+
/**
|
|
957
|
+
* Move layer `id` in the stack to `index`.
|
|
958
|
+
*
|
|
959
|
+
* @param id - id of the layer.
|
|
960
|
+
* @param index - expected index of the layer.
|
|
961
|
+
*/
|
|
962
|
+
moveLayer(id, index) {
|
|
963
|
+
const currentIndex = this.getLayerIndex(id);
|
|
964
|
+
if (currentIndex === index || currentIndex === -1) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const layer = this.getLayer(id);
|
|
968
|
+
let nextIndex = index;
|
|
969
|
+
// should not be undefined since the id exists above
|
|
970
|
+
if (layer === undefined) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
this._Map.getLayers().removeAt(currentIndex);
|
|
974
|
+
if (currentIndex < index) {
|
|
975
|
+
nextIndex -= 1;
|
|
976
|
+
}
|
|
977
|
+
this._Map.getLayers().insertAt(nextIndex, layer);
|
|
978
|
+
}
|
|
711
979
|
_onLayersChanged(_, change) {
|
|
712
980
|
var _a;
|
|
713
981
|
// Avoid concurrency update on layers on first load, if layersTreeChanged and
|
|
@@ -757,16 +1025,110 @@ export class MainView extends React.Component {
|
|
|
757
1025
|
}
|
|
758
1026
|
});
|
|
759
1027
|
}
|
|
1028
|
+
_computeAnnotationPosition(annotation) {
|
|
1029
|
+
const { x, y } = annotation.position;
|
|
1030
|
+
const pixels = this._Map.getPixelFromCoordinate([x, y]);
|
|
1031
|
+
if (pixels) {
|
|
1032
|
+
return { x: pixels[0], y: pixels[1] };
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
_updateAnnotation() {
|
|
1036
|
+
Object.keys(this.state.annotations).forEach(key => {
|
|
1037
|
+
var _a;
|
|
1038
|
+
const el = document.getElementById(key);
|
|
1039
|
+
if (el) {
|
|
1040
|
+
const annotation = (_a = this._model.annotationModel) === null || _a === void 0 ? void 0 : _a.getAnnotation(key);
|
|
1041
|
+
if (annotation) {
|
|
1042
|
+
const screenPosition = this._computeAnnotationPosition(annotation);
|
|
1043
|
+
if (screenPosition) {
|
|
1044
|
+
el.style.left = `${Math.round(screenPosition.x)}px`;
|
|
1045
|
+
el.style.top = `${Math.round(screenPosition.y)}px`;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
_onZoomToAnnotation(_, id) {
|
|
1052
|
+
var _a;
|
|
1053
|
+
const annotation = (_a = this._model.annotationModel) === null || _a === void 0 ? void 0 : _a.getAnnotation(id);
|
|
1054
|
+
if (annotation) {
|
|
1055
|
+
this._moveToPosition(annotation.position, annotation.zoom);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
_moveToPosition(center, zoom, duration = 1000) {
|
|
1059
|
+
const view = this._Map.getView();
|
|
1060
|
+
// Zoom needs to be set before changing center
|
|
1061
|
+
if (!view.animate === undefined) {
|
|
1062
|
+
view.animate({ zoom, duration });
|
|
1063
|
+
view.animate({ center: [center.x, center.y], duration });
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
view.setZoom(zoom);
|
|
1067
|
+
view.setCenter([center.x, center.y]);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
_onPointerMove(e) {
|
|
1071
|
+
const pixel = this._Map.getEventPixel(e);
|
|
1072
|
+
const coordinates = this._Map.getCoordinateFromPixel(pixel);
|
|
1073
|
+
this._syncPointer(coordinates);
|
|
1074
|
+
}
|
|
1075
|
+
_identifyFeature(e) {
|
|
1076
|
+
var _a, _b;
|
|
1077
|
+
if (!this._model.isIdentifying) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const localState = (_a = this._model) === null || _a === void 0 ? void 0 : _a.sharedModel.awareness.getLocalState();
|
|
1081
|
+
const selectedLayer = (_b = localState === null || localState === void 0 ? void 0 : localState.selected) === null || _b === void 0 ? void 0 : _b.value;
|
|
1082
|
+
if (!selectedLayer) {
|
|
1083
|
+
console.warn('Layer must be selected to use identify tool');
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const layerId = Object.keys(selectedLayer)[0];
|
|
1087
|
+
const jgisLayer = this._model.getLayer(layerId);
|
|
1088
|
+
switch (jgisLayer === null || jgisLayer === void 0 ? void 0 : jgisLayer.type) {
|
|
1089
|
+
case 'WebGlLayer': {
|
|
1090
|
+
const layer = this.getLayer(layerId);
|
|
1091
|
+
const data = layer.getData(e.pixel);
|
|
1092
|
+
// TODO: Handle dataviews?
|
|
1093
|
+
if (!data || data instanceof DataView) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const bandValues = {};
|
|
1097
|
+
// Data is an array of band values
|
|
1098
|
+
for (let i = 0; i < data.length - 1; i++) {
|
|
1099
|
+
bandValues[`Band ${i + 1}`] = data[i];
|
|
1100
|
+
}
|
|
1101
|
+
// last element is alpha
|
|
1102
|
+
bandValues['Alpha'] = data[data.length - 1];
|
|
1103
|
+
this._model.syncIdentifiedFeatures([bandValues], this._mainViewModel.id);
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
760
1108
|
render() {
|
|
761
|
-
return (React.createElement(
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1109
|
+
return (React.createElement(React.Fragment, null,
|
|
1110
|
+
Object.entries(this.state.annotations).map(([key, annotation]) => {
|
|
1111
|
+
if (!this._model.annotationModel) {
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
const screenPosition = this._computeAnnotationPosition(annotation);
|
|
1115
|
+
return (screenPosition && (React.createElement("div", { key: key, id: key, style: {
|
|
1116
|
+
left: screenPosition.x,
|
|
1117
|
+
top: screenPosition.y
|
|
1118
|
+
}, className: 'jGIS-Popup-Wrapper' },
|
|
1119
|
+
React.createElement(AnnotationFloater, { itemId: key, annotationModel: this._model.annotationModel, open: false }))));
|
|
1120
|
+
}),
|
|
1121
|
+
React.createElement("div", { className: "jGIS-Mainview", style: {
|
|
1122
|
+
border: this.state.remoteUser
|
|
1123
|
+
? `solid 3px ${this.state.remoteUser.color}`
|
|
1124
|
+
: 'unset'
|
|
1125
|
+
} },
|
|
1126
|
+
React.createElement(Spinner, { loading: this.state.loading }),
|
|
1127
|
+
React.createElement(FollowIndicator, { remoteUser: this.state.remoteUser }),
|
|
1128
|
+
React.createElement(CollaboratorPointers, { clients: this.state.clientPointers }),
|
|
1129
|
+
React.createElement("div", { ref: this.divRef, style: {
|
|
1130
|
+
width: '100%',
|
|
1131
|
+
height: 'calc(100%)'
|
|
1132
|
+
} }))));
|
|
771
1133
|
}
|
|
772
1134
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IJupyterGISModel } from '@jupytergis/schema';
|
|
1
|
+
import { IAnnotation, IJupyterGISModel } from '@jupytergis/schema';
|
|
2
2
|
import { ObservableMap } from '@jupyterlab/observables';
|
|
3
3
|
import { JSONValue } from '@lumino/coreutils';
|
|
4
4
|
import { IDisposable } from '@lumino/disposable';
|
|
@@ -10,6 +10,7 @@ export declare class MainViewModel implements IDisposable {
|
|
|
10
10
|
get viewSettingChanged(): import("@lumino/signaling").ISignal<ObservableMap<JSONValue>, import("@jupyterlab/observables").IObservableMap.IChangedArgs<JSONValue>>;
|
|
11
11
|
dispose(): void;
|
|
12
12
|
initSignal(): void;
|
|
13
|
+
addAnnotation(value: IAnnotation): void;
|
|
13
14
|
private _onsharedLayersChanged;
|
|
14
15
|
private _jGISModel;
|
|
15
16
|
private _viewSetting;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { v4 as uuid } from 'uuid';
|
|
1
2
|
export class MainViewModel {
|
|
2
3
|
constructor(options) {
|
|
3
4
|
this._isDisposed = false;
|
|
@@ -26,6 +27,10 @@ export class MainViewModel {
|
|
|
26
27
|
initSignal() {
|
|
27
28
|
this._jGISModel.sharedLayersChanged.connect(this._onsharedLayersChanged, this);
|
|
28
29
|
}
|
|
30
|
+
addAnnotation(value) {
|
|
31
|
+
var _a;
|
|
32
|
+
(_a = this._jGISModel.annotationModel) === null || _a === void 0 ? void 0 : _a.addAnnotation(uuid(), value);
|
|
33
|
+
}
|
|
29
34
|
async _onsharedLayersChanged(_, change) {
|
|
30
35
|
if (change.layerChange) {
|
|
31
36
|
// TODO STUFF with the new updated shared model
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PanelWithToolbar } from '@jupyterlab/ui-components';
|
|
2
|
+
import { Component } from 'react';
|
|
3
|
+
import { IAnnotationModel } from '@jupytergis/schema';
|
|
4
|
+
import { IControlPanelModel } from '../types';
|
|
5
|
+
interface IAnnotationPanelProps {
|
|
6
|
+
annotationModel: IAnnotationModel;
|
|
7
|
+
rightPanelModel: IControlPanelModel;
|
|
8
|
+
}
|
|
9
|
+
export declare class AnnotationsPanel extends Component<IAnnotationPanelProps> {
|
|
10
|
+
constructor(props: IAnnotationPanelProps);
|
|
11
|
+
render(): JSX.Element;
|
|
12
|
+
private _annotationModel;
|
|
13
|
+
private _rightPanelModel;
|
|
14
|
+
}
|
|
15
|
+
export declare class Annotations extends PanelWithToolbar {
|
|
16
|
+
constructor(options: Annotations.IOptions);
|
|
17
|
+
private _widget;
|
|
18
|
+
private _annotationModel;
|
|
19
|
+
private _rightPanelModel;
|
|
20
|
+
}
|
|
21
|
+
export declare namespace Annotations {
|
|
22
|
+
interface IOptions {
|
|
23
|
+
annotationModel: IAnnotationModel;
|
|
24
|
+
rightPanelModel: IControlPanelModel;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PanelWithToolbar, ReactWidget } from '@jupyterlab/ui-components';
|
|
2
|
+
import React, { Component } from 'react';
|
|
3
|
+
import Annotation from '../annotations/components/Annotation';
|
|
4
|
+
export class AnnotationsPanel extends Component {
|
|
5
|
+
constructor(props) {
|
|
6
|
+
super(props);
|
|
7
|
+
const updateCallback = () => {
|
|
8
|
+
this.forceUpdate();
|
|
9
|
+
};
|
|
10
|
+
this._annotationModel = props.annotationModel;
|
|
11
|
+
this._rightPanelModel = props.rightPanelModel;
|
|
12
|
+
this._annotationModel.contextChanged.connect(async () => {
|
|
13
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
14
|
+
await ((_b = (_a = this._annotationModel) === null || _a === void 0 ? void 0 : _a.context) === null || _b === void 0 ? void 0 : _b.ready);
|
|
15
|
+
(_e = (_d = (_c = this._annotationModel) === null || _c === void 0 ? void 0 : _c.context) === null || _d === void 0 ? void 0 : _d.model) === null || _e === void 0 ? void 0 : _e.sharedMetadataChanged.disconnect(updateCallback);
|
|
16
|
+
this._annotationModel = props.annotationModel;
|
|
17
|
+
(_h = (_g = (_f = this._annotationModel) === null || _f === void 0 ? void 0 : _f.context) === null || _g === void 0 ? void 0 : _g.model) === null || _h === void 0 ? void 0 : _h.sharedMetadataChanged.connect(updateCallback);
|
|
18
|
+
this.forceUpdate();
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
render() {
|
|
22
|
+
var _a;
|
|
23
|
+
const annotationIds = (_a = this._annotationModel) === null || _a === void 0 ? void 0 : _a.getAnnotationIds();
|
|
24
|
+
if (!annotationIds || !this._annotationModel) {
|
|
25
|
+
return React.createElement("div", null);
|
|
26
|
+
}
|
|
27
|
+
const annotations = annotationIds.map((id) => {
|
|
28
|
+
return (React.createElement("div", null,
|
|
29
|
+
React.createElement(Annotation, { rightPanelModel: this._rightPanelModel, annotationModel: this._annotationModel, itemId: id }),
|
|
30
|
+
React.createElement("hr", { className: "jGIS-Annotations-Separator" })));
|
|
31
|
+
});
|
|
32
|
+
return React.createElement("div", null, annotations);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class Annotations extends PanelWithToolbar {
|
|
36
|
+
constructor(options) {
|
|
37
|
+
super({});
|
|
38
|
+
this.title.label = 'Annotations';
|
|
39
|
+
this.addClass('jGIS-Annotations');
|
|
40
|
+
this._annotationModel = options.annotationModel;
|
|
41
|
+
this._rightPanelModel = options.rightPanelModel;
|
|
42
|
+
this._widget = ReactWidget.create(React.createElement(AnnotationsPanel, { rightPanelModel: this._rightPanelModel, annotationModel: this._annotationModel }));
|
|
43
|
+
this.addWidget(this._widget);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -2,15 +2,20 @@ import { IJupyterGISTracker } from '@jupytergis/schema';
|
|
|
2
2
|
import { Panel } from '@lumino/widgets';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { IControlPanelModel } from '../../../types';
|
|
5
|
-
import { RightPanelWidget } from '../../rightpanel';
|
|
6
5
|
/**
|
|
7
6
|
* The filters panel widget.
|
|
8
7
|
*/
|
|
9
8
|
export declare class FilterPanel extends Panel {
|
|
10
|
-
constructor(options:
|
|
9
|
+
constructor(options: FilterPanel.IOptions);
|
|
11
10
|
private _model;
|
|
12
11
|
private _tracker;
|
|
13
12
|
}
|
|
13
|
+
export declare namespace FilterPanel {
|
|
14
|
+
interface IOptions {
|
|
15
|
+
model: IControlPanelModel;
|
|
16
|
+
tracker: IJupyterGISTracker;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
14
19
|
interface IFilterComponentProps {
|
|
15
20
|
model: IControlPanelModel;
|
|
16
21
|
tracker: IJupyterGISTracker;
|