@jupytergis/base 0.2.1 → 0.3.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/commands.js +30 -1
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +1 -0
- package/lib/dialogs/formdialog.d.ts +5 -0
- package/lib/dialogs/formdialog.js +2 -2
- package/lib/dialogs/symbology/components/color_ramp/ModeSelectRow.js +2 -1
- package/lib/dialogs/symbology/hooks/useGetBandInfo.d.ts +27 -0
- package/lib/dialogs/symbology/hooks/useGetBandInfo.js +59 -0
- package/lib/dialogs/symbology/hooks/useGetProperties.js +6 -1
- package/lib/dialogs/symbology/tiff_layer/TiffRendering.d.ts +1 -1
- package/lib/dialogs/symbology/tiff_layer/TiffRendering.js +14 -1
- package/lib/dialogs/symbology/tiff_layer/components/BandRow.d.ts +16 -3
- package/lib/dialogs/symbology/tiff_layer/components/BandRow.js +21 -7
- package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.d.ts +4 -0
- package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +84 -0
- package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.d.ts +0 -19
- package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +18 -59
- package/lib/formbuilder/creationform.d.ts +5 -0
- package/lib/formbuilder/creationform.js +2 -2
- package/lib/formbuilder/editform.d.ts +1 -0
- package/lib/formbuilder/editform.js +8 -3
- package/lib/formbuilder/formselectors.js +7 -0
- package/lib/formbuilder/objectform/baseform.d.ts +10 -0
- package/lib/formbuilder/objectform/baseform.js +39 -0
- package/lib/formbuilder/objectform/fileselectorwidget.d.ts +2 -0
- package/lib/formbuilder/objectform/fileselectorwidget.js +81 -0
- package/lib/formbuilder/objectform/geojsonsource.d.ts +5 -7
- package/lib/formbuilder/objectform/geojsonsource.js +8 -24
- package/lib/formbuilder/objectform/layerform.d.ts +2 -0
- package/lib/formbuilder/objectform/layerform.js +6 -0
- package/lib/formbuilder/objectform/pathbasedsource.d.ts +19 -0
- package/lib/formbuilder/objectform/pathbasedsource.js +98 -0
- package/lib/icons.d.ts +1 -0
- package/lib/icons.js +5 -0
- package/lib/keybindings.json +62 -0
- package/lib/mainview/mainView.d.ts +22 -5
- package/lib/mainview/mainView.js +224 -75
- package/lib/panelview/components/filter-panel/Filter.js +6 -1
- package/lib/statusbar/StatusBar.d.ts +13 -0
- package/lib/statusbar/StatusBar.js +52 -0
- package/lib/toolbar/widget.js +0 -5
- package/lib/tools.d.ts +40 -1
- package/lib/tools.js +308 -0
- package/package.json +16 -5
- package/style/base.css +1 -0
- package/style/icons/logo_mini_qgz.svg +31 -0
- package/style/leftPanel.css +8 -0
- package/style/statusBar.css +16 -0
package/lib/mainview/mainView.js
CHANGED
|
@@ -1,37 +1,106 @@
|
|
|
1
1
|
import { JupyterGISModel } from '@jupytergis/schema';
|
|
2
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
2
3
|
import { UUID } from '@lumino/coreutils';
|
|
4
|
+
import { ContextMenu } from '@lumino/widgets';
|
|
3
5
|
import { Collection, Map as OlMap, View, getUid } from 'ol';
|
|
6
|
+
//@ts-expect-error no types for ol-pmtiles
|
|
7
|
+
import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
|
|
4
8
|
import { ScaleLine } from 'ol/control';
|
|
9
|
+
import { singleClick } from 'ol/events/condition';
|
|
5
10
|
import { GeoJSON, MVT } from 'ol/format';
|
|
6
11
|
import { DragAndDrop, Select } from 'ol/interaction';
|
|
7
12
|
import { Image as ImageLayer, Vector as VectorLayer, VectorTile as VectorTileLayer, WebGLTile as WebGlTileLayer } from 'ol/layer';
|
|
8
13
|
import TileLayer from 'ol/layer/Tile';
|
|
9
|
-
import { fromLonLat, toLonLat } from 'ol/proj';
|
|
14
|
+
import { fromLonLat, get as getRegisteredProjection, toLonLat, transformExtent } from 'ol/proj';
|
|
15
|
+
import { get as getProjection } from 'ol/proj.js';
|
|
16
|
+
import { register } from 'ol/proj/proj4.js';
|
|
10
17
|
import Feature from 'ol/render/Feature';
|
|
11
18
|
import { GeoTIFF as GeoTIFFSource, ImageTile as ImageTileSource, Vector as VectorSource, VectorTile as VectorTileSource, XYZ as XYZSource } from 'ol/source';
|
|
12
19
|
import Static from 'ol/source/ImageStatic';
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import { register } from 'ol/proj/proj4.js';
|
|
16
|
-
import { get as getProjection } from 'ol/proj.js';
|
|
20
|
+
import TileSource from 'ol/source/Tile';
|
|
21
|
+
import { Circle, Fill, Stroke, Style } from 'ol/style';
|
|
17
22
|
import proj4 from 'proj4';
|
|
18
|
-
|
|
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
|
+
//@ts-expect-error no types for proj4list
|
|
23
24
|
import proj4list from 'proj4-list';
|
|
24
|
-
import
|
|
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
|
|
28
|
+
import StatusBar from '../statusbar/StatusBar';
|
|
29
|
+
import { isLightTheme, loadFile, loadGeoTIFFWithCache, throttle } from '../tools';
|
|
29
30
|
import CollaboratorPointers from './CollaboratorPointers';
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
31
|
+
import { FollowIndicator } from './FollowIndicator';
|
|
32
|
+
import { Spinner } from './spinner';
|
|
32
33
|
export class MainView extends React.Component {
|
|
33
34
|
constructor(props) {
|
|
34
35
|
super(props);
|
|
36
|
+
this.createSelectInteraction = () => {
|
|
37
|
+
const pointStyle = new Style({
|
|
38
|
+
image: new Circle({
|
|
39
|
+
radius: 5,
|
|
40
|
+
fill: new Fill({
|
|
41
|
+
color: '#C52707'
|
|
42
|
+
}),
|
|
43
|
+
stroke: new Stroke({
|
|
44
|
+
color: '#171717',
|
|
45
|
+
width: 2
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
const lineStyle = new Style({
|
|
50
|
+
stroke: new Stroke({
|
|
51
|
+
color: '#171717',
|
|
52
|
+
width: 2
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
const polygonStyle = new Style({
|
|
56
|
+
fill: new Fill({ color: '#C5270780' }),
|
|
57
|
+
stroke: new Stroke({
|
|
58
|
+
color: '#171717',
|
|
59
|
+
width: 2
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
const styleFunction = (feature) => {
|
|
63
|
+
var _a;
|
|
64
|
+
const geometryType = (_a = feature.getGeometry()) === null || _a === void 0 ? void 0 : _a.getType();
|
|
65
|
+
switch (geometryType) {
|
|
66
|
+
case 'Point':
|
|
67
|
+
case 'MultiPoint':
|
|
68
|
+
return pointStyle;
|
|
69
|
+
case 'LineString':
|
|
70
|
+
case 'MultiLineString':
|
|
71
|
+
return lineStyle;
|
|
72
|
+
case 'Polygon':
|
|
73
|
+
case 'MultiPolygon':
|
|
74
|
+
return polygonStyle;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const selectInteraction = new Select({
|
|
78
|
+
hitTolerance: 5,
|
|
79
|
+
multi: true,
|
|
80
|
+
layers: layer => {
|
|
81
|
+
var _a, _b;
|
|
82
|
+
const localState = (_a = this._model) === null || _a === void 0 ? void 0 : _a.sharedModel.awareness.getLocalState();
|
|
83
|
+
const selectedLayers = (_b = localState === null || localState === void 0 ? void 0 : localState.selected) === null || _b === void 0 ? void 0 : _b.value;
|
|
84
|
+
if (!selectedLayers) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const selectedLayerId = Object.keys(selectedLayers)[0];
|
|
88
|
+
return layer === this.getLayer(selectedLayerId);
|
|
89
|
+
},
|
|
90
|
+
condition: (event) => {
|
|
91
|
+
return singleClick(event) && this._model.isIdentifying;
|
|
92
|
+
},
|
|
93
|
+
style: styleFunction
|
|
94
|
+
});
|
|
95
|
+
selectInteraction.on('select', event => {
|
|
96
|
+
const identifiedFeatures = [];
|
|
97
|
+
selectInteraction.getFeatures().forEach(feature => {
|
|
98
|
+
identifiedFeatures.push(feature.getProperties());
|
|
99
|
+
});
|
|
100
|
+
this._model.syncIdentifiedFeatures(identifiedFeatures, this._mainViewModel.id);
|
|
101
|
+
});
|
|
102
|
+
this._Map.addInteraction(selectInteraction);
|
|
103
|
+
};
|
|
35
104
|
this.addContextMenu = () => {
|
|
36
105
|
this._commands.addCommand(CommandIDs.addAnnotation, {
|
|
37
106
|
execute: () => {
|
|
@@ -278,8 +347,6 @@ export class MainView extends React.Component {
|
|
|
278
347
|
this.divRef = React.createRef(); // Reference of render div
|
|
279
348
|
this._ready = false;
|
|
280
349
|
this._sourceToLayerMap = new Map();
|
|
281
|
-
proj4.defs(Array.from(proj4list));
|
|
282
|
-
register(proj4);
|
|
283
350
|
this._mainViewModel = this.props.viewModel;
|
|
284
351
|
this._mainViewModel.viewSettingChanged.connect(this._onViewChanged, this);
|
|
285
352
|
this._model = this._mainViewModel.jGISModel;
|
|
@@ -291,16 +358,20 @@ export class MainView extends React.Component {
|
|
|
291
358
|
this._model.sharedSourcesChanged.connect(this._onSourcesChange, this);
|
|
292
359
|
this._model.sharedModel.changed.connect(this._onSharedModelStateChange);
|
|
293
360
|
this._mainViewModel.jGISModel.sharedMetadataChanged.connect(this._onSharedMetadataChanged, this);
|
|
294
|
-
this._model.
|
|
361
|
+
this._model.zoomToPositionSignal.connect(this._onZoomToPosition, this);
|
|
295
362
|
this.state = {
|
|
296
363
|
id: this._mainViewModel.id,
|
|
297
364
|
lightTheme: isLightTheme(),
|
|
298
365
|
loading: true,
|
|
299
366
|
firstLoad: true,
|
|
300
367
|
annotations: {},
|
|
301
|
-
clientPointers: {}
|
|
368
|
+
clientPointers: {},
|
|
369
|
+
viewProjection: { code: '', units: '' },
|
|
370
|
+
loadingLayer: false,
|
|
371
|
+
scale: 0
|
|
302
372
|
};
|
|
303
373
|
this._sources = [];
|
|
374
|
+
this._loadingLayers = new Set();
|
|
304
375
|
this._commands = new CommandRegistry();
|
|
305
376
|
this._contextMenu = new ContextMenu({ commands: this._commands });
|
|
306
377
|
}
|
|
@@ -364,43 +435,7 @@ export class MainView extends React.Component {
|
|
|
364
435
|
this._model.addLayer(layerId, layerModel);
|
|
365
436
|
});
|
|
366
437
|
this._Map.addInteraction(dragAndDropInteraction);
|
|
367
|
-
|
|
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);
|
|
438
|
+
this.createSelectInteraction();
|
|
404
439
|
const view = this._Map.getView();
|
|
405
440
|
// TODO: Note for the future, will need to update listeners if view changes
|
|
406
441
|
view.on('change:center', throttle(() => {
|
|
@@ -433,6 +468,7 @@ export class MainView extends React.Component {
|
|
|
433
468
|
const projection = view.getProjection();
|
|
434
469
|
const latLng = toLonLat(center, projection);
|
|
435
470
|
const bearing = view.getRotation();
|
|
471
|
+
const resolution = view.getResolution();
|
|
436
472
|
const updatedOptions = {
|
|
437
473
|
latitude: latLng[1],
|
|
438
474
|
longitude: latLng[0],
|
|
@@ -442,6 +478,14 @@ export class MainView extends React.Component {
|
|
|
442
478
|
};
|
|
443
479
|
updatedOptions.extent = view.calculateExtent();
|
|
444
480
|
this._model.setOptions(Object.assign(Object.assign({}, currentOptions), updatedOptions));
|
|
481
|
+
// Calculate scale
|
|
482
|
+
if (resolution) {
|
|
483
|
+
// DPI and inches per meter values taken from OpenLayers
|
|
484
|
+
const dpi = 25.4 / 0.28;
|
|
485
|
+
const inchesPerMeter = 1000 / 25.4;
|
|
486
|
+
const scale = resolution * inchesPerMeter * dpi;
|
|
487
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { scale })));
|
|
488
|
+
}
|
|
445
489
|
});
|
|
446
490
|
this._Map.on('click', this._identifyFeature.bind(this));
|
|
447
491
|
this._Map
|
|
@@ -460,19 +504,10 @@ export class MainView extends React.Component {
|
|
|
460
504
|
this._clickCoords = coordinate;
|
|
461
505
|
this._contextMenu.open(event);
|
|
462
506
|
});
|
|
463
|
-
this.setState(old => (Object.assign(Object.assign({}, old), { loading: false
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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;
|
|
507
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { loading: false, viewProjection: {
|
|
508
|
+
code: view.getProjection().getCode(),
|
|
509
|
+
units: view.getProjection().getUnits()
|
|
510
|
+
} })));
|
|
476
511
|
}
|
|
477
512
|
}
|
|
478
513
|
async _loadGeoTIFFWithCache(sourceInfo) {
|
|
@@ -542,7 +577,11 @@ export class MainView extends React.Component {
|
|
|
542
577
|
}
|
|
543
578
|
case 'GeoJSONSource': {
|
|
544
579
|
const data = ((_a = source.parameters) === null || _a === void 0 ? void 0 : _a.data) ||
|
|
545
|
-
(await
|
|
580
|
+
(await loadFile({
|
|
581
|
+
filepath: (_b = source.parameters) === null || _b === void 0 ? void 0 : _b.path,
|
|
582
|
+
type: 'GeoJSONSource',
|
|
583
|
+
model: this._model
|
|
584
|
+
}));
|
|
546
585
|
const format = new GeoJSON({
|
|
547
586
|
featureProjection: this._Map.getView().getProjection()
|
|
548
587
|
});
|
|
@@ -562,7 +601,11 @@ export class MainView extends React.Component {
|
|
|
562
601
|
}
|
|
563
602
|
case 'ShapefileSource': {
|
|
564
603
|
const parameters = source.parameters;
|
|
565
|
-
const geojson = await
|
|
604
|
+
const geojson = await loadFile({
|
|
605
|
+
filepath: parameters.path,
|
|
606
|
+
type: 'ShapefileSource',
|
|
607
|
+
model: this._model
|
|
608
|
+
});
|
|
566
609
|
const geojsonData = Array.isArray(geojson) ? geojson[0] : geojson;
|
|
567
610
|
const format = new GeoJSON();
|
|
568
611
|
newSource = new VectorSource({
|
|
@@ -590,10 +633,15 @@ export class MainView extends React.Component {
|
|
|
590
633
|
const maxX = bottomRight[0];
|
|
591
634
|
const minY = bottomRight[1];
|
|
592
635
|
const extent = [minX, minY, maxX, maxY];
|
|
636
|
+
const imageUrl = await loadFile({
|
|
637
|
+
filepath: sourceParameters.path,
|
|
638
|
+
type: 'ImageSource',
|
|
639
|
+
model: this._model
|
|
640
|
+
});
|
|
593
641
|
newSource = new Static({
|
|
594
642
|
imageExtent: extent,
|
|
595
|
-
url:
|
|
596
|
-
interpolate:
|
|
643
|
+
url: imageUrl,
|
|
644
|
+
interpolate: false,
|
|
597
645
|
crossOrigin: ''
|
|
598
646
|
});
|
|
599
647
|
break;
|
|
@@ -731,9 +779,12 @@ export class MainView extends React.Component {
|
|
|
731
779
|
if (!source) {
|
|
732
780
|
return;
|
|
733
781
|
}
|
|
782
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { loadingLayer: true })));
|
|
783
|
+
this._loadingLayers.add(id);
|
|
734
784
|
if (!this._sources[sourceId]) {
|
|
735
785
|
await this.addSource(sourceId, source, id);
|
|
736
786
|
}
|
|
787
|
+
this._loadingLayers.add(id);
|
|
737
788
|
let newMapLayer;
|
|
738
789
|
let layerParameters;
|
|
739
790
|
// TODO: OpenLayers provides a bunch of sources for specific tile
|
|
@@ -800,12 +851,40 @@ export class MainView extends React.Component {
|
|
|
800
851
|
break;
|
|
801
852
|
}
|
|
802
853
|
}
|
|
854
|
+
await this._waitForSourceReady(newMapLayer);
|
|
803
855
|
// OpenLayers doesn't have name/id field so add it
|
|
804
856
|
newMapLayer.set('id', id);
|
|
805
857
|
// we need to keep track of which source has which layers
|
|
806
858
|
this._sourceToLayerMap.set(layerParameters.source, id);
|
|
859
|
+
this.addProjection(newMapLayer);
|
|
860
|
+
this._loadingLayers.delete(id);
|
|
807
861
|
return newMapLayer;
|
|
808
862
|
}
|
|
863
|
+
addProjection(newMapLayer) {
|
|
864
|
+
var _a;
|
|
865
|
+
const sourceProjection = (_a = newMapLayer.getSource()) === null || _a === void 0 ? void 0 : _a.getProjection();
|
|
866
|
+
if (!sourceProjection) {
|
|
867
|
+
console.warn('Layer source projection is undefined or invalid');
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const projectionCode = sourceProjection.getCode();
|
|
871
|
+
const isProjectionRegistered = getRegisteredProjection(projectionCode);
|
|
872
|
+
if (!isProjectionRegistered) {
|
|
873
|
+
// Check if the projection exists in proj4list
|
|
874
|
+
if (!proj4list[projectionCode]) {
|
|
875
|
+
console.warn(`Projection code '${projectionCode}' not found in proj4list`);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
proj4.defs([proj4list[projectionCode]]);
|
|
880
|
+
register(proj4);
|
|
881
|
+
}
|
|
882
|
+
catch (error) {
|
|
883
|
+
console.warn(`Failed to register projection '${projectionCode}'. Error: ${error.message}`);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
809
888
|
/**
|
|
810
889
|
* Add a layer to the map.
|
|
811
890
|
*
|
|
@@ -820,6 +899,7 @@ export class MainView extends React.Component {
|
|
|
820
899
|
}
|
|
821
900
|
const newMapLayer = await this._buildMapLayer(id, layer);
|
|
822
901
|
if (newMapLayer !== undefined) {
|
|
902
|
+
await this._waitForReady();
|
|
823
903
|
this._Map.getLayers().insertAt(index, newMapLayer);
|
|
824
904
|
}
|
|
825
905
|
}
|
|
@@ -875,6 +955,46 @@ export class MainView extends React.Component {
|
|
|
875
955
|
}
|
|
876
956
|
}
|
|
877
957
|
}
|
|
958
|
+
/**
|
|
959
|
+
* Wait for all layers to be loaded.
|
|
960
|
+
*/
|
|
961
|
+
_waitForReady() {
|
|
962
|
+
return new Promise(resolve => {
|
|
963
|
+
const checkReady = () => {
|
|
964
|
+
if (this._loadingLayers.size === 0) {
|
|
965
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { loadingLayer: false })));
|
|
966
|
+
resolve();
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
setTimeout(checkReady, 50);
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
checkReady();
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Wait for a layers source state to be 'ready'
|
|
977
|
+
* @param layer The Layer to check
|
|
978
|
+
*/
|
|
979
|
+
_waitForSourceReady(layer) {
|
|
980
|
+
return new Promise((resolve, reject) => {
|
|
981
|
+
const checkState = () => {
|
|
982
|
+
const state = layer.getSourceState();
|
|
983
|
+
if (state === 'ready') {
|
|
984
|
+
layer.un('change', checkState);
|
|
985
|
+
resolve();
|
|
986
|
+
}
|
|
987
|
+
else if (state === 'error') {
|
|
988
|
+
layer.un('change', checkState);
|
|
989
|
+
reject(new Error('Source failed to load.'));
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
// Listen for state changes
|
|
993
|
+
layer.on('change', checkState);
|
|
994
|
+
// Check the state immediately in case it's already 'ready'
|
|
995
|
+
checkState();
|
|
996
|
+
});
|
|
997
|
+
}
|
|
878
998
|
/**
|
|
879
999
|
* Remove a layer from the map.
|
|
880
1000
|
*
|
|
@@ -1051,12 +1171,40 @@ export class MainView extends React.Component {
|
|
|
1051
1171
|
}
|
|
1052
1172
|
});
|
|
1053
1173
|
}
|
|
1054
|
-
|
|
1174
|
+
_onZoomToPosition(_, id) {
|
|
1055
1175
|
var _a;
|
|
1176
|
+
// Check if the id is an annotation
|
|
1056
1177
|
const annotation = (_a = this._model.annotationModel) === null || _a === void 0 ? void 0 : _a.getAnnotation(id);
|
|
1057
1178
|
if (annotation) {
|
|
1058
1179
|
this._moveToPosition(annotation.position, annotation.zoom);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
// The id is a layer
|
|
1183
|
+
let extent;
|
|
1184
|
+
const layer = this.getLayer(id);
|
|
1185
|
+
const source = layer === null || layer === void 0 ? void 0 : layer.getSource();
|
|
1186
|
+
if (source instanceof VectorSource) {
|
|
1187
|
+
extent = source.getExtent();
|
|
1188
|
+
}
|
|
1189
|
+
if (source instanceof TileSource) {
|
|
1190
|
+
// Tiled sources don't have getExtent() so we get it from the grid
|
|
1191
|
+
const tileGrid = source.getTileGrid();
|
|
1192
|
+
extent = tileGrid === null || tileGrid === void 0 ? void 0 : tileGrid.getExtent();
|
|
1059
1193
|
}
|
|
1194
|
+
if (!extent) {
|
|
1195
|
+
console.warn('Layer has no extent.');
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
// Convert layer extent value to view projection if needed
|
|
1199
|
+
const sourceProjection = source === null || source === void 0 ? void 0 : source.getProjection();
|
|
1200
|
+
const viewProjection = this._Map.getView().getProjection();
|
|
1201
|
+
const transformedExtent = sourceProjection && sourceProjection !== viewProjection
|
|
1202
|
+
? transformExtent(extent, sourceProjection, viewProjection)
|
|
1203
|
+
: extent;
|
|
1204
|
+
this._Map.getView().fit(transformedExtent, {
|
|
1205
|
+
size: this._Map.getSize(),
|
|
1206
|
+
duration: 500
|
|
1207
|
+
});
|
|
1060
1208
|
}
|
|
1061
1209
|
_moveToPosition(center, zoom, duration = 1000) {
|
|
1062
1210
|
const view = this._Map.getView();
|
|
@@ -1132,6 +1280,7 @@ export class MainView extends React.Component {
|
|
|
1132
1280
|
React.createElement("div", { ref: this.divRef, style: {
|
|
1133
1281
|
width: '100%',
|
|
1134
1282
|
height: 'calc(100%)'
|
|
1135
|
-
} }))
|
|
1283
|
+
} })),
|
|
1284
|
+
React.createElement(StatusBar, { jgisModel: this._model, loading: this.state.loadingLayer, projection: this.state.viewProjection, scale: this.state.scale })));
|
|
1136
1285
|
}
|
|
1137
1286
|
}
|
|
@@ -4,6 +4,7 @@ import { cloneDeep } from 'lodash';
|
|
|
4
4
|
import React, { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { debounce, getLayerTileInfo } from '../../../tools';
|
|
6
6
|
import FilterRow from './FilterRow';
|
|
7
|
+
import { loadFile } from '../../../tools';
|
|
7
8
|
/**
|
|
8
9
|
* The filters panel widget.
|
|
9
10
|
*/
|
|
@@ -150,7 +151,11 @@ const FilterComponent = (props) => {
|
|
|
150
151
|
break;
|
|
151
152
|
}
|
|
152
153
|
case 'GeoJSONSource': {
|
|
153
|
-
const data = await (
|
|
154
|
+
const data = await loadFile({
|
|
155
|
+
filepath: (_d = source.parameters) === null || _d === void 0 ? void 0 : _d.path,
|
|
156
|
+
type: 'GeoJSONSource',
|
|
157
|
+
model: model
|
|
158
|
+
});
|
|
154
159
|
data === null || data === void 0 ? void 0 : data.features.forEach((feature) => {
|
|
155
160
|
feature.properties &&
|
|
156
161
|
addFeatureValue(feature.properties, aggregatedProperties);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { IJupyterGISModel } from '@jupytergis/schema';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
interface IStatusBarProps {
|
|
4
|
+
jgisModel: IJupyterGISModel;
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
projection?: {
|
|
7
|
+
code: string;
|
|
8
|
+
units: string;
|
|
9
|
+
};
|
|
10
|
+
scale: number;
|
|
11
|
+
}
|
|
12
|
+
declare const StatusBar: ({ jgisModel, loading, projection, scale }: IStatusBarProps) => React.JSX.Element;
|
|
13
|
+
export default StatusBar;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { faGlobe, faLocationDot, faRuler } from '@fortawesome/free-solid-svg-icons';
|
|
2
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
3
|
+
import { Progress } from '@jupyter/react-components';
|
|
4
|
+
import React, { useEffect, useState } from 'react';
|
|
5
|
+
import { version } from '../../package.json'; // Adjust the path as necessary
|
|
6
|
+
const StatusBar = ({ jgisModel, loading, projection, scale }) => {
|
|
7
|
+
var _a;
|
|
8
|
+
const [coords, setCoords] = useState({ x: 0, y: 0 });
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const handleClientStateChanged = () => {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
const pointer = (_b = (_a = jgisModel === null || jgisModel === void 0 ? void 0 : jgisModel.localState) === null || _a === void 0 ? void 0 : _a.pointer) === null || _b === void 0 ? void 0 : _b.value;
|
|
13
|
+
if (!pointer) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
setCoords({ x: pointer === null || pointer === void 0 ? void 0 : pointer.coordinates.x, y: pointer === null || pointer === void 0 ? void 0 : pointer.coordinates.y });
|
|
17
|
+
};
|
|
18
|
+
jgisModel.clientStateChanged.connect(handleClientStateChanged);
|
|
19
|
+
return () => {
|
|
20
|
+
jgisModel.clientStateChanged.disconnect(handleClientStateChanged);
|
|
21
|
+
};
|
|
22
|
+
}, [jgisModel]);
|
|
23
|
+
return (React.createElement("div", { className: "jgis-status-bar" },
|
|
24
|
+
loading && (React.createElement("div", { style: { width: '16%', padding: '0 6px' } },
|
|
25
|
+
React.createElement(Progress, { height: 14 }))),
|
|
26
|
+
React.createElement("div", { className: "jgis-status-bar-item" },
|
|
27
|
+
React.createElement("span", null,
|
|
28
|
+
"jgis: ",
|
|
29
|
+
version)),
|
|
30
|
+
React.createElement("div", { className: "jgis-status-bar-item jgis-status-bar-coords" },
|
|
31
|
+
React.createElement(FontAwesomeIcon, { icon: faLocationDot }),
|
|
32
|
+
React.createElement("span", null,
|
|
33
|
+
' ',
|
|
34
|
+
"x: ",
|
|
35
|
+
Math.trunc(coords.x),
|
|
36
|
+
" y: ",
|
|
37
|
+
Math.trunc(coords.y))),
|
|
38
|
+
React.createElement("div", { className: "jgis-status-bar-item" },
|
|
39
|
+
React.createElement(FontAwesomeIcon, { icon: faRuler }),
|
|
40
|
+
' ',
|
|
41
|
+
React.createElement("span", null,
|
|
42
|
+
"Scale: 1: ",
|
|
43
|
+
Math.trunc(scale))),
|
|
44
|
+
React.createElement("div", { className: "jgis-status-bar-item" },
|
|
45
|
+
React.createElement(FontAwesomeIcon, { icon: faGlobe }),
|
|
46
|
+
' ',
|
|
47
|
+
React.createElement("span", null, (_a = projection === null || projection === void 0 ? void 0 : projection.code) !== null && _a !== void 0 ? _a : null)),
|
|
48
|
+
React.createElement("div", { className: "jgis-status-bar-item" },
|
|
49
|
+
"Units: ", projection === null || projection === void 0 ? void 0 :
|
|
50
|
+
projection.units)));
|
|
51
|
+
};
|
|
52
|
+
export default StatusBar;
|
package/lib/toolbar/widget.js
CHANGED
|
@@ -101,11 +101,6 @@ export class ToolbarWidget extends ReactiveToolbar {
|
|
|
101
101
|
label: '',
|
|
102
102
|
commands: options.commands
|
|
103
103
|
}));
|
|
104
|
-
options.commands.addKeyBinding({
|
|
105
|
-
command: CommandIDs.identify,
|
|
106
|
-
keys: ['Escape'],
|
|
107
|
-
selector: '#main'
|
|
108
|
-
});
|
|
109
104
|
this.addItem('spacer', ReactiveToolbar.createSpacerItem());
|
|
110
105
|
// Users
|
|
111
106
|
this.addItem('users', ReactWidget.create(React.createElement(UsersItem, { model: options.model })));
|
package/lib/tools.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VectorTile } from '@mapbox/vector-tile';
|
|
2
|
-
import { IDict, IJGISLayerBrowserRegistry, IJGISOptions } from '@jupytergis/schema';
|
|
2
|
+
import { IDict, IJGISLayerBrowserRegistry, IJGISOptions, IJGISSource, IJupyterGISModel } from '@jupytergis/schema';
|
|
3
3
|
export declare const debounce: (func: CallableFunction, timeout?: number) => CallableFunction;
|
|
4
4
|
export declare function throttle<T extends (...args: any[]) => void>(callback: T, delay?: number): T;
|
|
5
5
|
export declare function getElementFromProperty(filePath?: string | null, prop?: string | null): HTMLElement | undefined | null;
|
|
@@ -67,3 +67,42 @@ export declare const loadGeoTIFFWithCache: (sourceInfo: {
|
|
|
67
67
|
metadata: any;
|
|
68
68
|
sourceUrl: string;
|
|
69
69
|
} | null>;
|
|
70
|
+
/**
|
|
71
|
+
* Generalized file reader for different source types.
|
|
72
|
+
*
|
|
73
|
+
* @param fileInfo - Object containing the file path and source type.
|
|
74
|
+
* @returns A promise that resolves to the file content.
|
|
75
|
+
*/
|
|
76
|
+
export declare const loadFile: (fileInfo: {
|
|
77
|
+
filepath: string;
|
|
78
|
+
type: IJGISSource["type"];
|
|
79
|
+
model: IJupyterGISModel;
|
|
80
|
+
}) => Promise<any>;
|
|
81
|
+
/**
|
|
82
|
+
* Converts a base64-encoded string to a Blob.
|
|
83
|
+
*
|
|
84
|
+
* @param base64 - The base64-encoded string representing the file data.
|
|
85
|
+
* @param mimeType - The MIME type of the data.
|
|
86
|
+
* @returns A promise that resolves to a Blob representing the decoded data.
|
|
87
|
+
*/
|
|
88
|
+
export declare const base64ToBlob: (base64: string, mimeType: string) => Promise<Blob>;
|
|
89
|
+
/**
|
|
90
|
+
* A mapping of file extensions to their corresponding MIME types.
|
|
91
|
+
*/
|
|
92
|
+
export declare const MIME_TYPES: {
|
|
93
|
+
[ext: string]: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Determine the MIME type based on the file extension.
|
|
97
|
+
*
|
|
98
|
+
* @param filename - The name of the file.
|
|
99
|
+
* @returns A string representing the MIME type.
|
|
100
|
+
*/
|
|
101
|
+
export declare const getMimeType: (filename: string) => string;
|
|
102
|
+
/**
|
|
103
|
+
* Helper to convert a string (base64) to ArrayBuffer.
|
|
104
|
+
*
|
|
105
|
+
* @param content - File content as a base64 string.
|
|
106
|
+
* @returns An ArrayBuffer.
|
|
107
|
+
*/
|
|
108
|
+
export declare const stringToArrayBuffer: (content: string) => Promise<ArrayBuffer>;
|