@jupytergis/base 0.3.0 → 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.
- package/lib/annotations/components/Annotation.js +1 -1
- package/lib/annotations/model.d.ts +6 -7
- package/lib/annotations/model.js +15 -15
- package/lib/commands.d.ts +2 -3
- package/lib/commands.js +117 -62
- package/lib/constants.d.ts +2 -0
- package/lib/constants.js +4 -1
- package/lib/dialogs/formdialog.js +2 -2
- package/lib/dialogs/layerBrowserDialog.d.ts +4 -5
- package/lib/dialogs/layerBrowserDialog.js +9 -9
- package/lib/dialogs/symbology/hooks/useGetBandInfo.d.ts +1 -2
- package/lib/dialogs/symbology/hooks/useGetBandInfo.js +11 -6
- package/lib/dialogs/symbology/hooks/useGetProperties.d.ts +1 -1
- package/lib/dialogs/symbology/hooks/useGetProperties.js +6 -8
- package/lib/dialogs/symbology/symbologyDialog.d.ts +2 -3
- package/lib/dialogs/symbology/symbologyDialog.js +10 -9
- package/lib/dialogs/symbology/tiff_layer/TiffRendering.d.ts +1 -1
- package/lib/dialogs/symbology/tiff_layer/TiffRendering.js +6 -6
- package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.d.ts +1 -1
- package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +5 -4
- package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.d.ts +1 -1
- package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +8 -7
- package/lib/dialogs/symbology/vector_layer/VectorRendering.d.ts +1 -1
- package/lib/dialogs/symbology/vector_layer/VectorRendering.js +18 -13
- package/lib/dialogs/symbology/vector_layer/types/Categorized.d.ts +1 -1
- package/lib/dialogs/symbology/vector_layer/types/Categorized.js +30 -19
- package/lib/dialogs/symbology/vector_layer/types/Graduated.d.ts +1 -1
- package/lib/dialogs/symbology/vector_layer/types/Graduated.js +16 -13
- package/lib/dialogs/symbology/vector_layer/types/Heatmap.d.ts +4 -0
- package/lib/dialogs/symbology/vector_layer/types/Heatmap.js +77 -0
- package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.d.ts +1 -1
- package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.js +4 -3
- package/lib/formbuilder/creationform.d.ts +1 -2
- package/lib/formbuilder/creationform.js +4 -4
- package/lib/formbuilder/editform.d.ts +1 -2
- package/lib/formbuilder/editform.js +7 -7
- package/lib/formbuilder/formselectors.js +5 -2
- package/lib/formbuilder/objectform/baseform.d.ts +3 -4
- package/lib/formbuilder/objectform/baseform.js +2 -2
- package/lib/formbuilder/objectform/fileselectorwidget.js +13 -6
- package/lib/formbuilder/objectform/geotiffsource.d.ts +5 -1
- package/lib/formbuilder/objectform/geotiffsource.js +64 -18
- package/lib/formbuilder/objectform/heatmapLayerForm.d.ts +11 -0
- package/lib/formbuilder/objectform/heatmapLayerForm.js +60 -0
- package/lib/formbuilder/objectform/vectorlayerform.d.ts +0 -2
- package/lib/formbuilder/objectform/vectorlayerform.js +0 -59
- package/lib/mainview/TemporalSlider.d.ts +8 -0
- package/lib/mainview/TemporalSlider.js +303 -0
- package/lib/mainview/mainView.d.ts +25 -4
- package/lib/mainview/mainView.js +213 -75
- package/lib/mainview/mainviewmodel.d.ts +4 -0
- package/lib/mainview/mainviewmodel.js +4 -0
- package/lib/mainview/mainviewwidget.d.ts +0 -2
- package/lib/mainview/mainviewwidget.js +0 -2
- package/lib/panelview/annotationPanel.js +5 -5
- package/lib/panelview/components/filter-panel/Filter.js +4 -25
- package/lib/panelview/components/identify-panel/IdentifyPanel.js +1 -1
- package/lib/panelview/components/layers.js +2 -2
- package/lib/panelview/components/sources.js +1 -1
- package/lib/panelview/leftpanel.d.ts +3 -0
- package/lib/panelview/leftpanel.js +5 -1
- package/lib/panelview/model.js +8 -8
- package/lib/panelview/objectproperties.js +10 -10
- package/lib/panelview/rightpanel.d.ts +1 -1
- package/lib/panelview/rightpanel.js +10 -10
- package/lib/toolbar/widget.d.ts +1 -1
- package/lib/toolbar/widget.js +44 -32
- package/lib/tools.d.ts +10 -6
- package/lib/tools.js +89 -15
- package/lib/types.d.ts +2 -0
- package/lib/widget.d.ts +29 -5
- package/lib/widget.js +41 -7
- package/package.json +4 -3
- package/style/base.css +10 -0
- package/style/symbologyDialog.css +7 -1
- package/style/temporalSlider.css +47 -0
package/lib/mainview/mainView.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
228
|
-
const
|
|
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 (((
|
|
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 = (
|
|
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.
|
|
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.
|
|
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();
|
|
@@ -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,10 +567,6 @@ 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
|
*
|
|
@@ -564,7 +617,7 @@ export class MainView extends React.Component {
|
|
|
564
617
|
minZoom: sourceParameters.minZoom,
|
|
565
618
|
maxZoom: sourceParameters.maxZoom,
|
|
566
619
|
url: url,
|
|
567
|
-
format: new MVT({ featureClass:
|
|
620
|
+
format: new MVT({ featureClass: RenderFeature })
|
|
568
621
|
});
|
|
569
622
|
}
|
|
570
623
|
else {
|
|
@@ -656,8 +709,13 @@ export class MainView extends React.Component {
|
|
|
656
709
|
return Object.assign(Object.assign({}, url), { nodata: 0 });
|
|
657
710
|
};
|
|
658
711
|
const sourcesWithBlobs = await Promise.all(sourceParameters.urls.map(async (sourceInfo) => {
|
|
659
|
-
|
|
660
|
-
|
|
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) });
|
|
661
719
|
}));
|
|
662
720
|
newSource = new GeoTIFFSource({
|
|
663
721
|
sources: sourcesWithBlobs,
|
|
@@ -773,7 +831,7 @@ export class MainView extends React.Component {
|
|
|
773
831
|
* @returns - the map layer.
|
|
774
832
|
*/
|
|
775
833
|
async _buildMapLayer(id, layer) {
|
|
776
|
-
var _a;
|
|
834
|
+
var _a, _b, _c;
|
|
777
835
|
const sourceId = (_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.source;
|
|
778
836
|
const source = this._model.sharedModel.getLayerSource(sourceId);
|
|
779
837
|
if (!source) {
|
|
@@ -815,7 +873,6 @@ export class MainView extends React.Component {
|
|
|
815
873
|
opacity: layerParameters.opacity,
|
|
816
874
|
source: this._sources[layerParameters.source]
|
|
817
875
|
});
|
|
818
|
-
this.updateLayer(id, layer, newMapLayer);
|
|
819
876
|
break;
|
|
820
877
|
}
|
|
821
878
|
case 'HillshadeLayer': {
|
|
@@ -850,6 +907,17 @@ export class MainView extends React.Component {
|
|
|
850
907
|
newMapLayer = new WebGlTileLayer(layerOptions);
|
|
851
908
|
break;
|
|
852
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
|
+
}
|
|
853
921
|
}
|
|
854
922
|
await this._waitForSourceReady(newMapLayer);
|
|
855
923
|
// OpenLayers doesn't have name/id field so add it
|
|
@@ -897,10 +965,29 @@ export class MainView extends React.Component {
|
|
|
897
965
|
// Layer already exists
|
|
898
966
|
return;
|
|
899
967
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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);
|
|
904
991
|
}
|
|
905
992
|
}
|
|
906
993
|
/**
|
|
@@ -909,8 +996,8 @@ export class MainView extends React.Component {
|
|
|
909
996
|
* @param id - id of the layer.
|
|
910
997
|
* @param layer - the layer object.
|
|
911
998
|
*/
|
|
912
|
-
async updateLayer(id, layer, mapLayer) {
|
|
913
|
-
var _a, _b, _c, _d;
|
|
999
|
+
async updateLayer(id, layer, mapLayer, oldLayer) {
|
|
1000
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
914
1001
|
const sourceId = (_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.source;
|
|
915
1002
|
const source = this._model.sharedModel.getLayerSource(sourceId);
|
|
916
1003
|
if (!source) {
|
|
@@ -953,6 +1040,16 @@ export class MainView extends React.Component {
|
|
|
953
1040
|
}
|
|
954
1041
|
break;
|
|
955
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
|
+
}
|
|
956
1053
|
}
|
|
957
1054
|
}
|
|
958
1055
|
/**
|
|
@@ -1006,11 +1103,11 @@ export class MainView extends React.Component {
|
|
|
1006
1103
|
this._Map.removeLayer(mapLayer);
|
|
1007
1104
|
}
|
|
1008
1105
|
}
|
|
1009
|
-
_onSharedOptionsChanged(
|
|
1010
|
-
if (!this.
|
|
1106
|
+
_onSharedOptionsChanged() {
|
|
1107
|
+
if (!this._isPositionInitialized) {
|
|
1011
1108
|
const options = this._model.getOptions();
|
|
1012
1109
|
this.updateOptions(options);
|
|
1013
|
-
this.
|
|
1110
|
+
this._isPositionInitialized = true;
|
|
1014
1111
|
}
|
|
1015
1112
|
}
|
|
1016
1113
|
async updateOptions(options) {
|
|
@@ -1097,7 +1194,20 @@ export class MainView extends React.Component {
|
|
|
1097
1194
|
if (currentIndex < index) {
|
|
1098
1195
|
nextIndex -= 1;
|
|
1099
1196
|
}
|
|
1100
|
-
|
|
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);
|
|
1101
1211
|
}
|
|
1102
1212
|
_onLayersChanged(_, change) {
|
|
1103
1213
|
var _a;
|
|
@@ -1107,21 +1217,25 @@ export class MainView extends React.Component {
|
|
|
1107
1217
|
return;
|
|
1108
1218
|
}
|
|
1109
1219
|
(_a = change.layerChange) === null || _a === void 0 ? void 0 : _a.forEach(change => {
|
|
1110
|
-
const
|
|
1111
|
-
if (!
|
|
1112
|
-
this.removeLayer(
|
|
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);
|
|
1113
1236
|
}
|
|
1114
1237
|
else {
|
|
1115
|
-
|
|
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
|
-
}
|
|
1238
|
+
this.updateLayers(layerTree);
|
|
1125
1239
|
}
|
|
1126
1240
|
});
|
|
1127
1241
|
}
|
|
@@ -1256,6 +1370,28 @@ export class MainView extends React.Component {
|
|
|
1256
1370
|
}
|
|
1257
1371
|
}
|
|
1258
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
|
+
}
|
|
1259
1395
|
render() {
|
|
1260
1396
|
return (React.createElement(React.Fragment, null,
|
|
1261
1397
|
Object.entries(this.state.annotations).map(([key, annotation]) => {
|
|
@@ -1269,18 +1405,20 @@ export class MainView extends React.Component {
|
|
|
1269
1405
|
}, className: 'jGIS-Popup-Wrapper' },
|
|
1270
1406
|
React.createElement(AnnotationFloater, { itemId: key, annotationModel: this._model.annotationModel, open: false }))));
|
|
1271
1407
|
}),
|
|
1272
|
-
React.createElement("div", { className: "jGIS-Mainview",
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
:
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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 }))));
|
|
1285
1423
|
}
|
|
1286
1424
|
}
|
|
@@ -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;
|
|
@@ -9,12 +9,12 @@ export class AnnotationsPanel extends Component {
|
|
|
9
9
|
};
|
|
10
10
|
this._annotationModel = props.annotationModel;
|
|
11
11
|
this._rightPanelModel = props.rightPanelModel;
|
|
12
|
-
this._annotationModel.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
(
|
|
12
|
+
this._annotationModel.modelChanged.connect(async () => {
|
|
13
|
+
// await this._annotationModel?.context?.ready;
|
|
14
|
+
var _a, _b, _c, _d;
|
|
15
|
+
(_b = (_a = this._annotationModel) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.sharedMetadataChanged.disconnect(updateCallback);
|
|
16
16
|
this._annotationModel = props.annotationModel;
|
|
17
|
-
(
|
|
17
|
+
(_d = (_c = this._annotationModel) === null || _c === void 0 ? void 0 : _c.model) === null || _d === void 0 ? void 0 : _d.sharedMetadataChanged.connect(updateCallback);
|
|
18
18
|
this.forceUpdate();
|
|
19
19
|
});
|
|
20
20
|
}
|
|
@@ -2,9 +2,8 @@ import { Button, ReactWidget } from '@jupyterlab/ui-components';
|
|
|
2
2
|
import { Panel } from '@lumino/widgets';
|
|
3
3
|
import { cloneDeep } from 'lodash';
|
|
4
4
|
import React, { useEffect, useRef, useState } from 'react';
|
|
5
|
-
import { debounce,
|
|
5
|
+
import { debounce, loadFile } from '../../../tools';
|
|
6
6
|
import FilterRow from './FilterRow';
|
|
7
|
-
import { loadFile } from '../../../tools';
|
|
8
7
|
/**
|
|
9
8
|
* The filters panel widget.
|
|
10
9
|
*/
|
|
@@ -29,7 +28,7 @@ const FilterComponent = (props) => {
|
|
|
29
28
|
const [featuresInLayer, setFeaturesInLayer] = useState({});
|
|
30
29
|
const [model, setModel] = useState(props.model.jGISModel);
|
|
31
30
|
(_a = props.model) === null || _a === void 0 ? void 0 : _a.documentChanged.connect((_, widget) => {
|
|
32
|
-
setModel(widget === null || widget === void 0 ? void 0 : widget.
|
|
31
|
+
setModel(widget === null || widget === void 0 ? void 0 : widget.model);
|
|
33
32
|
});
|
|
34
33
|
// Reset state values when current widget changes
|
|
35
34
|
useEffect(() => {
|
|
@@ -108,13 +107,12 @@ const FilterComponent = (props) => {
|
|
|
108
107
|
featuresInLayerRef.current = featuresInLayer;
|
|
109
108
|
}, [featuresInLayer]);
|
|
110
109
|
const buildFilterObject = async (currentLayer) => {
|
|
111
|
-
var _a, _b
|
|
110
|
+
var _a, _b;
|
|
112
111
|
if (!model) {
|
|
113
112
|
return;
|
|
114
113
|
}
|
|
115
114
|
const layer = model.getLayer(currentLayer !== null && currentLayer !== void 0 ? currentLayer : selectedLayer);
|
|
116
115
|
const source = model.getSource((_a = layer === null || layer === void 0 ? void 0 : layer.parameters) === null || _a === void 0 ? void 0 : _a.source);
|
|
117
|
-
const { latitude, longitude, extent, zoom } = model.getOptions();
|
|
118
116
|
if (!source || !layer) {
|
|
119
117
|
return;
|
|
120
118
|
}
|
|
@@ -131,28 +129,9 @@ const FilterComponent = (props) => {
|
|
|
131
129
|
});
|
|
132
130
|
}
|
|
133
131
|
switch (source.type) {
|
|
134
|
-
case 'VectorTileSource': {
|
|
135
|
-
try {
|
|
136
|
-
const tile = await getLayerTileInfo((_b = source === null || source === void 0 ? void 0 : source.parameters) === null || _b === void 0 ? void 0 : _b.url, {
|
|
137
|
-
latitude,
|
|
138
|
-
longitude,
|
|
139
|
-
extent,
|
|
140
|
-
zoom
|
|
141
|
-
});
|
|
142
|
-
const layerValue = tile.layers[(_c = layer.parameters) === null || _c === void 0 ? void 0 : _c.sourceLayer];
|
|
143
|
-
for (let i = 0; i < layerValue.length; i++) {
|
|
144
|
-
const feature = layerValue.feature(i);
|
|
145
|
-
addFeatureValue(feature.properties, aggregatedProperties);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
console.warn(`Error fetching tile info: ${error}`);
|
|
150
|
-
}
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
132
|
case 'GeoJSONSource': {
|
|
154
133
|
const data = await loadFile({
|
|
155
|
-
filepath: (
|
|
134
|
+
filepath: (_b = source.parameters) === null || _b === void 0 ? void 0 : _b.path,
|
|
156
135
|
type: 'GeoJSONSource',
|
|
157
136
|
model: model
|
|
158
137
|
});
|
|
@@ -26,7 +26,7 @@ const IdentifyPanelComponent = ({ controlPanelModel, tracker }) => {
|
|
|
26
26
|
* Update the model when it changes.
|
|
27
27
|
*/
|
|
28
28
|
controlPanelModel === null || controlPanelModel === void 0 ? void 0 : controlPanelModel.documentChanged.connect((_, widget) => {
|
|
29
|
-
setJgisModel(widget === null || widget === void 0 ? void 0 : widget.
|
|
29
|
+
setJgisModel(widget === null || widget === void 0 ? void 0 : widget.model);
|
|
30
30
|
});
|
|
31
31
|
// Reset state values when current widget changes
|
|
32
32
|
useEffect(() => {
|