diff --git a/app/public/data/esa/globalDataLayerImages/World-E13c_ship_detections.png b/app/public/data/esa/globalDataLayerImages/World-E13c_ship_detections.png new file mode 100644 index 0000000000..1d502405b1 Binary files /dev/null and b/app/public/data/esa/globalDataLayerImages/World-E13c_ship_detections.png differ diff --git a/app/public/eodash-data/stories/E13c_ship_detections.md b/app/public/eodash-data/stories/E13c_ship_detections.md new file mode 100644 index 0000000000..48cdce8528 --- /dev/null +++ b/app/public/eodash-data/stories/E13c_ship_detections.md @@ -0,0 +1 @@ +placeholder diff --git a/app/src/components/map/CustomAreaButtons.vue b/app/src/components/map/CustomAreaButtons.vue index b9b0191c3b..45210412d7 100644 --- a/app/src/components/map/CustomAreaButtons.vue +++ b/app/src/components/map/CustomAreaButtons.vue @@ -86,7 +86,6 @@ import Feature from 'ol/Feature'; import { mapState, } from 'vuex'; -// import { getArea } from 'ol/extent'; import Text from 'ol/style/Text'; import { Polygon } from 'ol/geom'; @@ -244,12 +243,14 @@ export default { * @param {*} geom OpenLayer geometry * @returns {Boolean} */ - isGeometryTooLarge(geom) { // eslint-disable-line - // for now commenting out previous logic + isGeometryTooLarge(geom) { + if (this.mergedConfigsData.length && this.mergedConfigsData[0]?.maxDrawnAreaSide) { + const extent = geom.getExtent(); + return extent && ( + (extent[3] - extent[1] > this.mergedConfigsData[0]?.maxDrawnAreaSide) + || (extent[2] - extent[0] > this.mergedConfigsData[0]?.maxDrawnAreaSide)); + } return false; - // const extent = geom.getExtent(); - // to do: use more exact turf calculations? - // return extent && (getArea(extent) > 50000000000); }, onDrawFinished(event) { const { map } = getMapInstance(this.mapId); diff --git a/app/src/components/map/CustomFeaturesFetchButton.vue b/app/src/components/map/CustomFeaturesFetchButton.vue new file mode 100644 index 0000000000..6994b9fb25 --- /dev/null +++ b/app/src/components/map/CustomFeaturesFetchButton.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/app/src/components/map/DatePickerControl.vue b/app/src/components/map/DatePickerControl.vue new file mode 100644 index 0000000000..a5ae307269 --- /dev/null +++ b/app/src/components/map/DatePickerControl.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/app/src/components/map/Map.vue b/app/src/components/map/Map.vue index d51e22dda6..3679b12d78 100644 --- a/app/src/components/map/Map.vue +++ b/app/src/components/map/Map.vue @@ -127,6 +127,24 @@ :key="dataLayerName + '_customArea'" :drawnArea.sync="drawnArea" /> + + +
config.timeFromProperty || config.usedTimes?.time?.length, + (config) => config.mapTimeDatepicker + || config.timeFromProperty + || config.usedTimes?.time?.length, ) .forEach((config) => { const layer = layers.find((l) => l.get('name') === config.name); - if (layer) { + if (layer instanceof VectorLayer && config.mapTimeDatepicker) { + // do not fetch new features on time changeempty + } else if (layer) { updateTimeLayer(layer, config, timeObj.value, area); } }); @@ -795,6 +824,12 @@ export default { this.$emit('update:center', e); this.currentCenter = e; }, + setDateFromDatePicker(date) { + this.dataLayerTime = { + name: date, + value: DateTime.fromISO(date), + }; + }, updateTime(time, compare) { // Define a function to update the data layer // direct match on name @@ -967,17 +1002,25 @@ export default { } } }, - updateSelectedAreaFeature() { + updateSelectedAreaFeature(manualTrigger = false) { const { map } = getMapInstance(this.mapId); const dataGroup = map.getLayers().getArray().find((l) => l.get('id') === 'dataGroup'); const layers = dataGroup.getLayers().getArray(); const area = this.drawnArea; const time = this.dataLayerTime?.value; - this.mergedConfigsDataIndexAware.filter((config) => config.usedTimes?.time?.length) + this.mergedConfigsDataIndexAware.filter( + (config) => config.mapTimeDatepicker || config.usedTimes?.time?.length, + ) .forEach((config) => { const layer = layers.find((l) => l.get('name') === config.name); if (layer) { - updateTimeLayer(layer, config, time, area, 'updateArea'); + if (manualTrigger) { + updateTimeLayer(layer, config, time, area, 'updateArea'); + } else if (layer instanceof VectorLayer && config.mapTimeDatepicker) { + // do nothing + } else { + updateTimeLayer(layer, config, time, area, 'updateArea'); + } } }); }, diff --git a/app/src/components/map/SliderControl.vue b/app/src/components/map/SliderControl.vue new file mode 100644 index 0000000000..6b7004790b --- /dev/null +++ b/app/src/components/map/SliderControl.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/app/src/config/esa.js b/app/src/config/esa.js index d9186ee826..9f547d0430 100644 --- a/app/src/config/esa.js +++ b/app/src/config/esa.js @@ -455,6 +455,10 @@ export const indicatorsDefinition = Object.freeze({ themes: ['economy'], story: '', }, + E13c_ship_detections: { + themes: ['economy'], + story: '/eodash-data/stories/E13c_ship_detections', + }, E13b: { indicatorSummary: 'Throughput at principal hub airports', themes: ['economy'], @@ -1818,6 +1822,66 @@ export const globalIndicators = [ }, }, }, + { + properties: { + indicatorObject: { + dataLoadFinished: true, + country: 'all', + city: 'World', + siteName: 'global', + description: 'Ship detection', + indicator: 'E13c_ship_detections', + indicatorName: 'Ships-detection on-the-fly', + subAoi: { + type: 'FeatureCollection', + features: [], + }, + aoiID: 'World', + time: [], + inputData: [''], + yAxis: '', + display: [{ + baseLayers: cloudlessBaseLayerDefault, + disableCompare: true, + dateFormatFunction: (date) => `${DateTime.fromISO(date).toFormat('yyyy-MM-dd')}/${DateTime.fromISO(date).toFormat('yyyy-MM-dd')}`, + layers: 'SENTINEL-2-L2A-TRUE-COLOR', + name: 'Daily Sentinel 2 L2A', + // 2500 pixel SH limit * 10 m resolution of S2 RGB bands + // and multiplied by 4/5 to cater for slowness of algorithm and data transfer + maxDrawnAreaSide: 20000, + minZoom: 7, + maxZoom: 18, + mapTimeDatepicker: true, + sliderConfig: { + title: 'Detection Threshold', + min: 0, + max: 1, + step: 0.01, + default: 0.5, + }, + drawnAreaLimitExtent: true, + // areaIndicator: trucksAreaIndicator, + features: { + url: 'https://gtif-backend.hub.eox.at/ship_detection?{area}&{featuresTime}&threshold={sliderValue}', + name: 'Ship detections on-the-fly', + style: { + strokeColor: '#00c3ff', + width: 2, + }, + dateFormatFunction: (date) => `start_date=${DateTime.fromISO(date).toFormat('yyyy-MM-dd')}&end_date=${DateTime.fromISO(date).toFormat('yyyy-MM-dd')}`, + areaFormatFunction: (area) => { + const extent = geojsonFormat.readGeometry(area).getExtent(); + const formattedArea = `lon_min=${extent[0]}&lat_min=${extent[1]}&lon_max=${extent[2]}&lat_max=${extent[3]}`; + return { + area: formattedArea, + }; + }, + }, + customAreaFeatures: true, + }], + }, + }, + }, { properties: { indicatorObject: { diff --git a/app/src/helpers/customAreaObjects.js b/app/src/helpers/customAreaObjects.js index 2d215283fc..9086e66ec3 100644 --- a/app/src/helpers/customAreaObjects.js +++ b/app/src/helpers/customAreaObjects.js @@ -29,6 +29,9 @@ export const fetchCustomDataOptions = (time, sourceOptionsObj, store) => { ); outputOptionsObj.site = currSite; } + if (store.state.features.sliderValue) { + outputOptionsObj.sliderValue = store.state.features.sliderValue; + } if (time) { // substitutes {time} template possibly utilizing dateFormatFunction @@ -440,6 +443,7 @@ export const fetchCustomAreaObjects = async ( return custom; }); } else { + window.dispatchEvent(new CustomEvent('set-custom-area-features-loading', { detail: true })); customObjects = await fetch(url, requestOpts).then((response) => { if (!response.ok) { return response.text().then((text) => { throw text; }); @@ -494,6 +498,9 @@ export const fetchCustomAreaObjects = async ( // If error message is an object it is probably the returned html console.log('Possible issue retrieving geoJSON for specified time'); } + }) + .finally(() => { + window.dispatchEvent(new CustomEvent('set-custom-area-features-loading', { detail: false })); }); } return customObjects; diff --git a/app/src/store/modules/features.js b/app/src/store/modules/features.js index 1e0962f114..72e54b4e33 100644 --- a/app/src/store/modules/features.js +++ b/app/src/store/modules/features.js @@ -18,6 +18,8 @@ const state = { custom: [], }, selectedArea: null, + selectedCalendarDate: null, + sliderValue: null, selectedFeatures: [], }; @@ -291,6 +293,12 @@ const mutations = { SET_SELECTED_AREA(state, area) { state.selectedArea = area; }, + SET_SELECTED_DATE(state, date) { + state.selectedCalendarDate = date; + }, + SET_SLIDER_VALUE(state, sliderValue) { + state.sliderValue = sliderValue; + }, SET_ADMIN_BORDER_FEATURE_SELECTED(state, feature) { state.adminBorderFeatureSelected = feature; },