From 2cee531239997d7b2173b0876ff1c30bfebe831d Mon Sep 17 00:00:00 2001 From: Vitor George Date: Thu, 11 Jan 2024 11:39:03 +0000 Subject: [PATCH 01/24] Fix spelling --- .../explore/prime-panel/tabs/predict/mosaic-selector/modal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/scripts/components/explore/prime-panel/tabs/predict/mosaic-selector/modal.js b/app/assets/scripts/components/explore/prime-panel/tabs/predict/mosaic-selector/modal.js index 54a11889..7fbb5f9f 100644 --- a/app/assets/scripts/components/explore/prime-panel/tabs/predict/mosaic-selector/modal.js +++ b/app/assets/scripts/components/explore/prime-panel/tabs/predict/mosaic-selector/modal.js @@ -59,7 +59,7 @@ export function MosaicSelectorModal({ {' '} - Select an base mosaic + Select a base mosaic + + + ); +}; From 3ec41ee38b2c38dd7f1b381e08ac0b25f34b7990 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Fri, 12 Jan 2024 12:51:24 +0000 Subject: [PATCH 05/24] Add create mosaic request --- .../mosaic-selector/sections/create-mosaic.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js index c09c0d90..5d39d28a 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js @@ -3,6 +3,7 @@ import { Heading } from '@devseed-ui/typography'; import { formatTimestampToSimpleUTC } from '../../../../../../../utils/dates'; import { ProjectMachineContext } from '../../../../../../../fsm/project'; import selectors from '../../../../../../../fsm/project/selectors'; +import toasts from '../../../../../../common/toasts'; const baseSentinelMosaic = { params: { @@ -25,14 +26,15 @@ export const CreateMosaicSection = () => { const currentImagerySource = ProjectMachineContext.useSelector( selectors.currentImagerySource ); + const apiClient = ProjectMachineContext.useSelector( + ({ context }) => context.apiClient + ); const handleDateChange = (event) => { setSelectedDate(event.target.value); - // const selectedDateUTCStart = new Date(Date.UTC(year, month - 1, day)); - const selectedDateUTCStart = new Date(event.target.value); - const selectedDateUTCEnd = - selectedDateUTCStart.getTime() + 90 * 24 * 60 * 60 * 1000; + const selectedDateUTCStart = new Date(event.target.value).getTime(); + const selectedDateUTCEnd = selectedDateUTCStart + 90 * 24 * 60 * 60 * 1000; setSelectedTimeframe({ start: selectedDateUTCStart, @@ -40,7 +42,7 @@ export const CreateMosaicSection = () => { }); }; - const handleMosaicCreate = () => { + const handleMosaicCreate = async () => { if (!selectedTimeframe) { alert('Select a date!'); return; @@ -54,7 +56,12 @@ export const CreateMosaicSection = () => { mosaic_ts_end: selectedTimeframe.end, }; - alert(JSON.stringify(newMosaic, null, 2)); + try { + const mosaic = await apiClient.post('mosaic', newMosaic); + alert(JSON.stringify(mosaic, null, 2)); + } catch (error) { + toasts.error('Error creating mosaic'); + } }; return ( From 94cabe8839ff9b257ce6eeb51f15c8ac8f684349 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Thu, 18 Jan 2024 11:27:03 +0000 Subject: [PATCH 06/24] model#abort is deprecated, send instance#terminate and call instance failure event --- app/assets/scripts/fsm/project/services.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/scripts/fsm/project/services.js b/app/assets/scripts/fsm/project/services.js index 92be1157..b29f9c53 100644 --- a/app/assets/scripts/fsm/project/services.js +++ b/app/assets/scripts/fsm/project/services.js @@ -93,7 +93,12 @@ export const services = { ); } catch (error) { logger('Error fetching tilejson'); - toasts.error('There was an error fetching the prediction layer.'); + + currentTimeframe = undefined; + currentMosaic = undefined; + toasts.error( + 'There was an error loading the prediction for the latest AOI timeframe, please run a prediction again.' + ); } } } @@ -407,7 +412,10 @@ export const services = { // Instance is processing a different timeframe, abort it if (instanceConfig.timeframe_id !== data.timeframe) { websocket.sendMessage({ - action: 'model#abort', + action: 'instance#terminate', + }); + callback({ + type: 'Instance activation has failed', }); } } else { From 23f0a8ffde8b5ff2a3ee20909b7e1930abe05918 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Thu, 18 Jan 2024 12:37:30 +0000 Subject: [PATCH 07/24] Set created mosaic --- .../mosaic-selector/sections/create-mosaic.js | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js index 5d39d28a..f0a4c9a1 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js @@ -4,6 +4,7 @@ import { formatTimestampToSimpleUTC } from '../../../../../../../utils/dates'; import { ProjectMachineContext } from '../../../../../../../fsm/project'; import selectors from '../../../../../../../fsm/project/selectors'; import toasts from '../../../../../../common/toasts'; +import { format, subDays } from 'date-fns'; const baseSentinelMosaic = { params: { @@ -19,10 +20,15 @@ const baseSentinelMosaic = { }, }; -export const CreateMosaicSection = () => { +const MOSAIC_DATE_RANGE_IN_DAYS = 90; + +export const CreateMosaicSection = ({ setShowModal }) => { + const actorRef = ProjectMachineContext.useActorRef(); const [selectedDate, setSelectedDate] = useState(''); const [selectedTimeframe, setSelectedTimeframe] = useState(null); + const maxDate = subDays(new Date(), MOSAIC_DATE_RANGE_IN_DAYS); + const currentImagerySource = ProjectMachineContext.useSelector( selectors.currentImagerySource ); @@ -51,14 +57,20 @@ export const CreateMosaicSection = () => { const newMosaic = { ...baseSentinelMosaic, imagery_source_id: currentImagerySource.id, - name: `Sentinel-2 ${selectedDate}`, + name: `Sentinel-2 Level-2A ${formatTimestampToSimpleUTC( + selectedTimeframe.start + )} - ${formatTimestampToSimpleUTC(selectedTimeframe.end)}`, mosaic_ts_start: selectedTimeframe.start, mosaic_ts_end: selectedTimeframe.end, }; try { const mosaic = await apiClient.post('mosaic', newMosaic); - alert(JSON.stringify(mosaic, null, 2)); + setShowModal(false); + actorRef.send({ + type: 'Mosaic was selected', + data: { mosaic }, + }); } catch (error) { toasts.error('Error creating mosaic'); } @@ -71,7 +83,12 @@ export const CreateMosaicSection = () => {
Selected date:{' '} - +
Start timestamp:{' '} From eef430a719e786684b8239758915a29ab4cf27a8 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Thu, 18 Jan 2024 13:08:14 +0000 Subject: [PATCH 08/24] Fix prop types --- .../tabs/predict/mosaic-selector/sections/create-mosaic.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js index f0a4c9a1..1eae9098 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import PropTypes from 'prop-types'; import { Heading } from '@devseed-ui/typography'; import { formatTimestampToSimpleUTC } from '../../../../../../../utils/dates'; import { ProjectMachineContext } from '../../../../../../../fsm/project'; @@ -110,3 +111,7 @@ export const CreateMosaicSection = ({ setShowModal }) => { ); }; + +CreateMosaicSection.propTypes = { + setShowModal: PropTypes.func.isRequired, +}; From e3ae6501a0b4c7dd6e5283a272f74b8691b30894 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Thu, 18 Jan 2024 17:00:10 +0000 Subject: [PATCH 09/24] Add TabbedBlock --- .../tabs/predict/mosaic-selector/modal.js | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js index 2bf480c2..b79caf20 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js @@ -7,6 +7,7 @@ import { Button } from '@devseed-ui/button'; import { Heading } from '@devseed-ui/typography'; import { ExistingMosaicsSection } from './sections/list-mosaics'; import { CreateMosaicSection } from './sections/create-mosaic'; +import TabbedBlock from '../../../../../common/tabbed-block-body'; const ModalHeader = styled.header` padding: ${glsp(2)} ${glsp(2)} 0; @@ -33,6 +34,8 @@ const Headline = styled.div` `; export function MosaicSelectorModal({ showModal, setShowModal }) { + const [activeTab, setActiveTab] = React.useState(0); + return ( ( - Select a base mosaic + Set mosaic
- + ); }; CreateMosaicSection.propTypes = { setShowModal: PropTypes.func.isRequired, + className: PropTypes.string, }; diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/list-mosaics.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/list-mosaics.js index 3ac7aeb7..0bd4bbd2 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/list-mosaics.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/list-mosaics.js @@ -17,7 +17,7 @@ const HeadingWrapper = styled.div` align-items: baseline; `; -export const ExistingMosaicsSection = ({ setShowModal }) => { +export const ExistingMosaicsSection = ({ setShowModal, className }) => { const actorRef = ProjectMachineContext.useActorRef(); const isProjectNew = ProjectMachineContext.useSelector((s) => guards.isProjectNew(s.context) @@ -35,7 +35,7 @@ export const ExistingMosaicsSection = ({ setShowModal }) => { ); return ( - <> +
Mosaics availble for your selected AOI @@ -84,10 +84,11 @@ export const ExistingMosaicsSection = ({ setShowModal }) => { ); }} /> - +
); }; ExistingMosaicsSection.propTypes = { setShowModal: PropTypes.func.isRequired, + className: PropTypes.string, }; From ba1530f3f8f31ccba94eb60407623642a71c9261 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Fri, 19 Jan 2024 14:05:57 +0000 Subject: [PATCH 13/24] Create mosaic using PC search id --- .../mosaic-selector/sections/create-mosaic.js | 29 ++----- app/assets/scripts/utils/mosaics.js | 87 +++++++++++++++++++ 2 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 app/assets/scripts/utils/mosaics.js diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js index 190e2760..c8047bab 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js @@ -6,20 +6,7 @@ import { ProjectMachineContext } from '../../../../../../../fsm/project'; import selectors from '../../../../../../../fsm/project/selectors'; import toasts from '../../../../../../common/toasts'; import { format, subDays } from 'date-fns'; - -const baseSentinelMosaic = { - params: { - assets: ['B04', 'B03', 'B02', 'B08'], - rescale: '0,10000', - collection: 'sentinel-2-l2a', - }, - imagery_source_id: 2, - ui_params: { - assets: ['B04', 'B03', 'B02'], - collection: 'sentinel-2-l2a', - color_formula: 'Gamma+RGB+3.2+Saturation+0.8+Sigmoidal+RGB+25+0.35', - }, -}; +import { generateSentinel2L2AMosaic } from '../../../../../../../utils/mosaics'; const MOSAIC_DATE_RANGE_IN_DAYS = 90; @@ -55,15 +42,11 @@ export const CreateMosaicSection = ({ setShowModal, className }) => { return; } - const newMosaic = { - ...baseSentinelMosaic, - imagery_source_id: currentImagerySource.id, - name: `Sentinel-2 Level-2A ${formatTimestampToSimpleUTC( - selectedTimeframe.start - )} - ${formatTimestampToSimpleUTC(selectedTimeframe.end)}`, - mosaic_ts_start: selectedTimeframe.start, - mosaic_ts_end: selectedTimeframe.end, - }; + const newMosaic = await generateSentinel2L2AMosaic({ + startTime: selectedTimeframe.start, + endTime: selectedTimeframe.end, + imagerySourceId: currentImagerySource?.id, + }); try { const mosaic = await apiClient.post('mosaic', newMosaic); diff --git a/app/assets/scripts/utils/mosaics.js b/app/assets/scripts/utils/mosaics.js new file mode 100644 index 00000000..c59d6720 --- /dev/null +++ b/app/assets/scripts/utils/mosaics.js @@ -0,0 +1,87 @@ +import { formatTimestampToSimpleUTC } from './dates'; + +/** + * Get planetary computer search id for a Sentinel 2 Level2a for a interval + * and other filters. + * + * @param {object} options + * @param {Date} options.start + * @param {Date} options.end + * @returns {string} searchid + */ +export async function getSentinel2PCSearchId({ start, end }) { + const res = await fetch( + 'https://planetarycomputer.microsoft.com/api/data/v1/mosaic/register', + { + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + 'filter-lang': 'cql2-json', + filter: { + op: 'and', + args: [ + { op: '=', args: [{ property: 'collection' }, 'sentinel-2-l2a'] }, + { + op: 'anyinteracts', + args: [ + { property: 'datetime' }, + { interval: [start.toISOString(), end.toISOString()] }, + ], + }, + { op: '<=', args: [{ property: 'eo:cloud_cover' }, 50] }, + ], + }, + sortby: [{ field: 'datetime', direction: 'desc' }], + }), + method: 'POST', + } + ); + + const { searchid } = await res.json(); + + return searchid; +} + +/** + * Generate a mosaic object for Sentinel 2 Level2a to be used in the API. + * + * @param {object} options + * @param {number} options.startTime + * @param {number} options.endTime + * @param {number} options.imagerySourceId + * @returns {object} mosaic + */ +export async function generateSentinel2L2AMosaic({ + startTime, + endTime, + imagerySourceId, +}) { + const baseSentinelMosaic = { + params: { + assets: ['B04', 'B03', 'B02', 'B08'], + rescale: '0,10000', + collection: 'sentinel-2-l2a', + }, + imagery_source_id: 2, + ui_params: { + assets: ['B04', 'B03', 'B02'], + collection: 'sentinel-2-l2a', + color_formula: 'Gamma+RGB+3.2+Saturation+0.8+Sigmoidal+RGB+25+0.35', + }, + }; + + return { + ...baseSentinelMosaic, + id: await getSentinel2PCSearchId({ + start: new Date(startTime), + end: new Date(endTime), + }), + imagery_source_id: imagerySourceId, + name: `Sentinel-2 Level-2A ${formatTimestampToSimpleUTC( + startTime + )} - ${formatTimestampToSimpleUTC(endTime)}`, + mosaic_ts_start: startTime, + mosaic_ts_end: endTime, + }; +} From 52a3755c9f3f426a7f96d92fa70a7b177385bac5 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Fri, 19 Jan 2024 15:24:01 +0000 Subject: [PATCH 14/24] Update mosaic list when a new mosaic is created --- .../prime-panel/tabs/predict/mosaic-selector/modal.js | 5 ++++- .../predict/mosaic-selector/sections/create-mosaic.js | 9 +++++---- app/assets/scripts/fsm/project/actions.js | 10 +++++----- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js index b403fc41..1f099256 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js @@ -79,7 +79,10 @@ export function MosaicSelectorModal({ showModal, setShowModal }) { className='create-mosaic' tabId='create-mosaic-tab-trigger' onTabClick={() => setActiveTab(1)} - setShowModal={setShowModal} + onMosaicCreated={() => { + setActiveTab(0); + setShowModal(false); + }} /> diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js index c8047bab..cf8ea11b 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js @@ -10,7 +10,7 @@ import { generateSentinel2L2AMosaic } from '../../../../../../../utils/mosaics'; const MOSAIC_DATE_RANGE_IN_DAYS = 90; -export const CreateMosaicSection = ({ setShowModal, className }) => { +export const CreateMosaicSection = ({ onMosaicCreated, className }) => { const actorRef = ProjectMachineContext.useActorRef(); const [selectedDate, setSelectedDate] = useState(''); const [selectedTimeframe, setSelectedTimeframe] = useState(null); @@ -50,10 +50,11 @@ export const CreateMosaicSection = ({ setShowModal, className }) => { try { const mosaic = await apiClient.post('mosaic', newMosaic); - setShowModal(false); + const { mosaics: mosaicsList } = await apiClient.get('mosaic'); + onMosaicCreated(); actorRef.send({ type: 'Mosaic was selected', - data: { mosaic }, + data: { mosaic, mosaicsList }, }); } catch (error) { toasts.error('Error creating mosaic'); @@ -96,6 +97,6 @@ export const CreateMosaicSection = ({ setShowModal, className }) => { }; CreateMosaicSection.propTypes = { - setShowModal: PropTypes.func.isRequired, + onMosaicCreated: PropTypes.func.isRequired, className: PropTypes.string, }; diff --git a/app/assets/scripts/fsm/project/actions.js b/app/assets/scripts/fsm/project/actions.js index 2e0835b5..5c64c0cc 100644 --- a/app/assets/scripts/fsm/project/actions.js +++ b/app/assets/scripts/fsm/project/actions.js @@ -73,12 +73,12 @@ export const actions = { ...nextContext, }; }), - setCurrentMosaic: assign((context, event) => { - const { mosaic } = event.data; + setCurrentMosaic: assign((_, event) => { + const { mosaic, mosaicsList } = event.data; + const currentMosaic = { ...mosaic, tileUrl: getMosaicTileUrl(mosaic) }; - return { - currentMosaic: { ...mosaic, tileUrl: getMosaicTileUrl(mosaic) }, - }; + // Optionally update mosaic list if provided + return mosaicsList ? { mosaicsList, currentMosaic } : { currentMosaic }; }), setCurrentModel: assign((context, event) => { const { currentModel, currentImagerySource } = context; From 55755e0421df8c41aee96469bf2bc502fef62389 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Mon, 22 Jan 2024 11:07:32 +0000 Subject: [PATCH 15/24] Display preview map --- .../components/common/map/constants.js | 2 + .../scripts/components/project/map/index.js | 6 +- .../mosaic-selector/sections/create-mosaic.js | 136 +++++++++++++----- app/assets/scripts/fsm/project/actions.js | 2 +- app/assets/scripts/fsm/project/helpers.js | 25 ---- app/assets/scripts/fsm/project/services.js | 2 +- app/assets/scripts/utils/mosaics.js | 31 ++++ 7 files changed, 138 insertions(+), 66 deletions(-) delete mode 100644 app/assets/scripts/fsm/project/helpers.js diff --git a/app/assets/scripts/components/common/map/constants.js b/app/assets/scripts/components/common/map/constants.js index 30d6429c..ed1b843f 100644 --- a/app/assets/scripts/components/common/map/constants.js +++ b/app/assets/scripts/components/common/map/constants.js @@ -1 +1,3 @@ export const BOUNDS_PADDING = [25, 25]; + +export const MOSAIC_LAYER_OPACITY = 0.8; diff --git a/app/assets/scripts/components/project/map/index.js b/app/assets/scripts/components/project/map/index.js index ad09f29b..a3fd10f8 100644 --- a/app/assets/scripts/components/project/map/index.js +++ b/app/assets/scripts/components/project/map/index.js @@ -25,13 +25,15 @@ import { RETRAIN_MAP_MODES } from '../../../fsm/project/constants'; import FreehandDrawControl from './freehand-draw-control'; import PolygonDrawControl from './polygon-draw-control'; import selectors from '../../../fsm/project/selectors'; -import { BOUNDS_PADDING } from '../../common/map/constants'; +import { + BOUNDS_PADDING, + MOSAIC_LAYER_OPACITY, +} from '../../common/map/constants'; const center = [19.22819, -99.995841]; const zoom = 12; const DEFAULT_PREDICTION_LAYER_OPACITY = 0.7; -const MOSAIC_LAYER_OPACITY = 0.8; const Container = styled.div` height: 100%; diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js index cf8ea11b..583c88ff 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js @@ -1,68 +1,62 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +import { MapContainer, TileLayer } from 'react-leaflet'; + import { Heading } from '@devseed-ui/typography'; import { formatTimestampToSimpleUTC } from '../../../../../../../utils/dates'; import { ProjectMachineContext } from '../../../../../../../fsm/project'; import selectors from '../../../../../../../fsm/project/selectors'; import toasts from '../../../../../../common/toasts'; import { format, subDays } from 'date-fns'; -import { generateSentinel2L2AMosaic } from '../../../../../../../utils/mosaics'; +import { + generateSentinel2L2AMosaic, + getMosaicTileUrl, +} from '../../../../../../../utils/mosaics'; +import { MOSAIC_LAYER_OPACITY } from '../../../../../../../fsm/project/constants'; const MOSAIC_DATE_RANGE_IN_DAYS = 90; -export const CreateMosaicSection = ({ onMosaicCreated, className }) => { - const actorRef = ProjectMachineContext.useActorRef(); - const [selectedDate, setSelectedDate] = useState(''); - const [selectedTimeframe, setSelectedTimeframe] = useState(null); +const SectionWrapper = styled.div` + display: flex; + justify-content: space-evenly; + height: 100%; +`; - const maxDate = subDays(new Date(), MOSAIC_DATE_RANGE_IN_DAYS); +const PanelWrapper = styled.div` + width: 100%; +`; +const CreateMosaicForm = ({ setNewMosaic, handleMosaicCreation }) => { const currentImagerySource = ProjectMachineContext.useSelector( selectors.currentImagerySource ); - const apiClient = ProjectMachineContext.useSelector( - ({ context }) => context.apiClient - ); + const [selectedDate, setSelectedDate] = useState(''); + const [selectedTimeframe, setSelectedTimeframe] = useState(null); - const handleDateChange = (event) => { + const maxDate = subDays(new Date(), MOSAIC_DATE_RANGE_IN_DAYS); + const handleDateChange = async (event) => { setSelectedDate(event.target.value); const selectedDateUTCStart = new Date(event.target.value).getTime(); const selectedDateUTCEnd = selectedDateUTCStart + 90 * 24 * 60 * 60 * 1000; - setSelectedTimeframe({ + const newTimeframe = { start: selectedDateUTCStart, end: selectedDateUTCEnd, - }); - }; - - const handleMosaicCreate = async () => { - if (!selectedTimeframe) { - alert('Select a date!'); - return; - } - + }; const newMosaic = await generateSentinel2L2AMosaic({ - startTime: selectedTimeframe.start, - endTime: selectedTimeframe.end, + startTime: newTimeframe.start, + endTime: newTimeframe.end, imagerySourceId: currentImagerySource?.id, }); - - try { - const mosaic = await apiClient.post('mosaic', newMosaic); - const { mosaics: mosaicsList } = await apiClient.get('mosaic'); - onMosaicCreated(); - actorRef.send({ - type: 'Mosaic was selected', - data: { mosaic, mosaicsList }, - }); - } catch (error) { - toasts.error('Error creating mosaic'); - } + setNewMosaic(newMosaic); + setSelectedTimeframe(newTimeframe); }; return ( -
+ Create a new mosaic @@ -88,11 +82,79 @@ export const CreateMosaicSection = ({ onMosaicCreated, className }) => { : '-'}
-
- + + ); +}; + +CreateMosaicForm.propTypes = { + setNewMosaic: PropTypes.func.isRequired, + handleMosaicCreation: PropTypes.func.isRequired, +}; + +const MosaicPreviewMap = ({ mosaicTileUrl }) => { + const center = [19.22819, -99.995841]; + const zoom = 12; + + return ( + + + {mosaicTileUrl && ( + + )} + + + ); +}; + +MosaicPreviewMap.propTypes = { + mosaicTileUrl: PropTypes.object, +}; + +export const CreateMosaicSection = ({ onMosaicCreated, className }) => { + const [newMosaic, setNewMosaic] = useState(null); + + const actorRef = ProjectMachineContext.useActorRef(); + const apiClient = ProjectMachineContext.useSelector( + ({ context }) => context.apiClient + ); + + const handleMosaicCreation = async () => { + if (!newMosaic) { + return; + } + + try { + const mosaic = await apiClient.post('mosaic', newMosaic); + const { mosaics: mosaicsList } = await apiClient.get('mosaic'); + onMosaicCreated(); + actorRef.send({ + type: 'Mosaic was selected', + data: { mosaic, mosaicsList }, + }); + } catch (error) { + toasts.error('Error creating mosaic'); + } + }; + + return ( + + + + ); }; diff --git a/app/assets/scripts/fsm/project/actions.js b/app/assets/scripts/fsm/project/actions.js index 5c64c0cc..231cf90c 100644 --- a/app/assets/scripts/fsm/project/actions.js +++ b/app/assets/scripts/fsm/project/actions.js @@ -5,7 +5,7 @@ import L from 'leaflet'; import turfBboxPolygon from '@turf/bbox-polygon'; import turfArea from '@turf/area'; import toasts from '../../components/common/toasts'; -import { getMosaicTileUrl } from './helpers'; +import { getMosaicTileUrl } from '../../utils/mosaics'; import history from '../../history'; import { RETRAIN_MAP_MODES, SESSION_MODES } from './constants'; diff --git a/app/assets/scripts/fsm/project/helpers.js b/app/assets/scripts/fsm/project/helpers.js deleted file mode 100644 index 05b6bf67..00000000 --- a/app/assets/scripts/fsm/project/helpers.js +++ /dev/null @@ -1,25 +0,0 @@ -export function getMosaicTileUrl(mosaic) { - if (!mosaic) return; - const mosaicParams = mosaic?.ui_params || mosaic?.params; - - if (!mosaicParams) return; - - const { assets, ...otherParams } = mosaicParams; - - let params = []; - - // The tiler doesn't support array[] notation in the querystring, this will - // generate a custom notation like 'asset=a&asset=b&asset=c' - if (Array.isArray(assets)) { - assets.forEach((a) => params.push(`assets=${a}`)); - } else { - params.push(`assets=${assets}`); - } - - // Serialize remaining params - for (var p in otherParams) params.push(p + '=' + otherParams[p]); - - return `https://planetarycomputer.microsoft.com/api/data/v1/mosaic/tiles/${ - mosaic.id - }/{z}/{x}/{y}?${params.join('&')}`; -} diff --git a/app/assets/scripts/fsm/project/services.js b/app/assets/scripts/fsm/project/services.js index b29f9c53..c991b97a 100644 --- a/app/assets/scripts/fsm/project/services.js +++ b/app/assets/scripts/fsm/project/services.js @@ -6,7 +6,7 @@ import { delay } from '../../utils/utils'; import { WebsocketClient } from './websocket-client'; import logger from '../../utils/logger'; import toasts from '../../components/common/toasts'; -import { getMosaicTileUrl } from './helpers'; +import { getMosaicTileUrl } from '../../utils/mosaics'; import { SESSION_MODES } from './constants'; import { round } from '../../utils/format'; diff --git a/app/assets/scripts/utils/mosaics.js b/app/assets/scripts/utils/mosaics.js index c59d6720..8aa393f5 100644 --- a/app/assets/scripts/utils/mosaics.js +++ b/app/assets/scripts/utils/mosaics.js @@ -1,5 +1,36 @@ import { formatTimestampToSimpleUTC } from './dates'; +/** + * Get the tile url for a mosaic. + * @param {object} mosaic + * @returns {string} url + */ +export function getMosaicTileUrl(mosaic) { + if (!mosaic) return; + const mosaicParams = mosaic?.ui_params || mosaic?.params; + + if (!mosaicParams) return; + + const { assets, ...otherParams } = mosaicParams; + + let params = []; + + // The tiler doesn't support array[] notation in the querystring, this will + // generate a custom notation like 'asset=a&asset=b&asset=c' + if (Array.isArray(assets)) { + assets.forEach((a) => params.push(`assets=${a}`)); + } else { + params.push(`assets=${assets}`); + } + + // Serialize remaining params + for (var p in otherParams) params.push(p + '=' + otherParams[p]); + + return `https://planetarycomputer.microsoft.com/api/data/v1/mosaic/tiles/${ + mosaic.id + }/{z}/{x}/{y}?${params.join('&')}`; +} + /** * Get planetary computer search id for a Sentinel 2 Level2a for a interval * and other filters. From 1f820e0892f97ee428bfee4750706eaed1411bc8 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Mon, 22 Jan 2024 11:22:39 +0000 Subject: [PATCH 16/24] Use current map zoom and center in create mosaic modal --- .../tabs/predict/mosaic-selector/modal.js | 16 ++++++- .../mosaic-selector/sections/create-mosaic.js | 46 +++++++++++++------ 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js index 1f099256..8b3fd2ac 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/modal.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo, useState } from 'react'; import T from 'prop-types'; import styled from 'styled-components'; import { Modal } from '@devseed-ui/modal'; @@ -8,6 +8,7 @@ import { Heading } from '@devseed-ui/typography'; import { ExistingMosaicsSection } from './sections/list-mosaics'; import { CreateMosaicSection } from './sections/create-mosaic'; import TabbedBlock from '../../../../../common/tabbed-block-body'; +import { ProjectMachineContext } from '../../../../../../fsm/project'; const ModalHeader = styled.header` padding: ${glsp(2)} ${glsp(2)} 0; @@ -38,6 +39,17 @@ const Headline = styled.div` export function MosaicSelectorModal({ showModal, setShowModal }) { const [activeTab, setActiveTab] = React.useState(0); + const mapRef = ProjectMachineContext.useSelector( + ({ context }) => context.mapRef + ); + + // Get the current map zoom and center on modal open + const [mapZoom, mapCenter] = useMemo(() => { + if (!showModal || !mapRef) return [null, null]; + const { lng, lat } = mapRef.getCenter(); + return [mapRef.getZoom(), [lat, lng]]; + }, [mapRef, showModal]); + return ( setActiveTab(1)} onMosaicCreated={() => { setActiveTab(0); diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js index 583c88ff..f37a0e26 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js @@ -95,30 +95,44 @@ CreateMosaicForm.propTypes = { handleMosaicCreation: PropTypes.func.isRequired, }; -const MosaicPreviewMap = ({ mosaicTileUrl }) => { - const center = [19.22819, -99.995841]; - const zoom = 12; - +const MosaicPreviewMap = ({ + mosaicTileUrl, + initialMapCenter, + initialMapZoom, +}) => { return ( - - {mosaicTileUrl && ( - - )} - + {initialMapZoom && initialMapCenter && ( + + {mosaicTileUrl && ( + + )} + + )} ); }; MosaicPreviewMap.propTypes = { mosaicTileUrl: PropTypes.object, + initialMapZoom: PropTypes.number, + initialMapCenter: PropTypes.array, }; -export const CreateMosaicSection = ({ onMosaicCreated, className }) => { +export const CreateMosaicSection = ({ + onMosaicCreated, + className, + initialMapZoom, + initialMapCenter, +}) => { const [newMosaic, setNewMosaic] = useState(null); const actorRef = ProjectMachineContext.useActorRef(); @@ -152,6 +166,8 @@ export const CreateMosaicSection = ({ onMosaicCreated, className }) => { handleMosaicCreation={handleMosaicCreation} /> @@ -161,4 +177,6 @@ export const CreateMosaicSection = ({ onMosaicCreated, className }) => { CreateMosaicSection.propTypes = { onMosaicCreated: PropTypes.func.isRequired, className: PropTypes.string, + initialMapZoom: PropTypes.number, + initialMapCenter: PropTypes.array, }; From faf63c27812ce7d4f6309ad4ef08876f2abd0b1c Mon Sep 17 00:00:00 2001 From: Vitor George Date: Mon, 22 Jan 2024 11:33:52 +0000 Subject: [PATCH 17/24] Update form --- .../mosaic-selector/sections/create-mosaic.js | 27 ++++++++++--------- app/assets/scripts/utils/dates.js | 6 +++++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js index f37a0e26..729751ef 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js @@ -5,7 +5,7 @@ import styled from 'styled-components'; import { MapContainer, TileLayer } from 'react-leaflet'; import { Heading } from '@devseed-ui/typography'; -import { formatTimestampToSimpleUTC } from '../../../../../../../utils/dates'; +import { formatTimestampToSimpleUTCDate } from '../../../../../../../utils/dates'; import { ProjectMachineContext } from '../../../../../../../fsm/project'; import selectors from '../../../../../../../fsm/project/selectors'; import toasts from '../../../../../../common/toasts'; @@ -58,10 +58,11 @@ const CreateMosaicForm = ({ setNewMosaic, handleMosaicCreation }) => { return ( - Create a new mosaic + Custom mosaic filters +
- Selected date:{' '} + Start date:{' '} { onChange={handleDateChange} />
-
- Start timestamp:{' '} - {selectedTimeframe?.start - ? formatTimestampToSimpleUTC(selectedTimeframe.start) - : '-'} -
End date:{' '} - {selectedTimeframe?.end - ? formatTimestampToSimpleUTC(selectedTimeframe.end) - : '-'} +
-
+
+ + Custom mosaic filters + + + + + Start date:{' '} + + + + + + + + End date:{' '} + + + + +
); }; @@ -125,7 +154,7 @@ const MosaicPreviewMap = ({ ) : ( - Pick date to display map preview + Set date to display map preview )} diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/list-mosaics.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/list-mosaics.js index 0bd4bbd2..af8a2a67 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/list-mosaics.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/list-mosaics.js @@ -7,6 +7,7 @@ import selectors from '../../../../../../../fsm/project/selectors'; import * as guards from '../../../../../../../fsm/project/guards'; import { Heading } from '@devseed-ui/typography'; +import { glsp } from '@devseed-ui/theme-provider'; import CardList, { Card } from '../../../../../../common/card-list'; import { formatTimestampToSimpleUTC } from '../../../../../../../utils/dates'; @@ -16,6 +17,11 @@ const HeadingWrapper = styled.div` justify-content: space-between; align-items: baseline; `; +const ModalInnerContent = styled.div` + display: flex; + flex-flow: column; + gap: ${glsp()}; +`; export const ExistingMosaicsSection = ({ setShowModal, className }) => { const actorRef = ProjectMachineContext.useActorRef(); @@ -35,7 +41,7 @@ export const ExistingMosaicsSection = ({ setShowModal, className }) => { ); return ( -
+ Mosaics availble for your selected AOI @@ -84,7 +90,7 @@ export const ExistingMosaicsSection = ({ setShowModal, className }) => { ); }} /> -
+ ); }; From cba7f3f64ca3b46c4b5649d47b854a0330967e6c Mon Sep 17 00:00:00 2001 From: Vitor George Date: Mon, 22 Jan 2024 18:01:50 +0000 Subject: [PATCH 23/24] Apply fixed width to the form --- .../mosaic-selector/sections/create-mosaic.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js index 137d54d1..418456e6 100644 --- a/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js +++ b/app/assets/scripts/components/project/prime-panel/tabs/predict/mosaic-selector/sections/create-mosaic.js @@ -35,7 +35,11 @@ const SectionWrapper = styled.div` height: 100%; `; -const PanelWrapper = styled.div` +const FormWrapper = styled.div` + width: 300px; +`; + +const MapPreviewWrapper = styled.div` width: 100%; `; @@ -77,7 +81,7 @@ const CreateMosaicForm = ({ setNewMosaic, handleMosaicCreation }) => { }; return ( - +
Custom mosaic filters @@ -123,7 +127,7 @@ const CreateMosaicForm = ({ setNewMosaic, handleMosaicCreation }) => { Create Mosaic -
+ ); }; @@ -138,7 +142,7 @@ const MosaicPreviewMap = ({ initialMapZoom, }) => { return ( - + {mosaicTileUrl && initialMapZoom && initialMapCenter ? ( Set date to display map preview )} - + ); }; From 9de078ba1a81b24724db201b4c725f0ebc536aaf Mon Sep 17 00:00:00 2001 From: Vitor George Date: Mon, 22 Jan 2024 18:11:49 +0000 Subject: [PATCH 24/24] Reduce maximum cloud percentage to 10% --- app/assets/scripts/utils/mosaics.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/scripts/utils/mosaics.js b/app/assets/scripts/utils/mosaics.js index 8aa393f5..1f1ef4cb 100644 --- a/app/assets/scripts/utils/mosaics.js +++ b/app/assets/scripts/utils/mosaics.js @@ -1,5 +1,7 @@ import { formatTimestampToSimpleUTC } from './dates'; +const MAX_CLOUD_COVER_PERCENTAGE = 10; + /** * Get the tile url for a mosaic. * @param {object} mosaic @@ -60,7 +62,13 @@ export async function getSentinel2PCSearchId({ start, end }) { { interval: [start.toISOString(), end.toISOString()] }, ], }, - { op: '<=', args: [{ property: 'eo:cloud_cover' }, 50] }, + { + op: '<=', + args: [ + { property: 'eo:cloud_cover' }, + MAX_CLOUD_COVER_PERCENTAGE, + ], + }, ], }, sortby: [{ field: 'datetime', direction: 'desc' }],