@jupytergis/base 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands.d.ts +11 -0
- package/lib/commands.js +809 -0
- package/lib/console/consoleview.d.ts +24 -0
- package/lib/console/consoleview.js +55 -0
- package/lib/console/index.d.ts +1 -0
- package/lib/console/index.js +1 -0
- package/lib/constants.d.ts +57 -0
- package/lib/constants.js +89 -0
- package/lib/dialogs/components/symbology/BandRendering.d.ts +4 -0
- package/lib/dialogs/components/symbology/BandRendering.js +29 -0
- package/lib/dialogs/components/symbology/BandRow.d.ts +10 -0
- package/lib/dialogs/components/symbology/BandRow.js +43 -0
- package/lib/dialogs/components/symbology/SingleBandPseudoColor.d.ts +20 -0
- package/lib/dialogs/components/symbology/SingleBandPseudoColor.js +281 -0
- package/lib/dialogs/components/symbology/StopRow.d.ts +11 -0
- package/lib/dialogs/components/symbology/StopRow.js +58 -0
- package/lib/dialogs/formdialog.d.ts +31 -0
- package/lib/dialogs/formdialog.js +68 -0
- package/lib/dialogs/layerBrowserDialog.d.ts +25 -0
- package/lib/dialogs/layerBrowserDialog.js +141 -0
- package/lib/dialogs/symbologyDialog.d.ts +23 -0
- package/lib/dialogs/symbologyDialog.js +68 -0
- package/lib/dialogs/terrainDialog.d.ts +21 -0
- package/lib/dialogs/terrainDialog.js +60 -0
- package/lib/formbuilder/creationform.d.ts +56 -0
- package/lib/formbuilder/creationform.js +117 -0
- package/lib/formbuilder/editform.d.ts +24 -0
- package/lib/formbuilder/editform.js +60 -0
- package/lib/formbuilder/formselectors.d.ts +5 -0
- package/lib/formbuilder/formselectors.js +38 -0
- package/lib/formbuilder/index.d.ts +6 -0
- package/lib/formbuilder/index.js +6 -0
- package/lib/formbuilder/objectform/baseform.d.ts +79 -0
- package/lib/formbuilder/objectform/baseform.js +167 -0
- package/lib/formbuilder/objectform/geojsonsource.d.ts +19 -0
- package/lib/formbuilder/objectform/geojsonsource.js +80 -0
- package/lib/formbuilder/objectform/hillshadeLayerForm.d.ts +8 -0
- package/lib/formbuilder/objectform/hillshadeLayerForm.js +12 -0
- package/lib/formbuilder/objectform/layerform.d.ts +19 -0
- package/lib/formbuilder/objectform/layerform.js +17 -0
- package/lib/formbuilder/objectform/tilesourceform.d.ts +7 -0
- package/lib/formbuilder/objectform/tilesourceform.js +60 -0
- package/lib/formbuilder/objectform/vectorlayerform.d.ts +15 -0
- package/lib/formbuilder/objectform/vectorlayerform.js +88 -0
- package/lib/formbuilder/objectform/webGlLayerForm.d.ts +8 -0
- package/lib/formbuilder/objectform/webGlLayerForm.js +10 -0
- package/lib/icons.d.ts +6 -0
- package/lib/icons.js +31 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +10 -0
- package/lib/mainview/index.d.ts +3 -0
- package/lib/mainview/index.js +3 -0
- package/lib/mainview/mainView.d.ts +113 -0
- package/lib/mainview/mainView.js +743 -0
- package/lib/mainview/mainviewmodel.d.ts +24 -0
- package/lib/mainview/mainviewmodel.js +34 -0
- package/lib/mainview/mainviewwidget.d.ts +14 -0
- package/lib/mainview/mainviewwidget.js +18 -0
- package/lib/mainview/spinner.d.ts +6 -0
- package/lib/mainview/spinner.js +5 -0
- package/lib/panelview/components/filter-panel/Filter.d.ts +19 -0
- package/lib/panelview/components/filter-panel/Filter.js +223 -0
- package/lib/panelview/components/filter-panel/FilterRow.d.ts +9 -0
- package/lib/panelview/components/filter-panel/FilterRow.js +61 -0
- package/lib/panelview/components/layers.d.ts +13 -0
- package/lib/panelview/components/layers.js +275 -0
- package/lib/panelview/components/sources.d.ts +10 -0
- package/lib/panelview/components/sources.js +147 -0
- package/lib/panelview/header.d.ts +11 -0
- package/lib/panelview/header.js +20 -0
- package/lib/panelview/index.d.ts +5 -0
- package/lib/panelview/index.js +5 -0
- package/lib/panelview/leftpanel.d.ts +51 -0
- package/lib/panelview/leftpanel.js +125 -0
- package/lib/panelview/model.d.ts +19 -0
- package/lib/panelview/model.js +30 -0
- package/lib/panelview/objectproperties.d.ts +17 -0
- package/lib/panelview/objectproperties.js +92 -0
- package/lib/panelview/rightpanel.d.ts +19 -0
- package/lib/panelview/rightpanel.js +45 -0
- package/lib/toolbar/index.d.ts +2 -0
- package/lib/toolbar/index.js +2 -0
- package/lib/toolbar/usertoolbaritem.d.ts +19 -0
- package/lib/toolbar/usertoolbaritem.js +57 -0
- package/lib/toolbar/widget.d.ts +22 -0
- package/lib/toolbar/widget.js +104 -0
- package/lib/tools.d.ts +25 -0
- package/lib/tools.js +215 -0
- package/lib/types.d.ts +11 -0
- package/lib/types.js +1 -0
- package/lib/widget.d.ts +49 -0
- package/lib/widget.js +144 -0
- package/package.json +95 -0
- package/style/base.css +55 -0
- package/style/colorExpression.css +36 -0
- package/style/dialog.css +8 -0
- package/style/filterPanel.css +70 -0
- package/style/icons/geojson.svg +12 -0
- package/style/icons/mound.svg +9 -0
- package/style/icons/nonvisibility.svg +8 -0
- package/style/icons/raster.svg +5 -0
- package/style/icons/visibility.svg +7 -0
- package/style/index.css +6 -0
- package/style/index.js +6 -0
- package/style/layerBrowser.css +256 -0
- package/style/leftPanel.css +150 -0
- package/style/symbologyDialog.css +86 -0
- package/style/terrainDialog.css +14 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import { JupyterGISModel } from '@jupytergis/schema';
|
|
2
|
+
import { UUID } from '@lumino/coreutils';
|
|
3
|
+
import { Map as OlMap, View } from 'ol';
|
|
4
|
+
import { GeoJSON, MVT } from 'ol/format';
|
|
5
|
+
import DragAndDrop from 'ol/interaction/DragAndDrop';
|
|
6
|
+
import { Image as ImageLayer, Vector as VectorLayer, VectorTile as VectorTileLayer, WebGLTile as WebGlTileLayer } from 'ol/layer';
|
|
7
|
+
import TileLayer from 'ol/layer/Tile';
|
|
8
|
+
import { fromLonLat, toLonLat } from 'ol/proj';
|
|
9
|
+
import Feature from 'ol/render/Feature';
|
|
10
|
+
import { GeoTIFF as GeoTIFFSource, ImageTile as ImageTileSource, Vector as VectorSource, VectorTile as VectorTileSource, XYZ as XYZSource } from 'ol/source';
|
|
11
|
+
import Static from 'ol/source/ImageStatic';
|
|
12
|
+
import { Circle, Fill, Stroke, Style } from 'ol/style';
|
|
13
|
+
//@ts-expect-error no types for ol-pmtiles
|
|
14
|
+
import { PMTilesRasterSource, PMTilesVectorSource } from 'ol-pmtiles';
|
|
15
|
+
import * as React from 'react';
|
|
16
|
+
import { isLightTheme } from '../tools';
|
|
17
|
+
import { Spinner } from './spinner';
|
|
18
|
+
export class MainView extends React.Component {
|
|
19
|
+
constructor(props) {
|
|
20
|
+
super(props);
|
|
21
|
+
this.vectorLayerStyleFunc = (currentFeature, layer) => {
|
|
22
|
+
var _a;
|
|
23
|
+
const layerParameters = layer.parameters;
|
|
24
|
+
// const flatStyle = {
|
|
25
|
+
// 'fill-color': 'rgba(255,255,255,0.4)',
|
|
26
|
+
// 'stroke-color': '#3399CC',
|
|
27
|
+
// 'stroke-width': 1.25,
|
|
28
|
+
// 'circle-radius': 5,
|
|
29
|
+
// 'circle-fill-color': 'rgba(255,255,255,0.4)',
|
|
30
|
+
// 'circle-stroke-width': 1.25,
|
|
31
|
+
// 'circle-stroke-color': '#3399CC'
|
|
32
|
+
// };
|
|
33
|
+
// TODO: Need to make a version that works with strings as well
|
|
34
|
+
const operators = {
|
|
35
|
+
'>': (a, b) => a > b,
|
|
36
|
+
'<': (a, b) => a < b,
|
|
37
|
+
'>=': (a, b) => a >= b,
|
|
38
|
+
'<=': (a, b) => a <= b,
|
|
39
|
+
'==': (a, b) => a === b,
|
|
40
|
+
'!=': (a, b) => a !== b
|
|
41
|
+
};
|
|
42
|
+
// TODO: I don't think this will work with fancy color expressions
|
|
43
|
+
const fill = new Fill({
|
|
44
|
+
color: layerParameters.type === 'fill' || layerParameters.type === 'circle'
|
|
45
|
+
? layerParameters.color
|
|
46
|
+
: '#F092DD'
|
|
47
|
+
});
|
|
48
|
+
const stroke = new Stroke({
|
|
49
|
+
color: layerParameters.type === 'line' || layerParameters.type === 'circle'
|
|
50
|
+
? layerParameters.color
|
|
51
|
+
: '#392F5A',
|
|
52
|
+
width: 2
|
|
53
|
+
});
|
|
54
|
+
const style = new Style({
|
|
55
|
+
fill,
|
|
56
|
+
stroke,
|
|
57
|
+
image: new Circle({
|
|
58
|
+
radius: 5,
|
|
59
|
+
fill,
|
|
60
|
+
stroke
|
|
61
|
+
})
|
|
62
|
+
});
|
|
63
|
+
if (layer.filters && ((_a = layer.filters) === null || _a === void 0 ? void 0 : _a.appliedFilters.length) !== 0) {
|
|
64
|
+
const props = currentFeature.getProperties();
|
|
65
|
+
let shouldDisplayFeature = true;
|
|
66
|
+
switch (layer.filters.logicalOp) {
|
|
67
|
+
case 'any': {
|
|
68
|
+
// Display the feature if any filter conditions apply
|
|
69
|
+
shouldDisplayFeature = layer.filters.appliedFilters.some(({ feature, operator, value }) => operators[operator](props[feature], value));
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case 'all': {
|
|
73
|
+
// Display the feature only if all the filter conditions apply
|
|
74
|
+
shouldDisplayFeature = layer.filters.appliedFilters.every(({ feature, operator, value }) => operators[operator](props[feature], value));
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (shouldDisplayFeature) {
|
|
79
|
+
return style;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return style;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Taken from https://openlayers.org/en/latest/examples/webgl-shaded-relief.html
|
|
91
|
+
* @returns
|
|
92
|
+
*/
|
|
93
|
+
this.hillshadeMath = () => {
|
|
94
|
+
// The method used to extract elevations from the DEM.
|
|
95
|
+
// In this case the format used is Terrarium
|
|
96
|
+
// red * 256 + green + blue / 256 - 32768
|
|
97
|
+
//
|
|
98
|
+
// Other frequently used methods include the Mapbox format
|
|
99
|
+
// (red * 256 * 256 + green * 256 + blue) * 0.1 - 10000
|
|
100
|
+
//
|
|
101
|
+
function elevation(xOffset, yOffset) {
|
|
102
|
+
const red = ['band', 1, xOffset, yOffset];
|
|
103
|
+
const green = ['band', 2, xOffset, yOffset];
|
|
104
|
+
const blue = ['band', 3, xOffset, yOffset];
|
|
105
|
+
// band math operates on normalized values from 0-1
|
|
106
|
+
// so we scale by 255
|
|
107
|
+
return [
|
|
108
|
+
'+',
|
|
109
|
+
['*', 255 * 256, red],
|
|
110
|
+
['*', 255, green],
|
|
111
|
+
['*', 255 / 256, blue],
|
|
112
|
+
-32768
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
// Generates a shaded relief image given elevation data. Uses a 3x3
|
|
116
|
+
// neighborhood for determining slope and aspect.
|
|
117
|
+
const dp = ['*', 2, ['resolution']];
|
|
118
|
+
const z0x = ['*', 2, elevation(-1, 0)];
|
|
119
|
+
const z1x = ['*', 2, elevation(1, 0)];
|
|
120
|
+
const dzdx = ['/', ['-', z1x, z0x], dp];
|
|
121
|
+
const z0y = ['*', 2, elevation(0, -1)];
|
|
122
|
+
const z1y = ['*', 2, elevation(0, 1)];
|
|
123
|
+
const dzdy = ['/', ['-', z1y, z0y], dp];
|
|
124
|
+
const slope = ['atan', ['sqrt', ['+', ['^', dzdx, 2], ['^', dzdy, 2]]]];
|
|
125
|
+
const aspect = ['clamp', ['atan', ['-', 0, dzdx], dzdy], -Math.PI, Math.PI];
|
|
126
|
+
const sunEl = ['*', Math.PI / 180, 45];
|
|
127
|
+
const sunAz = ['*', Math.PI / 180, 46];
|
|
128
|
+
const cosIncidence = [
|
|
129
|
+
'+',
|
|
130
|
+
['*', ['sin', sunEl], ['cos', slope]],
|
|
131
|
+
['*', ['cos', sunEl], ['sin', slope], ['cos', ['-', sunAz, aspect]]]
|
|
132
|
+
];
|
|
133
|
+
const scaled = ['*', 255, cosIncidence];
|
|
134
|
+
return scaled;
|
|
135
|
+
};
|
|
136
|
+
this._onClientSharedStateChanged = (sender, clients) => {
|
|
137
|
+
// TODO SOMETHING
|
|
138
|
+
};
|
|
139
|
+
this._handleThemeChange = () => {
|
|
140
|
+
const lightTheme = isLightTheme();
|
|
141
|
+
// TODO SOMETHING
|
|
142
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { lightTheme })));
|
|
143
|
+
};
|
|
144
|
+
this._handleWindowResize = () => {
|
|
145
|
+
// TODO SOMETHING
|
|
146
|
+
};
|
|
147
|
+
this._initializedPosition = false;
|
|
148
|
+
this.divRef = React.createRef(); // Reference of render div
|
|
149
|
+
this._ready = false;
|
|
150
|
+
this._sourceToLayerMap = new Map();
|
|
151
|
+
this._mainViewModel = this.props.viewModel;
|
|
152
|
+
this._mainViewModel.viewSettingChanged.connect(this._onViewChanged, this);
|
|
153
|
+
this._model = this._mainViewModel.jGISModel;
|
|
154
|
+
this._model.themeChanged.connect(this._handleThemeChange, this);
|
|
155
|
+
this._model.sharedOptionsChanged.connect(this._onSharedOptionsChanged, this);
|
|
156
|
+
this._model.clientStateChanged.connect(this._onClientSharedStateChanged, this);
|
|
157
|
+
this._model.sharedLayersChanged.connect(this._onLayersChanged, this);
|
|
158
|
+
this._model.sharedLayerTreeChanged.connect(this._onLayerTreeChange, this);
|
|
159
|
+
this._model.sharedSourcesChanged.connect(this._onSourcesChange, this);
|
|
160
|
+
this.state = {
|
|
161
|
+
id: this._mainViewModel.id,
|
|
162
|
+
lightTheme: isLightTheme(),
|
|
163
|
+
loading: true,
|
|
164
|
+
firstLoad: true
|
|
165
|
+
};
|
|
166
|
+
this._sources = [];
|
|
167
|
+
}
|
|
168
|
+
async componentDidMount() {
|
|
169
|
+
window.addEventListener('resize', this._handleWindowResize);
|
|
170
|
+
await this.generateScene();
|
|
171
|
+
this._mainViewModel.initSignal();
|
|
172
|
+
}
|
|
173
|
+
componentWillUnmount() {
|
|
174
|
+
window.removeEventListener('resize', this._handleWindowResize);
|
|
175
|
+
this._mainViewModel.viewSettingChanged.disconnect(this._onViewChanged, this);
|
|
176
|
+
this._model.themeChanged.disconnect(this._handleThemeChange, this);
|
|
177
|
+
this._model.sharedOptionsChanged.disconnect(this._onSharedOptionsChanged, this);
|
|
178
|
+
this._model.clientStateChanged.disconnect(this._onClientSharedStateChanged, this);
|
|
179
|
+
this._mainViewModel.dispose();
|
|
180
|
+
}
|
|
181
|
+
async generateScene() {
|
|
182
|
+
if (this.divRef.current) {
|
|
183
|
+
this._Map = new OlMap({
|
|
184
|
+
target: this.divRef.current,
|
|
185
|
+
layers: [],
|
|
186
|
+
view: new View({
|
|
187
|
+
center: [0, 0],
|
|
188
|
+
zoom: 1
|
|
189
|
+
})
|
|
190
|
+
});
|
|
191
|
+
const dragAndDropInteraction = new DragAndDrop({
|
|
192
|
+
formatConstructors: [GeoJSON]
|
|
193
|
+
});
|
|
194
|
+
dragAndDropInteraction.on('addfeatures', event => {
|
|
195
|
+
const sourceId = UUID.uuid4();
|
|
196
|
+
const sourceModel = {
|
|
197
|
+
type: 'GeoJSONSource',
|
|
198
|
+
name: 'Drag and Drop source',
|
|
199
|
+
parameters: { path: event.file.name }
|
|
200
|
+
};
|
|
201
|
+
this.addSource(sourceId, sourceModel);
|
|
202
|
+
this._model.sharedModel.addSource(sourceId, sourceModel);
|
|
203
|
+
const layerModel = {
|
|
204
|
+
type: 'VectorLayer',
|
|
205
|
+
visible: true,
|
|
206
|
+
name: 'Drag and Drop layer',
|
|
207
|
+
parameters: {
|
|
208
|
+
color: '#FF0000',
|
|
209
|
+
opacity: 1.0,
|
|
210
|
+
type: 'line',
|
|
211
|
+
source: sourceId
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const layerId = UUID.uuid4();
|
|
215
|
+
this.addLayer(layerId, layerModel, this.getLayers().length);
|
|
216
|
+
this._model.addLayer(layerId, layerModel);
|
|
217
|
+
});
|
|
218
|
+
this._Map.addInteraction(dragAndDropInteraction);
|
|
219
|
+
this._Map.on('moveend', () => {
|
|
220
|
+
if (!this._initializedPosition) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const currentOptions = this._model.getOptions();
|
|
224
|
+
const view = this._Map.getView();
|
|
225
|
+
const center = view.getCenter() || [0, 0];
|
|
226
|
+
const zoom = view.getZoom() || 0;
|
|
227
|
+
const projection = view.getProjection();
|
|
228
|
+
const latLng = toLonLat(center, projection);
|
|
229
|
+
const bearing = view.getRotation();
|
|
230
|
+
const updatedOptions = {
|
|
231
|
+
latitude: latLng[1],
|
|
232
|
+
longitude: latLng[0],
|
|
233
|
+
bearing,
|
|
234
|
+
projection: projection.getCode(),
|
|
235
|
+
zoom
|
|
236
|
+
};
|
|
237
|
+
// Update the extent only if has been initially provided.
|
|
238
|
+
if (currentOptions.extent) {
|
|
239
|
+
updatedOptions.extent = view.calculateExtent();
|
|
240
|
+
}
|
|
241
|
+
this._model.setOptions(Object.assign(Object.assign({}, currentOptions), updatedOptions));
|
|
242
|
+
});
|
|
243
|
+
if (JupyterGISModel.getOrderedLayerIds(this._model).length !== 0) {
|
|
244
|
+
await this._updateLayersImpl(JupyterGISModel.getOrderedLayerIds(this._model));
|
|
245
|
+
const options = this._model.getOptions();
|
|
246
|
+
this.updateOptions(options);
|
|
247
|
+
}
|
|
248
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { loading: false })));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Add a source in the map.
|
|
253
|
+
*
|
|
254
|
+
* @param id - the source id.
|
|
255
|
+
* @param source - the source object.
|
|
256
|
+
*/
|
|
257
|
+
async addSource(id, source) {
|
|
258
|
+
var _a, _b;
|
|
259
|
+
let newSource;
|
|
260
|
+
switch (source.type) {
|
|
261
|
+
case 'RasterSource': {
|
|
262
|
+
const sourceParameters = source.parameters;
|
|
263
|
+
const pmTiles = sourceParameters.url.endsWith('.pmtiles');
|
|
264
|
+
const url = this.computeSourceUrl(source);
|
|
265
|
+
if (!pmTiles) {
|
|
266
|
+
newSource = new XYZSource({
|
|
267
|
+
attributions: sourceParameters.attribution,
|
|
268
|
+
minZoom: sourceParameters.minZoom,
|
|
269
|
+
maxZoom: sourceParameters.maxZoom,
|
|
270
|
+
tileSize: 256,
|
|
271
|
+
url: url
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
newSource = new PMTilesRasterSource({
|
|
276
|
+
attributions: sourceParameters.attribution,
|
|
277
|
+
tileSize: 256,
|
|
278
|
+
url: url
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case 'RasterDemSource': {
|
|
284
|
+
const sourceParameters = source.parameters;
|
|
285
|
+
newSource = new ImageTileSource({
|
|
286
|
+
url: this.computeSourceUrl(source),
|
|
287
|
+
attributions: sourceParameters.attribution
|
|
288
|
+
});
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case 'VectorTileSource': {
|
|
292
|
+
const sourceParameters = source.parameters;
|
|
293
|
+
const pmTiles = sourceParameters.url.endsWith('.pmtiles');
|
|
294
|
+
const url = this.computeSourceUrl(source);
|
|
295
|
+
if (!pmTiles) {
|
|
296
|
+
newSource = new VectorTileSource({
|
|
297
|
+
attributions: sourceParameters.attribution,
|
|
298
|
+
minZoom: sourceParameters.minZoom,
|
|
299
|
+
maxZoom: sourceParameters.maxZoom,
|
|
300
|
+
url: url,
|
|
301
|
+
format: new MVT({ featureClass: Feature })
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
newSource = new PMTilesVectorSource({
|
|
306
|
+
attributions: sourceParameters.attribution,
|
|
307
|
+
url: url
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case 'GeoJSONSource': {
|
|
313
|
+
const data = ((_a = source.parameters) === null || _a === void 0 ? void 0 : _a.data) ||
|
|
314
|
+
(await this._model.readGeoJSON((_b = source.parameters) === null || _b === void 0 ? void 0 : _b.path));
|
|
315
|
+
const format = new GeoJSON();
|
|
316
|
+
// TODO: Don't hardcode projection
|
|
317
|
+
newSource = new VectorSource({
|
|
318
|
+
features: format.readFeatures(data, {
|
|
319
|
+
dataProjection: 'EPSG:4326',
|
|
320
|
+
featureProjection: this._Map.getView().getProjection()
|
|
321
|
+
})
|
|
322
|
+
});
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
case 'ImageSource': {
|
|
326
|
+
const sourceParameters = source.parameters;
|
|
327
|
+
// Convert lon/lat array to extent
|
|
328
|
+
// Get lon/lat from source coordinates
|
|
329
|
+
const leftSide = Math.min(...sourceParameters.coordinates.map(corner => corner[0]));
|
|
330
|
+
const bottomSide = Math.min(...sourceParameters.coordinates.map(corner => corner[1]));
|
|
331
|
+
const rightSide = Math.max(...sourceParameters.coordinates.map(corner => corner[0]));
|
|
332
|
+
const topSide = Math.max(...sourceParameters.coordinates.map(corner => corner[1]));
|
|
333
|
+
// Convert lon/lat to OpenLayer coordinates
|
|
334
|
+
const topLeft = fromLonLat([leftSide, topSide]);
|
|
335
|
+
const bottomRight = fromLonLat([rightSide, bottomSide]);
|
|
336
|
+
// Get extent from coordinates
|
|
337
|
+
const minX = topLeft[0];
|
|
338
|
+
const maxY = topLeft[1];
|
|
339
|
+
const maxX = bottomRight[0];
|
|
340
|
+
const minY = bottomRight[1];
|
|
341
|
+
const extent = [minX, minY, maxX, maxY];
|
|
342
|
+
newSource = new Static({
|
|
343
|
+
imageExtent: extent,
|
|
344
|
+
url: sourceParameters.url,
|
|
345
|
+
interpolate: true,
|
|
346
|
+
crossOrigin: ''
|
|
347
|
+
});
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case 'VideoSource': {
|
|
351
|
+
console.warn('Video Tiles not supported with Open Layers');
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case 'GeoTiffSource': {
|
|
355
|
+
const sourceParameters = source.parameters;
|
|
356
|
+
newSource = new GeoTIFFSource({
|
|
357
|
+
sources: sourceParameters.urls,
|
|
358
|
+
normalize: sourceParameters.normalize,
|
|
359
|
+
wrapX: sourceParameters.wrapX
|
|
360
|
+
});
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
newSource.set('id', id);
|
|
365
|
+
// _sources is a list of OpenLayers sources
|
|
366
|
+
this._sources[id] = newSource;
|
|
367
|
+
}
|
|
368
|
+
computeSourceUrl(source) {
|
|
369
|
+
const parameters = source.parameters;
|
|
370
|
+
const urlParameters = parameters.urlParameters || {};
|
|
371
|
+
let url = parameters.url;
|
|
372
|
+
for (const parameterName of Object.keys(urlParameters)) {
|
|
373
|
+
url = url.replace(`{${parameterName}}`, urlParameters[parameterName]);
|
|
374
|
+
}
|
|
375
|
+
// Special case for max_zoom and min_zoom
|
|
376
|
+
if (url.includes('{max_zoom}')) {
|
|
377
|
+
url = url.replace('{max_zoom}', parameters.maxZoom.toString());
|
|
378
|
+
}
|
|
379
|
+
if (url.includes('{min_zoom}')) {
|
|
380
|
+
url = url.replace('{min_zoom}', parameters.minZoom.toString());
|
|
381
|
+
}
|
|
382
|
+
return url;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Update a source in the map.
|
|
386
|
+
*
|
|
387
|
+
* @param id - the source id.
|
|
388
|
+
* @param source - the source object.
|
|
389
|
+
*/
|
|
390
|
+
async updateSource(id, source) {
|
|
391
|
+
// get the layer id associated with this source
|
|
392
|
+
const layerId = this._sourceToLayerMap.get(id);
|
|
393
|
+
// get the OL layer
|
|
394
|
+
const mapLayer = this.getLayer(layerId);
|
|
395
|
+
if (!mapLayer) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
// remove source being updated
|
|
399
|
+
this.removeSource(id);
|
|
400
|
+
// create updated source
|
|
401
|
+
this.addSource(id, source);
|
|
402
|
+
// change source of target layer
|
|
403
|
+
mapLayer.setSource(this._sources[id]);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Remove a source from the map.
|
|
407
|
+
*
|
|
408
|
+
* @param id - the source id.
|
|
409
|
+
*/
|
|
410
|
+
removeSource(id) {
|
|
411
|
+
delete this._sources[id];
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Add or move the layers of the map.
|
|
415
|
+
*
|
|
416
|
+
* @param layerIds - the list of layers in the depth order (beneath first).
|
|
417
|
+
*/
|
|
418
|
+
updateLayers(layerIds) {
|
|
419
|
+
this._updateLayersImpl(layerIds);
|
|
420
|
+
}
|
|
421
|
+
async _updateLayersImpl(layerIds) {
|
|
422
|
+
const previousLayerIds = this.getLayers();
|
|
423
|
+
// We use the reverse order of the list to add the layer from the top to the
|
|
424
|
+
// bottom.
|
|
425
|
+
// This is to ensure that the beforeId (layer on top of the one we add/move)
|
|
426
|
+
// is already added/moved in the map.
|
|
427
|
+
const reversedLayerIds = layerIds.slice().reverse();
|
|
428
|
+
for (const layerId of reversedLayerIds) {
|
|
429
|
+
const layer = this._model.sharedModel.getLayer(layerId);
|
|
430
|
+
if (!layer) {
|
|
431
|
+
console.log(`Layer id ${layerId} does not exist`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// Get the expected index in the map.
|
|
435
|
+
const currentLayerIds = [...previousLayerIds];
|
|
436
|
+
let indexInMap = currentLayerIds.length;
|
|
437
|
+
const nextLayer = layerIds[layerIds.indexOf(layerId) + 1];
|
|
438
|
+
if (nextLayer !== undefined) {
|
|
439
|
+
indexInMap = currentLayerIds.indexOf(nextLayer);
|
|
440
|
+
if (indexInMap === -1) {
|
|
441
|
+
indexInMap = currentLayerIds.length;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (this.getLayer(layerId)) {
|
|
445
|
+
this.moveLayer(layerId, indexInMap);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
await this.addLayer(layerId, layer, indexInMap);
|
|
449
|
+
}
|
|
450
|
+
// Remove the element of the previous list as treated.
|
|
451
|
+
const index = previousLayerIds.indexOf(layerId);
|
|
452
|
+
if (index > -1) {
|
|
453
|
+
previousLayerIds.splice(index, 1);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Remove the layers not used anymore.
|
|
457
|
+
previousLayerIds.forEach(layerId => {
|
|
458
|
+
this._Map.removeLayer(layerId);
|
|
459
|
+
});
|
|
460
|
+
this._ready = true;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Add a layer to the map.
|
|
464
|
+
*
|
|
465
|
+
* @param id - id of the layer.
|
|
466
|
+
* @param layer - the layer object.
|
|
467
|
+
* @param index - expected index of the layer.
|
|
468
|
+
*/
|
|
469
|
+
async addLayer(id, layer, index) {
|
|
470
|
+
var _a;
|
|
471
|
+
if (this.getLayer(id)) {
|
|
472
|
+
// Layer already exists
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const sourceId = (_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.source;
|
|
476
|
+
const source = this._model.sharedModel.getSource(sourceId);
|
|
477
|
+
if (!source) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (!this._sources[sourceId]) {
|
|
481
|
+
await this.addSource(sourceId, source);
|
|
482
|
+
}
|
|
483
|
+
let newLayer;
|
|
484
|
+
let layerParameters;
|
|
485
|
+
// TODO: OpenLayers provides a bunch of sources for specific tile
|
|
486
|
+
// providers, so maybe set up some way to use those
|
|
487
|
+
switch (layer.type) {
|
|
488
|
+
case 'RasterLayer': {
|
|
489
|
+
layerParameters = layer.parameters;
|
|
490
|
+
newLayer = new TileLayer({
|
|
491
|
+
opacity: layerParameters.opacity,
|
|
492
|
+
visible: layer.visible,
|
|
493
|
+
source: this._sources[layerParameters.source]
|
|
494
|
+
});
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
case 'VectorLayer': {
|
|
498
|
+
layerParameters = layer.parameters;
|
|
499
|
+
newLayer = new VectorLayer({
|
|
500
|
+
opacity: layerParameters.opacity,
|
|
501
|
+
visible: layer.visible,
|
|
502
|
+
source: this._sources[layerParameters.source],
|
|
503
|
+
style: currentFeature => this.vectorLayerStyleFunc(currentFeature, layer)
|
|
504
|
+
});
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case 'VectorTileLayer': {
|
|
508
|
+
layerParameters = layer.parameters;
|
|
509
|
+
newLayer = new VectorTileLayer({
|
|
510
|
+
opacity: layerParameters.opacity,
|
|
511
|
+
source: this._sources[layerParameters.source],
|
|
512
|
+
style: currentFeature => this.vectorLayerStyleFunc(currentFeature, layer)
|
|
513
|
+
});
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
case 'HillshadeLayer': {
|
|
517
|
+
layerParameters = layer.parameters;
|
|
518
|
+
newLayer = new WebGlTileLayer({
|
|
519
|
+
opacity: 0.3,
|
|
520
|
+
source: this._sources[layerParameters.source],
|
|
521
|
+
style: {
|
|
522
|
+
color: ['color', this.hillshadeMath()]
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
case 'ImageLayer': {
|
|
528
|
+
layerParameters = layer.parameters;
|
|
529
|
+
newLayer = new ImageLayer({
|
|
530
|
+
opacity: layerParameters.opacity,
|
|
531
|
+
source: this._sources[layerParameters.source]
|
|
532
|
+
});
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
case 'WebGlLayer': {
|
|
536
|
+
layerParameters = layer.parameters;
|
|
537
|
+
newLayer = new WebGlTileLayer({
|
|
538
|
+
opacity: layerParameters.opacity,
|
|
539
|
+
source: this._sources[layerParameters.source],
|
|
540
|
+
style: {
|
|
541
|
+
color: layerParameters.color
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// OpenLayers doesn't have name/id field so add it
|
|
548
|
+
newLayer.set('id', id);
|
|
549
|
+
// we need to keep track of which source has which layers
|
|
550
|
+
this._sourceToLayerMap.set(layerParameters.source, id);
|
|
551
|
+
this._Map.getLayers().insertAt(index, newLayer);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Move a layer in the stack.
|
|
555
|
+
*
|
|
556
|
+
* @param id - id of the layer.
|
|
557
|
+
* @param index - expected index of the layer.
|
|
558
|
+
*/
|
|
559
|
+
moveLayer(id, index) {
|
|
560
|
+
// Get the beforeId value according to the expected index.
|
|
561
|
+
const currentLayerIds = this.getLayers();
|
|
562
|
+
let beforeId = undefined;
|
|
563
|
+
if (!(index === undefined) && index < currentLayerIds.length) {
|
|
564
|
+
beforeId = currentLayerIds[index];
|
|
565
|
+
}
|
|
566
|
+
const layerArray = this._Map.getLayers().getArray();
|
|
567
|
+
const movingLayer = this.getLayer(id);
|
|
568
|
+
if (!movingLayer || !index || !beforeId) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const indexOfMovingLayer = layerArray.indexOf(movingLayer);
|
|
572
|
+
layerArray.splice(indexOfMovingLayer, 1);
|
|
573
|
+
const beforeLayer = this.getLayer(beforeId);
|
|
574
|
+
if (!beforeLayer) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const indexOfBeforeLayer = layerArray.indexOf(beforeLayer);
|
|
578
|
+
layerArray.splice(indexOfBeforeLayer, 0, movingLayer);
|
|
579
|
+
this._Map.setLayers(layerArray);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Update a layer of the map.
|
|
583
|
+
*
|
|
584
|
+
* @param id - id of the layer.
|
|
585
|
+
* @param layer - the layer object.
|
|
586
|
+
*/
|
|
587
|
+
async updateLayer(id, layer, mapLayer) {
|
|
588
|
+
var _a, _b, _c, _d;
|
|
589
|
+
const sourceId = (_a = layer.parameters) === null || _a === void 0 ? void 0 : _a.source;
|
|
590
|
+
const source = this._model.sharedModel.getSource(sourceId);
|
|
591
|
+
if (!source) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (!this._sources[sourceId]) {
|
|
595
|
+
await this.addSource(sourceId, source);
|
|
596
|
+
}
|
|
597
|
+
mapLayer.setVisible(layer.visible);
|
|
598
|
+
switch (layer.type) {
|
|
599
|
+
case 'RasterLayer': {
|
|
600
|
+
mapLayer.setOpacity(((_b = layer.parameters) === null || _b === void 0 ? void 0 : _b.opacity) || 1);
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
case 'VectorLayer': {
|
|
604
|
+
const layerParams = layer.parameters;
|
|
605
|
+
mapLayer.setOpacity(layerParams.opacity || 1);
|
|
606
|
+
mapLayer.setStyle(currentFeature => this.vectorLayerStyleFunc(currentFeature, layer));
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case 'VectorTileLayer': {
|
|
610
|
+
const layerParams = layer.parameters;
|
|
611
|
+
mapLayer.setOpacity(layerParams.opacity || 1);
|
|
612
|
+
mapLayer.setStyle(currentFeature => this.vectorLayerStyleFunc(currentFeature, layer));
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case 'HillshadeLayer': {
|
|
616
|
+
// TODO figure out color here
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
case 'ImageLayer': {
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
case 'WebGlLayer': {
|
|
623
|
+
mapLayer.setOpacity((_c = layer.parameters) === null || _c === void 0 ? void 0 : _c.opacity);
|
|
624
|
+
mapLayer.setStyle({
|
|
625
|
+
color: (_d = layer === null || layer === void 0 ? void 0 : layer.parameters) === null || _d === void 0 ? void 0 : _d.color
|
|
626
|
+
});
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Remove a layer from the map.
|
|
633
|
+
*
|
|
634
|
+
* @param id - the id of the layer.
|
|
635
|
+
*/
|
|
636
|
+
removeLayer(id) {
|
|
637
|
+
const mapLayer = this.getLayer(id);
|
|
638
|
+
if (mapLayer) {
|
|
639
|
+
this._Map.removeLayer(mapLayer);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
_onSharedOptionsChanged(sender, change) {
|
|
643
|
+
if (!this._initializedPosition) {
|
|
644
|
+
const options = this._model.getOptions();
|
|
645
|
+
this.updateOptions(options);
|
|
646
|
+
this._initializedPosition = true;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
updateOptions(options) {
|
|
650
|
+
const view = this._Map.getView();
|
|
651
|
+
// use the extent if provided.
|
|
652
|
+
if (options.extent) {
|
|
653
|
+
view.fit(options.extent);
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
const centerCoord = fromLonLat([options.longitude || 0, options.latitude || 0], this._Map.getView().getProjection());
|
|
657
|
+
this._Map.getView().setZoom(options.zoom || 0);
|
|
658
|
+
this._Map.getView().setCenter(centerCoord);
|
|
659
|
+
}
|
|
660
|
+
view.setRotation(options.bearing || 0);
|
|
661
|
+
}
|
|
662
|
+
_onViewChanged(sender, change) {
|
|
663
|
+
// TODO SOMETHING
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Convenience method to get a specific layer from OpenLayers Map
|
|
667
|
+
* @param id Layer to retrieve
|
|
668
|
+
*/
|
|
669
|
+
getLayer(id) {
|
|
670
|
+
return this._Map
|
|
671
|
+
.getLayers()
|
|
672
|
+
.getArray()
|
|
673
|
+
.find(layer => layer.get('id') === id);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Convenience method to get list layer IDs from the OpenLayers Map
|
|
677
|
+
*/
|
|
678
|
+
getLayers() {
|
|
679
|
+
return this._Map
|
|
680
|
+
.getLayers()
|
|
681
|
+
.getArray()
|
|
682
|
+
.map(layer => layer.get('id'));
|
|
683
|
+
}
|
|
684
|
+
_onLayersChanged(_, change) {
|
|
685
|
+
var _a;
|
|
686
|
+
// Avoid concurrency update on layers on first load, if layersTreeChanged and
|
|
687
|
+
// LayersChanged are triggered simultaneously.
|
|
688
|
+
if (!this._ready) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
(_a = change.layerChange) === null || _a === void 0 ? void 0 : _a.forEach(change => {
|
|
692
|
+
const layer = change.newValue;
|
|
693
|
+
if (!layer || Object.keys(layer).length === 0) {
|
|
694
|
+
this.removeLayer(change.id);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
const mapLayer = this.getLayer(change.id);
|
|
698
|
+
if (mapLayer &&
|
|
699
|
+
JupyterGISModel.getOrderedLayerIds(this._model).includes(change.id)) {
|
|
700
|
+
this.updateLayer(change.id, layer, mapLayer);
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
this.updateLayers(JupyterGISModel.getOrderedLayerIds(this._model));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
_onLayerTreeChange(sender, change) {
|
|
709
|
+
this._ready = false;
|
|
710
|
+
// We can't properly use the change, because of the nested groups in the the shared
|
|
711
|
+
// document which is flattened for the map tool.
|
|
712
|
+
this.updateLayers(JupyterGISModel.getOrderedLayerIds(this._model));
|
|
713
|
+
}
|
|
714
|
+
_onSourcesChange(_, change) {
|
|
715
|
+
var _a;
|
|
716
|
+
if (!this._ready) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
(_a = change.sourceChange) === null || _a === void 0 ? void 0 : _a.forEach(change => {
|
|
720
|
+
if (!change.newValue || Object.keys(change.newValue).length === 0) {
|
|
721
|
+
this.removeSource(change.id);
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
const source = this._model.getSource(change.id);
|
|
725
|
+
if (source) {
|
|
726
|
+
this.updateSource(change.id, source);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
render() {
|
|
732
|
+
return (React.createElement("div", { className: "jGIS-Mainview", style: {
|
|
733
|
+
border: this.state.remoteUser
|
|
734
|
+
? `solid 3px ${this.state.remoteUser.color}`
|
|
735
|
+
: 'unset'
|
|
736
|
+
} },
|
|
737
|
+
React.createElement(Spinner, { loading: this.state.loading }),
|
|
738
|
+
React.createElement("div", { ref: this.divRef, style: {
|
|
739
|
+
width: '100%',
|
|
740
|
+
height: 'calc(100%)'
|
|
741
|
+
} })));
|
|
742
|
+
}
|
|
743
|
+
}
|