@eeacms/volto-arcgis-block 0.1.31 → 0.1.34

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/CHANGELOG.md CHANGED
@@ -4,8 +4,36 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [0.1.34](https://github.com/eea/volto-arcgis-block/compare/0.1.33...0.1.34)
8
+
9
+ - Bugs n improvements [`#105`](https://github.com/eea/volto-arcgis-block/pull/105)
10
+ - ESLint fix [`ac207b7`](https://github.com/eea/volto-arcgis-block/commit/ac207b727d43bc669b93ee273ca94e593f5fb190)
11
+ - ESLint fix [`96d2405`](https://github.com/eea/volto-arcgis-block/commit/96d240507f4e39e93d5d9bb43e8c66bc3962b3c3)
12
+ - Info widget [`4876e2b`](https://github.com/eea/volto-arcgis-block/commit/4876e2b13b81281202fcfd0d7f84eb03a7a96c8f)
13
+ - Promises.allSettled added [`95cbfc2`](https://github.com/eea/volto-arcgis-block/commit/95cbfc20b6117aeaa277378d56493dc9dcfbfd1f)
14
+
15
+ #### [0.1.33](https://github.com/eea/volto-arcgis-block/compare/0.1.32...0.1.33)
16
+
17
+ > 1 March 2022
18
+
19
+ - Develop [`#104`](https://github.com/eea/volto-arcgis-block/pull/104)
20
+ - Button fix [`#103`](https://github.com/eea/volto-arcgis-block/pull/103)
21
+
22
+ #### [0.1.32](https://github.com/eea/volto-arcgis-block/compare/0.1.31...0.1.32)
23
+
24
+ > 28 February 2022
25
+
26
+ - Develop [`#102`](https://github.com/eea/volto-arcgis-block/pull/102)
27
+ - Metadata [`#101`](https://github.com/eea/volto-arcgis-block/pull/101)
28
+ - linting fix [`a3f1b7d`](https://github.com/eea/volto-arcgis-block/commit/a3f1b7d29e7cfa91b5d48c5e6eaed9e79a707e14)
29
+ - Metadata info button implemented [`06659e0`](https://github.com/eea/volto-arcgis-block/commit/06659e090051659a28c3f8026a606d9e473e9ebd)
30
+ - Metadata Info icon implementation [`4750835`](https://github.com/eea/volto-arcgis-block/commit/475083515975126e0986a65eb2058552b2ba05bd)
31
+
7
32
  #### [0.1.31](https://github.com/eea/volto-arcgis-block/compare/0.1.30...0.1.31)
8
33
 
34
+ > 11 February 2022
35
+
36
+ - Feature Time_Slider_Widget [`#100`](https://github.com/eea/volto-arcgis-block/pull/100)
9
37
  - First commit [`#97`](https://github.com/eea/volto-arcgis-block/pull/97)
10
38
  - REFACT: Delete comments [`a7a9296`](https://github.com/eea/volto-arcgis-block/commit/a7a929647749280ccf92d97ea17a3d743f1cfa20)
11
39
  - FIX: Watch event on Time Extent for props and services. TimeSlider disabled if no time dimension [`2af7f50`](https://github.com/eea/volto-arcgis-block/commit/2af7f50d2ae1d5e033606d690bab0dffaa83b79e)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-arcgis-block",
3
- "version": "0.1.31",
3
+ "version": "0.1.34",
4
4
  "description": "volto-arcgis-block: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: CodeSyntax",
@@ -1,8 +1,10 @@
1
1
  import React, { createRef } from 'react';
2
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
2
3
  import { loadModules } from 'esri-loader';
3
4
  import Highcharts from 'highcharts';
4
5
  import HighchartsReact from 'highcharts-react-official';
5
- var GeometryEngine, Graphic;
6
+ import { Loader } from 'semantic-ui-react';
7
+ var GeometryEngine, Graphic, esriRequest;
6
8
 
7
9
  class InfoWidget extends React.Component {
8
10
  /**
@@ -15,7 +17,7 @@ class InfoWidget extends React.Component {
15
17
  this.container = createRef();
16
18
  //Initially, we set the state of the component to
17
19
  //not be showing the basemap panel
18
- this.state = { showMapMenu: false, timeLayers: {} };
20
+ this.state = { showMapMenu: false };
19
21
  this.map = this.props.map;
20
22
  this.menuClass =
21
23
  'esri-icon-description esri-widget--button esri-widget esri-interactive';
@@ -23,11 +25,17 @@ class InfoWidget extends React.Component {
23
25
  }
24
26
 
25
27
  loader() {
26
- return loadModules(['esri/geometry/geometryEngine', 'esri/Graphic']).then(
27
- ([_GeometryEngine, _Graphic]) => {
28
- [GeometryEngine, Graphic] = [_GeometryEngine, _Graphic];
29
- },
30
- );
28
+ return loadModules([
29
+ 'esri/geometry/geometryEngine',
30
+ 'esri/Graphic',
31
+ 'esri/request',
32
+ ]).then(([_GeometryEngine, _Graphic, _esriRequest]) => {
33
+ [GeometryEngine, Graphic, esriRequest] = [
34
+ _GeometryEngine,
35
+ _Graphic,
36
+ _esriRequest,
37
+ ];
38
+ });
31
39
  }
32
40
 
33
41
  /**
@@ -49,7 +57,6 @@ class InfoWidget extends React.Component {
49
57
  showMapMenu: false,
50
58
  pixelInfo: false,
51
59
  popup: false,
52
- timeLayers: {},
53
60
  });
54
61
  //this.props.view.popup.autoOpenEnabled = true;
55
62
  this.removeMarker();
@@ -64,13 +71,11 @@ class InfoWidget extends React.Component {
64
71
  // and ensure that the component is rendered again
65
72
  this.setState({ showMapMenu: true });
66
73
  this.props.mapViewer.view.popup.close();
67
- //this.props.view.popup.autoOpenEnabled = false;
68
74
  }
69
75
  }
70
76
  /**
71
77
  * This method is executed after the rener method is executed
72
- */
73
- async componentDidMount() {
78
+ */ async componentDidMount() {
74
79
  await this.loader();
75
80
  this.props.view.ui.add(this.container.current, 'top-right');
76
81
  this.props.view.on('click', (e) => {
@@ -78,72 +83,208 @@ class InfoWidget extends React.Component {
78
83
  x: e.x,
79
84
  y: e.y,
80
85
  };
86
+ let layers;
81
87
  if (this.props.mapViewer.activeWidget === this) {
82
- let layers = this.map.layers.items.filter(
88
+ this.setState({
89
+ loading: true,
90
+ });
91
+ layers = this.map.layers.items.filter(
83
92
  (a) => a.visible && a.title !== 'nuts',
84
93
  );
85
- //let promises = [];
86
94
  this.infoData = {};
95
+ let promises = [];
96
+ let layerTypes = [];
87
97
  layers.forEach((layer, index) => {
88
98
  let title = this.getLayerTitle(layer);
89
99
  if (layer.isTimeSeries) {
90
100
  if (layer.url.toLowerCase().includes('wms')) {
101
+ layerTypes.push({
102
+ isTimeSeries: true,
103
+ type: 'wms',
104
+ title: title,
105
+ });
106
+ promises.push(this.identifyWMS(layer, e));
91
107
  } else if (layer.url.toLowerCase().includes('wmts')) {
108
+ layerTypes.push({
109
+ isTimeSeries: true,
110
+ type: 'wmts',
111
+ title: title,
112
+ });
92
113
  } else {
93
- //promises['p' + index] =
94
- this.identify(layer, e).then((response) => {
95
- this.infoData[index] = {
96
- title: title,
97
- data: response,
98
- };
99
- this.setState({
100
- pixelInfo: true,
101
- });
114
+ layerTypes.push({
115
+ isTimeSeries: true,
116
+ type: 'featureLayer',
117
+ title: title,
102
118
  });
119
+ promises.push(this.identify(layer, e));
103
120
  }
104
121
  } else {
105
122
  if (layer.url.toLowerCase().includes('wms')) {
106
- let coords = '';
107
- //promises['p' + index] =
108
- this.getFeatureInfo(coords, (data) => {
109
- if (data.features.length > 0) {
110
- let properties = data.features[0].properties;
111
- this.infoData[index] = {
112
- title: title,
113
- data: Object.entries(properties),
114
- };
115
- }
116
- this.setState({
117
- popup: true,
118
- });
123
+ layerTypes.push({
124
+ isTimeSeries: false,
125
+ type: 'wms',
126
+ title: title,
119
127
  });
128
+ promises.push(this.identifyWMS(layer, e));
120
129
  } else if (layer.url.toLowerCase().includes('wmts')) {
130
+ layerTypes.push({
131
+ isTimeSeries: false,
132
+ type: 'wmts',
133
+ title: title,
134
+ });
121
135
  } else {
122
- //promises['p' + index] =
123
- this.props.view.hitTest(screenPoint).then((response) => {
124
- if (response.results.length) {
125
- var graphic = response.results.filter((result) => {
126
- return result.graphic.layer === layer;
127
- })[0].graphic;
128
- if (graphic) {
129
- this.infoData[index] = {
130
- title: title,
131
- data: Object.entries(graphic.attributes),
132
- };
136
+ layerTypes.push({
137
+ isTimeSeries: false,
138
+ type: 'featureLayer',
139
+ title: title,
140
+ });
141
+ promises.push(this.props.view.hitTest(screenPoint));
142
+ }
143
+ }
144
+ Promise.allSettled(promises).then((values) => {
145
+ if (promises.length === values.length) {
146
+ values.forEach((response, index) => {
147
+ let data = response.value;
148
+ let layer = layerTypes[index];
149
+ let properties = [];
150
+ if (layer.isTimeSeries) {
151
+ switch (layer.type) {
152
+ case 'wms':
153
+ if (data.type === 'FeatureCollection') {
154
+ if (data.features.length) {
155
+ let obj = data.features.map((a) => {
156
+ return a.properties;
157
+ });
158
+ properties = this.transformWmsData(obj);
159
+ }
160
+ } else if (data.doctype && data.doctype.name === 'html') {
161
+ let th = data.querySelectorAll('tbody th');
162
+ let tr = data.querySelectorAll(
163
+ 'tbody tr:not(:first-of-type)',
164
+ );
165
+ if (th.length) {
166
+ let obj = Array.from(tr).map((a) => {
167
+ let x = [];
168
+ a.querySelectorAll('td').forEach((td, index) => {
169
+ x[th[index].textContent] = td.textContent;
170
+ });
171
+ return x;
172
+ });
173
+ properties = this.transformWmsData(obj);
174
+ }
175
+ } else if (
176
+ data.getElementsByTagName('FIELDS').length &&
177
+ typeof data !== 'undefined'
178
+ ) {
179
+ let fields = data.getElementsByTagName('FIELDS');
180
+ if (fields.length) {
181
+ let obj = Array.from(fields).map((a) => {
182
+ let x = [];
183
+ Object.entries(a.attributes).forEach((b) => {
184
+ x[b[1].name] = b[1].value;
185
+ });
186
+ return x;
187
+ });
188
+ properties = this.transformWmsData(obj);
189
+ }
190
+ }
191
+ this.infoData[index] = {
192
+ title: layer.title,
193
+ data: properties,
194
+ time: true,
195
+ };
196
+ break;
197
+ case 'wmts':
198
+ this.infoData[index] = {
199
+ title: layer.title,
200
+ data: properties,
201
+ time: true,
202
+ };
203
+ break;
204
+ case 'featureLayer':
205
+ this.infoData[index] = {
206
+ title: layer.title,
207
+ data: data,
208
+ time: true,
209
+ };
210
+ break;
211
+ default:
212
+ break;
213
+ }
214
+ } else {
215
+ switch (layer.type) {
216
+ case 'wms':
217
+ if (data.type === 'FeatureCollection') {
218
+ if (data.features.length) {
219
+ properties = data.features[0].properties;
220
+ properties = Object.entries(properties);
221
+ }
222
+ } else if (data.doctype && data.doctype.name === 'html') {
223
+ let th = data.querySelectorAll('tbody th');
224
+ let td = data.querySelectorAll('tbody td');
225
+ if (th.length) {
226
+ let fields = [];
227
+ th.forEach((item, index) => {
228
+ fields.push([
229
+ item.textContent,
230
+ td[index].textContent,
231
+ ]);
232
+ });
233
+ properties = fields;
234
+ }
235
+ } else if (
236
+ data.getElementsByTagName('FIELDS').length &&
237
+ typeof data !== 'undefined'
238
+ ) {
239
+ let fields = data.getElementsByTagName('FIELDS');
240
+ if (fields.length) {
241
+ properties = Object.entries(fields[0].attributes).map(
242
+ (a) => {
243
+ return [a[1].name, a[1].value];
244
+ },
245
+ );
246
+ }
247
+ }
248
+ this.infoData[index] = {
249
+ title: layer.title,
250
+ data: properties,
251
+ };
252
+ break;
253
+ case 'wmts':
254
+ this.infoData[index] = {
255
+ title: layer.title,
256
+ data: properties,
257
+ };
258
+ break;
259
+ case 'featureLayer':
260
+ if (data.results.length) {
261
+ var graphic = data.results.filter((result) => {
262
+ return result.graphic.layer === layers[index];
263
+ })[0].graphic;
264
+ if (graphic) {
265
+ properties = graphic.attributes;
266
+ }
267
+ }
268
+ this.infoData[index] = {
269
+ title: layer.title,
270
+ data: Object.entries(properties),
271
+ };
272
+ break;
273
+ default:
274
+ break;
133
275
  }
134
276
  }
135
- this.setState({
136
- popup: true,
137
- });
277
+ });
278
+ let layerIndex = layers.length - 1;
279
+ this.setState({
280
+ layerIndex: layerIndex,
281
+ pixelInfo: layerTypes[layerIndex].isTimeSeries ? true : false,
282
+ popup: !layerTypes[layerIndex].isTimeSeries ? true : false,
283
+ loading: false,
138
284
  });
139
285
  }
140
- }
286
+ });
141
287
  this.addMarker(e);
142
- // Promise.all(promises).then((values) => {
143
- // this.setState({
144
- // popup: true,
145
- // });
146
- // });
147
288
  });
148
289
  }
149
290
  });
@@ -161,36 +302,158 @@ class InfoWidget extends React.Component {
161
302
  return title;
162
303
  }
163
304
 
164
- getFeatureInfo(coords, callback) {
165
- let xmlhttp = new XMLHttpRequest();
166
- const url =
167
- 'https://geoserveis.icgc.cat/icgc_sentinel2/wms/service?service=WMS&request=GetFeatureInfo&bbox=41,1.8,41.2,2.2&layers=sen2rgb&query_layers=sen2rgb&crs=EPSG:4326&version=1.3.0&width=780&height=330&info_format=application/geojson&time=2018-09';
168
- xmlhttp.onreadystatechange = () => {
169
- if (xmlhttp.readyState === 4 && xmlhttp.status === 200)
170
- callback(JSON.parse(xmlhttp.responseText), this.props.mapViewer);
171
- };
172
- xmlhttp.open('GET', url, true);
173
- xmlhttp.send();
305
+ getLayerName(layer) {
306
+ let title;
307
+ if (layer.sublayers) {
308
+ title = layer.sublayers.items[0].name;
309
+ } else if (layer.activeLayer) {
310
+ title = layer.activeLayer.name;
311
+ } else {
312
+ title = layer.name;
313
+ }
314
+ return title;
174
315
  }
175
316
 
176
- loadInfoChart(index) {
177
- let response = this.infoData[index].data;
178
- let title = this.infoData[index].title;
179
- //let variables = response.variables.options;
180
- let variable = response.variables.selected;
181
- let data = {
182
- x: response.timeFields.values
183
- .map((a) => {
184
- return a[response.timeFields.start];
317
+ identifyWMS(layer, event) {
318
+ let layerId = this.getLayerName(layer);
319
+ let url = layer.featureInfoUrl ? layer.featureInfoUrl : layer.url;
320
+ return this.wmsCapabilities(url).then((xml) => {
321
+ let version = this.parseCapabilities(xml, 'wms_capabilities')[0]
322
+ .attributes['version'];
323
+ let format = this.parseFormat(xml, layerId);
324
+ let times = '';
325
+ let nTimes = 1;
326
+ if (layer.isTimeSeries) {
327
+ times = this.parseTime(xml, layerId);
328
+ nTimes = times.length;
329
+ }
330
+ return esriRequest(url, {
331
+ responseType: 'html',
332
+ sync: 'true',
333
+ query: {
334
+ request: 'GetFeatureInfo',
335
+ service: 'WMS',
336
+ version: version,
337
+ SRS: 'EPSG:' + this.props.view.spatialReference.latestWkid,
338
+ CRS: 'EPSG:' + this.props.view.spatialReference.latestWkid,
339
+ BBOX:
340
+ '' +
341
+ this.props.view.extent.xmin +
342
+ ', ' +
343
+ this.props.view.extent.ymin +
344
+ ', ' +
345
+ this.props.view.extent.xmax +
346
+ ', ' +
347
+ this.props.view.extent.ymax,
348
+ HEIGHT: this.props.view.height,
349
+ WIDTH: this.props.view.width,
350
+ X: event.screenPoint.x,
351
+ Y: event.screenPoint.y,
352
+ QUERY_LAYERS: layerId,
353
+ INFO_FORMAT: format,
354
+ TIME: times ? times[0] + '/' + times[nTimes - 1] : '',
355
+ FEATURE_COUNT: '' + nTimes,
356
+ },
357
+ })
358
+ .then((response) => {
359
+ let format = response.requestOptions.query.INFO_FORMAT;
360
+ let data;
361
+ if (format.includes('text')) {
362
+ data = new window.DOMParser().parseFromString(
363
+ response.data,
364
+ 'text/html',
365
+ );
366
+ } else if (format.includes('json')) {
367
+ data = JSON.parse(response.data);
368
+ }
369
+ return data;
185
370
  })
186
- .sort((a, b) => {
187
- return new Date(a).getTime() - new Date(b).getTime();
188
- }),
189
- y: response.data.values.map((a) => {
190
- return Math.round(a[variable] * 100) / 100;
191
- }),
192
- };
193
- return this.createChart(title, variable, data);
371
+ .then((data) => {
372
+ return data;
373
+ });
374
+ });
375
+ }
376
+
377
+ wmsCapabilities(url) {
378
+ return esriRequest(url, {
379
+ responseType: 'html',
380
+ sync: 'true',
381
+ query: {
382
+ request: 'GetCapabilities',
383
+ service: 'WMS',
384
+ },
385
+ }).then((response) => {
386
+ let parser = new DOMParser();
387
+ let xml = parser.parseFromString(response.data, 'text/html');
388
+ return xml;
389
+ });
390
+ }
391
+
392
+ parseCapabilities(xml, tag) {
393
+ return xml.getElementsByTagName(tag);
394
+ }
395
+
396
+ parseFormat(xml) {
397
+ let formats = Array.from(
398
+ Array.from(this.parseCapabilities(xml, 'getFeatureInfo')).map(
399
+ (f) => f.children,
400
+ )[0],
401
+ ).map((v) => v.textContent);
402
+ let format = formats.filter((v) => v.includes('json'))[0];
403
+ if (!format) format = formats.filter((v) => v.includes('html'))[0];
404
+ if (!format) format = formats.filter((v) => v.includes('text/xml'))[0];
405
+ if (!format) format = formats[0];
406
+ return format;
407
+ }
408
+
409
+ parseTime(xml, layerId) {
410
+ let layers = Array.from(xml.querySelectorAll('Layer')).filter(
411
+ (v) => v.querySelectorAll('Layer').length === 0,
412
+ );
413
+ let layer = layers.find((a) => {
414
+ return a.querySelector('Name').innerText === layerId;
415
+ });
416
+ let times = layer.querySelector('Dimension').innerText.split(',');
417
+ return times;
418
+ }
419
+
420
+ transformWmsData(obj) {
421
+ let values = { timeFields: {}, data: {}, variables: {} };
422
+ let startField = Object.keys(obj[0]).find(
423
+ (a, i) =>
424
+ a.toUpperCase().includes('DATE') &&
425
+ !isNaN(parseInt(Object.values(obj[0])[i])),
426
+ );
427
+ values.timeFields['start'] = startField;
428
+ let fields = Object.keys(obj[0]).filter((a, i) => {
429
+ return (
430
+ !isNaN(parseInt(Object.values(obj[0])[i])) &&
431
+ a.toUpperCase() !== 'OBJECTID' &&
432
+ !a.toUpperCase().includes('DATE')
433
+ );
434
+ });
435
+ let field = fields[0];
436
+ values.variables = { options: fields, selected: field };
437
+ values.timeFields['values'] = obj.map((a, i) => {
438
+ let date = {};
439
+ Object.entries(obj[i]).forEach(([key, value]) => {
440
+ if (key === startField) {
441
+ date[key] = value;
442
+ }
443
+ });
444
+ return date;
445
+ });
446
+ values.data['outFields'] = field;
447
+ values.data['values'] = obj.map((a, i) => {
448
+ let x = {};
449
+ Object.entries(obj[i]).forEach(([key, value]) => {
450
+ if (fields.includes(key)) {
451
+ x[key] = parseFloat(value);
452
+ }
453
+ });
454
+ return x;
455
+ });
456
+ return values;
194
457
  }
195
458
 
196
459
  addMarker(evt) {
@@ -262,7 +525,9 @@ class InfoWidget extends React.Component {
262
525
  let p1 = layer.queryFeatures(timeQuery).then((r) => {
263
526
  let timevals = [];
264
527
  r.features.forEach((e) => timevals.push(e.attributes));
265
- values.timeFields['values'] = timevals;
528
+ values.timeFields['values'] = timevals.sort((a, b) => {
529
+ return a[values.timeFields.start] - b[values.timeFields.start];
530
+ });
266
531
  });
267
532
 
268
533
  //Query for data
@@ -309,13 +574,27 @@ class InfoWidget extends React.Component {
309
574
  });
310
575
  }
311
576
 
312
- createChart(title, variable, chartData) {
577
+ loadInfoChart(index) {
578
+ let response = this.infoData[index].data;
579
+ let variable = response.variables.selected;
580
+ let data = {
581
+ x: response.timeFields.values.map((a) => {
582
+ return a[response.timeFields.start];
583
+ }),
584
+ y: response.data.values.map((a) => {
585
+ return Math.round(a[variable] * 100) / 100;
586
+ }),
587
+ };
588
+ return this.createChart(variable, data);
589
+ }
590
+
591
+ createChart(variable, chartData) {
313
592
  let chartOptions = {
314
593
  chart: {
315
594
  height: 208,
316
595
  },
317
596
  title: {
318
- text: title,
597
+ text: '',
319
598
  style: {},
320
599
  },
321
600
  subtitle: {
@@ -389,7 +668,7 @@ class InfoWidget extends React.Component {
389
668
  return (
390
669
  <tr key={item}>
391
670
  {Object.values(item).map((val) => (
392
- <td>{val}</td>
671
+ <td key={val}>{val}</td>
393
672
  ))}
394
673
  </tr>
395
674
  );
@@ -403,7 +682,6 @@ class InfoWidget extends React.Component {
403
682
 
404
683
  loadVariableSelector(index) {
405
684
  let response = this.infoData[index].data;
406
- //let title = this.infoData[index].title;
407
685
  let variables = response.variables.options;
408
686
  let variable = response.variables.selected;
409
687
  let options = variables.map((option) => {
@@ -429,17 +707,120 @@ class InfoWidget extends React.Component {
429
707
  );
430
708
  }
431
709
 
710
+ loadStatisticsSelector(index) {
711
+ let statistics = ['Mean', 'Median', 'Variance', 'Standard deviation'];
712
+ let selected =
713
+ this.infoData[index].data.statistics &&
714
+ this.infoData[index].data.statistics.selected
715
+ ? this.infoData[index].data.statistics.selected
716
+ : statistics[0];
717
+ let value =
718
+ this.infoData[index].data.statistics &&
719
+ this.infoData[index].data.statistics.value
720
+ ? this.infoData[index].data.statistics.value
721
+ : this.calculateStatistics(selected);
722
+ this.infoData[index].data.statistics = {
723
+ selected: selected,
724
+ value: value,
725
+ };
726
+ let options = statistics.map((option) => {
727
+ return (
728
+ <option key={option} value={option}>
729
+ {option}
730
+ </option>
731
+ );
732
+ });
733
+ return (
734
+ <>
735
+ <label>
736
+ Statistics
737
+ <select
738
+ className="esri-select"
739
+ id="info_statistics"
740
+ value={selected}
741
+ onBlur={(e) => e.preventDefault()}
742
+ onChange={(e) => this.selectStatistics(e.target.value)}
743
+ >
744
+ {options}
745
+ </select>
746
+ </label>
747
+ <div className="info-statistics-panel">
748
+ {this.infoData[index].data.statistics.value && (
749
+ <span className="info-statistics-result">
750
+ {this.infoData[index].data.statistics.value}
751
+ </span>
752
+ )}
753
+ </div>
754
+ </>
755
+ );
756
+ }
757
+
758
+ calculateStatistics(statistic) {
759
+ let index = this.state.layerIndex;
760
+ let response = this.infoData[index].data;
761
+ let variable = response.variables.selected;
762
+ let data = response.data.values.map((a) => {
763
+ return a[variable];
764
+ });
765
+ let mean = data.reduce((a, b) => a + b) / data.length;
766
+ let result;
767
+ switch (statistic) {
768
+ case 'Mean':
769
+ result = mean;
770
+ break;
771
+ case 'Median':
772
+ data = data.sort((a, b) => {
773
+ return a - b;
774
+ });
775
+ var i = data.length / 2;
776
+ result =
777
+ i % 1 === 0 ? (data[i - 1] + data[i]) / 2 : data[Math.floor(i)];
778
+ break;
779
+ case 'Variance':
780
+ result =
781
+ data.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) /
782
+ data.length;
783
+ break;
784
+ case 'Standard deviation':
785
+ let variance =
786
+ data.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) /
787
+ data.length;
788
+ result = Math.sqrt(variance);
789
+ break;
790
+ default:
791
+ break;
792
+ }
793
+ return result;
794
+ }
795
+
796
+ selectStatistics(option) {
797
+ let index = this.state.layerIndex;
798
+ this.infoData[index].data.statistics = {
799
+ selected: option,
800
+ value: this.calculateStatistics(option),
801
+ };
802
+ this.setState({
803
+ pixelInfo: true,
804
+ });
805
+ }
806
+
432
807
  /**
433
808
  * This method renders the component
434
809
  * @returns jsx
435
810
  */
436
811
  render() {
437
- let noData = this.state.pixelInfo
438
- ? this.infoData[0].data.data.values.map((a) => {
439
- return a[this.infoData[0].data.variables.selected];
440
- }).length === 0
441
- : Object.keys(this.infoData).length === 0;
442
- let layer = 0;
812
+ let noData = true;
813
+ if (this.state.pixelInfo) {
814
+ noData = this.infoData[this.state.layerIndex].data.data
815
+ ? this.infoData[this.state.layerIndex].data.data.values.map((a) => {
816
+ return a[
817
+ this.infoData[this.state.layerIndex].data.variables.selected
818
+ ];
819
+ }).length === 0
820
+ : true;
821
+ } else if (this.state.popup) {
822
+ noData = this.infoData[this.state.layerIndex].data.length === 0 && true;
823
+ }
443
824
  return (
444
825
  <>
445
826
  <div ref={this.container} className="info-container">
@@ -453,29 +834,84 @@ class InfoWidget extends React.Component {
453
834
  role="button"
454
835
  ></div>
455
836
  <div className="info-panel">
456
- {(this.state.pixelInfo || this.state.popup) && (
457
- <span className="info-panel-title">
458
- {this.infoData[layer].title}
459
- </span>
460
- )}
461
- {this.state.pixelInfo && !noData && (
837
+ {this.state.loading ? (
838
+ <Loader active inline="centered" size="small" />
839
+ ) : (
462
840
  <>
463
- {this.loadVariableSelector(0)}
464
-
465
- <HighchartsReact
466
- highcharts={Highcharts}
467
- options={this.loadInfoChart(layer)}
468
- />
841
+ {(this.state.pixelInfo || this.state.popup) && (
842
+ <>
843
+ <div className="info-panel-buttons">
844
+ <button
845
+ className="ccl-button ccl-button--default info-button-left"
846
+ onClick={() =>
847
+ this.setState({
848
+ layerIndex: this.state.layerIndex + 1,
849
+ pixelInfo: this.infoData[this.state.layerIndex + 1]
850
+ .time
851
+ ? true
852
+ : false,
853
+ popup: !this.infoData[this.state.layerIndex + 1]
854
+ .time
855
+ ? true
856
+ : false,
857
+ })
858
+ }
859
+ disabled={
860
+ this.state.layerIndex ===
861
+ Object.keys(this.infoData).length - 1
862
+ }
863
+ >
864
+ <FontAwesomeIcon icon={['fas', 'chevron-left']} />
865
+ <span>Previous layer</span>
866
+ </button>
867
+ <button
868
+ className="ccl-button ccl-button--default info-button-right"
869
+ onClick={() =>
870
+ this.setState({
871
+ layerIndex: this.state.layerIndex - 1,
872
+ pixelInfo: this.infoData[this.state.layerIndex - 1]
873
+ .time
874
+ ? true
875
+ : false,
876
+ popup: !this.infoData[this.state.layerIndex - 1]
877
+ .time
878
+ ? true
879
+ : false,
880
+ })
881
+ }
882
+ disabled={this.state.layerIndex === 0}
883
+ >
884
+ <span>Next layer</span>
885
+ <FontAwesomeIcon icon={['fas', 'chevron-right']} />
886
+ </button>
887
+ </div>
888
+ <span className="info-panel-title">
889
+ {this.infoData[this.state.layerIndex].title}
890
+ </span>
891
+ </>
892
+ )}
893
+ {this.state.pixelInfo && !noData && (
894
+ <>
895
+ {this.loadVariableSelector(this.state.layerIndex)}
896
+ <HighchartsReact
897
+ highcharts={Highcharts}
898
+ options={this.loadInfoChart(this.state.layerIndex)}
899
+ />
900
+ {this.loadStatisticsSelector(this.state.layerIndex)}
901
+ </>
902
+ )}
903
+ {this.state.popup &&
904
+ !noData &&
905
+ this.loadInfoTable(this.state.layerIndex)}
906
+ {this.state.pixelInfo || this.state.popup ? (
907
+ noData && <span className="info-panel-empty">No data</span>
908
+ ) : (
909
+ <span className="info-panel-empty">
910
+ Click on the map to get pixel info
911
+ </span>
912
+ )}
469
913
  </>
470
914
  )}
471
- {this.state.popup && !noData && this.loadInfoTable(layer)}
472
- {this.state.pixelInfo || this.state.popup ? (
473
- noData && <span className="info-panel-empty">No data</span>
474
- ) : (
475
- <span className="info-panel-empty">
476
- Click on the map to get pixel info
477
- </span>
478
- )}
479
915
  </div>
480
916
  </div>
481
917
  </>
@@ -273,15 +273,13 @@ export const AddCartItem = ({
273
273
  </div>
274
274
  </Modal.Actions>
275
275
  </Modal>
276
- <div className={'map-menu-icons' + (isLoggedIn ? '' : ' locked')}>
277
- <FontAwesomeIcon
278
- className="map-menu-icon"
279
- icon={['fas', 'download']}
280
- onClick={() => {
281
- isLoggedIn && showModal();
282
- }}
283
- />
284
- </div>
276
+ <FontAwesomeIcon
277
+ className={'map-menu-icon' + (isLoggedIn ? '' : ' locked')}
278
+ icon={['fas', 'download']}
279
+ onClick={() => {
280
+ isLoggedIn && showModal();
281
+ }}
282
+ />
285
283
  </>
286
284
  )}
287
285
  </>
@@ -615,6 +613,7 @@ class MenuWidget extends React.Component {
615
613
  index,
616
614
  inheritedIndexDataset,
617
615
  dataset.ViewService,
616
+ dataset.TimeSeriesService,
618
617
  checkIndex,
619
618
  dataset.IsTimeSeries,
620
619
  layer_default,
@@ -659,16 +658,24 @@ class MenuWidget extends React.Component {
659
658
  >
660
659
  <span>{dataset.DatasetTitle}</span>
661
660
  </label>
662
- {!this.props.download && dataset.Downloadable && (
663
- <AddCartItem
664
- cartData={this.compCfg}
665
- props={this.props}
666
- mapViewer={this.props.mapViewer}
667
- download={this.props.download}
668
- areaData={this.props.area}
669
- dataset={dataset}
670
- />
671
- )}
661
+ <div className="map-menu-icons">
662
+ <a href={dataset.DatasetURL} target="_blank" rel="noreferrer">
663
+ <FontAwesomeIcon
664
+ className="map-menu-icon"
665
+ icon={['fa', 'info-circle']}
666
+ />
667
+ </a>
668
+ {!this.props.download && dataset.Downloadable && (
669
+ <AddCartItem
670
+ cartData={this.compCfg}
671
+ props={this.props}
672
+ mapViewer={this.props.mapViewer}
673
+ download={this.props.download}
674
+ areaData={this.props.area}
675
+ dataset={dataset}
676
+ />
677
+ )}
678
+ </div>
672
679
  </div>
673
680
  <div
674
681
  className="ccl-form map-menu-layers-container"
@@ -711,6 +718,7 @@ class MenuWidget extends React.Component {
711
718
  layerIndex,
712
719
  inheritedIndex,
713
720
  urlWMS,
721
+ featureInfoUrl,
714
722
  parentIndex,
715
723
  isTimeSeries,
716
724
  layer_default,
@@ -742,6 +750,7 @@ class MenuWidget extends React.Component {
742
750
  visible: true,
743
751
  legendEnabled: true,
744
752
  legendUrl: urlWMS + legendRequest + layer.LayerId,
753
+ featureInfoUrl: featureInfoUrl,
745
754
  },
746
755
  ],
747
756
  isTimeSeries: isTimeSeries,
@@ -754,6 +763,7 @@ class MenuWidget extends React.Component {
754
763
  activeLayer: {
755
764
  id: layer.LayerId,
756
765
  title: layer.Title,
766
+ featureInfoUrl: featureInfoUrl,
757
767
  },
758
768
  isTimeSeries: isTimeSeries,
759
769
  });
@@ -764,6 +774,7 @@ class MenuWidget extends React.Component {
764
774
  url: urlWMS + (urlWMS.endsWith('/') ? '' : '/') + layer.LayerId,
765
775
  id: layer.LayerId,
766
776
  title: layer.Title,
777
+ featureInfoUrl: featureInfoUrl,
767
778
  popupEnabled: true,
768
779
  isTimeSeries: isTimeSeries,
769
780
  });
@@ -1216,10 +1227,10 @@ class MenuWidget extends React.Component {
1216
1227
  }
1217
1228
  });
1218
1229
  if (layers.length === 0 && document.querySelector('.info-container')) {
1219
- //this.props.mapViewer.closeActiveWidget();
1220
- //document.querySelector('.info-container').style.display = 'none';
1230
+ this.props.mapViewer.closeActiveWidget();
1231
+ document.querySelector('.info-container').style.display = 'none';
1221
1232
  } else if (layers.length > 0) {
1222
- //document.querySelector('.info-container').style.display = 'flex';
1233
+ document.querySelector('.info-container').style.display = 'flex';
1223
1234
  }
1224
1235
  }
1225
1236
 
@@ -425,7 +425,7 @@
425
425
  .map-menu-dataset > div:first-of-type {
426
426
  display: flex;
427
427
  width: 100%;
428
- justify-content: space-between;
428
+ justify-content: flex-start;
429
429
  }
430
430
 
431
431
  .map-menu .map-menu-layers-container {
@@ -595,7 +595,7 @@
595
595
  overflow: auto;
596
596
  }
597
597
 
598
- /* Pixel info */
598
+ /* Info widget */
599
599
  .info-container {
600
600
  display: none;
601
601
  box-shadow: none !important;
@@ -611,6 +611,29 @@
611
611
  font-size: 1rem;
612
612
  }
613
613
 
614
+ .info-panel-buttons {
615
+ display: flex;
616
+ justify-content: space-between;
617
+ }
618
+
619
+ .info-panel-buttons button {
620
+ padding: 0.5rem 1rem;
621
+ margin-bottom: 1rem;
622
+ font-size: 0.875rem;
623
+ }
624
+
625
+ .info-panel-buttons .info-button-left svg {
626
+ margin-right: 0.25rem;
627
+ }
628
+
629
+ .info-panel-buttons .info-button-right svg {
630
+ margin-left: 0.25rem;
631
+ }
632
+
633
+ .info-panel-buttons button svg {
634
+ color: inherit;
635
+ }
636
+
614
637
  .info-panel-title {
615
638
  display: block;
616
639
  margin-bottom: 1rem;
@@ -635,11 +658,11 @@
635
658
  }
636
659
 
637
660
  .info-table tr:nth-child(even) {
638
- background-color: #a0b1280f;
661
+ background-color: #ebebeb33;
639
662
  }
640
663
 
641
664
  .info-table tr:nth-child(odd) {
642
- background-color: #a0b12833;
665
+ background-color: #ebebeb;
643
666
  }
644
667
 
645
668
  /* Time slider*/