From bfb3444def51358166228c34a4927646c3a38648 Mon Sep 17 00:00:00 2001 From: jbyrne Date: Wed, 28 Aug 2024 23:00:09 -0700 Subject: [PATCH] feature/release-1.2.0 stash changes --- .github/workflows/build.yml | 7 +- CHANGE | 1 - CHANGELOG.md | 34 ++ package-lock.json | 4 +- package.json | 2 +- src/components/about/About.tsx | 2 +- src/components/history/DataPagination.tsx | 26 +- .../history/GeneratedProductHistory.tsx | 66 +-- src/components/history/HistoryFilters.tsx | 98 +++- src/components/map/WorldMap.tsx | 66 ++- src/components/sidebar/GranulesTable.tsx | 501 +++++++++--------- .../sidebar/ProductCustomization.tsx | 60 ++- .../sidebar/actions/productSlice.ts | 6 + src/constants/graphqlQueries.ts | 66 ++- src/constants/rasterParameterConstants.ts | 33 +- src/types/constantTypes.ts | 37 +- src/types/graphqlTypes.ts | 20 +- src/types/historyPageTypes.ts | 3 +- 18 files changed, 638 insertions(+), 394 deletions(-) delete mode 100644 CHANGE create mode 100644 CHANGELOG.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76c717d..d842d02 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ on: commit: type: string description: Custom commit hash - + env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} @@ -58,11 +58,14 @@ jobs: - name: Initial checkout ${{ github.ref }} if: github.event.inputs.commit == '' uses: actions/checkout@v4 + with: + token: ${{ steps.podaac-cicd.outputs.token }} - name: Adjust to proper commit hash ${{ github.event.inputs.commit }} if: github.event.inputs.commit != '' uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.commit }} + token: ${{ steps.podaac-cicd.outputs.token }} - name: get-npm-version id: package-version uses: martinbeentjes/npm-get-version-action@v1.3.1 @@ -121,7 +124,7 @@ jobs: echo "deploy_env=${{ env.TARGET_ENV }}" >> $GITHUB_OUTPUT VENUE=$(echo "${{ env.TARGET_ENV }}" | tr '[:upper:]' '[:lower:]') echo "deploy_env_lower=$VENUE" >> $GITHUB_OUTPUT - ## Build + ## Build - uses: hashicorp/setup-terraform@v2 with: terraform_version: ${{ env.TERRAFORM_VERSION }} diff --git a/CHANGE b/CHANGE deleted file mode 100644 index acca8be..0000000 --- a/CHANGE +++ /dev/null @@ -1 +0,0 @@ -podaac/swodlr-ui deployment diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..31e5526 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +## [1.2.0] + +### Changed + - [issues/114](https://github.com/podaac/swodlr-ui/issues/114): My data page now uses server side filtering for most columns + +## [1.1.1] + +### Fixed + - UI was getting API version from incorrect venue + +## [1.1.0] + +### Added + - Feature/swodlr UI 102 - refactor spatial search validation (#111) + - Feature/swodlr UI sci orbit message (#112) + - Feature/swodlr UI 107 - gray out some product customization options (#110) + - Feature/demo uwg june2024 - bug fixes (#109) + - Feature/swodlr UI 70 - add filtering to my data page and re-generation button (#105) + - Feature/swodlr UI 101 - add file name to granule footprint (#103) + - Issues/swodlr UI 92 - fix tutorial bugs (#99) + - Feature/swodlr UI 71 - add pagination to my data page (#98) + - Feature/swodlr UI cps verification (#100) + + diff --git a/package-lock.json b/package-lock.json index 4121754..b929561 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "swodlr-ui", - "version": "1.0.1-1", + "version": "1.3.0-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "swodlr-ui", - "version": "1.0.1-1", + "version": "1.3.0-0", "dependencies": { "@hexagon/base64": "^1.1.28", "@reduxjs/toolkit": "^1.9.5", diff --git a/package.json b/package.json index 5f31da9..1155898 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "swodlr-ui", - "version": "1.0.1-1", + "version": "1.3.0-0", "private": true, "engines": { "node": ">=18.0.0" diff --git a/src/components/about/About.tsx b/src/components/about/About.tsx index 83c9a26..5d8a39c 100644 --- a/src/components/about/About.tsx +++ b/src/components/about/About.tsx @@ -11,7 +11,7 @@ const About = () => { const [backendVersion, setBackendVersion] = useState('') useEffect(() => { const fetchData = async () => { - setBackendVersion(await fetch('https://swodlr.podaac.sit.earthdatacloud.nasa.gov/api/about').then((version) => version.json()).then(response => response.version)) + setBackendVersion(await fetch(`${process.env.REACT_APP_SWODLR_API_BASE_URI}/about`).then((version) => version.json()).then(response => response.version)) } fetchData() .catch(console.error); diff --git a/src/components/history/DataPagination.tsx b/src/components/history/DataPagination.tsx index 95f8d14..b60b28b 100644 --- a/src/components/history/DataPagination.tsx +++ b/src/components/history/DataPagination.tsx @@ -1,4 +1,4 @@ -import { Col, Pagination, Row, Spinner } from "react-bootstrap"; +import { Col, Pagination, Row } from "react-bootstrap"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { setUserProducts } from "../sidebar/actions/productSlice"; import { productsPerPage } from "../../constants/rasterParameterConstants"; @@ -6,13 +6,12 @@ import { useState } from "react"; const DataPagination = (props: {totalNumberOfProducts: number, totalNumberOfFilteredProducts: number, }) => { - const {totalNumberOfProducts, totalNumberOfFilteredProducts} = props + const { totalNumberOfFilteredProducts} = props const dispatch = useAppDispatch() const userProducts = useAppSelector((state) => state.product.userProducts) const allUserProducts = useAppSelector((state) => state.product.allUserProducts) const [noNextPage, setNoNextPage] = useState(false) const [noPreviousPage, setNoPreviousPage] = useState(true) - const [waitingForPagination, setWaitingForPagination] = useState(false) const [currentPageNumber, setCurrentPageNumber] = useState(1) const numberOfTotalPages = Math.ceil(allUserProducts.length / parseInt(productsPerPage)) @@ -33,16 +32,6 @@ const DataPagination = (props: {totalNumberOfProducts: number, totalNumberOfFilt } } - const waitingForPaginationSpinner = () => { - return ( -
- - Loading... - -
- ) - } - const getPaginationItemsWithEllipsis = () => { let numberOfSlotsFreeLeft = 0 if(currentPageNumber >= numberOfTotalPages-4) { @@ -78,12 +67,12 @@ const DataPagination = (props: {totalNumberOfProducts: number, totalNumberOfFilt } } - if(pagesAllowed[0] > 2) pagesToShow.unshift() - if(pagesAllowed[pagesAllowed.length-1] < numberOfTotalPages-1) pagesToShow.push() + if(pagesAllowed[0] > 2) pagesToShow.unshift() + if(pagesAllowed[pagesAllowed.length-1] < numberOfTotalPages-1) pagesToShow.push() return pagesToShow } - return waitingForPagination ? waitingForPaginationSpinner() : ( + return ( @@ -99,10 +88,9 @@ const DataPagination = (props: {totalNumberOfProducts: number, totalNumberOfFilt : null } -
{totalNumberOfProducts} Total Generated Products
+
{totalNumberOfFilteredProducts} Total Generated Products
) } -export default DataPagination; - +export default DataPagination; \ No newline at end of file diff --git a/src/components/history/GeneratedProductHistory.tsx b/src/components/history/GeneratedProductHistory.tsx index 729617f..afa0cdc 100644 --- a/src/components/history/GeneratedProductHistory.tsx +++ b/src/components/history/GeneratedProductHistory.tsx @@ -1,57 +1,17 @@ import { Alert, Col, OverlayTrigger, Row, Table, Tooltip, Spinner, Form, DropdownButton, Dropdown, Badge } from "react-bootstrap"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; -import { Product, ProductState } from "../../types/graphqlTypes"; +import { Product } from "../../types/graphqlTypes"; import { useEffect, useState } from "react"; import { InfoCircle } from "react-bootstrap-icons"; import { generatedProductsLabels, infoIconsToRender, parameterHelp, productsPerPage } from "../../constants/rasterParameterConstants"; import { getUserProducts } from "../../user/userData"; import { useLocation, useNavigate } from "react-router-dom"; import DataPagination from "./DataPagination"; -import HistoryFilters from "./HistoryFilters"; -import { Adjust, FilterParameters, OutputGranuleExtentFlagOptions, OutputSamplingGridType, RasterResolution } from "../../types/historyPageTypes"; +import HistoryFilters, { getFilterParameters, productPassesFilterCheck } from "./HistoryFilters"; import { setShowReGenerateProductModalTrue } from "../sidebar/actions/modalSlice"; import ReGenerateProductsModal from "./ReGenerateProductsModal"; -import { setAllUserProducts, setGranulesToReGenerate, setUserProducts, setWaitingForMyDataFiltering, setWaitingForProductsToLoad } from "../sidebar/actions/productSlice"; - -export const productPassesFilterCheck = (currentFilters: FilterParameters, cycle: number, pass: number, scene: number, outputGranuleExtentFlag: boolean, status: string, outputSamplingGridType: string, rasterResolution: number, dateGenerated: string, utmZoneAdjust?: number, mgrsBandAdjust?: number): boolean => { - let productPassesFilter = true - const outputGranuleExtentFlagMap = ['128 x 128','256 x 128'] - - if(currentFilters.cycle !== 'none' && currentFilters.cycle !== String(cycle)) { - productPassesFilter = false - } - if (currentFilters.pass !== 'none' && currentFilters.pass !== String(pass)) { - productPassesFilter = false - } - if (currentFilters.scene !== 'none' && currentFilters.scene !== String(scene)) { - productPassesFilter = false - } - if (currentFilters.outputGranuleExtentFlag.length > 0 && !currentFilters.outputGranuleExtentFlag.includes(outputGranuleExtentFlagMap[+outputGranuleExtentFlag] as OutputGranuleExtentFlagOptions)) { - productPassesFilter = false - } - if (currentFilters.status.length > 0 && !currentFilters.status.includes(status as ProductState)) { - productPassesFilter = false - } - if (currentFilters.outputSamplingGridType.length > 0 && !currentFilters.outputSamplingGridType.includes(outputSamplingGridType as OutputSamplingGridType)) { - productPassesFilter = false - } - if (currentFilters.rasterResolution.length > 0 && !currentFilters.rasterResolution.includes(String(rasterResolution) as RasterResolution)) { - productPassesFilter = false - } - if (utmZoneAdjust !== undefined && currentFilters.utmZoneAdjust.length > 0 && !currentFilters.utmZoneAdjust.includes(String(utmZoneAdjust) as Adjust)) { - productPassesFilter = false - } - if (mgrsBandAdjust !== undefined && currentFilters.mgrsBandAdjust.length > 0 && !currentFilters.mgrsBandAdjust.includes(String(mgrsBandAdjust) as Adjust)) { - productPassesFilter = false - } - if(currentFilters.startDate !== 'none' && new Date(dateGenerated) < currentFilters.startDate) { - productPassesFilter = false - } - if(currentFilters.endDate !== 'none' && new Date(dateGenerated) > currentFilters.endDate) { - productPassesFilter = false - } - return productPassesFilter -} +import { setAllUserProducts, setGranulesToReGenerate, setUserProducts, setWaitingForMyDataFiltering, setWaitingForMyDataFilteringReset, setWaitingForProductsToLoad } from "../sidebar/actions/productSlice"; +import { defaultUserProductsLimit } from "../../constants/graphqlQueries"; const GeneratedProductHistory = () => { const dispatch = useAppDispatch() @@ -60,28 +20,32 @@ const GeneratedProductHistory = () => { const currentFilters = useAppSelector((state) => state.product.currentFilters) const waitingForProductsToLoad = useAppSelector((state) => state.product.waitingForProductsToLoad) const waitingForMyDataFiltering = useAppSelector((state) => state.product.waitingForMyDataFiltering) + const waitingForMyDataFilteringReset = useAppSelector((state) => state.product.waitingForMyDataFilteringReset) const { search } = useLocation() const navigate = useNavigate() const [totalNumberOfProducts, setTotalNumberOfProducts] = useState(0) const [totalNumberOfFilteredProducts, setTotalNumberOfFilteredProducts] = useState(0) const [checkedProducts, setCheckedProducts] = useState([]) const [allChecked, setAllChecked] = useState(false) + const [hasAlreadyLoadedInitialProducts, setHasAlreadyLoadedInitialProducts] = useState(false) useEffect(() => { // get the data for the first page // go through all the user product data to get the id of each one so that const fetchData = async () => { if(!waitingForMyDataFiltering) dispatch(setWaitingForProductsToLoad(true)) - await getUserProducts({limit: '1000000'}).then(response => { + + const productQueryParameters = getFilterParameters(currentFilters, defaultUserProductsLimit) + // add variables for filters + await getUserProducts(productQueryParameters).then(response => { dispatch(setWaitingForProductsToLoad(false)) // filter products for what is in the filter const allProducts = response.products as Product[] setTotalNumberOfProducts(allProducts.length) const filteredProducts = allProducts.filter(product => { - const {status, utmZoneAdjust, mgrsBandAdjust, outputGranuleExtentFlag, outputSamplingGridType, rasterResolution, timestamp: dateGenerated, cycle, pass, scene, granules} = product + const {status, utmZoneAdjust, mgrsBandAdjust, rasterResolution} = product const statusToUse = status[0].state - const outputSamplingGridTypeToUse = outputSamplingGridType === 'GEO' ? 'LAT/LON' : outputSamplingGridType - const productPassesFilter = productPassesFilterCheck(currentFilters, cycle, pass, scene, outputGranuleExtentFlag, statusToUse, outputSamplingGridTypeToUse, rasterResolution, dateGenerated, utmZoneAdjust, mgrsBandAdjust) + const productPassesFilter = productPassesFilterCheck(currentFilters, statusToUse, rasterResolution, utmZoneAdjust, mgrsBandAdjust) if(productPassesFilter) { return product } else { @@ -89,14 +53,16 @@ const GeneratedProductHistory = () => { } }) setTotalNumberOfFilteredProducts(filteredProducts.length) + setHasAlreadyLoadedInitialProducts(true) dispatch(setAllUserProducts(filteredProducts)) const productsPerPageToInt = parseInt(productsPerPage) dispatch(setUserProducts(filteredProducts.slice(0, productsPerPageToInt))) dispatch(setWaitingForMyDataFiltering(false)) + dispatch(setWaitingForMyDataFilteringReset(false)) }) } fetchData().catch(console.error) - }, [currentFilters]); + }, [dispatch, currentFilters, waitingForMyDataFiltering, waitingForMyDataFilteringReset]); // reset all checked checkbox when going to next page useEffect(() => { @@ -226,7 +192,7 @@ const GeneratedProductHistory = () => { {} {!waitingForProductsToLoad && userProducts.length === 0 ? {productHistoryAlert()} : null} - {waitingForProductsToLoad ? waitingForProductsToLoadSpinner() : null} + {waitingForProductsToLoad && !hasAlreadyLoadedInitialProducts ? waitingForProductsToLoadSpinner() : null} diff --git a/src/components/history/HistoryFilters.tsx b/src/components/history/HistoryFilters.tsx index bdb748f..b0a048d 100644 --- a/src/components/history/HistoryFilters.tsx +++ b/src/components/history/HistoryFilters.tsx @@ -1,7 +1,7 @@ import { Accordion, Button, Col, Form, Row, Spinner } from "react-bootstrap"; -import { ProductState } from "../../types/graphqlTypes"; +import { GridType, ProductQueryParameters, ProductState, UserProductQueryVariables } from "../../types/graphqlTypes"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; -import { setCurrentFilter, setWaitingForMyDataFiltering } from "../sidebar/actions/productSlice"; +import { setCurrentFilter, setWaitingForMyDataFiltering, setWaitingForMyDataFilteringReset } from "../sidebar/actions/productSlice"; import { defaultFilterParameters, defaultSpatialSearchEndDate, defaultSpatialSearchStartDate, inputBounds, parameterOptionValues, rasterResolutionOptions } from "../../constants/rasterParameterConstants"; import { useState } from "react"; import { OutputGranuleExtentFlagOptions, OutputSamplingGridType, RasterResolution, Adjust, FilterParameters, FilterAction } from "../../types/historyPageTypes"; @@ -9,6 +9,62 @@ import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import { Formik } from "formik"; +export const getFilterParameters = (currentFilters: FilterParameters, limit?: number, after?: string): UserProductQueryVariables => { + const productVariablesObject: ProductQueryParameters = {} + const {cycle, pass, scene, outputGranuleExtentFlag, outputSamplingGridType, startDate, endDate} = currentFilters + + if(typeof limit !== 'undefined') productVariablesObject.limit = limit + if(typeof after !== 'undefined') productVariablesObject.after = after + if(cycle !== 'none') { + productVariablesObject.cycle = parseInt(cycle) + } + if (pass !== 'none') { + productVariablesObject.pass = parseInt(pass) + } + if (scene !== 'none') { + productVariablesObject.scene = parseInt(scene) + } + if (outputGranuleExtentFlag.length > 0) { + let outputGranuleExtentFlagToUse = null + if(outputGranuleExtentFlag.includes('128 x 128')) outputGranuleExtentFlagToUse = false + if(outputGranuleExtentFlag.includes('256 x 128')) outputGranuleExtentFlagToUse = true + if(outputGranuleExtentFlagToUse !== null) productVariablesObject.outputGranuleExtentFlag = outputGranuleExtentFlagToUse + } + if (outputSamplingGridType.length > 0) { + let outputSamplingGridTypeToUse = null + if(outputSamplingGridType.includes('UTM')) outputSamplingGridTypeToUse = 'UTM' + if(outputSamplingGridType.includes('LAT/LON')) outputSamplingGridTypeToUse = 'GEO' + if(outputSamplingGridTypeToUse !== null) productVariablesObject.outputSamplingGridType = outputSamplingGridTypeToUse as GridType + } + + // filter [status, rasterResolution, utmZoneAdjust, mgrsBandAdjust] after products gotten. + // TODO: Implement these into this function once the filtering endpoint supports it. + if(startDate !== 'none') { + productVariablesObject.afterTimestamp = startDate.toISOString().replace('Z','') + } + if(endDate !== 'none') { + productVariablesObject.beforeTimestamp = endDate.toISOString().replace('Z','') + } + return productVariablesObject as UserProductQueryVariables +} + +export const productPassesFilterCheck = (currentFilters: FilterParameters, status: string, rasterResolution: number, utmZoneAdjust?: number, mgrsBandAdjust?: number): boolean => { + let productPassesFilter = true + + if (currentFilters.status.length > 0 && !currentFilters.status.includes(status as ProductState)) { + return false + } + if (currentFilters.rasterResolution.length > 0 && !currentFilters.rasterResolution.includes(String(rasterResolution) as RasterResolution)) { + return false + } + if (utmZoneAdjust !== undefined && currentFilters.utmZoneAdjust.length > 0 && !currentFilters.utmZoneAdjust.includes(String(utmZoneAdjust) as Adjust)) { + return false + } + if (mgrsBandAdjust !== undefined && currentFilters.mgrsBandAdjust.length > 0 && !currentFilters.mgrsBandAdjust.includes(String(mgrsBandAdjust) as Adjust)) { + return false + } + return productPassesFilter +} const HistoryFilters = () => { const dispatch = useAppDispatch() @@ -16,11 +72,12 @@ const HistoryFilters = () => { const [endDateToUse, setEndDateToUse] = useState(defaultSpatialSearchEndDate) const [startDateToUse, setStartDateToUse] = useState(defaultSpatialSearchStartDate) const waitingForMyDataFiltering = useAppSelector((state) => state.product.waitingForMyDataFiltering) + const waitingForMyDataFilteringReset = useAppSelector((state) => state.product.waitingForMyDataFilteringReset) const [cycleIsValid, setCycleIsValid] = useState(true) const [passIsValid, setPassIsValid] = useState(true) const [sceneIsValid, setSceneIsValid] = useState(true) - const handleChangeFilters = (filter: FilterAction, value: string, valueValidity?: boolean) => { + const handleChangeFilters = (filter: FilterAction, value: string) => { const currentFiltersToModify: FilterParameters = structuredClone(currentFilters) switch(filter) { case 'cycle': @@ -119,6 +176,20 @@ const HistoryFilters = () => { setStartDateToUse(new Date(value)) } break; + case 'reset': + + currentFiltersToModify['cycle'] = 'none' + currentFiltersToModify['pass'] = 'none' + currentFiltersToModify['scene'] = 'none' + currentFiltersToModify['status'] = [] + currentFiltersToModify['outputGranuleExtentFlag'] = [] + currentFiltersToModify['outputSamplingGridType'] = [] + currentFiltersToModify['rasterResolution'] = [] + currentFiltersToModify['utmZoneAdjust'] = [] + currentFiltersToModify['mgrsBandAdjust'] = [] + currentFiltersToModify['endDate'] = 'none' + currentFiltersToModify['startDate'] = 'none' + break; default: } setCurrentFilters(currentFiltersToModify) @@ -129,6 +200,12 @@ const HistoryFilters = () => { dispatch(setWaitingForMyDataFiltering(true)) } + const handleResetFilters = () => { + dispatch(setCurrentFilter(defaultFilterParameters)) + handleChangeFilters('reset', 'random value') + dispatch(setWaitingForMyDataFilteringReset(true)) + } + const statusOptions = ['NEW', 'UNAVAILABLE', 'GENERATING', 'ERROR', 'READY', 'AVAILABLE'] const outputGranuleExtentFlagOptions = ['128 x 128', '256 x 128'] const outputSamplingGridTypeOptions = parameterOptionValues.outputSamplingGridType.values.map(value => { @@ -149,10 +226,10 @@ const HistoryFilters = () => { Cycle - + {}} initialValues={{}}>
- handleChangeFilters('cycle', String(e.target.value), !cycleIsInvalid)}/> + handleChangeFilters('cycle', String(e.target.value))}/>
{`Valid Values: ${inputBounds.cycle.min} - ${inputBounds.cycle.max}`}
@@ -166,7 +243,7 @@ const HistoryFilters = () => {
- handleChangeFilters('pass', String(e.target.value), !passIsInvalid)}/> + handleChangeFilters('pass', String(e.target.value))}/>
{`Valid Values: ${inputBounds.pass.min} - ${inputBounds.pass.max}`}
@@ -179,7 +256,7 @@ const HistoryFilters = () => {
- handleChangeFilters('scene', String(e.target.value), !sceneIsInvalid)}/> + handleChangeFilters('scene', String(e.target.value))}/>
{`Valid Values: ${inputBounds.scene.min} - ${inputBounds.scene.max}`}
@@ -319,11 +396,16 @@ const HistoryFilters = () => {
- + {!cycleIsValid || !passIsValid || !sceneIsValid ?
{applyFilterErrorMessage}
: null} ) diff --git a/src/components/map/WorldMap.tsx b/src/components/map/WorldMap.tsx index 55544ec..afe3618 100644 --- a/src/components/map/WorldMap.tsx +++ b/src/components/map/WorldMap.tsx @@ -14,6 +14,7 @@ import { addSpatialSearchResults, setMapFocus, setWaitingForSpatialSearch } from import { SpatialSearchResult } from '../../types/constantTypes'; import { useLocation, useSearchParams } from 'react-router-dom'; import { useEffect } from 'react'; +import { getGranules, getSpatialSearchGranuleVariables } from '../../constants/graphqlQueries'; let DefaultIcon = L.icon({ iconUrl: icon, @@ -105,34 +106,57 @@ const WorldMap = () => { } // create string with polygon array - let polygonString = '&polygon[]=' + // let polygonString = '&polygon[]=' + let polygonString = '' polygonCoordinates.forEach((lngLatPair, index) => { polygonString += `${index === 0 ? '' : ',' }${lngLatPair[0]},${lngLatPair[1]}` }) return polygonString }).join() - const spatialSearchUrl = `https://cmr.earthdata.nasa.gov/search/granules?collection_concept_id=${spatialSearchCollectionConceptId}${polygonUrlString}&page_size=${spatialSearchResultLimit}` - const spatialSearchResponse = await fetch(spatialSearchUrl, { - method: 'GET', - credentials: 'omit', + + const spatialSearchResponse = await fetch('https://graphql.earthdata.nasa.gov/api', { + method: 'POST', headers: { - Authorization: `Bearer ${authToken}` - } + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ query: getGranules, variables: getSpatialSearchGranuleVariables(polygonUrlString, spatialSearchCollectionConceptId, spatialSearchResultLimit) }) }).then(async data => { - const responseText = await data.text() + const responseJson = await data.json() // TODO: make subsequent calls to get granules in spatial search area till everything is found. - // current issue is that 1000 (2000 total divided by 2) is limited by the cmr api. - const parser = new DOMParser(); - const xml = parser.parseFromString(responseText, "application/xml"); - const references: SpatialSearchResult[] = Array.from(new Set(Array.from(xml.getElementsByTagName("name")).map(nameElement => { - return (nameElement.textContent)?.match(`${beforeCPS}([0-9]+(_[0-9]+)+)(${afterCPSR}|${afterCPSL})`)?.[1] - }))).map(foundIdString => { - const cyclePassSceneStringArray = foundIdString?.split('_').map(id => parseInt(id).toString()) + + const updatedGranules = responseJson.data.granules.items.map((item: any) => { + const itemCopy = structuredClone(item) + const cpsString = item.granuleUr.match(`${beforeCPS}([0-9]+(_[0-9]+)+)(${afterCPSR}|${afterCPSL})`)?.[1] + itemCopy.cpsString = cpsString + return itemCopy + }) + const cpsStringTracker: string[] = [] + const updatedGranulesToUse = updatedGranules.filter((updatedGranuleObject: any) => { + // if cpsString not in tracker, it has not been repeated yet. Add to tracker and return + const granuleRepeated = cpsStringTracker.includes(updatedGranuleObject.cpsString) + if(!granuleRepeated) cpsStringTracker.push(updatedGranuleObject.cpsString) + return !granuleRepeated + // if cpsString in tracker, it has been repeated. Do not return + }) + const spatialSearchResults = updatedGranulesToUse.map((updatedGranuleObject: any) => { + const {producerGranuleId, granuleUr, cpsString, polygons, timeStart, timeEnd} = updatedGranuleObject + const cyclePassSceneStringArray = cpsString.split('_').map((id: string) => parseInt(id).toString()) const tileValue = parseInt(cyclePassSceneStringArray?.[2] as string) const sceneToUse = String(Math.floor(tileValue)) - return {cycle: cyclePassSceneStringArray?.[0], pass: cyclePassSceneStringArray?.[1], scene : sceneToUse} as SpatialSearchResult + const returnObject: SpatialSearchResult = { + cycle: cyclePassSceneStringArray?.[0], + pass: cyclePassSceneStringArray?.[1], + scene : sceneToUse, + producerGranuleId, + granuleUr, + timeStart, + timeEnd, + polygons + } + return returnObject }) - return references + return spatialSearchResults }) dispatch(addSpatialSearchResults(spatialSearchResponse as SpatialSearchResult[])) } catch (err) { @@ -206,7 +230,13 @@ const WorldMap = () => { {addedProducts.map((productObject, index) => ( - {[
{`Cycle: ${productObject.cycle}`}
,
{`Pass: ${productObject.pass}`}
,
{`Scene: ${productObject.scene}`}
,
{`File Name: ${productObject.fileName}`}
]}
+ {[ +
{`Cycle: ${productObject.cycle}`}
, +
{`Pass: ${productObject.pass}`}
, +
{`Scene: ${productObject.scene}`}
, +
{`UTM Zone: ${productObject.utmZone}`}
, +
{`File Name: ${productObject.producerGranuleId}`}
+ ]}
))} diff --git a/src/components/sidebar/GranulesTable.tsx b/src/components/sidebar/GranulesTable.tsx index cdf13bf..fc52973 100644 --- a/src/components/sidebar/GranulesTable.tsx +++ b/src/components/sidebar/GranulesTable.tsx @@ -1,14 +1,13 @@ import { ReactElement, useEffect, useState } from 'react'; import Table from 'react-bootstrap/Table'; import { useAppSelector, useAppDispatch } from '../../redux/hooks' -import { granuleAlertMessageConstant, granuleSelectionLabels, productCustomizationLabelsUTM, productCustomizationLabelsGEO, parameterOptionValues, parameterHelp, infoIconsToRender, inputBounds, sampleFootprint, granuleTableLimit, +import { granuleAlertMessageConstant, granuleSelectionLabels, productCustomizationLabelsUTM, productCustomizationLabelsGEO, parameterOptionValues, parameterHelp, infoIconsToRender, inputBounds, granuleTableLimit, beforeCPS, afterCPSL, - afterCPSR, - spatialSearchCollectionConceptId} from '../../constants/rasterParameterConstants'; -import { Button, Col, Form, OverlayTrigger, Row, Tooltip, Spinner } from 'react-bootstrap'; + afterCPSR} from '../../constants/rasterParameterConstants'; +import { Button, Col, Form, OverlayTrigger, Row, Tooltip, Spinner, Alert } from 'react-bootstrap'; import { InfoCircle, Plus, Trash } from 'react-bootstrap-icons'; -import { AdjustType, AdjustValueDecoder, GranuleForTable, GranuleTableProps, InputType, SaveType, SpatialSearchResult, TableTypes, alertMessageInput, allProductParameters, handleSaveResult, validScene } from '../../types/constantTypes'; +import { AdjustType, AdjustValueDecoder, GranuleForTable, GranuleTableProps, InputType, SaveType, SpatialSearchResult, TableTypes, alertMessageInput, allProductParameters, cpsParams, granuleMetadata, handleSaveResult, validScene } from '../../types/constantTypes'; import { addProduct, setSelectedGranules, setGranuleFocus, addGranuleTableAlerts, editProduct, addSpatialSearchResults, clearGranuleTableAlerts, setWaitingForSpatialSearch } from './actions/productSlice'; import { setShowDeleteProductModalTrue } from './actions/modalSlice'; import DeleteGranulesModal from './DeleteGranulesModal'; @@ -55,60 +54,80 @@ const GranuleTable = (props: GranuleTableProps) => { // if any cycle scene and pass parameters in url, add them to table const cyclePassSceneParameters = searchParams.get('cyclePassScene') if (cyclePassSceneParameters) { + setWaitingForScenesToBeAdded(true) const sceneParamArray = Array.from(new Set(cyclePassSceneParameters.split('-'))) sceneParamArray.forEach((sceneParams, index) => { const splitSceneParams = sceneParams.split('_') - handleSave('urlParameter', sceneParamArray.length, index, splitSceneParams[0], splitSceneParams[1], splitSceneParams[2]) + const cpsParams: cpsParams = { + cycleParam: splitSceneParams[0], + passParam: splitSceneParams[1], + sceneParam: splitSceneParams[2] + } + handleSave('urlParameter', sceneParamArray.length, index, [cpsParams]) }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [tableType === 'granuleSelection' ? null : addedProducts, startTutorial ? searchParams : null]) - const validateCPS = async (cycleToUse: number, passToUse: number, sceneToUse: number[]) => { - const session = await Session.getCurrent(); - if (session === null) { - throw new Error('No current session'); - } - const authToken = await session.getAccessToken(); - if (authToken === null) { - throw new Error('Failed to get authentication token'); + const getValidationObject = (cycleToUse: number, passToUse: number, sceneToUse: number[], granuleJsonData: SpatialSearchResult[]) => { + const validationObject = {} as validScene + if(granuleJsonData.length !== 0) { + let responseTiles: string[] = [] + let granuleMetadata: granuleMetadata = {} + granuleJsonData.forEach((item: SpatialSearchResult) => { + const {granuleUr, polygons, producerGranuleId, timeEnd, timeStart} = item + const responseTileString = granuleUr.match(`${beforeCPS}([0-9]+(_[0-9]+)+)(${afterCPSR}|${afterCPSL})`)?.[1].split('_').map(item2 => parseInt(item2)).join('_') as string + responseTiles.push(responseTileString) + const polygonToUse = polygons ? getGranuleFootprint(polygons[0]) : [] + const timeStartToUse = new Date(timeStart) + const timeEndToUse = new Date(timeEnd) + granuleMetadata[responseTileString] = {polygons: polygonToUse, producerGranuleId, timeStart: timeStartToUse, timeEnd: timeEndToUse} + }) + + // go through each cycle pass scene combo and see if it is in the return results TODO + sceneToUse.forEach(sceneInput => { + const sceneInputId = `${cycleToUse}_${passToUse}_${sceneInput}` + const validityBool = responseTiles.includes(sceneInputId) + validationObject[sceneInputId] = validityBool ? {'valid': validityBool, polygons: granuleMetadata[sceneInputId].polygons, timeEnd: granuleMetadata[sceneInputId].timeEnd, timeStart: granuleMetadata[sceneInputId].timeStart, producerGranuleId: granuleMetadata[sceneInputId].producerGranuleId} : {valid: false} + }) } - const validationObjectToReturn = await fetch('https://graphql.earthdata.nasa.gov/api', { - method: 'POST', - headers: { - Authorization: `Bearer ${authToken}`, - "Content-Type": "application/json" - }, - body: JSON.stringify({ query: getGranules, variables: getGranuleVariables(cycleToUse, passToUse, sceneToUse)}) - }) - .then(async data => { - const responseJson = await data.json() - let responseTiles: string[] = [] - if(responseJson.data) { - responseTiles = (responseJson.data.tiles.items.map((item: {granuleUr: string}) => item.granuleUr.match(`${beforeCPS}([0-9]+(_[0-9]+)+)(${afterCPSR}|${afterCPSL})`)?.[1].split('_').map(item2 => parseInt(item2)).join('_')) as string[]) - } - const validationObject = {} as validScene + return validationObject + } - // go through each cycle pass scene combo and see if it is in the return results TODO - sceneToUse.forEach(sceneInput => { - const sceneInputId = `${cycleToUse}_${passToUse}_${sceneInput}` - const validityBool = responseTiles.includes(sceneInputId) - validationObject[sceneInputId] = validityBool - }) + const validateCPS = async (cycleToUse: number, passToUse: number, sceneToUse: number[], saveType: SaveType) => { + let validationObjectToReturn = {} + if(saveType === 'spatialSearch') { + // use spatial search data from redux + return getValidationObject(cycleToUse, passToUse, sceneToUse, spatialSearchResults) + } else { + // make calls to get the data + const session = await Session.getCurrent(); + if (session === null) { + throw new Error('No current session'); + } + const authToken = await session.getAccessToken(); + if (authToken === null) { + throw new Error('Failed to get authentication token'); + } + validationObjectToReturn = await fetch('https://graphql.earthdata.nasa.gov/api', { + method: 'POST', + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ query: getGranules, variables: getGranuleVariables(cycleToUse, passToUse, sceneToUse)}) + }).then(async data => { + const responseJson = await data.json() + return getValidationObject(cycleToUse, passToUse, sceneToUse, responseJson.data.granules.items) + }) + } - return validationObject - }) return validationObjectToReturn } const validateSceneAvailability = async (cycleToUse: number, passToUse: number, sceneToUse: number[], saveType: SaveType): Promise => { try { - if(saveType !== 'spatialSearch') { - return validateCPS(cycleToUse, passToUse, sceneToUse) - } else { - // add all scenes to be valid because spatial search granules don't need to be validated (already in cmr) - return Object.fromEntries(sceneToUse.map(sceneValue => [`${cycleToUse}_${passToUse}_${sceneValue}`, true])) - } + return validateCPS(cycleToUse, passToUse, sceneToUse, saveType) } catch(err) { console.log(err) return {} @@ -127,11 +146,18 @@ const GranuleTable = (props: GranuleTableProps) => { // don't let more than 10 be added scenesFoundArray.push('hit granule limit') } else { - await handleSave('spatialSearch', spatialSearchResults.length, i, spatialSearchResults[i].cycle, spatialSearchResults[i].pass, spatialSearchResults[i].scene).then(result => { - if(result.savedScenes) { - addedScenes.push(...(result.savedScenes).map(productObject => productObject.granuleId)) - } - scenesFoundArray.push(result.result) + const cpsParams: cpsParams = { + cycleParam: spatialSearchResults[i].cycle, + passParam: spatialSearchResults[i].pass, + sceneParam: spatialSearchResults[i].scene + } + await handleSave('spatialSearch', spatialSearchResults.length, i, [cpsParams]).then(results => { + results.forEach(result => { + if(result.savedScenes) { + addedScenes.push(...(result.savedScenes).map(productObject => productObject.granuleId)) + } + scenesFoundArray.push(result.result) + }) }) } } @@ -218,7 +244,6 @@ const GranuleTable = (props: GranuleTableProps) => { const [scene, setScene] = useState(''); const allAddedGranules = addedProducts.map(parameterObject => parameterObject.granuleId) const [waitingForScenesToBeAdded, setWaitingForScenesToBeAdded] = useState(false) - const [waitingForFootprintSearch, setWaitingForFootprintSearch] = useState(false) const getScenesArray = (sceneString: string): string[] => { const scenesArray = [] @@ -294,208 +319,203 @@ const GranuleTable = (props: GranuleTableProps) => { return existingValue } - const getSceneFootprint = async (collectionId: string, granuleId: string) => { - try { - // get session token to use in spatial search query - const session = await Session.getCurrent(); - if (session === null) { - throw new Error('No current session'); - } - const authToken = await session.getAccessToken(); - if (authToken === null) { - throw new Error('Failed to get authentication token'); + const getGranuleFootprint = (polygons: string): LatLngExpression[] => { + const footprintCoordinatesSingleArray = (polygons[0]).split(' ').map((coordinateString: string) => parseFloat(coordinateString)) + let footprintLatLongArray: LatLngExpression[] = [] + for(let i=0; i response.json()).then(data => { - if (data.feed.entry.length > 0) { - const timeStart = new Date(data.feed.entry[0].time_start) - const timeEnd = new Date(data.feed.entry[0].time_end) - const spatialSearchStartDateToUse = new Date(spatialSearchStartDate) - const spatialSearchEndDateToUse = new Date(spatialSearchEndDate) - const granuleInTimeRange: boolean = timeStart > spatialSearchStartDateToUse && timeStart < spatialSearchEndDateToUse && timeEnd > spatialSearchStartDateToUse && timeEnd < spatialSearchEndDateToUse - const footprintCoordinatesSingleArray = (data.feed.entry[0].polygons[0][0]).split(' ').map((coordinateString: string) => parseFloat(coordinateString)) - const granuleFilename = data.feed.entry[0].producer_granule_id - let footprintLatLongArray: LatLngExpression[] = [] - for(let i=0; i { + const spatialSearchStartDateToUse = new Date(spatialSearchStartDate) + const spatialSearchEndDateToUse = new Date(spatialSearchEndDate) + const granuleInTimeRange: boolean = timeStart > spatialSearchStartDateToUse && timeStart < spatialSearchEndDateToUse && timeEnd > spatialSearchStartDateToUse && timeEnd < spatialSearchEndDateToUse + return granuleInTimeRange } - const handleSave = async (saveType: SaveType, totalRuns: number, index: number, cycleParam?: string, passParam?: string, sceneParam?: string): Promise => { + /** + * Handles the save products operation based on the provided parameters. + * + * @param {SaveType} saveType - The type of save operation to perform. + * @param {number} totalRuns - The total number of runs for the save operation. + * @param {number} index - The index of the current run. + * @param {cpsParams[]} [cpsParams] - Optional array of cpsParams (cycle, pass, and scene). + * @return {Promise} A promise that resolves to an array of handleSaveResult objects. + */ + const handleSave = async (saveType: SaveType, totalRuns: number, index: number, cpsParams?: cpsParams[]): Promise => { if (saveType === 'manual') dispatch(clearGranuleTableAlerts()) setWaitingForScenesToBeAdded(true) - // String(+(stringParam)) is used to remove the leading zeros - const cycleToUse = String(+(cycleParam ?? cycle)) - const passToUse = String(+(passParam ?? pass)) - const sceneToUse = (sceneParam ?? scene).split('-').map((sceneValueSplit: string) => String(+sceneValueSplit)).join('-') - // check if cycle pass and scene are all within a valid range - const validCycle = inputIsValid('cycle', cycleToUse) - const validPass = inputIsValid('pass', passToUse) - const validScene = inputIsValid('scene', sceneToUse) - - if (!validCycle || !validPass || !validScene) { - setWaitingForScenesToBeAdded(false) - if (!validCycle) setSaveGranulesAlert('invalidCycle') - if (!validPass) setSaveGranulesAlert('invalidPass') - if (!validScene) setSaveGranulesAlert('invalidScene') - return {result: 'first step'} - } - else { - const granulesToAdd: allProductParameters[] = [] - let someGranulesAlreadyAdded = false - let cyclePassSceneSearchParams = searchParams.get('cyclePassScene') ? String(searchParams.get('cyclePassScene')) : '' - const sceneArray = getScenesArray(sceneToUse) - let validScenesThatCouldNotBeAdded: string[] = [] - // check scenes availability - if(saveType !== 'spatialSearch') { - - } - const validationResult = await validateSceneAvailability(parseInt(cycleToUse), parseInt(passToUse), sceneArray.map(sceneValue => parseInt(sceneValue)), saveType).then(scenesAvailable => { - // return response - setWaitingForScenesToBeAdded(false) - const someScenesNotAvailable = Object.entries(scenesAvailable).some(sceneObjectValidityEntry => { - return !sceneObjectValidityEntry[1] - }) - - const allScenesNotAvailable = Object.entries(scenesAvailable).every(sceneObjectValidityEntry => { - return !sceneObjectValidityEntry[1] - }) - // TODO: make alert more verbose if some granules are added and others are not when adding more than one with scene hyphen - sceneArray.filter(sceneNumber => scenesAvailable[`${cycleToUse}_${passToUse}_${sceneNumber}`]).forEach(async sceneId => { - if ((granulesToAdd.length + addedProducts.length) >= granuleTableLimit) { - validScenesThatCouldNotBeAdded.push(sceneId) - setSaveGranulesAlert('granuleLimit') - } else { - // check if granule exists with that scene, cycle, and pass - const comboAlreadyAdded = alreadyAddedCyclePassScene(cycleToUse, passToUse, sceneId) - const cyclePassSceneInBounds = checkInBounds('cycle', cycleToUse) && checkInBounds('pass', passToUse) && checkInBounds('scene', sceneId) - if (cyclePassSceneInBounds && !comboAlreadyAdded) { - // get the granuleId from it and pass it to the parameters - const parameters: allProductParameters = { - granuleId: `${cycleToUse}_${passToUse}_${sceneId}`, - name: '', - cycle: cycleToUse, - pass: passToUse, - scene: sceneId, - outputGranuleExtentFlag: parameterOptionValues.outputGranuleExtentFlag.default as number, - outputSamplingGridType: parameterOptionValues.outputSamplingGridType.default as string, - rasterResolution: parameterOptionValues.rasterResolutionUTM.default as number, - utmZoneAdjust: parameterOptionValues.utmZoneAdjust.default as string, - mgrsBandAdjust: parameterOptionValues.mgrsBandAdjust.default as string, - footprint: sampleFootprint - } - // add cycle/pass/scene to url parameters - if (!searchParamSceneComboAlreadyInUrl(cyclePassSceneSearchParams, cycleToUse, passToUse, sceneId)) { - cyclePassSceneSearchParams += `${cyclePassSceneSearchParams.length === 0 ? '' : '-'}${cycleToUse}_${passToUse}_${sceneId}` - } - granulesToAdd.push(parameters) - } else if (comboAlreadyAdded) { - someGranulesAlreadyAdded = true - } - } - }) - if (saveType !== 'spatialSearch' && saveType !== 'urlParameter') { - // check if any granules could not be found or they were already added - if (someGranulesAlreadyAdded) { - setSaveGranulesAlert('alreadyAdded') + const cpsParamsIfUndefined = + [{ + cycleParam: cycle, + passParam: pass, + sceneParam: scene, + }] + const cpsParamsToUse = cpsParams ?? cpsParamsIfUndefined + + const getSaveResults = async () => { + const handleSaveResults: handleSaveResult[] = [] + for(let i=0; i String(+sceneValueSplit)).join('-') + + // check if cycle pass and scene are all within a valid range + const validCycle = inputIsValid('cycle', cycleToUse) + const validPass = inputIsValid('pass', passToUse) + const validScene = inputIsValid('scene', sceneToUse) + + if (!validCycle || !validPass || !validScene) { + setWaitingForScenesToBeAdded(false) + if (!validCycle) setSaveGranulesAlert('invalidCycle') + if (!validPass) setSaveGranulesAlert('invalidPass') + if (!validScene) setSaveGranulesAlert('invalidScene') + handleSaveResults.push({result: 'first step'}) } - if (allScenesNotAvailable) { - setSaveGranulesAlert('allScenesNotAvailable') - } - if (someScenesNotAvailable) { - setSaveGranulesAlert('someScenesNotAvailable') - // set granule alert to show which scenes are missing but also say that you were successful - } - } - return granulesToAdd - }).then(async granulesToAdd => { - if (granulesToAdd.length > 0) { - await Promise.all(granulesToAdd.map(async granule => { - const granuleIdForFootprint = `*${padCPSForCmrQuery(cycleToUse)}_${padCPSForCmrQuery(passToUse)}_${padCPSForCmrQuery(String(Math.floor(parseInt(granule.scene)*2)))}*` - //TODO: change back to spatialSearchCollectionConceptId - return Promise.resolve(await getSceneFootprint(spatialSearchCollectionConceptId as string, granuleIdForFootprint).then(retrievedFootprint => { - - const validFootprintResultArray = retrievedFootprint as (boolean | LatLngExpression[] | string)[] - const footprintResult = validFootprintResultArray[0] - const isInTimeRange = validFootprintResultArray[1] - const granuleFilename = validFootprintResultArray[2] - return {...granule, footprint: footprintResult, inTimeRange: isInTimeRange, fileName: granuleFilename} as allProductParameters - })) - })).then(async productsWithFootprints => { - // don't run time range check if granule was manually entered - if (saveType === 'manual' || saveType === 'urlParameter') { - addSearchParamToCurrentUrlState({'cyclePassScene': cyclePassSceneSearchParams}) - if (saveType !== 'urlParameter' || startTutorial) { - if (validScenesThatCouldNotBeAdded.length > 0) { - setSaveGranulesAlert('someSuccess') + else { + const granulesToAdd: allProductParameters[] = [] + let someGranulesAlreadyAdded = false + let cyclePassSceneSearchParams = searchParams.get('cyclePassScene') ? String(searchParams.get('cyclePassScene')) : '' + const sceneArray = getScenesArray(sceneToUse) + let validScenesThatCouldNotBeAdded: string[] = [] + // check scenes availability + if(saveType !== 'spatialSearch') { + + } + const validationResult = await validateSceneAvailability(parseInt(cycleToUse), parseInt(passToUse), sceneArray.map(sceneValue => parseInt(sceneValue)), saveType).then(sceneValidityResults => { + // return response + setWaitingForScenesToBeAdded(false) + + const sceneValidityList = Object.entries(sceneValidityResults).map(validityObject => validityObject[1].valid) + const someScenesNotAvailable = sceneValidityList.some(sceneObjectValidityEntry => { + return !sceneObjectValidityEntry + }) + + const allScenesNotAvailable = sceneValidityList.every(sceneObjectValidityEntry => { + return !sceneObjectValidityEntry + }) + + // TODO: make alert more verbose if some granules are added and others are not when adding more than one with scene hyphen + sceneArray.filter(sceneNumber => sceneValidityResults[`${cycleToUse}_${passToUse}_${sceneNumber}`].valid).forEach(async sceneId => { + if ((granulesToAdd.length + addedProducts.length) >= granuleTableLimit) { + validScenesThatCouldNotBeAdded.push(sceneId) + setSaveGranulesAlert('granuleLimit') } else { - setSaveGranulesAlert('success') - } - } - dispatch(addProduct(productsWithFootprints)) - } else { - const productsInTimeRange: allProductParameters[] = [] - const productsNotInTimeRange:allProductParameters[] = [] - productsWithFootprints.forEach(product => { - if (product.inTimeRange){ - delete product.inTimeRange - productsInTimeRange.push(product) - } else if (!product.inTimeRange) { - delete product.inTimeRange - productsNotInTimeRange.push(product) + // check if granule exists with that scene, cycle, and pass + const comboAlreadyAdded = alreadyAddedCyclePassScene(cycleToUse, passToUse, sceneId) + const cyclePassSceneInBounds = checkInBounds('cycle', cycleToUse) && checkInBounds('pass', passToUse) && checkInBounds('scene', sceneId) + if (cyclePassSceneInBounds && !comboAlreadyAdded) { + const granuleId = `${cycleToUse}_${passToUse}_${sceneId}` + const footprint = sceneValidityResults[granuleId].polygons as LatLngExpression[] + const timeStart = sceneValidityResults[granuleId].timeStart as Date + const timeEnd = sceneValidityResults[granuleId].timeEnd as Date + const producerGranuleId = sceneValidityResults[granuleId].producerGranuleId as string + const utmZone = producerGranuleId.substring(producerGranuleId.indexOf('_UTM') + 4, producerGranuleId.indexOf('_N') - 1) + + // get the granuleId from it and pass it to the parameters + const parameters: allProductParameters = { + granuleId, + name: '', + cycle: cycleToUse, + pass: passToUse, + scene: sceneId, + outputGranuleExtentFlag: parameterOptionValues.outputGranuleExtentFlag.default as number, + outputSamplingGridType: parameterOptionValues.outputSamplingGridType.default as string, + rasterResolution: parameterOptionValues.rasterResolutionUTM.default as number, + utmZoneAdjust: parameterOptionValues.utmZoneAdjust.default as string, + mgrsBandAdjust: parameterOptionValues.mgrsBandAdjust.default as string, + timeStart, + timeEnd, + producerGranuleId, + footprint, + utmZone + } + // add cycle/pass/scene to url parameters + if (!searchParamSceneComboAlreadyInUrl(cyclePassSceneSearchParams, cycleToUse, passToUse, sceneId)) { + cyclePassSceneSearchParams += `${cyclePassSceneSearchParams.length === 0 ? '' : '-'}${cycleToUse}_${passToUse}_${sceneId}` + } + granulesToAdd.push(parameters) + } else if (comboAlreadyAdded) { + someGranulesAlreadyAdded = true + } } }) - if (productsInTimeRange.length > 0) { - setSaveGranulesAlert('success') - dispatch(addProduct(productsInTimeRange)) + if (saveType !== 'spatialSearch' && saveType !== 'urlParameter') { + // check if any granules could not be found or they were already added + if (someGranulesAlreadyAdded) { + setSaveGranulesAlert('alreadyAdded') + } + if (allScenesNotAvailable) { + setSaveGranulesAlert('allScenesNotAvailable') + } + if (someScenesNotAvailable) { + setSaveGranulesAlert('someScenesNotAvailable') + // set granule alert to show which scenes are missing but also say that you were successful + } } - if (productsNotInTimeRange.length > 0) { - // set alerts for not in range - setSaveGranulesAlert('notInTimeRange') + return granulesToAdd + }).then(async granulesToAdd => { + if (granulesToAdd.length > 0) { + // don't run time range check if granule was manually entered + if (saveType === 'manual' || saveType === 'urlParameter') { + addSearchParamToCurrentUrlState({'cyclePassScene': cyclePassSceneSearchParams}) + if (saveType !== 'urlParameter' || startTutorial) { + if (validScenesThatCouldNotBeAdded.length > 0) { + setSaveGranulesAlert('someSuccess') + } else { + setSaveGranulesAlert('success') + } + } + dispatch(addProduct(granulesToAdd)) + } else { + const productsInTimeRange: allProductParameters[] = [] + const productsNotInTimeRange:allProductParameters[] = [] + granulesToAdd.forEach(product => { + const granuleInTimeRangeResult = granuleInTimeRange(product.timeStart, product.timeEnd) + if (granuleInTimeRangeResult){ + delete product.inTimeRange + productsInTimeRange.push(product) + } else if (!granuleInTimeRangeResult) { + delete product.inTimeRange + productsNotInTimeRange.push(product) + } + }) + if (productsInTimeRange.length > 0) { + setSaveGranulesAlert('success') + dispatch(addProduct(productsInTimeRange)) + } + if (productsNotInTimeRange.length > 0) { + // set alerts for not in range + setSaveGranulesAlert('notInTimeRange') + } + } + return {result: 'found something', savedScenes: granulesToAdd} + } else { + if (index+1 === totalRuns){ + return {result: 'noScenesFound'} + } else { + return {result: 'not applicable'} + } } - } - }) - return {result: 'found something', savedScenes: granulesToAdd} - } else { - if (index+1 === totalRuns){ - return {result: 'noScenesFound'} - } else { - return {result: 'not applicable'} + }) + handleSaveResults.push(validationResult) } - } - }) - return validationResult + + } + return handleSaveResults } + + return getSaveResults() + } const handleAllChecked = () => { @@ -740,7 +760,7 @@ const GranuleTable = (props: GranuleTableProps) => { setScene(event.target.value)}/> - Valid Values: + Valid Values: {renderInfoIcon('validCPSValues')} {`${inputBounds.cycle.min} - ${inputBounds.cycle.max}`} {`${inputBounds.pass.min} - ${inputBounds.pass.max}`} {`${inputBounds.scene.min} - ${inputBounds.scene.max}`} @@ -755,9 +775,12 @@ const GranuleTable = (props: GranuleTableProps) => { {tableType === 'granuleSelection' ? ( <> To add multiple scenes at once, enter two numbers into the scene input field separated by a hyphen (e.g. 1-10) + + Only scientific orbit cycle, pass, scene values are supported at this time. + - {waitingForScenesToBeAdded || waitingForSpatialSearch || waitingForFootprintSearch ? + {waitingForScenesToBeAdded || waitingForSpatialSearch ? Loading... : diff --git a/src/components/sidebar/ProductCustomization.tsx b/src/components/sidebar/ProductCustomization.tsx index 9b7ad43..1bfa89d 100644 --- a/src/components/sidebar/ProductCustomization.tsx +++ b/src/components/sidebar/ProductCustomization.tsx @@ -1,13 +1,15 @@ import { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '../../redux/hooks' import Form from 'react-bootstrap/Form'; -import { Col, OverlayTrigger, Row, Tooltip } from 'react-bootstrap'; +import { Alert, Col, OverlayTrigger, Row, Tooltip } from 'react-bootstrap'; import { parameterHelp, parameterOptionValues } from '../../constants/rasterParameterConstants' import { InfoCircle } from 'react-bootstrap-icons'; import { setGenerateProductParameters, setShowUTMAdvancedOptions } from "./actions/productSlice"; import { useSearchParams } from 'react-router-dom'; import { GenerateProductParameters } from '../../types/constantTypes'; +// TODO: revert back after ONLY RESOLUTION SELECTION period is over. Search todos related to disabled and featureNotAvailable + const ProductCustomization = () => { const colorModeClass = useAppSelector((state) => state.navbar.colorModeClass) const generateProductParameters = useAppSelector((state) => state.product.generateProductParameters) @@ -149,20 +151,40 @@ const ProductCustomization = () => { ) + const renderFeatureNotAvailable = (jsxContents: JSX.Element, featureDisabled: boolean) => { + return featureDisabled ? + Option not currently available. + } + > + {{jsxContents}} + + : jsxContents + } + const renderOutputSamplingGridTypeInputs = (outputSamplingGridType: string) => { const inputArray = parameterOptionValues.outputSamplingGridType.values.map((value, index) => { + // TODO: Remove featureNotAvailable after ONLY RESOLUTION SELECTION period is over + const featureNotAvailable = value === 'lat/lon' const resolutionToUse: number = value === 'utm' ? rasterResolutionUTM : rasterResolutionGEO return ( - setOutputSamplingGridType(value as string, resolutionToUse)} - key={`outputSamplingGridTypeGroup-radio-key-${index}`} - /> + renderFeatureNotAvailable( + setOutputSamplingGridType(value as string, resolutionToUse)} + key={`outputSamplingGridTypeGroup-radio-key-${index}`} + />, + featureNotAvailable + ) )} ) inputArray.push( @@ -198,17 +220,24 @@ const ProductCustomization = () => { {parameterOptionValues.outputGranuleExtentFlag.values.map((value, index) => { + // TODO: Remove featureNotAvailable after ONLY RESOLUTION SELECTION period is over + const featureNotAvailable: boolean = Boolean(value) return ( + renderFeatureNotAvailable( setOutputGranuleExtentFlag(value)} key={`outputGranuleExtentFlagTypeGroup-radio-key-${index}`} - /> + />, + featureNotAvailable + ) ) })} @@ -236,6 +265,13 @@ const ProductCustomization = () => { {renderRasterResolutionUnits(outputSamplingGridType)} + + + + Some options are not enabled in the current version of SWODLR and will be enabled at a later date + + + ); diff --git a/src/components/sidebar/actions/productSlice.ts b/src/components/sidebar/actions/productSlice.ts index 068b43a..84bc802 100644 --- a/src/components/sidebar/actions/productSlice.ts +++ b/src/components/sidebar/actions/productSlice.ts @@ -30,6 +30,7 @@ interface GranuleState { currentFilters: FilterParameters, granulesToReGenerate: Product[], waitingForMyDataFiltering: boolean, + waitingForMyDataFilteringReset: boolean, waitingForProductsToLoad: boolean } @@ -60,6 +61,7 @@ const initialState: GranuleState = { currentFilters: defaultFilterParameters, granulesToReGenerate: [], waitingForMyDataFiltering: false, + waitingForMyDataFilteringReset: false, waitingForProductsToLoad: false } @@ -181,6 +183,9 @@ export const productSlice = createSlice({ setWaitingForMyDataFiltering: (state, action: PayloadAction) => { state.waitingForMyDataFiltering = action.payload }, + setWaitingForMyDataFilteringReset: (state, action: PayloadAction) => { + state.waitingForMyDataFilteringReset = action.payload + }, setWaitingForProductsToLoad: (state, action: PayloadAction) => { state.waitingForProductsToLoad = action.payload } @@ -212,6 +217,7 @@ export const { addPageToHistoryPageState, setCurrentFilter, setWaitingForMyDataFiltering, + setWaitingForMyDataFilteringReset, setWaitingForProductsToLoad } = productSlice.actions diff --git a/src/constants/graphqlQueries.ts b/src/constants/graphqlQueries.ts index c60eb96..962c78d 100644 --- a/src/constants/graphqlQueries.ts +++ b/src/constants/graphqlQueries.ts @@ -28,10 +28,10 @@ export const generateL2RasterProductQuery = ` ` export const userProductsQuery = ` - query getUserProducts($limit: Int, $after: ID) + query getUserProducts($limit: Int, $after: ID, $cycle: Int, $pass: Int, $scene: Int, $outputGranuleExtentFlag: Boolean, $outputSamplingGridType: GridType, $beforeTimestamp: String, $afterTimestamp: String) { currentUser { - products (limit: $limit, after: $after) { + products (limit: $limit, after: $after, cycle: $cycle, pass: $pass, scene: $scene, outputGranuleExtentFlag: $outputGranuleExtentFlag, outputSamplingGridType: $outputSamplingGridType, beforeTimestamp: $beforeTimestamp, afterTimestamp: $afterTimestamp) { id timestamp cycle @@ -60,20 +60,40 @@ export const userProductsQuery = ` } ` +export const defaultUserProductsLimit = 1000000 + export const getGranules = ` - query($tileParams: GranulesInput) { - tiles: granules(params: $tileParams) { - items { - granuleUr - } +query($params: GranulesInput) { + granules(params: $params) { + items { + producerGranuleId + granuleUr + timeStart + timeEnd + polygons } } +} ` +// export const getSpatialSearchGranules = ` +// query GetSpatialSearchGranules($params: GranulesInput) { +// granules(params: $params) { +// items { +// producerGranuleId +// granuleUr +// timeStart +// timeEnd +// polygons +// } +// } +// } +// ` + export const getGranuleVariables = (cycle: number, pass: number, sceneIds: number[]) => { const sceneIdsForGranuleName = sceneIds.map(sceneId => `SWOT_L2_HR_Raster_*_${padCPSForCmrQuery(String(sceneId))}F_*`) const variables = { - "tileParams": { + "params": { 'collectionConceptIds': [spatialSearchCollectionConceptId], "limit": 100, "cycle": cycle, @@ -87,4 +107,34 @@ export const getGranuleVariables = (cycle: number, pass: number, sceneIds: numbe } } return variables +} + +export const getSpatialSearchGranuleVariables = (polygon: string, collectionConceptId: string, limit: number) => { + const variables = { + "params": { + polygon, + collectionConceptId, + limit + } + } + return variables +} + +export const getFootprintVariables = (cycle: number, pass: number, sceneIds: number[]) => { + const sceneIdsForGranuleName = sceneIds.map(sceneId => `SWOT_L2_HR_Raster_*_${padCPSForCmrQuery(String(sceneId))}F_*`) + const variables = { + "params": { + 'collectionConceptIds': [spatialSearchCollectionConceptId], + "limit": 100, + "cycle": cycle, + "passes": {"0": {"pass": pass}}, + "readableGranuleName": sceneIdsForGranuleName, + "options": { + "readableGranuleName": { + "pattern": true + } + }, + } + } + return variables } \ No newline at end of file diff --git a/src/constants/rasterParameterConstants.ts b/src/constants/rasterParameterConstants.ts index 408fe36..57a4418 100644 --- a/src/constants/rasterParameterConstants.ts +++ b/src/constants/rasterParameterConstants.ts @@ -1,4 +1,3 @@ -import { LatLngExpression } from "leaflet" import { ParameterHelp, ParameterOptions, granuleAlertMessageConstantType, inputValuesDictionary, parameterValuesDictionary } from "../types/constantTypes" import { FilterParameters } from "../types/historyPageTypes" @@ -108,7 +107,8 @@ export const parameterHelp: ParameterHelp = { pass: `Predefined sections of the orbit between the maximum and minimum latitudes. SWOT has 584 passes in one cycle, split into ascending and descending passes`, scene: `Predefined 128 x 128 km squares of the SWOT observations.`, status: `The processing status of your custom product. The status types are as follows: NEW, UNAVAILABLE, GENERATING, ERROR, READY, AVAILABLE`, - granuleTableLimit: `There is a limit of ${granuleTableLimit} scenes allowed to be added to the scene table at a time. This is to ensure our scene processing pipeline can handle the demand of all of SWODLR's users.` + granuleTableLimit: `There is a limit of ${granuleTableLimit} scenes allowed to be added to the scene table at a time. This is to ensure our scene processing pipeline can handle the demand of all of SWODLR's users.`, + validCPSValues: `There are two types of orbits, calibration and scientific. Calibration orbits have a 400-578 cycle range and science orbits have a 0-399 cycle range. Cycles in the calibration orbit range are not currently supported at this time but will be in the future.`, } export interface InputBounds { @@ -118,10 +118,12 @@ export interface InputBounds { } } +// cycle for calibration orbit is 400-578 +// TODO: change cycle max back to 578 when calibration orbit is implemented export const inputBounds: inputValuesDictionary = { cycle: { - min: 0, - max: 578 + min: 1, + max: 399 }, pass: { min: 1, @@ -212,29 +214,6 @@ export const granuleAlertMessageConstant: granuleAlertMessageConstantType = { mgrsBandAdjust: 'test', } - export const sampleFootprint: LatLngExpression[] = [ - [ - 33.62959926136482, - -119.59722240610449 - ], - [ - 33.93357164098772, - -119.01030070905898 - ], - [ - 33.445222247065175, - -118.6445806486702 - ], - [ - 33.137055033294544, - -119.23445170097719 - ], - [ - 33.629599562267856, - -119.59722227107866 - ] - ] - export const spatialSearchResultLimit = 2000 export const beforeCPS = '_x_x_x_' export const afterCPSR = 'F_' diff --git a/src/types/constantTypes.ts b/src/types/constantTypes.ts index 618dfc0..a44a674 100644 --- a/src/types/constantTypes.ts +++ b/src/types/constantTypes.ts @@ -25,8 +25,11 @@ export interface allProductParameters { utmZoneAdjust: string, mgrsBandAdjust: string, footprint: LatLngExpression[], + producerGranuleId: string, + timeEnd: Date, + timeStart: Date, + utmZone: string, inTimeRange?: boolean, - fileName?: string } export interface GranuleForTable { @@ -134,8 +137,27 @@ export interface newUrlParamsObject { [key: string]: string | number | boolean } +export interface validSceneInfo { + valid: boolean, + polygons?: LatLngExpression[], + timeEnd?: Date, + timeStart?: Date, + producerGranuleId?: string +} + export interface validScene { - [key: string]: boolean + [key: string]: validSceneInfo +} + +export interface granuleMetadataInfo { + polygons: LatLngExpression[], + timeEnd: Date, + timeStart: Date, + producerGranuleId: string +} + +export interface granuleMetadata { + [key: string]: granuleMetadataInfo } export type alertMessageInput = 'success' | 'alreadyAdded' | 'allScenesNotAvailable' | 'alreadyAddedAndNotFound' | 'noScenesAdded' | 'readyForGeneration' | 'invalidCycle' | 'invalidPass' | 'invalidScene' | 'invalidScene' | 'someScenesNotAvailable' | 'granuleLimit' | 'notInTimeRange' | 'noScenesFound' | 'someSuccess' | 'successfullyGenerated' | 'spatialSearchAreaTooLarge' | 'successfullyReGenerated' @@ -143,7 +165,12 @@ export type alertMessageInput = 'success' | 'alreadyAdded' | 'allScenesNotAvaila export interface SpatialSearchResult { cycle: string, pass: string, - scene: string + scene: string, + timeStart: string, + timeEnd: string, + producerGranuleId: string, + granuleUr: string, + polygons: string[] } export type footprintResponse = LatLngExpression[] | boolean @@ -162,4 +189,8 @@ export interface handleSaveResult { // key is the page number and the value is the product ID of the last element on a page export interface RetrievedDataHistory { [key: string]: string +} + +export interface cpsParams { + cycleParam: string, passParam: string, sceneParam: string } \ No newline at end of file diff --git a/src/types/graphqlTypes.ts b/src/types/graphqlTypes.ts index 5cc308a..27a4c3b 100644 --- a/src/types/graphqlTypes.ts +++ b/src/types/graphqlTypes.ts @@ -37,6 +37,24 @@ export interface Product { status: Status[] } +export type GridType = 'UTM' | 'GEO' + +export interface ProductQueryParameters { + cycle?: number, + pass?: number, + scene?: number, + outputGranuleExtentFlag?: Boolean, + outputSamplingGridType?: GridType, + rasterResolution?: number, + utmZoneAdjust?: number, + mgrsBandAdjust?: number, + beforeTimestamp?: string, + afterTimestamp?: string, + // pagination + after?: string, + limit?: number +} + export interface CurrentUser { id: string, email: string, @@ -72,7 +90,7 @@ export interface getUserProductsResponse { } export interface UserProductQueryVariables { - [key: string]: string + [key: string]: string | number | GridType } export interface cpsValidationResponse { diff --git a/src/types/historyPageTypes.ts b/src/types/historyPageTypes.ts index 0b51c4d..678e7b0 100644 --- a/src/types/historyPageTypes.ts +++ b/src/types/historyPageTypes.ts @@ -16,7 +16,6 @@ export interface FilterParameters { mgrsBandAdjust: Adjust[], startDate: Date | 'none', endDate: Date | 'none' - } -export type FilterAction = 'cycle' | 'scene' | 'pass' | 'status' | 'outputGranuleExtentFlag' | 'outputSamplingGridType' | 'rasterResolution' | 'utmZoneAdjust' | 'mgrsBandAdjust' | 'startDate' | 'endDate' \ No newline at end of file +export type FilterAction = 'cycle' | 'scene' | 'pass' | 'status' | 'outputGranuleExtentFlag' | 'outputSamplingGridType' | 'rasterResolution' | 'utmZoneAdjust' | 'mgrsBandAdjust' | 'startDate' | 'endDate' | 'reset' \ No newline at end of file