diff --git a/src/components/container/App.jsx b/src/components/container/App.jsx index c95d4330032526a2bd1d53ccb04806834afc80ff..7d1103edc7eb6c4a5b7ab6d433cdeae80307a784 100644 --- a/src/components/container/App.jsx +++ b/src/components/container/App.jsx @@ -35,10 +35,10 @@ export default function App() { let jsonPromise = {}; // Fetched Maps - var mapsJson = {}; + let mapsJson = {}; // Combined Data - var aggregateMapList = {}; + let aggregateMapList = {}; // Init fetchStatus[astroWebMaps] = "Not Started"; diff --git a/src/components/container/GeoStacApp.jsx b/src/components/container/GeoStacApp.jsx index ab83fee2bee2cf389a2c3b653d3ebb638f1290a3..0e0bcc8a168d4d63ca5ea8ab362aecbb3175922c 100644 --- a/src/components/container/GeoStacApp.jsx +++ b/src/components/container/GeoStacApp.jsx @@ -29,7 +29,7 @@ let css = { * @component */ export default function GeoStacApp(props) { - const [targetPlanet, setTargetPlanet] = React.useState("Mars"); + const [targetPlanet, setTargetPlanet] = React.useState(props.mapList.systems[4].bodies[0]); const [footprintData, setFootprintData] = React.useState([]); @@ -68,7 +68,7 @@ export default function GeoStacApp(props) { bodyChange={handleTargetBodyChange} /> <div id="map-area"> - <MapContainer target={targetPlanet} mapList={props.mapList} /> + <MapContainer target={targetPlanet.name} mapList={props.mapList}/> </div> <QueryConsole /> </div> diff --git a/src/components/presentational/ConsoleTargetInfo.jsx b/src/components/presentational/ConsoleTargetInfo.jsx index 1aaae9798384993aa61f8bec512bfd95c0bc032d..3e59ef4a7a72cb95a059aac2f057efaa281019dc 100644 --- a/src/components/presentational/ConsoleTargetInfo.jsx +++ b/src/components/presentational/ConsoleTargetInfo.jsx @@ -120,18 +120,15 @@ function PlanetDialog(props) { onClose(value); }; - console.log(props.mapList) - return ( <Dialog PaperProps={{sx: {overflowY: "scroll"}}} onClose={handleClose} open={open}> <DialogTitle sx={{ minWidth: 225 }}>Select Target Body</DialogTitle> <List sx={{ pt: 0 }}> <ListSubheader value="None">Systems</ListSubheader> {props.mapList.systems.map((system, sysIndex) => ( - <> + <React.Fragment key={system.name}> <ListItemButton onClick={() => handleSysOpen(sysIndex)} - key={system.name} > <ListItemAvatar> <Avatar sx={{ bgcolor: blue[100] }}> @@ -147,7 +144,7 @@ function PlanetDialog(props) { {props.mapList.systems[sysIndex].bodies.map((body, bodIndex) => ( <ListItemButton sx={{ pl: 4 }} - onClick={() => handleListItemClick(body.name)} + onClick={() => handleListItemClick(body)} key={body.name} > <ListItemAvatar> @@ -161,7 +158,7 @@ function PlanetDialog(props) { ))} </List> </Collapse> - </> + </React.Fragment> ))} </List> </Dialog> @@ -171,7 +168,7 @@ function PlanetDialog(props) { PlanetDialog.propTypes = { onClose: PropTypes.func.isRequired, open: PropTypes.bool.isRequired, - selectedValue: PropTypes.string.isRequired, + selectedValue: PropTypes.object.isRequired, }; /** @@ -187,7 +184,7 @@ PlanetDialog.propTypes = { */ export default function ConsoleTargetInfo(props) { const [open, setOpen] = React.useState(false); - const [selectedValue, setSelectedValue] = React.useState(planets[3][0]); + const [selectedValue, setSelectedValue] = React.useState(props.mapList.systems[4].bodies[0]); const handleClickOpen = () => { setOpen(true); @@ -215,7 +212,7 @@ export default function ConsoleTargetInfo(props) { variant="h4" onClick={handleClickOpen} > - {props.target.toUpperCase()} <ArrowDropDownIcon fontSize="large" /> + {props.target.name.toUpperCase()} <ArrowDropDownIcon fontSize="large" /> </Typography> </Grid> <PlanetDialog diff --git a/src/components/presentational/FootprintResults.jsx b/src/components/presentational/FootprintResults.jsx index 4c4cef3880cfcc26b56be50b566f5a9e9e650fc2..c60a86e23977e0d90a8a3d6e9fd357d2a796dac9 100644 --- a/src/components/presentational/FootprintResults.jsx +++ b/src/components/presentational/FootprintResults.jsx @@ -1,5 +1,6 @@ import React, { useEffect } from "react"; import Checkbox from "@mui/material/Checkbox"; +import {Card, CardContent, CardActions} from "@mui/material"; // result action links import Chip from "@mui/material/Chip"; @@ -10,6 +11,7 @@ import PreviewIcon from "@mui/icons-material/Preview"; import LaunchIcon from "@mui/icons-material/Launch"; import OpenInFullIcon from "@mui/icons-material/OpenInFull"; import CloseFullscreenIcon from "@mui/icons-material/CloseFullscreen"; +import TravelExploreIcon from '@mui/icons-material/TravelExplore'; // Footprints.] // object with results import { getFeatures } from "../../js/ApiJsonCollection"; @@ -17,6 +19,61 @@ import { getFeatures } from "../../js/ApiJsonCollection"; // geotiff thumbnail viewer import DisplayGeoTiff from "../presentational/DisplayGeoTiff.jsx"; import GeoTiffViewer from "../../js/geoTiffViewer.js"; +import { Skeleton } from "@mui/material"; + + +/** + * Skeleton to show when footprints are loading + */ +function LoadingFootprints() { + + return ( + <div className="resultsList"> + { Array(5).fill(null).map((_, i) => ( + <Card sx={{ width: 250, margin: 1}} key={i}> + <CardContent sx={{padding: 0.9, paddingBottom: 0}}> + <div className="resultContainer"> + <div className="resultImgDiv"> + <Skeleton variant="rectangular" width={32} height={32}/> + </div> + <div className="resultData"> + <Skeleton/> + <Skeleton/> + <Skeleton/> + <Skeleton/> + <Skeleton/> + </div> + </div> + </CardContent> + <CardActions> + <div className="resultLinks"> + <Stack direction="row" spacing={1} sx={{marginTop:1}}> + <Skeleton variant="rounded" width={100} height={20} sx={{borderRadius:5}}/> + <Skeleton variant="rounded" width={100} height={20} sx={{borderRadius:5}}/> + </Stack> + </div> + </CardActions> + </Card> + + ))} + </div> + ); +} + +function NoFootprints(){ + return( + <div style={{padding: 10, maxWidth: 268}}> + <p> + This target has no footprints. To see + footprints, go to the dropdown menu + in the upper left and pick a target + body with the <TravelExploreIcon sx={{fontSize: 16, verticalAlign: "middle"}}/> icon next to it. + </p> + </div> + ); +} + + /** * Controls css styling for this component using js to css @@ -42,10 +99,14 @@ let css = { * */ export default function FootprintResults(props) { + const [features, setFeatures] = React.useState([]); - const geoTiffViewer = new GeoTiffViewer("GeoTiffAsset"); + const [isLoading, setIsLoading] = React.useState(true); + const [hasFootprints, setHasFootprints] = React.useState(true); + // Metadata Popup + const geoTiffViewer = new GeoTiffViewer("GeoTiffAsset"); const showMetadata = (value) => () => { geoTiffViewer.displayGeoTiff(value.assets.thumbnail.href); geoTiffViewer.changeMetaData( @@ -58,10 +119,97 @@ export default function FootprintResults(props) { }; useEffect(() => { - setTimeout(() => { - setFeatures(getFeatures); - }, 1000); - }); + + // If target has collections (of footprints) + if (props.target.collections.length > 0) { + + // Set Loading + setIsLoading(true); + setHasFootprints(true); + + // Promise tracking + let fetchPromise = {}; + let jsonPromise = {}; + // Result + let jsonRes = {}; + + let itemCollectionUrls = []; + for(const collection of props.target.collections) { + // Get "items" link for each collection + let newItemCollectionUrl = + collection.links.find(obj => obj.rel === "items").href + + props.queryString; + itemCollectionUrls.push(newItemCollectionUrl); + } + + for(const itemCollectionUrl of itemCollectionUrls) { + fetchPromise[itemCollectionUrl] = "Not Started"; + jsonPromise[itemCollectionUrl] = "Not Started"; + jsonRes[itemCollectionUrl] = []; + } + + // Fetch JSON and read into object + async function startFetch(targetUrl) { + fetchPromise[targetUrl] = fetch( + targetUrl + ).then((res)=>{ + jsonPromise[targetUrl] = res.json().then((jsonData)=>{ + jsonRes[targetUrl] = jsonData; + }).catch((err)=>{ + console.log(err); + }); + }).catch((err) => { + console.log(err); + }); + } + + async function awaitFetch(targetUrl) { + await fetchPromise[targetUrl]; + await jsonPromise[targetUrl]; + } + + async function fetchAndWait() { + // Start fetching + for(const itemCollectionUrl of itemCollectionUrls) { + startFetch(itemCollectionUrl); + } + + // Wait for completion + for(const itemCollectionUrl of itemCollectionUrls) { + await awaitFetch(itemCollectionUrl); + } + + // Extract footprints into array + let resultsArr = []; + let myFeatures = []; + for(const itemCollectionUrl of itemCollectionUrls) { + myFeatures.push(jsonRes[itemCollectionUrl]); + } + for(const featCollection of myFeatures) { + resultsArr.push(...featCollection.features) + } + + return resultsArr; + } + + (async () => { + // Wait + let myFeatures = await fetchAndWait() + setFeatures(myFeatures); + setHasFootprints(myFeatures.length > 0); + setIsLoading(false); + })(); + + } else { + setIsLoading(false); + setHasFootprints(false); + } + + // setTimeout(() => { + // setFeatures(getFeatures); + // }, 1000); + + }, [props.target.name, props.queryString]); return ( <div style={css.root} className="scroll-parent"> @@ -81,49 +229,60 @@ export default function FootprintResults(props) { /> </span> </div> - <div className="resultsList"> - {features.map((feature) => ( - <div className="resultContainer" key={feature.id}> - <div className="resultImgDiv"> - <img className="resultImg" src={feature.assets.thumbnail.href} /> - </div> - <div className="resultData"> - <div className="resultSub"> - <strong>Collection:</strong> {feature.collection} - </div> - <div className="resultSub"> - <strong>ID:</strong> {feature.id} - </div> - <div className="resultSub"> - <strong>Date:</strong> {feature.properties.datetime} - </div> - </div> - <div className="resultLinks"> - <Stack direction="row" spacing={1}> - <Chip - label="Metadata" - icon={<PreviewIcon />} - size="small" - onClick={showMetadata(feature)} - variant="outlined" - clickable - /> - <Chip - label="STAC Browser" - icon={<LaunchIcon />} - size="small" - component="a" - href={`https://stac.astrogeology.usgs.gov/browser-dev/#/collections/${feature.collection}/items/${feature.id}`} - target="_blank" - //href="https://stac.astrogeology.usgs.gov/browser-dev/" - variant="outlined" - clickable - /> - </Stack> - </div> - </div> - ))} - </div> + {isLoading ? + <LoadingFootprints/> + : hasFootprints ? + <div className="resultsList"> + {features.map((feature) => ( + <Card sx={{ width: 250, margin: 1}} key={feature.id}> + <CardContent sx={{padding: 1.2, paddingBottom: 0}}> + <div className="resultContainer" > + <div className="resultImgDiv"> + <img className="resultImg" src={feature.assets.thumbnail.href} /> + </div> + <div className="resultData"> + <div className="resultSub"> + <strong>Collection:</strong> {feature.collection} + </div> + <div className="resultSub"> + <strong>ID:</strong> {feature.id} + </div> + <div className="resultSub"> + <strong>Date:</strong> {feature.properties.datetime} + </div> + </div> + </div> + </CardContent> + <CardActions> + <div className="resultLinks"> + <Stack direction="row" spacing={1}> + <Chip + label="Metadata" + icon={<PreviewIcon />} + size="small" + onClick={showMetadata(feature)} + variant="outlined" + clickable + /> + <Chip + label="STAC Browser" + icon={<LaunchIcon />} + size="small" + component="a" + href={`https://stac.astrogeology.usgs.gov/browser-dev/#/collections/${feature.collection}/items/${feature.id}`} + target="_blank" + variant="outlined" + clickable + /> + </Stack> + </div> + </CardActions> + </Card> + ))} + </div> + : + <NoFootprints/> + } </div> ); } diff --git a/src/components/presentational/SearchAndFilterInput.jsx b/src/components/presentational/SearchAndFilterInput.jsx index a4ba8128c9dc1ec585099519008c55a83a7b0149..3fae89c8a6395dbc9dd6ddda30958c9e55a598b9 100644 --- a/src/components/presentational/SearchAndFilterInput.jsx +++ b/src/components/presentational/SearchAndFilterInput.jsx @@ -95,32 +95,41 @@ let css = { * */ export default function SearchAndFilterInput(props) { + + // Allows showing/hiding of fields const keywordDetails = React.useRef(null); const dateDetails = React.useRef(null); - // React States - const [sortVal, setSortVal] = React.useState(""); - const [sortAscCheckVal, setSortAscCheckVal] = React.useState(false); - const [areaCheckVal, setAreaCheckVal] = React.useState(false); + // Sort By + const [sortVal, setSortVal] = React.useState(""); // Sort By What? + const [sortAscCheckVal, setSortAscCheckVal] = React.useState(false); // Sort Ascending or Descending + + // Filter by X checkboxes + const [areaCheckVal, setAreaCheckVal] = React.useState(false); // Area + const [keywordCheckVal, setKeywordCheckVal] = React.useState(false); // Keyword + const [dateCheckVal, setDateCheckVal] = React.useState(false); // Date + + // Filter by X values + const [areaTextVal, setAreaTextVal] = React.useState(""); // Area (received by window message from AstroDrawFilterControl) + const [keywordTextVal, setKeywordTextVal] = React.useState(""); // Keyword + const [dateFromVal, setDateFromVal] = React.useState(null); // From Date + const [dateToVal, setDateToVal] = React.useState(null); // To Date - const [keywordCheckVal, setKeywordCheckVal] = React.useState(false); - const [keywordTextVal, setKeywordTextVal] = React.useState(""); + // Page Number + const [pageNumber, setPageNumber] = React.useState(1); - const [dateCheckVal, setDateCheckVal] = React.useState(false); - const [dateFromVal, setDateFromVal] = React.useState(null); - const [dateToVal, setDateToVal] = React.useState(null); + // Pagination const [maxPages, setMaxPages] = React.useState(10); const [maxNumberFootprints, setMaxNumberFootprints] = React.useState(10); const [numberReturned, setNumberReturned] = React.useState(10); - const [limitVal, setLimitVal] = React.useState(10); - - const [applyChipVisStyle, setApplyChipVisStyle] = React.useState( - css.chipHidden - ); - const [gotoPage, setGotopage] = React.useState("Apply to go to page 2"); + const [limitVal, setLimitVal] = React.useState(10); // Max Number of footprints requested per collection + + // Apply/Alert Chip + const [applyChipVisStyle, setApplyChipVisStyle] = React.useState(css.chipHidden); + const [chipMessage, setChipMessage] = React.useState("Apply to Show Footprints on Map"); const setApplyChip = (value) => { - setGotopage(value); + setChipMessage("Apply to Show Footprints on Map"); setApplyChipVisStyle(css.chipShown); }; @@ -148,12 +157,56 @@ export default function SearchAndFilterInput(props) { setMaxPages(1); setMaxNumberFootprints(0); setNumberReturned(0); - setApplyChip("Apply to show Footprints"); + setApplyChip("Apply to Show Footprints on Map"); //// Uncomment to close details on clear // keywordDetails.current.open = false; // dateDetails.current.open = false; }; + const buildQueryString = () => { + let myQueryString = "?"; + + // Page Number + if (pageNumber != 1) myQueryString += "page=" + pageNumber + "&"; + + // Number of footprints requested per request + if (limitVal != 10) myQueryString += "limit=" + limitVal + "&" + + // Date + if (dateCheckVal) { + let d = new Date(); + let fromDate = "1970-01-01T00:00:00Z"; // From start of 1970 by default + let toDate = d.getFullYear() + "-12-31T23:59:59Z"; // To end of current year by default + + // From + if(dateFromVal instanceof Date && !isNaN(dateFromVal.valueOf())) { + fromDate = dateFromVal.toISOString(); + } + + // To + if(dateToVal instanceof Date && !isNaN(dateToVal.valueOf())) { + toDate = dateToVal.toISOString(); + } + + myQueryString += "datetime=" + fromDate + "/" + toDate + "&"; + } + + // Keyword + if(keywordCheckVal) myQueryString += "keywords=[" + keywordTextVal.split(" ") + "]&"; + + // Area + if(areaCheckVal && areaTextVal !== "") myQueryString += areaTextVal; + + // Sorting... Not supported by the API? + const sortAscDesc = sortAscCheckVal ? "asc" : "desc"; + if (sortVal === "date" || sortVal === "location") { + console.log("Warning: Sorting not Supported!"); + // myQueryString += 'sort=[{field:datetime,direction:' + sortAscDesc + '}]&' + } + + props.setQueryString(myQueryString); + } + // Sorting const handleSortChange = (event) => { setSortVal(event.target.value); @@ -211,9 +264,18 @@ export default function SearchAndFilterInput(props) { setApplyChip("Apply to show " + value + " footprints"); }; + // Pagination + const handlePageChange = (event, value) => { + setPageNumber(value); + setCurrentPage(value); + setApplyChip("Apply to go to page " + value); + }; + // resets pagination and limit when switching targets useEffect(() => { setTimeout(() => { + setCurrentPage(1); + setPageNumber(1); setMaxNumberFootprints(getNumberMatched); setNumberReturned(getNumberReturned); setLimitVal(10); @@ -221,13 +283,27 @@ export default function SearchAndFilterInput(props) { setMaxPages(getMaxNumberPages); props.footprintNavClick(); }, 2000); - }, [props.target]); + }, [props.target.name]); - // Pagination - const handlePageChange = (event, value) => { - setCurrentPage(value); - setApplyChip("Apply to go to page " + value); - }; + // Listen for any state change (input) and update the query string based on it + useEffect(() => { + buildQueryString(); + }, [sortVal, sortAscCheckVal, areaCheckVal, areaTextVal, keywordCheckVal, keywordTextVal, dateCheckVal, dateFromVal, dateToVal, limitVal, pageNumber]); + + const onBoxDraw = event => { + if(typeof event.data == "string" && event.data.includes("bbox")){ + setAreaTextVal(event.data); + setAreaCheckVal(true); + } + } + + useEffect(() => { + window.addEventListener("message", onBoxDraw); + + return () => { + window.removeEventListener("message", onBoxDraw); + } + }, []); /* Control IDs for reference: applyButton @@ -429,7 +505,7 @@ export default function SearchAndFilterInput(props) { <div style={applyChipVisStyle}> <Chip id="applyChip" - label={gotoPage} + label={chipMessage} icon={<FlagIcon />} onClick={handleApply} variant="outlined" diff --git a/src/components/presentational/Sidebar.jsx b/src/components/presentational/Sidebar.jsx index d52b44923eab6a8b7504e11ff8f9ac7ac2a1f1ce..baa1050c5f6b16f869fbedaba7f07d07dbe40c7f 100644 --- a/src/components/presentational/Sidebar.jsx +++ b/src/components/presentational/Sidebar.jsx @@ -46,10 +46,11 @@ export default function Sidebar(props) { ); const [showSidePanel, setShowSidePanel] = React.useState(true); - const [sidePanelSubStyle, setSidePanelSubStyle] = React.useState(css.shown); const [expandResults, setExpandResults] = React.useState(true); + const [queryString, setQueryString] = React.useState("?"); + const showHideSort = () => { setShowSidePanel(!showSidePanel); }; @@ -72,6 +73,7 @@ export default function Sidebar(props) { > <SearchAndFilterInput target={props.target} + setQueryString={setQueryString} footprintNavClick={props.footprintNavClick} /> {!expandResults && <OutPortal node={footprintResultPortalNode} />} @@ -81,7 +83,7 @@ export default function Sidebar(props) { )} </div> <InPortal node={footprintResultPortalNode}> - <FootprintResults changeLayout={handlePanelLayout} /> + <FootprintResults target={props.target} queryString={queryString} changeLayout={handlePanelLayout} /> </InPortal> </> ); diff --git a/src/components/presentational/SplashScreen.jsx b/src/components/presentational/SplashScreen.jsx index bc6ef2269b72c5ef859c419697241889b4ff95d4..b6ff306cc245c9d4ac06dd218777facde5dc4953 100644 --- a/src/components/presentational/SplashScreen.jsx +++ b/src/components/presentational/SplashScreen.jsx @@ -1,6 +1,8 @@ import React from "react"; +import CircularProgress from '@mui/material/LinearProgress'; import SvgIcon from "@mui/material/SvgIcon"; import loadingImage from "../../images/logos/geostac-logo.svg"; +import LinearProgress from "@mui/material/LinearProgress"; export default function SplashScreen() { return( @@ -20,6 +22,7 @@ export default function SplashScreen() { }} component={loadingImage} /> + <LinearProgress/> </div> </div> ) diff --git a/src/js/AstroDrawFilterControl.js b/src/js/AstroDrawFilterControl.js index b77ad8af5d09159a10dfb0523717f76beecf6670..d3fd395dd762892e6932beccc4fa52a8a0cbb472 100644 --- a/src/js/AstroDrawFilterControl.js +++ b/src/js/AstroDrawFilterControl.js @@ -116,6 +116,7 @@ export default L.Control.AstroDrawFilterControl = L.Control.Draw.extend({ geoJson = geoJson["geometry"]; this.wkt.read(JSON.stringify(geoJson)); + window.postMessage(this.shapesToFootprint(this.wkt.components[0]), "*"); }, /** diff --git a/src/styles.css b/src/styles.css index b302234c3ada39c485a7c3e37e0e62b129625a19..488c3442335ca61a2675156919cdb79a258d9872 100644 --- a/src/styles.css +++ b/src/styles.css @@ -316,14 +316,12 @@ Controls the CSS for projection buttons when there is no available projection } .resultContainer { - width: 268px; + width: 230px; display: grid; grid-template: "ra rb" "rc rc" / 20% 80%; - border-bottom: 3px double rgba(0, 0, 0, 0.4); - padding: 10px; } .resultSub {