@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.
Files changed (51) hide show
  1. package/lib/annotations/components/Annotation.d.ts +11 -0
  2. package/lib/annotations/components/Annotation.js +61 -0
  3. package/lib/annotations/components/AnnotationFloater.d.ts +7 -0
  4. package/lib/annotations/components/AnnotationFloater.js +30 -0
  5. package/lib/annotations/components/Message.d.ts +8 -0
  6. package/lib/annotations/components/Message.js +17 -0
  7. package/lib/annotations/index.d.ts +3 -0
  8. package/lib/annotations/index.js +3 -0
  9. package/lib/annotations/model.d.ts +28 -0
  10. package/lib/annotations/model.js +67 -0
  11. package/lib/commands.js +51 -6
  12. package/lib/constants.d.ts +2 -0
  13. package/lib/constants.js +5 -1
  14. package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +25 -33
  15. package/lib/formbuilder/formselectors.js +4 -0
  16. package/lib/formbuilder/objectform/baseform.d.ts +1 -1
  17. package/lib/formbuilder/objectform/baseform.js +31 -42
  18. package/lib/formbuilder/objectform/geojsonsource.js +33 -30
  19. package/lib/formbuilder/objectform/geotiffsource.d.ts +16 -0
  20. package/lib/formbuilder/objectform/geotiffsource.js +71 -0
  21. package/lib/index.d.ts +1 -0
  22. package/lib/index.js +1 -0
  23. package/lib/mainview/CollaboratorPointers.d.ts +17 -0
  24. package/lib/mainview/CollaboratorPointers.js +37 -0
  25. package/lib/mainview/FollowIndicator.d.ts +7 -0
  26. package/lib/mainview/FollowIndicator.js +9 -0
  27. package/lib/mainview/mainView.d.ts +36 -2
  28. package/lib/mainview/mainView.js +393 -28
  29. package/lib/mainview/mainviewmodel.d.ts +2 -1
  30. package/lib/mainview/mainviewmodel.js +5 -0
  31. package/lib/panelview/annotationPanel.d.ts +27 -0
  32. package/lib/panelview/annotationPanel.js +45 -0
  33. package/lib/panelview/components/filter-panel/Filter.d.ts +7 -2
  34. package/lib/panelview/components/filter-panel/Filter.js +1 -1
  35. package/lib/panelview/components/filter-panel/FilterRow.js +3 -3
  36. package/lib/panelview/components/identify-panel/IdentifyPanel.d.ts +15 -0
  37. package/lib/panelview/components/identify-panel/IdentifyPanel.js +108 -0
  38. package/lib/panelview/components/layers.js +4 -4
  39. package/lib/panelview/leftpanel.js +8 -0
  40. package/lib/panelview/rightpanel.d.ts +4 -1
  41. package/lib/panelview/rightpanel.js +28 -7
  42. package/lib/toolbar/widget.js +11 -1
  43. package/lib/tools.d.ts +35 -0
  44. package/lib/tools.js +86 -0
  45. package/package.json +5 -6
  46. package/style/base.css +4 -8
  47. package/style/dialog.css +1 -1
  48. package/style/icons/logo_mini.svg +70 -148
  49. package/style/icons/nonvisibility.svg +2 -7
  50. package/style/icons/visibility.svg +2 -6
  51. package/style/leftPanel.css +5 -0
@@ -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/DragAndDrop';
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
- // TODO SOMETHING
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._model.sharedModel.changed.connect(this._onSharedModelStateChange);
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.getLayers().length);
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: sourceParameters.urls.map(addNoData),
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
- const mapLayers = [];
467
- for (const layerId of layerIds) {
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.log(`Layer id ${layerId} does not exist`);
696
+ console.warn(`Layer with ID ${layerId} does not exist in the shared model.`);
471
697
  continue;
472
698
  }
473
- const newMapLayer = await this._buildMapLayer(layerId, layer);
474
- if (newMapLayer !== undefined) {
475
- mapLayers.push(newMapLayer);
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
- this._Map.setLayers(mapLayers);
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
- view.setCenter(centerCoord);
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
- getLayers() {
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("div", { className: "jGIS-Mainview", style: {
762
- border: this.state.remoteUser
763
- ? `solid 3px ${this.state.remoteUser.color}`
764
- : 'unset'
765
- } },
766
- React.createElement(Spinner, { loading: this.state.loading }),
767
- React.createElement("div", { ref: this.divRef, style: {
768
- width: '100%',
769
- height: 'calc(100%)'
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
+ }