@jupytergis/base 0.3.0 → 0.4.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 (77) hide show
  1. package/lib/annotations/components/Annotation.js +1 -1
  2. package/lib/annotations/model.d.ts +6 -7
  3. package/lib/annotations/model.js +15 -15
  4. package/lib/commands.d.ts +2 -3
  5. package/lib/commands.js +117 -62
  6. package/lib/constants.d.ts +2 -0
  7. package/lib/constants.js +4 -1
  8. package/lib/dialogs/formdialog.js +2 -2
  9. package/lib/dialogs/layerBrowserDialog.d.ts +4 -5
  10. package/lib/dialogs/layerBrowserDialog.js +9 -9
  11. package/lib/dialogs/symbology/hooks/useGetBandInfo.d.ts +3 -8
  12. package/lib/dialogs/symbology/hooks/useGetBandInfo.js +16 -28
  13. package/lib/dialogs/symbology/hooks/useGetProperties.d.ts +1 -1
  14. package/lib/dialogs/symbology/hooks/useGetProperties.js +6 -8
  15. package/lib/dialogs/symbology/symbologyDialog.d.ts +2 -3
  16. package/lib/dialogs/symbology/symbologyDialog.js +10 -9
  17. package/lib/dialogs/symbology/tiff_layer/TiffRendering.d.ts +1 -1
  18. package/lib/dialogs/symbology/tiff_layer/TiffRendering.js +6 -6
  19. package/lib/dialogs/symbology/tiff_layer/components/BandRow.js +3 -1
  20. package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.d.ts +1 -1
  21. package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +5 -4
  22. package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.d.ts +1 -1
  23. package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +8 -7
  24. package/lib/dialogs/symbology/vector_layer/VectorRendering.d.ts +1 -1
  25. package/lib/dialogs/symbology/vector_layer/VectorRendering.js +18 -13
  26. package/lib/dialogs/symbology/vector_layer/types/Categorized.d.ts +1 -1
  27. package/lib/dialogs/symbology/vector_layer/types/Categorized.js +30 -19
  28. package/lib/dialogs/symbology/vector_layer/types/Graduated.d.ts +1 -1
  29. package/lib/dialogs/symbology/vector_layer/types/Graduated.js +16 -13
  30. package/lib/dialogs/symbology/vector_layer/types/Heatmap.d.ts +4 -0
  31. package/lib/dialogs/symbology/vector_layer/types/Heatmap.js +77 -0
  32. package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.d.ts +1 -1
  33. package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.js +4 -3
  34. package/lib/formbuilder/creationform.d.ts +1 -2
  35. package/lib/formbuilder/creationform.js +4 -4
  36. package/lib/formbuilder/editform.d.ts +1 -2
  37. package/lib/formbuilder/editform.js +7 -7
  38. package/lib/formbuilder/formselectors.js +5 -2
  39. package/lib/formbuilder/objectform/baseform.d.ts +3 -4
  40. package/lib/formbuilder/objectform/baseform.js +2 -2
  41. package/lib/formbuilder/objectform/fileselectorwidget.js +13 -6
  42. package/lib/formbuilder/objectform/geotiffsource.d.ts +5 -1
  43. package/lib/formbuilder/objectform/geotiffsource.js +51 -18
  44. package/lib/formbuilder/objectform/heatmapLayerForm.d.ts +11 -0
  45. package/lib/formbuilder/objectform/heatmapLayerForm.js +60 -0
  46. package/lib/formbuilder/objectform/vectorlayerform.d.ts +0 -2
  47. package/lib/formbuilder/objectform/vectorlayerform.js +0 -59
  48. package/lib/mainview/TemporalSlider.d.ts +8 -0
  49. package/lib/mainview/TemporalSlider.js +303 -0
  50. package/lib/mainview/mainView.d.ts +26 -5
  51. package/lib/mainview/mainView.js +221 -108
  52. package/lib/mainview/mainviewmodel.d.ts +4 -0
  53. package/lib/mainview/mainviewmodel.js +4 -0
  54. package/lib/mainview/mainviewwidget.d.ts +0 -2
  55. package/lib/mainview/mainviewwidget.js +0 -2
  56. package/lib/panelview/annotationPanel.js +5 -5
  57. package/lib/panelview/components/filter-panel/Filter.js +4 -25
  58. package/lib/panelview/components/identify-panel/IdentifyPanel.js +1 -1
  59. package/lib/panelview/components/layers.js +2 -2
  60. package/lib/panelview/components/sources.js +1 -1
  61. package/lib/panelview/leftpanel.d.ts +3 -0
  62. package/lib/panelview/leftpanel.js +5 -1
  63. package/lib/panelview/model.js +8 -8
  64. package/lib/panelview/objectproperties.js +10 -10
  65. package/lib/panelview/rightpanel.d.ts +1 -1
  66. package/lib/panelview/rightpanel.js +10 -10
  67. package/lib/toolbar/widget.d.ts +1 -1
  68. package/lib/toolbar/widget.js +44 -32
  69. package/lib/tools.d.ts +6 -16
  70. package/lib/tools.js +54 -56
  71. package/lib/types.d.ts +2 -0
  72. package/lib/widget.d.ts +30 -6
  73. package/lib/widget.js +43 -9
  74. package/package.json +4 -3
  75. package/style/base.css +10 -0
  76. package/style/symbologyDialog.css +7 -1
  77. package/style/temporalSlider.css +47 -0
@@ -1,4 +1,5 @@
1
1
  import { JupyterGISModel } from '@jupytergis/schema';
2
+ import { showErrorMessage } from '@jupyterlab/apputils';
2
3
  import { CommandRegistry } from '@lumino/commands';
3
4
  import { UUID } from '@lumino/coreutils';
4
5
  import { ContextMenu } from '@lumino/widgets';
@@ -9,26 +10,26 @@ import { ScaleLine } from 'ol/control';
9
10
  import { singleClick } from 'ol/events/condition';
10
11
  import { GeoJSON, MVT } from 'ol/format';
11
12
  import { DragAndDrop, Select } from 'ol/interaction';
12
- import { Image as ImageLayer, Vector as VectorLayer, VectorTile as VectorTileLayer, WebGLTile as WebGlTileLayer } from 'ol/layer';
13
+ import { Heatmap as HeatmapLayer, Image as ImageLayer, Vector as VectorLayer, VectorTile as VectorTileLayer, WebGLTile as WebGlTileLayer } from 'ol/layer';
13
14
  import TileLayer from 'ol/layer/Tile';
14
15
  import { fromLonLat, get as getRegisteredProjection, toLonLat, transformExtent } from 'ol/proj';
15
16
  import { get as getProjection } from 'ol/proj.js';
16
17
  import { register } from 'ol/proj/proj4.js';
17
- import Feature from 'ol/render/Feature';
18
+ import RenderFeature from 'ol/render/Feature';
18
19
  import { GeoTIFF as GeoTIFFSource, ImageTile as ImageTileSource, Vector as VectorSource, VectorTile as VectorTileSource, XYZ as XYZSource } from 'ol/source';
19
20
  import Static from 'ol/source/ImageStatic';
20
21
  import TileSource from 'ol/source/Tile';
21
22
  import { Circle, Fill, Stroke, Style } from 'ol/style';
22
23
  import proj4 from 'proj4';
23
- //@ts-expect-error no types for proj4list
24
24
  import proj4list from 'proj4-list';
25
25
  import * as React from 'react';
26
26
  import AnnotationFloater from '../annotations/components/AnnotationFloater';
27
27
  import { CommandIDs } from '../constants';
28
28
  import StatusBar from '../statusbar/StatusBar';
29
- import { isLightTheme, loadFile, loadGeoTIFFWithCache, throttle } from '../tools';
29
+ import { isLightTheme, loadFile, throttle } from '../tools';
30
30
  import CollaboratorPointers from './CollaboratorPointers';
31
31
  import { FollowIndicator } from './FollowIndicator';
32
+ import TemporalSlider from './TemporalSlider';
32
33
  import { Spinner } from './spinner';
33
34
  export class MainView extends React.Component {
34
35
  constructor(props) {
@@ -128,6 +129,7 @@ export class MainView extends React.Component {
128
129
  });
129
130
  };
130
131
  this.vectorLayerStyleRuleBuilder = (layer) => {
132
+ var _a, _b;
131
133
  const layerParams = layer.parameters;
132
134
  if (!layerParams) {
133
135
  return;
@@ -145,27 +147,25 @@ export class MainView extends React.Component {
145
147
  style: defaultStyle
146
148
  };
147
149
  const layerStyle = Object.assign({}, defaultRules);
148
- if (layer.filters &&
149
- layer.filters.logicalOp &&
150
- layer.filters.appliedFilters.length !== 0) {
151
- const filterExpr = [];
150
+ if (((_a = layer.filters) === null || _a === void 0 ? void 0 : _a.logicalOp) && ((_b = layer.filters.appliedFilters) === null || _b === void 0 ? void 0 : _b.length) > 0) {
151
+ const buildCondition = (filter) => {
152
+ const base = [filter.operator, ['get', filter.feature]];
153
+ return filter.operator === 'between'
154
+ ? [...base, filter.betweenMin, filter.betweenMax]
155
+ : [...base, filter.value];
156
+ };
157
+ let filterExpr;
152
158
  // 'Any' and 'All' operators require more than one argument
153
159
  // So if there's only one filter, skip that part to avoid error
154
160
  if (layer.filters.appliedFilters.length === 1) {
155
- layer.filters.appliedFilters.forEach(filter => {
156
- filterExpr.push(filter.operator, ['get', filter.feature], filter.value);
157
- });
161
+ filterExpr = buildCondition(layer.filters.appliedFilters[0]);
158
162
  }
159
163
  else {
160
- filterExpr.push(layer.filters.logicalOp);
161
164
  // Arguments for "Any" and 'All' need to be wrapped in brackets
162
- layer.filters.appliedFilters.forEach(filter => {
163
- filterExpr.push([
164
- filter.operator,
165
- ['get', filter.feature],
166
- filter.value
167
- ]);
168
- });
165
+ filterExpr = [
166
+ layer.filters.logicalOp,
167
+ ...layer.filters.appliedFilters.map(buildCondition)
168
+ ];
169
169
  }
170
170
  layerStyle.filter = filterExpr;
171
171
  }
@@ -223,16 +223,64 @@ export class MainView extends React.Component {
223
223
  const scaled = ['*', 255, cosIncidence];
224
224
  return scaled;
225
225
  };
226
+ /**
227
+ * Heatmap layers don't work with style based filtering.
228
+ * This modifies the features in the underlying source
229
+ * to work with the temporal controller
230
+ */
231
+ this.handleTemporalController = (id, layer) => {
232
+ var _a, _b, _c, _d, _e, _f;
233
+ const selectedLayer = (_c = (_b = (_a = this._model) === null || _a === void 0 ? void 0 : _a.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
234
+ // Temporal Controller shouldn't be active if more than one layer is selected
235
+ if (!selectedLayer || Object.keys(selectedLayer).length !== 1) {
236
+ return;
237
+ }
238
+ const selectedLayerId = Object.keys(selectedLayer)[0];
239
+ // Don't do anything to unselected layers
240
+ if (selectedLayerId !== id) {
241
+ return;
242
+ }
243
+ const layerParams = layer.parameters;
244
+ const source = this._sources[layerParams.source];
245
+ if ((_d = layer.filters) === null || _d === void 0 ? void 0 : _d.appliedFilters.length) {
246
+ // Heatmaps don't work with existing filter system so this should be fine
247
+ const activeFilter = layer.filters.appliedFilters[0];
248
+ // Save original features on first filter application
249
+ if (!Object.keys(this._originalFeatures).includes(id)) {
250
+ this._originalFeatures[id] = source.getFeatures();
251
+ }
252
+ // clear current features
253
+ source.clear();
254
+ const startTime = (_e = activeFilter.betweenMin) !== null && _e !== void 0 ? _e : 0;
255
+ const endTime = (_f = activeFilter.betweenMax) !== null && _f !== void 0 ? _f : 1000;
256
+ const filteredFeatures = this._originalFeatures[id].filter(feature => {
257
+ const featureTime = feature.get(activeFilter.feature);
258
+ return featureTime >= startTime && featureTime <= endTime;
259
+ });
260
+ // set state for restoration
261
+ this.setState(old => (Object.assign(Object.assign({}, old), { filterStates: Object.assign(Object.assign({}, this.state.filterStates), { [selectedLayerId]: activeFilter }) })));
262
+ source.addFeatures(filteredFeatures);
263
+ }
264
+ else {
265
+ // Restore original features when no filters are applied
266
+ source.addFeatures(this._originalFeatures[id]);
267
+ delete this._originalFeatures[id];
268
+ }
269
+ };
226
270
  this._onClientSharedStateChanged = (sender, clients) => {
227
- var _a, _b, _c, _d, _e;
228
- const remoteUser = (_a = this._model.localState) === null || _a === void 0 ? void 0 : _a.remoteUser;
271
+ var _a, _b, _c;
272
+ const localState = this._model.localState;
273
+ if (!localState) {
274
+ return;
275
+ }
276
+ const remoteUser = localState.remoteUser;
229
277
  // If we are in following mode, we update our position and selection
230
278
  if (remoteUser) {
231
279
  const remoteState = clients.get(remoteUser);
232
280
  if (!remoteState) {
233
281
  return;
234
282
  }
235
- if (((_b = remoteState.user) === null || _b === void 0 ? void 0 : _b.username) !== ((_c = this.state.remoteUser) === null || _c === void 0 ? void 0 : _c.username)) {
283
+ if (((_a = remoteState.user) === null || _a === void 0 ? void 0 : _a.username) !== ((_b = this.state.remoteUser) === null || _b === void 0 ? void 0 : _b.username)) {
236
284
  this.setState(old => (Object.assign(Object.assign({}, old), { remoteUser: remoteState.user })));
237
285
  }
238
286
  const remoteViewport = remoteState.viewportState;
@@ -246,7 +294,7 @@ export class MainView extends React.Component {
246
294
  // If we are unfollowing a remote user, we reset our center and zoom to their previous values
247
295
  if (this.state.remoteUser !== null) {
248
296
  this.setState(old => (Object.assign(Object.assign({}, old), { remoteUser: null })));
249
- const viewportState = (_e = (_d = this._model.localState) === null || _d === void 0 ? void 0 : _d.viewportState) === null || _e === void 0 ? void 0 : _e.value;
297
+ const viewportState = (_c = localState.viewportState) === null || _c === void 0 ? void 0 : _c.value;
250
298
  if (viewportState) {
251
299
  this._moveToPosition(viewportState.coordinates, viewportState.zoom);
252
300
  }
@@ -289,6 +337,13 @@ export class MainView extends React.Component {
289
337
  }
290
338
  this.setState(old => (Object.assign(Object.assign({}, old), { clientPointers: clientPointers })));
291
339
  });
340
+ // Temporal controller bit
341
+ // ? There's probably a better way to get changes in the model to trigger react rerenders
342
+ const isTemporalControllerActive = localState.isTemporalControllerActive;
343
+ if (isTemporalControllerActive !== this.state.displayTemporalController) {
344
+ this.setState(old => (Object.assign(Object.assign({}, old), { displayTemporalController: isTemporalControllerActive })));
345
+ this._mainViewModel.commands.notifyCommandChanged(CommandIDs.temporalController);
346
+ }
292
347
  };
293
348
  this._onSharedModelStateChange = (_, change) => {
294
349
  var _a;
@@ -343,10 +398,11 @@ export class MainView extends React.Component {
343
398
  this._handleWindowResize = () => {
344
399
  // TODO SOMETHING
345
400
  };
346
- this._initializedPosition = false;
401
+ this._isPositionInitialized = false;
347
402
  this.divRef = React.createRef(); // Reference of render div
348
403
  this._ready = false;
349
404
  this._sourceToLayerMap = new Map();
405
+ this._originalFeatures = {};
350
406
  this._mainViewModel = this.props.viewModel;
351
407
  this._mainViewModel.viewSettingChanged.connect(this._onViewChanged, this);
352
408
  this._model = this._mainViewModel.jGISModel;
@@ -357,8 +413,10 @@ export class MainView extends React.Component {
357
413
  this._model.sharedLayerTreeChanged.connect(this._onLayerTreeChange, this);
358
414
  this._model.sharedSourcesChanged.connect(this._onSourcesChange, this);
359
415
  this._model.sharedModel.changed.connect(this._onSharedModelStateChange);
360
- this._mainViewModel.jGISModel.sharedMetadataChanged.connect(this._onSharedMetadataChanged, this);
416
+ this._model.sharedMetadataChanged.connect(this._onSharedMetadataChanged, this);
361
417
  this._model.zoomToPositionSignal.connect(this._onZoomToPosition, this);
418
+ this._model.updateLayerSignal.connect(this._triggerLayerUpdate, this);
419
+ this._model.addFeatureAsMsSignal.connect(this._convertFeatureToMs, this);
362
420
  this.state = {
363
421
  id: this._mainViewModel.id,
364
422
  lightTheme: isLightTheme(),
@@ -368,7 +426,10 @@ export class MainView extends React.Component {
368
426
  clientPointers: {},
369
427
  viewProjection: { code: '', units: '' },
370
428
  loadingLayer: false,
371
- scale: 0
429
+ scale: 0,
430
+ loadingErrors: [],
431
+ displayTemporalController: false,
432
+ filterStates: {}
372
433
  };
373
434
  this._sources = [];
374
435
  this._loadingLayers = new Set();
@@ -418,7 +479,7 @@ export class MainView extends React.Component {
418
479
  parameters: { path: event.file.name }
419
480
  };
420
481
  const layerId = UUID.uuid4();
421
- this.addSource(sourceId, sourceModel, layerId);
482
+ this.addSource(sourceId, sourceModel);
422
483
  this._model.sharedModel.addSource(sourceId, sourceModel);
423
484
  const layerModel = {
424
485
  type: 'VectorLayer',
@@ -458,9 +519,6 @@ export class MainView extends React.Component {
458
519
  }
459
520
  });
460
521
  this._Map.on('moveend', () => {
461
- if (!this._initializedPosition) {
462
- return;
463
- }
464
522
  const currentOptions = this._model.getOptions();
465
523
  const view = this._Map.getView();
466
524
  const center = view.getCenter() || [0, 0];
@@ -495,7 +553,6 @@ export class MainView extends React.Component {
495
553
  await this._updateLayersImpl(JupyterGISModel.getOrderedLayerIds(this._model));
496
554
  const options = this._model.getOptions();
497
555
  this.updateOptions(options);
498
- this._initializedPosition = true;
499
556
  }
500
557
  this._Map.getViewport().addEventListener('contextmenu', event => {
501
558
  event.preventDefault();
@@ -510,18 +567,17 @@ export class MainView extends React.Component {
510
567
  } })));
511
568
  }
512
569
  }
513
- async _loadGeoTIFFWithCache(sourceInfo) {
514
- const result = await loadGeoTIFFWithCache(sourceInfo);
515
- return result === null || result === void 0 ? void 0 : result.file;
516
- }
517
570
  /**
518
571
  * Add a source in the map.
519
572
  *
520
573
  * @param id - the source id.
521
574
  * @param source - the source object.
522
575
  */
523
- async addSource(id, source, layerId) {
576
+ async addSource(id, source) {
524
577
  var _a, _b;
578
+ const rasterSourceCommon = {
579
+ interpolate: false
580
+ };
525
581
  let newSource;
526
582
  switch (source.type) {
527
583
  case 'RasterSource': {
@@ -529,29 +585,16 @@ export class MainView extends React.Component {
529
585
  const pmTiles = sourceParameters.url.endsWith('.pmtiles');
530
586
  const url = this.computeSourceUrl(source);
531
587
  if (!pmTiles) {
532
- newSource = new XYZSource({
533
- attributions: sourceParameters.attribution,
534
- minZoom: sourceParameters.minZoom,
535
- maxZoom: sourceParameters.maxZoom,
536
- tileSize: 256,
537
- url: url
538
- });
588
+ newSource = new XYZSource(Object.assign(Object.assign({}, rasterSourceCommon), { attributions: sourceParameters.attribution, minZoom: sourceParameters.minZoom, maxZoom: sourceParameters.maxZoom, tileSize: 256, url: url }));
539
589
  }
540
590
  else {
541
- newSource = new PMTilesRasterSource({
542
- attributions: sourceParameters.attribution,
543
- tileSize: 256,
544
- url: url
545
- });
591
+ newSource = new PMTilesRasterSource(Object.assign(Object.assign({}, rasterSourceCommon), { attributions: sourceParameters.attribution, tileSize: 256, url: url }));
546
592
  }
547
593
  break;
548
594
  }
549
595
  case 'RasterDemSource': {
550
596
  const sourceParameters = source.parameters;
551
- newSource = new ImageTileSource({
552
- url: this.computeSourceUrl(source),
553
- attributions: sourceParameters.attribution
554
- });
597
+ newSource = new ImageTileSource(Object.assign(Object.assign({}, rasterSourceCommon), { url: this.computeSourceUrl(source), attributions: sourceParameters.attribution }));
555
598
  break;
556
599
  }
557
600
  case 'VectorTileSource': {
@@ -564,7 +607,7 @@ export class MainView extends React.Component {
564
607
  minZoom: sourceParameters.minZoom,
565
608
  maxZoom: sourceParameters.maxZoom,
566
609
  url: url,
567
- format: new MVT({ featureClass: Feature })
610
+ format: new MVT({ featureClass: RenderFeature })
568
611
  });
569
612
  }
570
613
  else {
@@ -638,12 +681,7 @@ export class MainView extends React.Component {
638
681
  type: 'ImageSource',
639
682
  model: this._model
640
683
  });
641
- newSource = new Static({
642
- imageExtent: extent,
643
- url: imageUrl,
644
- interpolate: false,
645
- crossOrigin: ''
646
- });
684
+ newSource = new Static(Object.assign(Object.assign({}, rasterSourceCommon), { imageExtent: extent, url: imageUrl, crossOrigin: '' }));
647
685
  break;
648
686
  }
649
687
  case 'VideoSource': {
@@ -655,15 +693,10 @@ export class MainView extends React.Component {
655
693
  const addNoData = (url) => {
656
694
  return Object.assign(Object.assign({}, url), { nodata: 0 });
657
695
  };
658
- const sourcesWithBlobs = await Promise.all(sourceParameters.urls.map(async (sourceInfo) => {
659
- const blob = await this._loadGeoTIFFWithCache(sourceInfo);
660
- return Object.assign(Object.assign({}, addNoData(sourceInfo)), { blob });
696
+ const sources = await Promise.all(sourceParameters.urls.map(async (sourceInfo) => {
697
+ return Object.assign(Object.assign({}, addNoData(sourceInfo)), { min: sourceInfo.min, max: sourceInfo.max, url: sourceInfo.url });
661
698
  }));
662
- newSource = new GeoTIFFSource({
663
- sources: sourcesWithBlobs,
664
- normalize: sourceParameters.normalize,
665
- wrapX: sourceParameters.wrapX
666
- });
699
+ newSource = new GeoTIFFSource(Object.assign(Object.assign({}, rasterSourceCommon), { sources, normalize: sourceParameters.normalize, wrapX: sourceParameters.wrapX }));
667
700
  break;
668
701
  }
669
702
  }
@@ -704,7 +737,7 @@ export class MainView extends React.Component {
704
737
  // remove source being updated
705
738
  this.removeSource(id);
706
739
  // create updated source
707
- await this.addSource(id, source, layerId);
740
+ await this.addSource(id, source);
708
741
  // change source of target layer
709
742
  mapLayer.setSource(this._sources[id]);
710
743
  }
@@ -773,7 +806,7 @@ export class MainView extends React.Component {
773
806
  * @returns - the map layer.
774
807
  */
775
808
  async _buildMapLayer(id, layer) {
776
- var _a;
809
+ var _a, _b, _c;
777
810
  const sourceId = (_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.source;
778
811
  const source = this._model.sharedModel.getLayerSource(sourceId);
779
812
  if (!source) {
@@ -782,7 +815,7 @@ export class MainView extends React.Component {
782
815
  this.setState(old => (Object.assign(Object.assign({}, old), { loadingLayer: true })));
783
816
  this._loadingLayers.add(id);
784
817
  if (!this._sources[sourceId]) {
785
- await this.addSource(sourceId, source, id);
818
+ await this.addSource(sourceId, source);
786
819
  }
787
820
  this._loadingLayers.add(id);
788
821
  let newMapLayer;
@@ -815,7 +848,6 @@ export class MainView extends React.Component {
815
848
  opacity: layerParameters.opacity,
816
849
  source: this._sources[layerParameters.source]
817
850
  });
818
- this.updateLayer(id, layer, newMapLayer);
819
851
  break;
820
852
  }
821
853
  case 'HillshadeLayer': {
@@ -850,6 +882,17 @@ export class MainView extends React.Component {
850
882
  newMapLayer = new WebGlTileLayer(layerOptions);
851
883
  break;
852
884
  }
885
+ case 'HeatmapLayer': {
886
+ layerParameters = layer.parameters;
887
+ newMapLayer = new HeatmapLayer({
888
+ opacity: layerParameters.opacity,
889
+ source: this._sources[layerParameters.source],
890
+ blur: (_b = layerParameters.blur) !== null && _b !== void 0 ? _b : 15,
891
+ radius: (_c = layerParameters.radius) !== null && _c !== void 0 ? _c : 8,
892
+ gradient: layerParameters.color
893
+ });
894
+ break;
895
+ }
853
896
  }
854
897
  await this._waitForSourceReady(newMapLayer);
855
898
  // OpenLayers doesn't have name/id field so add it
@@ -897,10 +940,29 @@ export class MainView extends React.Component {
897
940
  // Layer already exists
898
941
  return;
899
942
  }
900
- const newMapLayer = await this._buildMapLayer(id, layer);
901
- if (newMapLayer !== undefined) {
902
- await this._waitForReady();
903
- this._Map.getLayers().insertAt(index, newMapLayer);
943
+ try {
944
+ const newMapLayer = await this._buildMapLayer(id, layer);
945
+ if (newMapLayer !== undefined) {
946
+ await this._waitForReady();
947
+ // Adjust index to ensure it's within bounds
948
+ const numLayers = this._Map.getLayers().getLength();
949
+ const safeIndex = Math.min(index, numLayers);
950
+ this._Map.getLayers().insertAt(safeIndex, newMapLayer);
951
+ }
952
+ }
953
+ catch (error) {
954
+ if (this.state.loadingErrors.find(item => item.id === id && item.error === error.message)) {
955
+ this._loadingLayers.delete(id);
956
+ return;
957
+ }
958
+ await showErrorMessage(`Error Adding ${layer.name}`, `Failed to add ${layer.name}: ${error.message || 'invalid file path'}`);
959
+ this.setState(old => (Object.assign(Object.assign({}, old), { loadingLayer: false })));
960
+ this.state.loadingErrors.push({
961
+ id,
962
+ error: error.message || 'invalid file path',
963
+ index
964
+ });
965
+ this._loadingLayers.delete(id);
904
966
  }
905
967
  }
906
968
  /**
@@ -909,15 +971,15 @@ export class MainView extends React.Component {
909
971
  * @param id - id of the layer.
910
972
  * @param layer - the layer object.
911
973
  */
912
- async updateLayer(id, layer, mapLayer) {
913
- var _a, _b, _c, _d;
974
+ async updateLayer(id, layer, mapLayer, oldLayer) {
975
+ var _a, _b, _c, _d, _e, _f, _g, _h;
914
976
  const sourceId = (_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.source;
915
977
  const source = this._model.sharedModel.getLayerSource(sourceId);
916
978
  if (!source) {
917
979
  return;
918
980
  }
919
981
  if (!this._sources[sourceId]) {
920
- await this.addSource(sourceId, source, id);
982
+ await this.addSource(sourceId, source);
921
983
  }
922
984
  mapLayer.setVisible(layer.visible);
923
985
  switch (layer.type) {
@@ -953,6 +1015,16 @@ export class MainView extends React.Component {
953
1015
  }
954
1016
  break;
955
1017
  }
1018
+ case 'HeatmapLayer': {
1019
+ const layerParams = layer.parameters;
1020
+ const heatmap = mapLayer;
1021
+ heatmap.setOpacity((_e = layerParams.opacity) !== null && _e !== void 0 ? _e : 1);
1022
+ heatmap.setBlur((_f = layerParams.blur) !== null && _f !== void 0 ? _f : 15);
1023
+ heatmap.setRadius((_g = layerParams.radius) !== null && _g !== void 0 ? _g : 8);
1024
+ heatmap.setGradient((_h = layerParams.color) !== null && _h !== void 0 ? _h : ['#00f', '#0ff', '#0f0', '#ff0', '#f00']);
1025
+ this.handleTemporalController(id, layer);
1026
+ break;
1027
+ }
956
1028
  }
957
1029
  }
958
1030
  /**
@@ -1006,11 +1078,11 @@ export class MainView extends React.Component {
1006
1078
  this._Map.removeLayer(mapLayer);
1007
1079
  }
1008
1080
  }
1009
- _onSharedOptionsChanged(sender, change) {
1010
- if (!this._initializedPosition) {
1081
+ _onSharedOptionsChanged() {
1082
+ if (!this._isPositionInitialized) {
1011
1083
  const options = this._model.getOptions();
1012
1084
  this.updateOptions(options);
1013
- this._initializedPosition = true;
1085
+ this._isPositionInitialized = true;
1014
1086
  }
1015
1087
  }
1016
1088
  async updateOptions(options) {
@@ -1097,7 +1169,20 @@ export class MainView extends React.Component {
1097
1169
  if (currentIndex < index) {
1098
1170
  nextIndex -= 1;
1099
1171
  }
1100
- this._Map.getLayers().insertAt(nextIndex, layer);
1172
+ // Adjust index to ensure it's within bounds
1173
+ const numLayers = this._Map.getLayers().getLength();
1174
+ const safeIndex = Math.min(index, numLayers);
1175
+ this._Map.getLayers().insertAt(safeIndex, layer);
1176
+ }
1177
+ /**
1178
+ * Remove and recreate layer
1179
+ * @param id ID of layer being replaced
1180
+ * @param layer New layer to replace with
1181
+ */
1182
+ replaceLayer(id, layer) {
1183
+ const layerIndex = this.getLayerIndex(id);
1184
+ this.removeLayer(id);
1185
+ this.addLayer(id, layer, layerIndex);
1101
1186
  }
1102
1187
  _onLayersChanged(_, change) {
1103
1188
  var _a;
@@ -1107,21 +1192,25 @@ export class MainView extends React.Component {
1107
1192
  return;
1108
1193
  }
1109
1194
  (_a = change.layerChange) === null || _a === void 0 ? void 0 : _a.forEach(change => {
1110
- const layer = change.newValue;
1111
- if (!layer || Object.keys(layer).length === 0) {
1112
- this.removeLayer(change.id);
1195
+ const { id, oldValue: oldLayer, newValue: newLayer } = change;
1196
+ if (!newLayer || Object.keys(newLayer).length === 0) {
1197
+ this.removeLayer(id);
1198
+ return;
1199
+ }
1200
+ if (oldLayer && oldLayer.type !== newLayer.type) {
1201
+ this.replaceLayer(id, newLayer);
1202
+ return;
1203
+ }
1204
+ const mapLayer = this.getLayer(id);
1205
+ const layerTree = JupyterGISModel.getOrderedLayerIds(this._model);
1206
+ if (!mapLayer) {
1207
+ return;
1208
+ }
1209
+ if (layerTree.includes(id)) {
1210
+ this.updateLayer(id, newLayer, mapLayer, oldLayer);
1113
1211
  }
1114
1212
  else {
1115
- const mapLayer = this.getLayer(change.id);
1116
- const layerTree = JupyterGISModel.getOrderedLayerIds(this._model);
1117
- if (mapLayer) {
1118
- if (layerTree.includes(change.id)) {
1119
- this.updateLayer(change.id, layer, mapLayer);
1120
- }
1121
- else {
1122
- this.updateLayers(layerTree);
1123
- }
1124
- }
1213
+ this.updateLayers(layerTree);
1125
1214
  }
1126
1215
  });
1127
1216
  }
@@ -1256,6 +1345,28 @@ export class MainView extends React.Component {
1256
1345
  }
1257
1346
  }
1258
1347
  }
1348
+ _triggerLayerUpdate(_, args) {
1349
+ // ? could send just the filters object and modify that instead of emitting whole layer
1350
+ const json = JSON.parse(args);
1351
+ const { layerId, layer: jgisLayer } = json;
1352
+ const olLayer = this.getLayer(layerId);
1353
+ if (!jgisLayer || !olLayer) {
1354
+ console.log('Layer not found');
1355
+ return;
1356
+ }
1357
+ this.updateLayer(layerId, jgisLayer, olLayer);
1358
+ }
1359
+ _convertFeatureToMs(_, args) {
1360
+ const json = JSON.parse(args);
1361
+ const { id: layerId, selectedFeature } = json;
1362
+ const olLayer = this.getLayer(layerId);
1363
+ const source = olLayer.getSource();
1364
+ source.forEachFeature(feature => {
1365
+ const time = feature.get(selectedFeature);
1366
+ const parsedTime = typeof time === 'string' ? Date.parse(time) : time;
1367
+ feature.set(`${selectedFeature}ms`, parsedTime);
1368
+ });
1369
+ }
1259
1370
  render() {
1260
1371
  return (React.createElement(React.Fragment, null,
1261
1372
  Object.entries(this.state.annotations).map(([key, annotation]) => {
@@ -1269,18 +1380,20 @@ export class MainView extends React.Component {
1269
1380
  }, className: 'jGIS-Popup-Wrapper' },
1270
1381
  React.createElement(AnnotationFloater, { itemId: key, annotationModel: this._model.annotationModel, open: false }))));
1271
1382
  }),
1272
- React.createElement("div", { className: "jGIS-Mainview", style: {
1273
- border: this.state.remoteUser
1274
- ? `solid 3px ${this.state.remoteUser.color}`
1275
- : 'unset'
1276
- } },
1277
- React.createElement(Spinner, { loading: this.state.loading }),
1278
- React.createElement(FollowIndicator, { remoteUser: this.state.remoteUser }),
1279
- React.createElement(CollaboratorPointers, { clients: this.state.clientPointers }),
1280
- React.createElement("div", { ref: this.divRef, style: {
1281
- width: '100%',
1282
- height: 'calc(100%)'
1283
- } })),
1284
- React.createElement(StatusBar, { jgisModel: this._model, loading: this.state.loadingLayer, projection: this.state.viewProjection, scale: this.state.scale })));
1383
+ React.createElement("div", { className: "jGIS-Mainview-Container" },
1384
+ this.state.displayTemporalController && (React.createElement(TemporalSlider, { model: this._model, filterStates: this.state.filterStates })),
1385
+ React.createElement("div", { className: "jGIS-Mainview", style: {
1386
+ border: this.state.remoteUser
1387
+ ? `solid 3px ${this.state.remoteUser.color}`
1388
+ : 'unset'
1389
+ } },
1390
+ React.createElement(Spinner, { loading: this.state.loading }),
1391
+ React.createElement(FollowIndicator, { remoteUser: this.state.remoteUser }),
1392
+ React.createElement(CollaboratorPointers, { clients: this.state.clientPointers }),
1393
+ React.createElement("div", { ref: this.divRef, style: {
1394
+ width: '100%',
1395
+ height: '100%'
1396
+ } })),
1397
+ React.createElement(StatusBar, { jgisModel: this._model, loading: this.state.loadingLayer, projection: this.state.viewProjection, scale: this.state.scale }))));
1285
1398
  }
1286
1399
  }
@@ -1,5 +1,6 @@
1
1
  import { IAnnotation, IJupyterGISModel } from '@jupytergis/schema';
2
2
  import { ObservableMap } from '@jupyterlab/observables';
3
+ import { CommandRegistry } from '@lumino/commands';
3
4
  import { JSONValue } from '@lumino/coreutils';
4
5
  import { IDisposable } from '@lumino/disposable';
5
6
  export declare class MainViewModel implements IDisposable {
@@ -8,12 +9,14 @@ export declare class MainViewModel implements IDisposable {
8
9
  get id(): string;
9
10
  get jGISModel(): IJupyterGISModel;
10
11
  get viewSettingChanged(): import("@lumino/signaling").ISignal<ObservableMap<JSONValue>, import("@jupyterlab/observables").IObservableMap.IChangedArgs<JSONValue>>;
12
+ get commands(): CommandRegistry;
11
13
  dispose(): void;
12
14
  initSignal(): void;
13
15
  addAnnotation(value: IAnnotation): void;
14
16
  private _onsharedLayersChanged;
15
17
  private _jGISModel;
16
18
  private _viewSetting;
19
+ private _commands;
17
20
  private _id;
18
21
  private _isDisposed;
19
22
  }
@@ -21,5 +24,6 @@ export declare namespace MainViewModel {
21
24
  interface IOptions {
22
25
  jGISModel: IJupyterGISModel;
23
26
  viewSetting: ObservableMap<JSONValue>;
27
+ commands: CommandRegistry;
24
28
  }
25
29
  }
@@ -4,6 +4,7 @@ export class MainViewModel {
4
4
  this._isDisposed = false;
5
5
  this._jGISModel = options.jGISModel;
6
6
  this._viewSetting = options.viewSetting;
7
+ this._commands = options.commands;
7
8
  }
8
9
  get isDisposed() {
9
10
  return this._isDisposed;
@@ -17,6 +18,9 @@ export class MainViewModel {
17
18
  get viewSettingChanged() {
18
19
  return this._viewSetting.changed;
19
20
  }
21
+ get commands() {
22
+ return this._commands;
23
+ }
20
24
  dispose() {
21
25
  if (this._isDisposed) {
22
26
  return;
@@ -3,8 +3,6 @@ import { MainViewModel } from './mainviewmodel';
3
3
  export declare class JupyterGISMainViewPanel extends ReactWidget {
4
4
  /**
5
5
  * Construct a `JupyterGISPanel`.
6
- *
7
- * @param context - The documents context.
8
6
  */
9
7
  constructor(options: {
10
8
  mainViewModel: MainViewModel;
@@ -4,8 +4,6 @@ import { MainView } from './mainView';
4
4
  export class JupyterGISMainViewPanel extends ReactWidget {
5
5
  /**
6
6
  * Construct a `JupyterGISPanel`.
7
- *
8
- * @param context - The documents context.
9
7
  */
10
8
  constructor(options) {
11
9
  super();