@jupytergis/base 0.1.7 → 0.2.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/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 +393 -28
- 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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { JupyterGISModel } from '@jupytergis/schema';
|
|
2
2
|
import { UUID } from '@lumino/coreutils';
|
|
3
|
-
import { Collection, Map as OlMap, View } from 'ol';
|
|
3
|
+
import { Collection, Map as OlMap, View, getUid } 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
|
*
|
|
@@ -345,6 +552,9 @@ export class MainView extends React.Component {
|
|
|
345
552
|
featureProjection: this._Map.getView().getProjection()
|
|
346
553
|
});
|
|
347
554
|
const featureCollection = new Collection(featureArray);
|
|
555
|
+
featureCollection.forEach(feature => {
|
|
556
|
+
feature.setId(getUid(feature));
|
|
557
|
+
});
|
|
348
558
|
newSource = new VectorSource({
|
|
349
559
|
features: featureCollection
|
|
350
560
|
});
|
|
@@ -397,8 +607,12 @@ export class MainView extends React.Component {
|
|
|
397
607
|
const addNoData = (url) => {
|
|
398
608
|
return Object.assign(Object.assign({}, url), { nodata: 0 });
|
|
399
609
|
};
|
|
610
|
+
const sourcesWithBlobs = await Promise.all(sourceParameters.urls.map(async (sourceInfo) => {
|
|
611
|
+
const blob = await this._loadGeoTIFFWithCache(sourceInfo);
|
|
612
|
+
return Object.assign(Object.assign({}, addNoData(sourceInfo)), { blob });
|
|
613
|
+
}));
|
|
400
614
|
newSource = new GeoTIFFSource({
|
|
401
|
-
sources:
|
|
615
|
+
sources: sourcesWithBlobs,
|
|
402
616
|
normalize: sourceParameters.normalize,
|
|
403
617
|
wrapX: sourceParameters.wrapX
|
|
404
618
|
});
|
|
@@ -462,20 +676,45 @@ export class MainView extends React.Component {
|
|
|
462
676
|
updateLayers(layerIds) {
|
|
463
677
|
this._updateLayersImpl(layerIds);
|
|
464
678
|
}
|
|
679
|
+
/**
|
|
680
|
+
* Updates the position and existence of layers in the OL map based on the layer IDs.
|
|
681
|
+
*
|
|
682
|
+
* @param layerIds - An array of layer IDs that should be present on the map.
|
|
683
|
+
* @returns {} Nothing is returned.
|
|
684
|
+
*/
|
|
465
685
|
async _updateLayersImpl(layerIds) {
|
|
466
|
-
|
|
467
|
-
|
|
686
|
+
// get layers that are currently on the OL map
|
|
687
|
+
const previousLayerIds = this.getLayerIDs();
|
|
688
|
+
// Iterate over the new layer IDs:
|
|
689
|
+
// * Add layers to the map that are present in the list but not the map.
|
|
690
|
+
// * Remove layers from the map that are present in the map but not the list.
|
|
691
|
+
// * Update layer positions to match the list.
|
|
692
|
+
for (let targetLayerPosition = 0; targetLayerPosition < layerIds.length; targetLayerPosition++) {
|
|
693
|
+
const layerId = layerIds[targetLayerPosition];
|
|
468
694
|
const layer = this._model.sharedModel.getLayer(layerId);
|
|
469
695
|
if (!layer) {
|
|
470
|
-
console.
|
|
696
|
+
console.warn(`Layer with ID ${layerId} does not exist in the shared model.`);
|
|
471
697
|
continue;
|
|
472
698
|
}
|
|
473
|
-
const
|
|
474
|
-
if (
|
|
475
|
-
|
|
699
|
+
const mapLayer = this.getLayer(layerId);
|
|
700
|
+
if (mapLayer !== undefined) {
|
|
701
|
+
this.moveLayer(layerId, targetLayerPosition);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
await this.addLayer(layerId, layer, targetLayerPosition);
|
|
705
|
+
}
|
|
706
|
+
const previousIndex = previousLayerIds.indexOf(layerId);
|
|
707
|
+
if (previousIndex > -1) {
|
|
708
|
+
previousLayerIds.splice(previousIndex, 1);
|
|
476
709
|
}
|
|
477
710
|
}
|
|
478
|
-
|
|
711
|
+
// Remove layers that are no longer in the `layerIds` list.
|
|
712
|
+
previousLayerIds.forEach(layerId => {
|
|
713
|
+
const layer = this.getLayer(layerId);
|
|
714
|
+
if (layer !== undefined) {
|
|
715
|
+
this._Map.removeLayer(layer);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
479
718
|
this._ready = true;
|
|
480
719
|
}
|
|
481
720
|
/**
|
|
@@ -675,8 +914,7 @@ export class MainView extends React.Component {
|
|
|
675
914
|
}
|
|
676
915
|
else {
|
|
677
916
|
const centerCoord = fromLonLat([longitude || 0, latitude || 0], view.getProjection());
|
|
678
|
-
|
|
679
|
-
view.setZoom(zoom || 0);
|
|
917
|
+
this._moveToPosition({ x: centerCoord[0], y: centerCoord[1] }, zoom || 0);
|
|
680
918
|
// Save the extent if it does not exists, to allow proper export to qgis.
|
|
681
919
|
if (!options.extent) {
|
|
682
920
|
options.extent = view.calculateExtent();
|
|
@@ -699,15 +937,48 @@ export class MainView extends React.Component {
|
|
|
699
937
|
.getArray()
|
|
700
938
|
.find(layer => layer.get('id') === id);
|
|
701
939
|
}
|
|
940
|
+
/**
|
|
941
|
+
* Convenience method to get a specific layer index from OpenLayers Map
|
|
942
|
+
* @param id Layer to retrieve
|
|
943
|
+
*/
|
|
944
|
+
getLayerIndex(id) {
|
|
945
|
+
return this._Map
|
|
946
|
+
.getLayers()
|
|
947
|
+
.getArray()
|
|
948
|
+
.findIndex(layer => layer.get('id') === id);
|
|
949
|
+
}
|
|
702
950
|
/**
|
|
703
951
|
* Convenience method to get list layer IDs from the OpenLayers Map
|
|
704
952
|
*/
|
|
705
|
-
|
|
953
|
+
getLayerIDs() {
|
|
706
954
|
return this._Map
|
|
707
955
|
.getLayers()
|
|
708
956
|
.getArray()
|
|
709
957
|
.map(layer => layer.get('id'));
|
|
710
958
|
}
|
|
959
|
+
/**
|
|
960
|
+
* Move layer `id` in the stack to `index`.
|
|
961
|
+
*
|
|
962
|
+
* @param id - id of the layer.
|
|
963
|
+
* @param index - expected index of the layer.
|
|
964
|
+
*/
|
|
965
|
+
moveLayer(id, index) {
|
|
966
|
+
const currentIndex = this.getLayerIndex(id);
|
|
967
|
+
if (currentIndex === index || currentIndex === -1) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const layer = this.getLayer(id);
|
|
971
|
+
let nextIndex = index;
|
|
972
|
+
// should not be undefined since the id exists above
|
|
973
|
+
if (layer === undefined) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
this._Map.getLayers().removeAt(currentIndex);
|
|
977
|
+
if (currentIndex < index) {
|
|
978
|
+
nextIndex -= 1;
|
|
979
|
+
}
|
|
980
|
+
this._Map.getLayers().insertAt(nextIndex, layer);
|
|
981
|
+
}
|
|
711
982
|
_onLayersChanged(_, change) {
|
|
712
983
|
var _a;
|
|
713
984
|
// Avoid concurrency update on layers on first load, if layersTreeChanged and
|
|
@@ -757,16 +1028,110 @@ export class MainView extends React.Component {
|
|
|
757
1028
|
}
|
|
758
1029
|
});
|
|
759
1030
|
}
|
|
1031
|
+
_computeAnnotationPosition(annotation) {
|
|
1032
|
+
const { x, y } = annotation.position;
|
|
1033
|
+
const pixels = this._Map.getPixelFromCoordinate([x, y]);
|
|
1034
|
+
if (pixels) {
|
|
1035
|
+
return { x: pixels[0], y: pixels[1] };
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
_updateAnnotation() {
|
|
1039
|
+
Object.keys(this.state.annotations).forEach(key => {
|
|
1040
|
+
var _a;
|
|
1041
|
+
const el = document.getElementById(key);
|
|
1042
|
+
if (el) {
|
|
1043
|
+
const annotation = (_a = this._model.annotationModel) === null || _a === void 0 ? void 0 : _a.getAnnotation(key);
|
|
1044
|
+
if (annotation) {
|
|
1045
|
+
const screenPosition = this._computeAnnotationPosition(annotation);
|
|
1046
|
+
if (screenPosition) {
|
|
1047
|
+
el.style.left = `${Math.round(screenPosition.x)}px`;
|
|
1048
|
+
el.style.top = `${Math.round(screenPosition.y)}px`;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
_onZoomToAnnotation(_, id) {
|
|
1055
|
+
var _a;
|
|
1056
|
+
const annotation = (_a = this._model.annotationModel) === null || _a === void 0 ? void 0 : _a.getAnnotation(id);
|
|
1057
|
+
if (annotation) {
|
|
1058
|
+
this._moveToPosition(annotation.position, annotation.zoom);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
_moveToPosition(center, zoom, duration = 1000) {
|
|
1062
|
+
const view = this._Map.getView();
|
|
1063
|
+
// Zoom needs to be set before changing center
|
|
1064
|
+
if (!view.animate === undefined) {
|
|
1065
|
+
view.animate({ zoom, duration });
|
|
1066
|
+
view.animate({ center: [center.x, center.y], duration });
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
view.setZoom(zoom);
|
|
1070
|
+
view.setCenter([center.x, center.y]);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
_onPointerMove(e) {
|
|
1074
|
+
const pixel = this._Map.getEventPixel(e);
|
|
1075
|
+
const coordinates = this._Map.getCoordinateFromPixel(pixel);
|
|
1076
|
+
this._syncPointer(coordinates);
|
|
1077
|
+
}
|
|
1078
|
+
_identifyFeature(e) {
|
|
1079
|
+
var _a, _b;
|
|
1080
|
+
if (!this._model.isIdentifying) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const localState = (_a = this._model) === null || _a === void 0 ? void 0 : _a.sharedModel.awareness.getLocalState();
|
|
1084
|
+
const selectedLayer = (_b = localState === null || localState === void 0 ? void 0 : localState.selected) === null || _b === void 0 ? void 0 : _b.value;
|
|
1085
|
+
if (!selectedLayer) {
|
|
1086
|
+
console.warn('Layer must be selected to use identify tool');
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const layerId = Object.keys(selectedLayer)[0];
|
|
1090
|
+
const jgisLayer = this._model.getLayer(layerId);
|
|
1091
|
+
switch (jgisLayer === null || jgisLayer === void 0 ? void 0 : jgisLayer.type) {
|
|
1092
|
+
case 'WebGlLayer': {
|
|
1093
|
+
const layer = this.getLayer(layerId);
|
|
1094
|
+
const data = layer.getData(e.pixel);
|
|
1095
|
+
// TODO: Handle dataviews?
|
|
1096
|
+
if (!data || data instanceof DataView) {
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
const bandValues = {};
|
|
1100
|
+
// Data is an array of band values
|
|
1101
|
+
for (let i = 0; i < data.length - 1; i++) {
|
|
1102
|
+
bandValues[`Band ${i + 1}`] = data[i];
|
|
1103
|
+
}
|
|
1104
|
+
// last element is alpha
|
|
1105
|
+
bandValues['Alpha'] = data[data.length - 1];
|
|
1106
|
+
this._model.syncIdentifiedFeatures([bandValues], this._mainViewModel.id);
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
760
1111
|
render() {
|
|
761
|
-
return (React.createElement(
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1112
|
+
return (React.createElement(React.Fragment, null,
|
|
1113
|
+
Object.entries(this.state.annotations).map(([key, annotation]) => {
|
|
1114
|
+
if (!this._model.annotationModel) {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
const screenPosition = this._computeAnnotationPosition(annotation);
|
|
1118
|
+
return (screenPosition && (React.createElement("div", { key: key, id: key, style: {
|
|
1119
|
+
left: screenPosition.x,
|
|
1120
|
+
top: screenPosition.y
|
|
1121
|
+
}, className: 'jGIS-Popup-Wrapper' },
|
|
1122
|
+
React.createElement(AnnotationFloater, { itemId: key, annotationModel: this._model.annotationModel, open: false }))));
|
|
1123
|
+
}),
|
|
1124
|
+
React.createElement("div", { className: "jGIS-Mainview", style: {
|
|
1125
|
+
border: this.state.remoteUser
|
|
1126
|
+
? `solid 3px ${this.state.remoteUser.color}`
|
|
1127
|
+
: 'unset'
|
|
1128
|
+
} },
|
|
1129
|
+
React.createElement(Spinner, { loading: this.state.loading }),
|
|
1130
|
+
React.createElement(FollowIndicator, { remoteUser: this.state.remoteUser }),
|
|
1131
|
+
React.createElement(CollaboratorPointers, { clients: this.state.clientPointers }),
|
|
1132
|
+
React.createElement("div", { ref: this.divRef, style: {
|
|
1133
|
+
width: '100%',
|
|
1134
|
+
height: 'calc(100%)'
|
|
1135
|
+
} }))));
|
|
771
1136
|
}
|
|
772
1137
|
}
|
|
@@ -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
|
+
}
|