@jupytergis/base 0.2.1 → 0.4.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 (95) hide show
  1. package/lib/annotations/components/Annotation.js +2 -2
  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 +146 -62
  6. package/lib/constants.d.ts +3 -0
  7. package/lib/constants.js +5 -1
  8. package/lib/dialogs/formdialog.d.ts +5 -0
  9. package/lib/dialogs/formdialog.js +2 -2
  10. package/lib/dialogs/layerBrowserDialog.d.ts +4 -5
  11. package/lib/dialogs/layerBrowserDialog.js +9 -9
  12. package/lib/dialogs/symbology/components/color_ramp/ModeSelectRow.js +2 -1
  13. package/lib/dialogs/symbology/hooks/useGetBandInfo.d.ts +26 -0
  14. package/lib/dialogs/symbology/hooks/useGetBandInfo.js +64 -0
  15. package/lib/dialogs/symbology/hooks/useGetProperties.d.ts +1 -1
  16. package/lib/dialogs/symbology/hooks/useGetProperties.js +12 -9
  17. package/lib/dialogs/symbology/symbologyDialog.d.ts +2 -3
  18. package/lib/dialogs/symbology/symbologyDialog.js +10 -9
  19. package/lib/dialogs/symbology/tiff_layer/TiffRendering.d.ts +1 -1
  20. package/lib/dialogs/symbology/tiff_layer/TiffRendering.js +16 -3
  21. package/lib/dialogs/symbology/tiff_layer/components/BandRow.d.ts +16 -3
  22. package/lib/dialogs/symbology/tiff_layer/components/BandRow.js +21 -7
  23. package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.d.ts +4 -0
  24. package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +85 -0
  25. package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.d.ts +1 -20
  26. package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +25 -65
  27. package/lib/dialogs/symbology/vector_layer/VectorRendering.d.ts +1 -1
  28. package/lib/dialogs/symbology/vector_layer/VectorRendering.js +18 -13
  29. package/lib/dialogs/symbology/vector_layer/types/Categorized.d.ts +1 -1
  30. package/lib/dialogs/symbology/vector_layer/types/Categorized.js +30 -19
  31. package/lib/dialogs/symbology/vector_layer/types/Graduated.d.ts +1 -1
  32. package/lib/dialogs/symbology/vector_layer/types/Graduated.js +16 -13
  33. package/lib/dialogs/symbology/vector_layer/types/Heatmap.d.ts +4 -0
  34. package/lib/dialogs/symbology/vector_layer/types/Heatmap.js +77 -0
  35. package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.d.ts +1 -1
  36. package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.js +4 -3
  37. package/lib/formbuilder/creationform.d.ts +6 -2
  38. package/lib/formbuilder/creationform.js +6 -6
  39. package/lib/formbuilder/editform.d.ts +2 -2
  40. package/lib/formbuilder/editform.js +14 -9
  41. package/lib/formbuilder/formselectors.js +11 -1
  42. package/lib/formbuilder/objectform/baseform.d.ts +12 -3
  43. package/lib/formbuilder/objectform/baseform.js +39 -0
  44. package/lib/formbuilder/objectform/fileselectorwidget.d.ts +2 -0
  45. package/lib/formbuilder/objectform/fileselectorwidget.js +88 -0
  46. package/lib/formbuilder/objectform/geojsonsource.d.ts +5 -7
  47. package/lib/formbuilder/objectform/geojsonsource.js +8 -24
  48. package/lib/formbuilder/objectform/geotiffsource.d.ts +5 -1
  49. package/lib/formbuilder/objectform/geotiffsource.js +64 -18
  50. package/lib/formbuilder/objectform/heatmapLayerForm.d.ts +11 -0
  51. package/lib/formbuilder/objectform/heatmapLayerForm.js +60 -0
  52. package/lib/formbuilder/objectform/layerform.d.ts +2 -0
  53. package/lib/formbuilder/objectform/layerform.js +6 -0
  54. package/lib/formbuilder/objectform/pathbasedsource.d.ts +19 -0
  55. package/lib/formbuilder/objectform/pathbasedsource.js +98 -0
  56. package/lib/formbuilder/objectform/vectorlayerform.d.ts +0 -2
  57. package/lib/formbuilder/objectform/vectorlayerform.js +0 -59
  58. package/lib/icons.d.ts +1 -0
  59. package/lib/icons.js +5 -0
  60. package/lib/keybindings.json +62 -0
  61. package/lib/mainview/TemporalSlider.d.ts +8 -0
  62. package/lib/mainview/TemporalSlider.js +303 -0
  63. package/lib/mainview/mainView.d.ts +46 -8
  64. package/lib/mainview/mainView.js +431 -144
  65. package/lib/mainview/mainviewmodel.d.ts +4 -0
  66. package/lib/mainview/mainviewmodel.js +4 -0
  67. package/lib/mainview/mainviewwidget.d.ts +0 -2
  68. package/lib/mainview/mainviewwidget.js +0 -2
  69. package/lib/panelview/annotationPanel.js +5 -5
  70. package/lib/panelview/components/filter-panel/Filter.js +8 -24
  71. package/lib/panelview/components/identify-panel/IdentifyPanel.js +1 -1
  72. package/lib/panelview/components/layers.js +2 -2
  73. package/lib/panelview/components/sources.js +1 -1
  74. package/lib/panelview/leftpanel.d.ts +3 -0
  75. package/lib/panelview/leftpanel.js +5 -1
  76. package/lib/panelview/model.js +8 -8
  77. package/lib/panelview/objectproperties.js +10 -10
  78. package/lib/panelview/rightpanel.d.ts +1 -1
  79. package/lib/panelview/rightpanel.js +10 -10
  80. package/lib/statusbar/StatusBar.d.ts +13 -0
  81. package/lib/statusbar/StatusBar.js +52 -0
  82. package/lib/toolbar/widget.d.ts +1 -1
  83. package/lib/toolbar/widget.js +44 -37
  84. package/lib/tools.d.ts +50 -7
  85. package/lib/tools.js +394 -12
  86. package/lib/types.d.ts +2 -0
  87. package/lib/widget.d.ts +29 -5
  88. package/lib/widget.js +41 -7
  89. package/package.json +17 -5
  90. package/style/base.css +11 -0
  91. package/style/icons/logo_mini_qgz.svg +31 -0
  92. package/style/leftPanel.css +8 -0
  93. package/style/statusBar.css +16 -0
  94. package/style/symbologyDialog.css +7 -1
  95. package/style/temporalSlider.css +47 -0
@@ -1,37 +1,107 @@
1
1
  import { JupyterGISModel } from '@jupytergis/schema';
2
+ import { showErrorMessage } from '@jupyterlab/apputils';
3
+ import { CommandRegistry } from '@lumino/commands';
2
4
  import { UUID } from '@lumino/coreutils';
5
+ import { ContextMenu } from '@lumino/widgets';
3
6
  import { Collection, Map as OlMap, View, getUid } from 'ol';
7
+ //@ts-expect-error no types for ol-pmtiles
8
+ import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
4
9
  import { ScaleLine } from 'ol/control';
10
+ import { singleClick } from 'ol/events/condition';
5
11
  import { GeoJSON, MVT } from 'ol/format';
6
12
  import { DragAndDrop, Select } from 'ol/interaction';
7
- 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';
8
14
  import TileLayer from 'ol/layer/Tile';
9
- import { fromLonLat, toLonLat } from 'ol/proj';
10
- import Feature from 'ol/render/Feature';
15
+ import { fromLonLat, get as getRegisteredProjection, toLonLat, transformExtent } from 'ol/proj';
16
+ import { get as getProjection } from 'ol/proj.js';
17
+ import { register } from 'ol/proj/proj4.js';
18
+ import RenderFeature from 'ol/render/Feature';
11
19
  import { GeoTIFF as GeoTIFFSource, ImageTile as ImageTileSource, Vector as VectorSource, VectorTile as VectorTileSource, XYZ as XYZSource } from 'ol/source';
12
20
  import Static from 'ol/source/ImageStatic';
13
- //@ts-expect-error no types for ol-pmtiles
14
- import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
15
- import { register } from 'ol/proj/proj4.js';
16
- import { get as getProjection } from 'ol/proj.js';
21
+ import TileSource from 'ol/source/Tile';
22
+ import { Circle, Fill, Stroke, Style } from 'ol/style';
17
23
  import proj4 from 'proj4';
18
- import * as React from 'react';
19
- import shp from 'shpjs';
20
- import { isLightTheme, loadGeoTIFFWithCache, throttle } from '../tools';
21
- import { Spinner } from './spinner';
22
- //@ts-expect-error no types for proj4-list
23
24
  import proj4list from 'proj4-list';
24
- import { ContextMenu } from '@lumino/widgets';
25
- import { CommandRegistry } from '@lumino/commands';
25
+ import * as React from 'react';
26
26
  import AnnotationFloater from '../annotations/components/AnnotationFloater';
27
27
  import { CommandIDs } from '../constants';
28
- import { FollowIndicator } from './FollowIndicator';
28
+ import StatusBar from '../statusbar/StatusBar';
29
+ import { isLightTheme, loadFile, throttle } from '../tools';
29
30
  import CollaboratorPointers from './CollaboratorPointers';
30
- import { Circle, Fill, Stroke, Style } from 'ol/style';
31
- import { singleClick } from 'ol/events/condition';
31
+ import { FollowIndicator } from './FollowIndicator';
32
+ import TemporalSlider from './TemporalSlider';
33
+ import { Spinner } from './spinner';
32
34
  export class MainView extends React.Component {
33
35
  constructor(props) {
34
36
  super(props);
37
+ this.createSelectInteraction = () => {
38
+ const pointStyle = new Style({
39
+ image: new Circle({
40
+ radius: 5,
41
+ fill: new Fill({
42
+ color: '#C52707'
43
+ }),
44
+ stroke: new Stroke({
45
+ color: '#171717',
46
+ width: 2
47
+ })
48
+ })
49
+ });
50
+ const lineStyle = new Style({
51
+ stroke: new Stroke({
52
+ color: '#171717',
53
+ width: 2
54
+ })
55
+ });
56
+ const polygonStyle = new Style({
57
+ fill: new Fill({ color: '#C5270780' }),
58
+ stroke: new Stroke({
59
+ color: '#171717',
60
+ width: 2
61
+ })
62
+ });
63
+ const styleFunction = (feature) => {
64
+ var _a;
65
+ const geometryType = (_a = feature.getGeometry()) === null || _a === void 0 ? void 0 : _a.getType();
66
+ switch (geometryType) {
67
+ case 'Point':
68
+ case 'MultiPoint':
69
+ return pointStyle;
70
+ case 'LineString':
71
+ case 'MultiLineString':
72
+ return lineStyle;
73
+ case 'Polygon':
74
+ case 'MultiPolygon':
75
+ return polygonStyle;
76
+ }
77
+ };
78
+ const selectInteraction = new Select({
79
+ hitTolerance: 5,
80
+ multi: true,
81
+ layers: layer => {
82
+ var _a, _b;
83
+ const localState = (_a = this._model) === null || _a === void 0 ? void 0 : _a.sharedModel.awareness.getLocalState();
84
+ const selectedLayers = (_b = localState === null || localState === void 0 ? void 0 : localState.selected) === null || _b === void 0 ? void 0 : _b.value;
85
+ if (!selectedLayers) {
86
+ return false;
87
+ }
88
+ const selectedLayerId = Object.keys(selectedLayers)[0];
89
+ return layer === this.getLayer(selectedLayerId);
90
+ },
91
+ condition: (event) => {
92
+ return singleClick(event) && this._model.isIdentifying;
93
+ },
94
+ style: styleFunction
95
+ });
96
+ selectInteraction.on('select', event => {
97
+ const identifiedFeatures = [];
98
+ selectInteraction.getFeatures().forEach(feature => {
99
+ identifiedFeatures.push(feature.getProperties());
100
+ });
101
+ this._model.syncIdentifiedFeatures(identifiedFeatures, this._mainViewModel.id);
102
+ });
103
+ this._Map.addInteraction(selectInteraction);
104
+ };
35
105
  this.addContextMenu = () => {
36
106
  this._commands.addCommand(CommandIDs.addAnnotation, {
37
107
  execute: () => {
@@ -59,6 +129,7 @@ export class MainView extends React.Component {
59
129
  });
60
130
  };
61
131
  this.vectorLayerStyleRuleBuilder = (layer) => {
132
+ var _a, _b;
62
133
  const layerParams = layer.parameters;
63
134
  if (!layerParams) {
64
135
  return;
@@ -76,27 +147,25 @@ export class MainView extends React.Component {
76
147
  style: defaultStyle
77
148
  };
78
149
  const layerStyle = Object.assign({}, defaultRules);
79
- if (layer.filters &&
80
- layer.filters.logicalOp &&
81
- layer.filters.appliedFilters.length !== 0) {
82
- 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;
83
158
  // 'Any' and 'All' operators require more than one argument
84
159
  // So if there's only one filter, skip that part to avoid error
85
160
  if (layer.filters.appliedFilters.length === 1) {
86
- layer.filters.appliedFilters.forEach(filter => {
87
- filterExpr.push(filter.operator, ['get', filter.feature], filter.value);
88
- });
161
+ filterExpr = buildCondition(layer.filters.appliedFilters[0]);
89
162
  }
90
163
  else {
91
- filterExpr.push(layer.filters.logicalOp);
92
164
  // Arguments for "Any" and 'All' need to be wrapped in brackets
93
- layer.filters.appliedFilters.forEach(filter => {
94
- filterExpr.push([
95
- filter.operator,
96
- ['get', filter.feature],
97
- filter.value
98
- ]);
99
- });
165
+ filterExpr = [
166
+ layer.filters.logicalOp,
167
+ ...layer.filters.appliedFilters.map(buildCondition)
168
+ ];
100
169
  }
101
170
  layerStyle.filter = filterExpr;
102
171
  }
@@ -154,16 +223,64 @@ export class MainView extends React.Component {
154
223
  const scaled = ['*', 255, cosIncidence];
155
224
  return scaled;
156
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
+ };
157
270
  this._onClientSharedStateChanged = (sender, clients) => {
158
- var _a, _b, _c, _d, _e;
159
- 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;
160
277
  // If we are in following mode, we update our position and selection
161
278
  if (remoteUser) {
162
279
  const remoteState = clients.get(remoteUser);
163
280
  if (!remoteState) {
164
281
  return;
165
282
  }
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)) {
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)) {
167
284
  this.setState(old => (Object.assign(Object.assign({}, old), { remoteUser: remoteState.user })));
168
285
  }
169
286
  const remoteViewport = remoteState.viewportState;
@@ -177,7 +294,7 @@ export class MainView extends React.Component {
177
294
  // If we are unfollowing a remote user, we reset our center and zoom to their previous values
178
295
  if (this.state.remoteUser !== null) {
179
296
  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;
297
+ const viewportState = (_c = localState.viewportState) === null || _c === void 0 ? void 0 : _c.value;
181
298
  if (viewportState) {
182
299
  this._moveToPosition(viewportState.coordinates, viewportState.zoom);
183
300
  }
@@ -220,6 +337,13 @@ export class MainView extends React.Component {
220
337
  }
221
338
  this.setState(old => (Object.assign(Object.assign({}, old), { clientPointers: clientPointers })));
222
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
+ }
223
347
  };
224
348
  this._onSharedModelStateChange = (_, change) => {
225
349
  var _a;
@@ -274,12 +398,11 @@ export class MainView extends React.Component {
274
398
  this._handleWindowResize = () => {
275
399
  // TODO SOMETHING
276
400
  };
277
- this._initializedPosition = false;
401
+ this._isPositionInitialized = false;
278
402
  this.divRef = React.createRef(); // Reference of render div
279
403
  this._ready = false;
280
404
  this._sourceToLayerMap = new Map();
281
- proj4.defs(Array.from(proj4list));
282
- register(proj4);
405
+ this._originalFeatures = {};
283
406
  this._mainViewModel = this.props.viewModel;
284
407
  this._mainViewModel.viewSettingChanged.connect(this._onViewChanged, this);
285
408
  this._model = this._mainViewModel.jGISModel;
@@ -290,17 +413,26 @@ export class MainView extends React.Component {
290
413
  this._model.sharedLayerTreeChanged.connect(this._onLayerTreeChange, this);
291
414
  this._model.sharedSourcesChanged.connect(this._onSourcesChange, this);
292
415
  this._model.sharedModel.changed.connect(this._onSharedModelStateChange);
293
- this._mainViewModel.jGISModel.sharedMetadataChanged.connect(this._onSharedMetadataChanged, this);
294
- this._model.zoomToAnnotationSignal.connect(this._onZoomToAnnotation, this);
416
+ this._model.sharedMetadataChanged.connect(this._onSharedMetadataChanged, this);
417
+ this._model.zoomToPositionSignal.connect(this._onZoomToPosition, this);
418
+ this._model.updateLayerSignal.connect(this._triggerLayerUpdate, this);
419
+ this._model.addFeatureAsMsSignal.connect(this._convertFeatureToMs, this);
295
420
  this.state = {
296
421
  id: this._mainViewModel.id,
297
422
  lightTheme: isLightTheme(),
298
423
  loading: true,
299
424
  firstLoad: true,
300
425
  annotations: {},
301
- clientPointers: {}
426
+ clientPointers: {},
427
+ viewProjection: { code: '', units: '' },
428
+ loadingLayer: false,
429
+ scale: 0,
430
+ loadingErrors: [],
431
+ displayTemporalController: false,
432
+ filterStates: {}
302
433
  };
303
434
  this._sources = [];
435
+ this._loadingLayers = new Set();
304
436
  this._commands = new CommandRegistry();
305
437
  this._contextMenu = new ContextMenu({ commands: this._commands });
306
438
  }
@@ -364,43 +496,7 @@ export class MainView extends React.Component {
364
496
  this._model.addLayer(layerId, layerModel);
365
497
  });
366
498
  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);
499
+ this.createSelectInteraction();
404
500
  const view = this._Map.getView();
405
501
  // TODO: Note for the future, will need to update listeners if view changes
406
502
  view.on('change:center', throttle(() => {
@@ -423,9 +519,6 @@ export class MainView extends React.Component {
423
519
  }
424
520
  });
425
521
  this._Map.on('moveend', () => {
426
- if (!this._initializedPosition) {
427
- return;
428
- }
429
522
  const currentOptions = this._model.getOptions();
430
523
  const view = this._Map.getView();
431
524
  const center = view.getCenter() || [0, 0];
@@ -433,6 +526,7 @@ export class MainView extends React.Component {
433
526
  const projection = view.getProjection();
434
527
  const latLng = toLonLat(center, projection);
435
528
  const bearing = view.getRotation();
529
+ const resolution = view.getResolution();
436
530
  const updatedOptions = {
437
531
  latitude: latLng[1],
438
532
  longitude: latLng[0],
@@ -442,6 +536,14 @@ export class MainView extends React.Component {
442
536
  };
443
537
  updatedOptions.extent = view.calculateExtent();
444
538
  this._model.setOptions(Object.assign(Object.assign({}, currentOptions), updatedOptions));
539
+ // Calculate scale
540
+ if (resolution) {
541
+ // DPI and inches per meter values taken from OpenLayers
542
+ const dpi = 25.4 / 0.28;
543
+ const inchesPerMeter = 1000 / 25.4;
544
+ const scale = resolution * inchesPerMeter * dpi;
545
+ this.setState(old => (Object.assign(Object.assign({}, old), { scale })));
546
+ }
445
547
  });
446
548
  this._Map.on('click', this._identifyFeature.bind(this));
447
549
  this._Map
@@ -451,7 +553,6 @@ export class MainView extends React.Component {
451
553
  await this._updateLayersImpl(JupyterGISModel.getOrderedLayerIds(this._model));
452
554
  const options = this._model.getOptions();
453
555
  this.updateOptions(options);
454
- this._initializedPosition = true;
455
556
  }
456
557
  this._Map.getViewport().addEventListener('contextmenu', event => {
457
558
  event.preventDefault();
@@ -460,25 +561,12 @@ export class MainView extends React.Component {
460
561
  this._clickCoords = coordinate;
461
562
  this._contextMenu.open(event);
462
563
  });
463
- this.setState(old => (Object.assign(Object.assign({}, old), { loading: false })));
564
+ this.setState(old => (Object.assign(Object.assign({}, old), { loading: false, viewProjection: {
565
+ code: view.getProjection().getCode(),
566
+ units: view.getProjection().getUnits()
567
+ } })));
464
568
  }
465
569
  }
466
- async _loadShapefileAsGeoJSON(url) {
467
- try {
468
- const response = await fetch(`/jupytergis_core/proxy?url=${url}`);
469
- const arrayBuffer = await response.arrayBuffer();
470
- const geojson = await shp(arrayBuffer);
471
- return geojson;
472
- }
473
- catch (error) {
474
- console.error('Error loading shapefile:', error);
475
- throw error;
476
- }
477
- }
478
- async _loadGeoTIFFWithCache(sourceInfo) {
479
- const result = await loadGeoTIFFWithCache(sourceInfo);
480
- return result === null || result === void 0 ? void 0 : result.file;
481
- }
482
570
  /**
483
571
  * Add a source in the map.
484
572
  *
@@ -529,7 +617,7 @@ export class MainView extends React.Component {
529
617
  minZoom: sourceParameters.minZoom,
530
618
  maxZoom: sourceParameters.maxZoom,
531
619
  url: url,
532
- format: new MVT({ featureClass: Feature })
620
+ format: new MVT({ featureClass: RenderFeature })
533
621
  });
534
622
  }
535
623
  else {
@@ -542,7 +630,11 @@ export class MainView extends React.Component {
542
630
  }
543
631
  case 'GeoJSONSource': {
544
632
  const data = ((_a = source.parameters) === null || _a === void 0 ? void 0 : _a.data) ||
545
- (await this._model.readGeoJSON((_b = source.parameters) === null || _b === void 0 ? void 0 : _b.path));
633
+ (await loadFile({
634
+ filepath: (_b = source.parameters) === null || _b === void 0 ? void 0 : _b.path,
635
+ type: 'GeoJSONSource',
636
+ model: this._model
637
+ }));
546
638
  const format = new GeoJSON({
547
639
  featureProjection: this._Map.getView().getProjection()
548
640
  });
@@ -562,7 +654,11 @@ export class MainView extends React.Component {
562
654
  }
563
655
  case 'ShapefileSource': {
564
656
  const parameters = source.parameters;
565
- const geojson = await this._loadShapefileAsGeoJSON(parameters.path);
657
+ const geojson = await loadFile({
658
+ filepath: parameters.path,
659
+ type: 'ShapefileSource',
660
+ model: this._model
661
+ });
566
662
  const geojsonData = Array.isArray(geojson) ? geojson[0] : geojson;
567
663
  const format = new GeoJSON();
568
664
  newSource = new VectorSource({
@@ -590,10 +686,15 @@ export class MainView extends React.Component {
590
686
  const maxX = bottomRight[0];
591
687
  const minY = bottomRight[1];
592
688
  const extent = [minX, minY, maxX, maxY];
689
+ const imageUrl = await loadFile({
690
+ filepath: sourceParameters.path,
691
+ type: 'ImageSource',
692
+ model: this._model
693
+ });
593
694
  newSource = new Static({
594
695
  imageExtent: extent,
595
- url: sourceParameters.url,
596
- interpolate: true,
696
+ url: imageUrl,
697
+ interpolate: false,
597
698
  crossOrigin: ''
598
699
  });
599
700
  break;
@@ -608,8 +709,13 @@ export class MainView extends React.Component {
608
709
  return Object.assign(Object.assign({}, url), { nodata: 0 });
609
710
  };
610
711
  const sourcesWithBlobs = await Promise.all(sourceParameters.urls.map(async (sourceInfo) => {
611
- const blob = await this._loadGeoTIFFWithCache(sourceInfo);
612
- return Object.assign(Object.assign({}, addNoData(sourceInfo)), { blob });
712
+ var _a;
713
+ const geotiff = await loadFile({
714
+ filepath: (_a = sourceInfo.url) !== null && _a !== void 0 ? _a : '',
715
+ type: 'GeoTiffSource',
716
+ model: this._model
717
+ });
718
+ return Object.assign(Object.assign({}, addNoData(sourceInfo)), { geotiff, url: URL.createObjectURL(geotiff.file) });
613
719
  }));
614
720
  newSource = new GeoTIFFSource({
615
721
  sources: sourcesWithBlobs,
@@ -725,15 +831,18 @@ export class MainView extends React.Component {
725
831
  * @returns - the map layer.
726
832
  */
727
833
  async _buildMapLayer(id, layer) {
728
- var _a;
834
+ var _a, _b, _c;
729
835
  const sourceId = (_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.source;
730
836
  const source = this._model.sharedModel.getLayerSource(sourceId);
731
837
  if (!source) {
732
838
  return;
733
839
  }
840
+ this.setState(old => (Object.assign(Object.assign({}, old), { loadingLayer: true })));
841
+ this._loadingLayers.add(id);
734
842
  if (!this._sources[sourceId]) {
735
843
  await this.addSource(sourceId, source, id);
736
844
  }
845
+ this._loadingLayers.add(id);
737
846
  let newMapLayer;
738
847
  let layerParameters;
739
848
  // TODO: OpenLayers provides a bunch of sources for specific tile
@@ -764,7 +873,6 @@ export class MainView extends React.Component {
764
873
  opacity: layerParameters.opacity,
765
874
  source: this._sources[layerParameters.source]
766
875
  });
767
- this.updateLayer(id, layer, newMapLayer);
768
876
  break;
769
877
  }
770
878
  case 'HillshadeLayer': {
@@ -799,13 +907,52 @@ export class MainView extends React.Component {
799
907
  newMapLayer = new WebGlTileLayer(layerOptions);
800
908
  break;
801
909
  }
910
+ case 'HeatmapLayer': {
911
+ layerParameters = layer.parameters;
912
+ newMapLayer = new HeatmapLayer({
913
+ opacity: layerParameters.opacity,
914
+ source: this._sources[layerParameters.source],
915
+ blur: (_b = layerParameters.blur) !== null && _b !== void 0 ? _b : 15,
916
+ radius: (_c = layerParameters.radius) !== null && _c !== void 0 ? _c : 8,
917
+ gradient: layerParameters.color
918
+ });
919
+ break;
920
+ }
802
921
  }
922
+ await this._waitForSourceReady(newMapLayer);
803
923
  // OpenLayers doesn't have name/id field so add it
804
924
  newMapLayer.set('id', id);
805
925
  // we need to keep track of which source has which layers
806
926
  this._sourceToLayerMap.set(layerParameters.source, id);
927
+ this.addProjection(newMapLayer);
928
+ this._loadingLayers.delete(id);
807
929
  return newMapLayer;
808
930
  }
931
+ addProjection(newMapLayer) {
932
+ var _a;
933
+ const sourceProjection = (_a = newMapLayer.getSource()) === null || _a === void 0 ? void 0 : _a.getProjection();
934
+ if (!sourceProjection) {
935
+ console.warn('Layer source projection is undefined or invalid');
936
+ return;
937
+ }
938
+ const projectionCode = sourceProjection.getCode();
939
+ const isProjectionRegistered = getRegisteredProjection(projectionCode);
940
+ if (!isProjectionRegistered) {
941
+ // Check if the projection exists in proj4list
942
+ if (!proj4list[projectionCode]) {
943
+ console.warn(`Projection code '${projectionCode}' not found in proj4list`);
944
+ return;
945
+ }
946
+ try {
947
+ proj4.defs([proj4list[projectionCode]]);
948
+ register(proj4);
949
+ }
950
+ catch (error) {
951
+ console.warn(`Failed to register projection '${projectionCode}'. Error: ${error.message}`);
952
+ return;
953
+ }
954
+ }
955
+ }
809
956
  /**
810
957
  * Add a layer to the map.
811
958
  *
@@ -818,9 +965,29 @@ export class MainView extends React.Component {
818
965
  // Layer already exists
819
966
  return;
820
967
  }
821
- const newMapLayer = await this._buildMapLayer(id, layer);
822
- if (newMapLayer !== undefined) {
823
- this._Map.getLayers().insertAt(index, newMapLayer);
968
+ try {
969
+ const newMapLayer = await this._buildMapLayer(id, layer);
970
+ if (newMapLayer !== undefined) {
971
+ await this._waitForReady();
972
+ // Adjust index to ensure it's within bounds
973
+ const numLayers = this._Map.getLayers().getLength();
974
+ const safeIndex = Math.min(index, numLayers);
975
+ this._Map.getLayers().insertAt(safeIndex, newMapLayer);
976
+ }
977
+ }
978
+ catch (error) {
979
+ if (this.state.loadingErrors.find(item => item.id === id && item.error === error.message)) {
980
+ this._loadingLayers.delete(id);
981
+ return;
982
+ }
983
+ await showErrorMessage(`Error Adding ${layer.name}`, `Failed to add ${layer.name}: ${error.message || 'invalid file path'}`);
984
+ this.setState(old => (Object.assign(Object.assign({}, old), { loadingLayer: false })));
985
+ this.state.loadingErrors.push({
986
+ id,
987
+ error: error.message || 'invalid file path',
988
+ index
989
+ });
990
+ this._loadingLayers.delete(id);
824
991
  }
825
992
  }
826
993
  /**
@@ -829,8 +996,8 @@ export class MainView extends React.Component {
829
996
  * @param id - id of the layer.
830
997
  * @param layer - the layer object.
831
998
  */
832
- async updateLayer(id, layer, mapLayer) {
833
- var _a, _b, _c, _d;
999
+ async updateLayer(id, layer, mapLayer, oldLayer) {
1000
+ var _a, _b, _c, _d, _e, _f, _g, _h;
834
1001
  const sourceId = (_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.source;
835
1002
  const source = this._model.sharedModel.getLayerSource(sourceId);
836
1003
  if (!source) {
@@ -873,8 +1040,58 @@ export class MainView extends React.Component {
873
1040
  }
874
1041
  break;
875
1042
  }
1043
+ case 'HeatmapLayer': {
1044
+ const layerParams = layer.parameters;
1045
+ const heatmap = mapLayer;
1046
+ heatmap.setOpacity((_e = layerParams.opacity) !== null && _e !== void 0 ? _e : 1);
1047
+ heatmap.setBlur((_f = layerParams.blur) !== null && _f !== void 0 ? _f : 15);
1048
+ heatmap.setRadius((_g = layerParams.radius) !== null && _g !== void 0 ? _g : 8);
1049
+ heatmap.setGradient((_h = layerParams.color) !== null && _h !== void 0 ? _h : ['#00f', '#0ff', '#0f0', '#ff0', '#f00']);
1050
+ this.handleTemporalController(id, layer);
1051
+ break;
1052
+ }
876
1053
  }
877
1054
  }
1055
+ /**
1056
+ * Wait for all layers to be loaded.
1057
+ */
1058
+ _waitForReady() {
1059
+ return new Promise(resolve => {
1060
+ const checkReady = () => {
1061
+ if (this._loadingLayers.size === 0) {
1062
+ this.setState(old => (Object.assign(Object.assign({}, old), { loadingLayer: false })));
1063
+ resolve();
1064
+ }
1065
+ else {
1066
+ setTimeout(checkReady, 50);
1067
+ }
1068
+ };
1069
+ checkReady();
1070
+ });
1071
+ }
1072
+ /**
1073
+ * Wait for a layers source state to be 'ready'
1074
+ * @param layer The Layer to check
1075
+ */
1076
+ _waitForSourceReady(layer) {
1077
+ return new Promise((resolve, reject) => {
1078
+ const checkState = () => {
1079
+ const state = layer.getSourceState();
1080
+ if (state === 'ready') {
1081
+ layer.un('change', checkState);
1082
+ resolve();
1083
+ }
1084
+ else if (state === 'error') {
1085
+ layer.un('change', checkState);
1086
+ reject(new Error('Source failed to load.'));
1087
+ }
1088
+ };
1089
+ // Listen for state changes
1090
+ layer.on('change', checkState);
1091
+ // Check the state immediately in case it's already 'ready'
1092
+ checkState();
1093
+ });
1094
+ }
878
1095
  /**
879
1096
  * Remove a layer from the map.
880
1097
  *
@@ -886,11 +1103,11 @@ export class MainView extends React.Component {
886
1103
  this._Map.removeLayer(mapLayer);
887
1104
  }
888
1105
  }
889
- _onSharedOptionsChanged(sender, change) {
890
- if (!this._initializedPosition) {
1106
+ _onSharedOptionsChanged() {
1107
+ if (!this._isPositionInitialized) {
891
1108
  const options = this._model.getOptions();
892
1109
  this.updateOptions(options);
893
- this._initializedPosition = true;
1110
+ this._isPositionInitialized = true;
894
1111
  }
895
1112
  }
896
1113
  async updateOptions(options) {
@@ -977,7 +1194,20 @@ export class MainView extends React.Component {
977
1194
  if (currentIndex < index) {
978
1195
  nextIndex -= 1;
979
1196
  }
980
- this._Map.getLayers().insertAt(nextIndex, layer);
1197
+ // Adjust index to ensure it's within bounds
1198
+ const numLayers = this._Map.getLayers().getLength();
1199
+ const safeIndex = Math.min(index, numLayers);
1200
+ this._Map.getLayers().insertAt(safeIndex, layer);
1201
+ }
1202
+ /**
1203
+ * Remove and recreate layer
1204
+ * @param id ID of layer being replaced
1205
+ * @param layer New layer to replace with
1206
+ */
1207
+ replaceLayer(id, layer) {
1208
+ const layerIndex = this.getLayerIndex(id);
1209
+ this.removeLayer(id);
1210
+ this.addLayer(id, layer, layerIndex);
981
1211
  }
982
1212
  _onLayersChanged(_, change) {
983
1213
  var _a;
@@ -987,21 +1217,25 @@ export class MainView extends React.Component {
987
1217
  return;
988
1218
  }
989
1219
  (_a = change.layerChange) === null || _a === void 0 ? void 0 : _a.forEach(change => {
990
- const layer = change.newValue;
991
- if (!layer || Object.keys(layer).length === 0) {
992
- this.removeLayer(change.id);
1220
+ const { id, oldValue: oldLayer, newValue: newLayer } = change;
1221
+ if (!newLayer || Object.keys(newLayer).length === 0) {
1222
+ this.removeLayer(id);
1223
+ return;
1224
+ }
1225
+ if (oldLayer && oldLayer.type !== newLayer.type) {
1226
+ this.replaceLayer(id, newLayer);
1227
+ return;
1228
+ }
1229
+ const mapLayer = this.getLayer(id);
1230
+ const layerTree = JupyterGISModel.getOrderedLayerIds(this._model);
1231
+ if (!mapLayer) {
1232
+ return;
1233
+ }
1234
+ if (layerTree.includes(id)) {
1235
+ this.updateLayer(id, newLayer, mapLayer, oldLayer);
993
1236
  }
994
1237
  else {
995
- const mapLayer = this.getLayer(change.id);
996
- const layerTree = JupyterGISModel.getOrderedLayerIds(this._model);
997
- if (mapLayer) {
998
- if (layerTree.includes(change.id)) {
999
- this.updateLayer(change.id, layer, mapLayer);
1000
- }
1001
- else {
1002
- this.updateLayers(layerTree);
1003
- }
1004
- }
1238
+ this.updateLayers(layerTree);
1005
1239
  }
1006
1240
  });
1007
1241
  }
@@ -1051,12 +1285,40 @@ export class MainView extends React.Component {
1051
1285
  }
1052
1286
  });
1053
1287
  }
1054
- _onZoomToAnnotation(_, id) {
1288
+ _onZoomToPosition(_, id) {
1055
1289
  var _a;
1290
+ // Check if the id is an annotation
1056
1291
  const annotation = (_a = this._model.annotationModel) === null || _a === void 0 ? void 0 : _a.getAnnotation(id);
1057
1292
  if (annotation) {
1058
1293
  this._moveToPosition(annotation.position, annotation.zoom);
1294
+ return;
1059
1295
  }
1296
+ // The id is a layer
1297
+ let extent;
1298
+ const layer = this.getLayer(id);
1299
+ const source = layer === null || layer === void 0 ? void 0 : layer.getSource();
1300
+ if (source instanceof VectorSource) {
1301
+ extent = source.getExtent();
1302
+ }
1303
+ if (source instanceof TileSource) {
1304
+ // Tiled sources don't have getExtent() so we get it from the grid
1305
+ const tileGrid = source.getTileGrid();
1306
+ extent = tileGrid === null || tileGrid === void 0 ? void 0 : tileGrid.getExtent();
1307
+ }
1308
+ if (!extent) {
1309
+ console.warn('Layer has no extent.');
1310
+ return;
1311
+ }
1312
+ // Convert layer extent value to view projection if needed
1313
+ const sourceProjection = source === null || source === void 0 ? void 0 : source.getProjection();
1314
+ const viewProjection = this._Map.getView().getProjection();
1315
+ const transformedExtent = sourceProjection && sourceProjection !== viewProjection
1316
+ ? transformExtent(extent, sourceProjection, viewProjection)
1317
+ : extent;
1318
+ this._Map.getView().fit(transformedExtent, {
1319
+ size: this._Map.getSize(),
1320
+ duration: 500
1321
+ });
1060
1322
  }
1061
1323
  _moveToPosition(center, zoom, duration = 1000) {
1062
1324
  const view = this._Map.getView();
@@ -1108,6 +1370,28 @@ export class MainView extends React.Component {
1108
1370
  }
1109
1371
  }
1110
1372
  }
1373
+ _triggerLayerUpdate(_, args) {
1374
+ // ? could send just the filters object and modify that instead of emitting whole layer
1375
+ const json = JSON.parse(args);
1376
+ const { layerId, layer: jgisLayer } = json;
1377
+ const olLayer = this.getLayer(layerId);
1378
+ if (!jgisLayer || !olLayer) {
1379
+ console.log('Layer not found');
1380
+ return;
1381
+ }
1382
+ this.updateLayer(layerId, jgisLayer, olLayer);
1383
+ }
1384
+ _convertFeatureToMs(_, args) {
1385
+ const json = JSON.parse(args);
1386
+ const { id: layerId, selectedFeature } = json;
1387
+ const olLayer = this.getLayer(layerId);
1388
+ const source = olLayer.getSource();
1389
+ source.forEachFeature(feature => {
1390
+ const time = feature.get(selectedFeature);
1391
+ const parsedTime = typeof time === 'string' ? Date.parse(time) : time;
1392
+ feature.set(`${selectedFeature}ms`, parsedTime);
1393
+ });
1394
+ }
1111
1395
  render() {
1112
1396
  return (React.createElement(React.Fragment, null,
1113
1397
  Object.entries(this.state.annotations).map(([key, annotation]) => {
@@ -1121,17 +1405,20 @@ export class MainView extends React.Component {
1121
1405
  }, className: 'jGIS-Popup-Wrapper' },
1122
1406
  React.createElement(AnnotationFloater, { itemId: key, annotationModel: this._model.annotationModel, open: false }))));
1123
1407
  }),
1124
- React.createElement("div", { className: "jGIS-Mainview", style: {
1125
- border: this.state.remoteUser
1126
- ? `solid 3px ${this.state.remoteUser.color}`
1127
- : 'unset'
1128
- } },
1129
- React.createElement(Spinner, { loading: this.state.loading }),
1130
- React.createElement(FollowIndicator, { remoteUser: this.state.remoteUser }),
1131
- React.createElement(CollaboratorPointers, { clients: this.state.clientPointers }),
1132
- React.createElement("div", { ref: this.divRef, style: {
1133
- width: '100%',
1134
- height: 'calc(100%)'
1135
- } }))));
1408
+ React.createElement("div", { className: "jGIS-Mainview-Container" },
1409
+ this.state.displayTemporalController && (React.createElement(TemporalSlider, { model: this._model, filterStates: this.state.filterStates })),
1410
+ React.createElement("div", { className: "jGIS-Mainview", style: {
1411
+ border: this.state.remoteUser
1412
+ ? `solid 3px ${this.state.remoteUser.color}`
1413
+ : 'unset'
1414
+ } },
1415
+ React.createElement(Spinner, { loading: this.state.loading }),
1416
+ React.createElement(FollowIndicator, { remoteUser: this.state.remoteUser }),
1417
+ React.createElement(CollaboratorPointers, { clients: this.state.clientPointers }),
1418
+ React.createElement("div", { ref: this.divRef, style: {
1419
+ width: '100%',
1420
+ height: '100%'
1421
+ } })),
1422
+ React.createElement(StatusBar, { jgisModel: this._model, loading: this.state.loadingLayer, projection: this.state.viewProjection, scale: this.state.scale }))));
1136
1423
  }
1137
1424
  }