@jupytergis/base 0.1.6 → 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 (96) 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/classificationModes.d.ts +13 -0
  12. package/lib/classificationModes.js +326 -0
  13. package/lib/commands.js +52 -7
  14. package/lib/constants.d.ts +2 -0
  15. package/lib/constants.js +5 -1
  16. package/lib/dialogs/symbology/classificationModes.d.ts +13 -0
  17. package/lib/dialogs/symbology/classificationModes.js +326 -0
  18. package/lib/dialogs/symbology/components/color_ramp/CanvasSelectComponent.d.ts +11 -0
  19. package/lib/dialogs/symbology/components/color_ramp/CanvasSelectComponent.js +119 -0
  20. package/lib/dialogs/symbology/components/color_ramp/ColorRamp.d.ts +15 -0
  21. package/lib/dialogs/symbology/components/color_ramp/ColorRamp.js +33 -0
  22. package/lib/dialogs/symbology/components/color_ramp/ColorRampEntry.d.ts +9 -0
  23. package/lib/dialogs/symbology/components/color_ramp/ColorRampEntry.js +24 -0
  24. package/lib/dialogs/symbology/components/color_ramp/ModeSelectRow.d.ts +10 -0
  25. package/lib/dialogs/symbology/components/color_ramp/ModeSelectRow.js +11 -0
  26. package/lib/dialogs/symbology/components/color_stops/StopContainer.d.ts +9 -0
  27. package/lib/dialogs/symbology/components/color_stops/StopContainer.js +28 -0
  28. package/lib/dialogs/{components/symbology → symbology/components/color_stops}/StopRow.js +9 -2
  29. package/lib/dialogs/symbology/hooks/useGetProperties.d.ts +12 -0
  30. package/lib/dialogs/symbology/hooks/useGetProperties.js +47 -0
  31. package/lib/dialogs/{symbologyDialog.js → symbology/symbologyDialog.js} +3 -3
  32. package/lib/dialogs/symbology/symbologyUtils.d.ts +9 -0
  33. package/lib/dialogs/symbology/symbologyUtils.js +94 -0
  34. package/lib/dialogs/symbology/tiff_layer/TiffRendering.d.ts +4 -0
  35. package/lib/dialogs/{components/symbology/BandRendering.js → symbology/tiff_layer/TiffRendering.js} +3 -3
  36. package/lib/dialogs/{components/symbology → symbology/tiff_layer/components}/BandRow.d.ts +1 -1
  37. package/lib/dialogs/{components/symbology → symbology/tiff_layer/types}/SingleBandPseudoColor.d.ts +9 -1
  38. package/lib/dialogs/{components/symbology → symbology/tiff_layer/types}/SingleBandPseudoColor.js +131 -83
  39. package/lib/dialogs/{components/symbology → symbology/vector_layer}/VectorRendering.d.ts +1 -1
  40. package/lib/dialogs/{components/symbology → symbology/vector_layer}/VectorRendering.js +10 -13
  41. package/lib/dialogs/symbology/vector_layer/components/ValueSelect.d.ts +8 -0
  42. package/lib/dialogs/symbology/vector_layer/components/ValueSelect.js +7 -0
  43. package/lib/dialogs/symbology/vector_layer/types/Categorized.d.ts +4 -0
  44. package/lib/dialogs/symbology/vector_layer/types/Categorized.js +94 -0
  45. package/lib/dialogs/symbology/vector_layer/types/Graduated.js +169 -0
  46. package/lib/dialogs/{components/symbology → symbology/vector_layer/types}/SimpleSymbol.js +8 -13
  47. package/lib/formbuilder/formselectors.js +4 -0
  48. package/lib/formbuilder/objectform/baseform.d.ts +1 -1
  49. package/lib/formbuilder/objectform/baseform.js +31 -42
  50. package/lib/formbuilder/objectform/geojsonsource.js +33 -30
  51. package/lib/formbuilder/objectform/geotiffsource.d.ts +16 -0
  52. package/lib/formbuilder/objectform/geotiffsource.js +71 -0
  53. package/lib/formbuilder/objectform/vectorlayerform.js +1 -0
  54. package/lib/formbuilder/objectform/webGlLayerForm.js +1 -0
  55. package/lib/index.d.ts +7 -4
  56. package/lib/index.js +7 -4
  57. package/lib/mainview/CollaboratorPointers.d.ts +17 -0
  58. package/lib/mainview/CollaboratorPointers.js +37 -0
  59. package/lib/mainview/FollowIndicator.d.ts +7 -0
  60. package/lib/mainview/FollowIndicator.js +9 -0
  61. package/lib/mainview/mainView.d.ts +39 -3
  62. package/lib/mainview/mainView.js +451 -41
  63. package/lib/mainview/mainviewmodel.d.ts +2 -1
  64. package/lib/mainview/mainviewmodel.js +5 -0
  65. package/lib/panelview/annotationPanel.d.ts +27 -0
  66. package/lib/panelview/annotationPanel.js +45 -0
  67. package/lib/panelview/components/filter-panel/Filter.d.ts +7 -2
  68. package/lib/panelview/components/filter-panel/Filter.js +1 -1
  69. package/lib/panelview/components/filter-panel/FilterRow.js +3 -3
  70. package/lib/panelview/components/identify-panel/IdentifyPanel.d.ts +15 -0
  71. package/lib/panelview/components/identify-panel/IdentifyPanel.js +108 -0
  72. package/lib/panelview/components/layers.js +4 -4
  73. package/lib/panelview/leftpanel.js +8 -0
  74. package/lib/panelview/rightpanel.d.ts +4 -1
  75. package/lib/panelview/rightpanel.js +28 -7
  76. package/lib/store.d.ts +9 -0
  77. package/lib/store.js +25 -0
  78. package/lib/toolbar/widget.js +12 -2
  79. package/lib/tools.d.ts +35 -0
  80. package/lib/tools.js +86 -0
  81. package/lib/types.d.ts +14 -0
  82. package/package.json +18 -20
  83. package/style/base.css +4 -8
  84. package/style/dialog.css +1 -1
  85. package/style/icons/logo_mini.svg +70 -148
  86. package/style/icons/nonvisibility.svg +2 -7
  87. package/style/icons/visibility.svg +2 -6
  88. package/style/leftPanel.css +5 -0
  89. package/style/symbologyDialog.css +104 -3
  90. package/lib/dialogs/components/symbology/BandRendering.d.ts +0 -4
  91. package/lib/dialogs/components/symbology/Graduated.js +0 -188
  92. /package/lib/dialogs/{components/symbology → symbology/components/color_stops}/StopRow.d.ts +0 -0
  93. /package/lib/dialogs/{symbologyDialog.d.ts → symbology/symbologyDialog.d.ts} +0 -0
  94. /package/lib/dialogs/{components/symbology → symbology/tiff_layer/components}/BandRow.js +0 -0
  95. /package/lib/dialogs/{components/symbology → symbology/vector_layer/types}/Graduated.d.ts +0 -0
  96. /package/lib/dialogs/{components/symbology → symbology/vector_layer/types}/SimpleSymbol.d.ts +0 -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';
@@ -12,13 +12,52 @@ import { GeoTIFF as GeoTIFFSource, ImageTile as ImageTileSource, Vector as Vecto
12
12
  import Static from 'ol/source/ImageStatic';
13
13
  //@ts-expect-error no types for ol-pmtiles
14
14
  import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
15
+ import { register } from 'ol/proj/proj4.js';
16
+ import { get as getProjection } from 'ol/proj.js';
17
+ import proj4 from 'proj4';
15
18
  import * as React from 'react';
16
19
  import shp from 'shpjs';
17
- import { isLightTheme } from '../tools';
20
+ import { isLightTheme, loadGeoTIFFWithCache, throttle } from '../tools';
18
21
  import { Spinner } from './spinner';
22
+ //@ts-expect-error no types for proj4-list
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';
19
32
  export class MainView extends React.Component {
20
33
  constructor(props) {
21
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
+ };
22
61
  this.vectorLayerStyleRuleBuilder = (layer) => {
23
62
  const layerParams = layer.parameters;
24
63
  if (!layerParams) {
@@ -116,8 +155,117 @@ export class MainView extends React.Component {
116
155
  return scaled;
117
156
  };
118
157
  this._onClientSharedStateChanged = (sender, clients) => {
119
- // 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
+ });
223
+ };
224
+ this._onSharedModelStateChange = (_, change) => {
225
+ var _a;
226
+ const changedState = (_a = change.stateChange) === null || _a === void 0 ? void 0 : _a.map(value => value.name);
227
+ if (!(changedState === null || changedState === void 0 ? void 0 : changedState.includes('path'))) {
228
+ return;
229
+ }
230
+ const path = this._model.sharedModel.getState('path');
231
+ if (path !== this._documentPath && typeof path === 'string') {
232
+ if (window.jupytergisMaps !== undefined && this._documentPath) {
233
+ delete window.jupytergisMaps[this._documentPath];
234
+ }
235
+ this._documentPath = path;
236
+ if (window.jupytergisMaps !== undefined) {
237
+ window.jupytergisMaps[this._documentPath] = this._Map;
238
+ }
239
+ }
120
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
+ });
121
269
  this._handleThemeChange = () => {
122
270
  const lightTheme = isLightTheme();
123
271
  // TODO SOMETHING
@@ -130,6 +278,8 @@ export class MainView extends React.Component {
130
278
  this.divRef = React.createRef(); // Reference of render div
131
279
  this._ready = false;
132
280
  this._sourceToLayerMap = new Map();
281
+ proj4.defs(Array.from(proj4list));
282
+ register(proj4);
133
283
  this._mainViewModel = this.props.viewModel;
134
284
  this._mainViewModel.viewSettingChanged.connect(this._onViewChanged, this);
135
285
  this._model = this._mainViewModel.jGISModel;
@@ -139,20 +289,34 @@ export class MainView extends React.Component {
139
289
  this._model.sharedLayersChanged.connect(this._onLayersChanged, this);
140
290
  this._model.sharedLayerTreeChanged.connect(this._onLayerTreeChange, this);
141
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);
142
295
  this.state = {
143
296
  id: this._mainViewModel.id,
144
297
  lightTheme: isLightTheme(),
145
298
  loading: true,
146
- firstLoad: true
299
+ firstLoad: true,
300
+ annotations: {},
301
+ clientPointers: {}
147
302
  };
148
303
  this._sources = [];
304
+ this._commands = new CommandRegistry();
305
+ this._contextMenu = new ContextMenu({ commands: this._commands });
149
306
  }
150
307
  async componentDidMount() {
151
308
  window.addEventListener('resize', this._handleWindowResize);
152
309
  await this.generateScene();
310
+ this.addContextMenu();
153
311
  this._mainViewModel.initSignal();
312
+ if (window.jupytergisMaps !== undefined && this._documentPath) {
313
+ window.jupytergisMaps[this._documentPath] = this._Map;
314
+ }
154
315
  }
155
316
  componentWillUnmount() {
317
+ if (window.jupytergisMaps !== undefined && this._documentPath) {
318
+ delete window.jupytergisMaps[this._documentPath];
319
+ }
156
320
  window.removeEventListener('resize', this._handleWindowResize);
157
321
  this._mainViewModel.viewSettingChanged.disconnect(this._onViewChanged, this);
158
322
  this._model.themeChanged.disconnect(this._handleThemeChange, this);
@@ -171,6 +335,7 @@ export class MainView extends React.Component {
171
335
  }),
172
336
  controls: [new ScaleLine()]
173
337
  });
338
+ // Add map interactions
174
339
  const dragAndDropInteraction = new DragAndDrop({
175
340
  formatConstructors: [GeoJSON]
176
341
  });
@@ -181,7 +346,8 @@ export class MainView extends React.Component {
181
346
  name: 'Drag and Drop source',
182
347
  parameters: { path: event.file.name }
183
348
  };
184
- this.addSource(sourceId, sourceModel);
349
+ const layerId = UUID.uuid4();
350
+ this.addSource(sourceId, sourceModel, layerId);
185
351
  this._model.sharedModel.addSource(sourceId, sourceModel);
186
352
  const layerModel = {
187
353
  type: 'VectorLayer',
@@ -194,11 +360,68 @@ export class MainView extends React.Component {
194
360
  source: sourceId
195
361
  }
196
362
  };
197
- const layerId = UUID.uuid4();
198
- this.addLayer(layerId, layerModel, this.getLayers().length);
363
+ this.addLayer(layerId, layerModel, this.getLayerIDs().length);
199
364
  this._model.addLayer(layerId, layerModel);
200
365
  });
201
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
+ });
202
425
  this._Map.on('moveend', () => {
203
426
  if (!this._initializedPosition) {
204
427
  return;
@@ -220,12 +443,23 @@ export class MainView extends React.Component {
220
443
  updatedOptions.extent = view.calculateExtent();
221
444
  this._model.setOptions(Object.assign(Object.assign({}, currentOptions), updatedOptions));
222
445
  });
446
+ this._Map.on('click', this._identifyFeature.bind(this));
447
+ this._Map
448
+ .getViewport()
449
+ .addEventListener('pointermove', this._onPointerMove.bind(this));
223
450
  if (JupyterGISModel.getOrderedLayerIds(this._model).length !== 0) {
224
451
  await this._updateLayersImpl(JupyterGISModel.getOrderedLayerIds(this._model));
225
452
  const options = this._model.getOptions();
226
453
  this.updateOptions(options);
227
454
  this._initializedPosition = true;
228
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
+ });
229
463
  this.setState(old => (Object.assign(Object.assign({}, old), { loading: false })));
230
464
  }
231
465
  }
@@ -241,13 +475,17 @@ export class MainView extends React.Component {
241
475
  throw error;
242
476
  }
243
477
  }
478
+ async _loadGeoTIFFWithCache(sourceInfo) {
479
+ const result = await loadGeoTIFFWithCache(sourceInfo);
480
+ return result === null || result === void 0 ? void 0 : result.file;
481
+ }
244
482
  /**
245
483
  * Add a source in the map.
246
484
  *
247
485
  * @param id - the source id.
248
486
  * @param source - the source object.
249
487
  */
250
- async addSource(id, source) {
488
+ async addSource(id, source, layerId) {
251
489
  var _a, _b;
252
490
  let newSource;
253
491
  switch (source.type) {
@@ -363,8 +601,15 @@ export class MainView extends React.Component {
363
601
  }
364
602
  case 'GeoTiffSource': {
365
603
  const sourceParameters = source.parameters;
604
+ const addNoData = (url) => {
605
+ return Object.assign(Object.assign({}, url), { nodata: 0 });
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
+ }));
366
611
  newSource = new GeoTIFFSource({
367
- sources: sourceParameters.urls,
612
+ sources: sourcesWithBlobs,
368
613
  normalize: sourceParameters.normalize,
369
614
  wrapX: sourceParameters.wrapX
370
615
  });
@@ -408,7 +653,7 @@ export class MainView extends React.Component {
408
653
  // remove source being updated
409
654
  this.removeSource(id);
410
655
  // create updated source
411
- this.addSource(id, source);
656
+ await this.addSource(id, source, layerId);
412
657
  // change source of target layer
413
658
  mapLayer.setSource(this._sources[id]);
414
659
  }
@@ -428,20 +673,45 @@ export class MainView extends React.Component {
428
673
  updateLayers(layerIds) {
429
674
  this._updateLayersImpl(layerIds);
430
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
+ */
431
682
  async _updateLayersImpl(layerIds) {
432
- const mapLayers = [];
433
- 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];
434
691
  const layer = this._model.sharedModel.getLayer(layerId);
435
692
  if (!layer) {
436
- console.log(`Layer id ${layerId} does not exist`);
693
+ console.warn(`Layer with ID ${layerId} does not exist in the shared model.`);
437
694
  continue;
438
695
  }
439
- const newMapLayer = await this._buildMapLayer(layerId, layer);
440
- if (newMapLayer !== undefined) {
441
- 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);
442
706
  }
443
707
  }
444
- 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
+ });
445
715
  this._ready = true;
446
716
  }
447
717
  /**
@@ -454,12 +724,12 @@ export class MainView extends React.Component {
454
724
  async _buildMapLayer(id, layer) {
455
725
  var _a;
456
726
  const sourceId = (_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.source;
457
- const source = this._model.sharedModel.getSource(sourceId);
727
+ const source = this._model.sharedModel.getLayerSource(sourceId);
458
728
  if (!source) {
459
729
  return;
460
730
  }
461
731
  if (!this._sources[sourceId]) {
462
- await this.addSource(sourceId, source);
732
+ await this.addSource(sourceId, source, id);
463
733
  }
464
734
  let newMapLayer;
465
735
  let layerParameters;
@@ -559,12 +829,12 @@ export class MainView extends React.Component {
559
829
  async updateLayer(id, layer, mapLayer) {
560
830
  var _a, _b, _c, _d;
561
831
  const sourceId = (_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.source;
562
- const source = this._model.sharedModel.getSource(sourceId);
832
+ const source = this._model.sharedModel.getLayerSource(sourceId);
563
833
  if (!source) {
564
834
  return;
565
835
  }
566
836
  if (!this._sources[sourceId]) {
567
- await this.addSource(sourceId, source);
837
+ await this.addSource(sourceId, source, id);
568
838
  }
569
839
  mapLayer.setVisible(layer.visible);
570
840
  switch (layer.type) {
@@ -620,23 +890,36 @@ export class MainView extends React.Component {
620
890
  this._initializedPosition = true;
621
891
  }
622
892
  }
623
- updateOptions(options) {
624
- const view = this._Map.getView();
893
+ async updateOptions(options) {
894
+ const { projection, extent, useExtent, latitude, longitude, zoom, bearing } = options;
895
+ let view = this._Map.getView();
896
+ const currentProjection = view.getProjection().getCode();
897
+ // Need to recreate view if the projection changes
898
+ if (projection !== undefined && currentProjection !== projection) {
899
+ const newProjection = getProjection(projection);
900
+ if (newProjection) {
901
+ view = new View({ projection: newProjection });
902
+ }
903
+ else {
904
+ console.warn(`Invalid projection: ${projection}`);
905
+ return;
906
+ }
907
+ }
625
908
  // Use the extent only if explicitly requested (QGIS files).
626
- if (options.extent && options.useExtent) {
627
- view.fit(options.extent);
909
+ if (useExtent && extent) {
910
+ view.fit(extent);
628
911
  }
629
912
  else {
630
- const centerCoord = fromLonLat([options.longitude || 0, options.latitude || 0], this._Map.getView().getProjection());
631
- this._Map.getView().setZoom(options.zoom || 0);
632
- this._Map.getView().setCenter(centerCoord);
913
+ const centerCoord = fromLonLat([longitude || 0, latitude || 0], view.getProjection());
914
+ this._moveToPosition({ x: centerCoord[0], y: centerCoord[1] }, zoom || 0);
633
915
  // Save the extent if it does not exists, to allow proper export to qgis.
634
- if (options.extent === undefined) {
916
+ if (!options.extent) {
635
917
  options.extent = view.calculateExtent();
636
918
  this._model.setOptions(options);
637
919
  }
638
920
  }
639
- view.setRotation(options.bearing || 0);
921
+ view.setRotation(bearing || 0);
922
+ this._Map.setView(view);
640
923
  }
641
924
  _onViewChanged(sender, change) {
642
925
  // TODO SOMETHING
@@ -651,15 +934,48 @@ export class MainView extends React.Component {
651
934
  .getArray()
652
935
  .find(layer => layer.get('id') === id);
653
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
+ }
654
947
  /**
655
948
  * Convenience method to get list layer IDs from the OpenLayers Map
656
949
  */
657
- getLayers() {
950
+ getLayerIDs() {
658
951
  return this._Map
659
952
  .getLayers()
660
953
  .getArray()
661
954
  .map(layer => layer.get('id'));
662
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
+ }
663
979
  _onLayersChanged(_, change) {
664
980
  var _a;
665
981
  // Avoid concurrency update on layers on first load, if layersTreeChanged and
@@ -709,16 +1025,110 @@ export class MainView extends React.Component {
709
1025
  }
710
1026
  });
711
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
+ }
712
1108
  render() {
713
- return (React.createElement("div", { className: "jGIS-Mainview", style: {
714
- border: this.state.remoteUser
715
- ? `solid 3px ${this.state.remoteUser.color}`
716
- : 'unset'
717
- } },
718
- React.createElement(Spinner, { loading: this.state.loading }),
719
- React.createElement("div", { ref: this.divRef, style: {
720
- width: '100%',
721
- height: 'calc(100%)'
722
- } })));
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
+ } }))));
723
1133
  }
724
1134
  }