@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.
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 +389 -27
  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
@@ -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/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
  *
@@ -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: sourceParameters.urls.map(addNoData),
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
- const mapLayers = [];
467
- for (const layerId of layerIds) {
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.log(`Layer id ${layerId} does not exist`);
693
+ console.warn(`Layer with ID ${layerId} does not exist in the shared model.`);
471
694
  continue;
472
695
  }
473
- const newMapLayer = await this._buildMapLayer(layerId, layer);
474
- if (newMapLayer !== undefined) {
475
- mapLayers.push(newMapLayer);
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
- this._Map.setLayers(mapLayers);
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
- view.setCenter(centerCoord);
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
- getLayers() {
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("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
- } })));
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: RightPanelWidget.IOptions);
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;