@jupytergis/base 0.2.0 → 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 +228 -76
- 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';
|
|
3
|
-
import {
|
|
4
|
+
import { ContextMenu } from '@lumino/widgets';
|
|
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
|
});
|
|
@@ -552,6 +591,9 @@ export class MainView extends React.Component {
|
|
|
552
591
|
featureProjection: this._Map.getView().getProjection()
|
|
553
592
|
});
|
|
554
593
|
const featureCollection = new Collection(featureArray);
|
|
594
|
+
featureCollection.forEach(feature => {
|
|
595
|
+
feature.setId(getUid(feature));
|
|
596
|
+
});
|
|
555
597
|
newSource = new VectorSource({
|
|
556
598
|
features: featureCollection
|
|
557
599
|
});
|
|
@@ -559,7 +601,11 @@ export class MainView extends React.Component {
|
|
|
559
601
|
}
|
|
560
602
|
case 'ShapefileSource': {
|
|
561
603
|
const parameters = source.parameters;
|
|
562
|
-
const geojson = await
|
|
604
|
+
const geojson = await loadFile({
|
|
605
|
+
filepath: parameters.path,
|
|
606
|
+
type: 'ShapefileSource',
|
|
607
|
+
model: this._model
|
|
608
|
+
});
|
|
563
609
|
const geojsonData = Array.isArray(geojson) ? geojson[0] : geojson;
|
|
564
610
|
const format = new GeoJSON();
|
|
565
611
|
newSource = new VectorSource({
|
|
@@ -587,10 +633,15 @@ export class MainView extends React.Component {
|
|
|
587
633
|
const maxX = bottomRight[0];
|
|
588
634
|
const minY = bottomRight[1];
|
|
589
635
|
const extent = [minX, minY, maxX, maxY];
|
|
636
|
+
const imageUrl = await loadFile({
|
|
637
|
+
filepath: sourceParameters.path,
|
|
638
|
+
type: 'ImageSource',
|
|
639
|
+
model: this._model
|
|
640
|
+
});
|
|
590
641
|
newSource = new Static({
|
|
591
642
|
imageExtent: extent,
|
|
592
|
-
url:
|
|
593
|
-
interpolate:
|
|
643
|
+
url: imageUrl,
|
|
644
|
+
interpolate: false,
|
|
594
645
|
crossOrigin: ''
|
|
595
646
|
});
|
|
596
647
|
break;
|
|
@@ -728,9 +779,12 @@ export class MainView extends React.Component {
|
|
|
728
779
|
if (!source) {
|
|
729
780
|
return;
|
|
730
781
|
}
|
|
782
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { loadingLayer: true })));
|
|
783
|
+
this._loadingLayers.add(id);
|
|
731
784
|
if (!this._sources[sourceId]) {
|
|
732
785
|
await this.addSource(sourceId, source, id);
|
|
733
786
|
}
|
|
787
|
+
this._loadingLayers.add(id);
|
|
734
788
|
let newMapLayer;
|
|
735
789
|
let layerParameters;
|
|
736
790
|
// TODO: OpenLayers provides a bunch of sources for specific tile
|
|
@@ -797,12 +851,40 @@ export class MainView extends React.Component {
|
|
|
797
851
|
break;
|
|
798
852
|
}
|
|
799
853
|
}
|
|
854
|
+
await this._waitForSourceReady(newMapLayer);
|
|
800
855
|
// OpenLayers doesn't have name/id field so add it
|
|
801
856
|
newMapLayer.set('id', id);
|
|
802
857
|
// we need to keep track of which source has which layers
|
|
803
858
|
this._sourceToLayerMap.set(layerParameters.source, id);
|
|
859
|
+
this.addProjection(newMapLayer);
|
|
860
|
+
this._loadingLayers.delete(id);
|
|
804
861
|
return newMapLayer;
|
|
805
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
|
+
}
|
|
806
888
|
/**
|
|
807
889
|
* Add a layer to the map.
|
|
808
890
|
*
|
|
@@ -817,6 +899,7 @@ export class MainView extends React.Component {
|
|
|
817
899
|
}
|
|
818
900
|
const newMapLayer = await this._buildMapLayer(id, layer);
|
|
819
901
|
if (newMapLayer !== undefined) {
|
|
902
|
+
await this._waitForReady();
|
|
820
903
|
this._Map.getLayers().insertAt(index, newMapLayer);
|
|
821
904
|
}
|
|
822
905
|
}
|
|
@@ -872,6 +955,46 @@ export class MainView extends React.Component {
|
|
|
872
955
|
}
|
|
873
956
|
}
|
|
874
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
|
+
}
|
|
875
998
|
/**
|
|
876
999
|
* Remove a layer from the map.
|
|
877
1000
|
*
|
|
@@ -1048,12 +1171,40 @@ export class MainView extends React.Component {
|
|
|
1048
1171
|
}
|
|
1049
1172
|
});
|
|
1050
1173
|
}
|
|
1051
|
-
|
|
1174
|
+
_onZoomToPosition(_, id) {
|
|
1052
1175
|
var _a;
|
|
1176
|
+
// Check if the id is an annotation
|
|
1053
1177
|
const annotation = (_a = this._model.annotationModel) === null || _a === void 0 ? void 0 : _a.getAnnotation(id);
|
|
1054
1178
|
if (annotation) {
|
|
1055
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();
|
|
1056
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
|
+
});
|
|
1057
1208
|
}
|
|
1058
1209
|
_moveToPosition(center, zoom, duration = 1000) {
|
|
1059
1210
|
const view = this._Map.getView();
|
|
@@ -1129,6 +1280,7 @@ export class MainView extends React.Component {
|
|
|
1129
1280
|
React.createElement("div", { ref: this.divRef, style: {
|
|
1130
1281
|
width: '100%',
|
|
1131
1282
|
height: 'calc(100%)'
|
|
1132
|
-
} }))
|
|
1283
|
+
} })),
|
|
1284
|
+
React.createElement(StatusBar, { jgisModel: this._model, loading: this.state.loadingLayer, projection: this.state.viewProjection, scale: this.state.scale })));
|
|
1133
1285
|
}
|
|
1134
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>;
|