Loading src/components/container/App.jsx +4 −5 Original line number Diff line number Diff line Loading @@ -6,7 +6,7 @@ import SplashScreen from "../presentational/SplashScreen.jsx"; /** * App is the parent component for all of the other components in the project. * It loads the data needed to initialize GeoStac. * It fetches and parses the data needed to initialize GeoStac. * It includes the main GeoStacApp and OCAP compliant headers and footers. * * @component Loading @@ -21,11 +21,11 @@ export default function App() { useEffect(() => { // Astro Web Maps, has the tile data // Astro Web Maps, has the tile base data for the map of each planetary body const astroWebMaps = "https://astrowebmaps.wr.usgs.gov/webmapatlas/Layers/maps.json"; // STAC API, has footprint data // STAC API, has footprint data for select planetary bodies const stacApiCollections = "https://stac.astrogeology.usgs.gov/api/collections"; Loading Loading @@ -227,7 +227,6 @@ export default function App() { setMainComponent(<GeoStacApp mapList={aggregateMapList}/>); })(); }, []) return ( Loading src/components/container/GeoStacApp.jsx +10 −8 Original line number Diff line number Diff line Loading @@ -2,7 +2,6 @@ import React from "react"; import ConsoleAppBar from "../presentational/ConsoleAppBar.jsx"; import MapContainer from "./MapContainer.jsx"; import QueryConsole from "../presentational/QueryConsole.jsx"; import { getFeatures } from "../../js/ApiJsonCollection"; import DisplayGeoTiff from "../presentational/DisplayGeoTiff.jsx"; import Sidebar from "../presentational/Sidebar.jsx"; import MenuBar from "../presentational/Menubar.jsx"; Loading Loading @@ -33,6 +32,9 @@ export default function GeoStacApp(props) { const [footprintData, setFootprintData] = React.useState([]); const [queryString, setQueryString] = React.useState("?"); const [collectionUrls, setCollectionUrls] = React.useState([]); const [appFullWindow, setAppFullWindow] = React.useState(true); const [appViewStyle, setAppViewStyle] = React.useState(css.appFlex); Loading @@ -49,11 +51,6 @@ export default function GeoStacApp(props) { setTargetPlanet(value); }; const handleFootprintClick = () => { setFootprintData(getFeatures); //console.log(footprintData); }; return ( <div style={appViewStyle} className="flex col scroll-parent"> <MenuBar Loading @@ -70,11 +67,16 @@ export default function GeoStacApp(props) { <div id="map-area"> <MapContainer target={targetPlanet.name} /> </div> <QueryConsole /> <QueryConsole queryString={queryString} setQueryString={setQueryString} collectionUrls={collectionUrls}/> </div> <Sidebar queryString={queryString} setQueryString={setQueryString} setCollectionUrls={setCollectionUrls} target={targetPlanet} footprintNavClick={handleFootprintClick} /> </div> <DisplayGeoTiff /> Loading src/components/presentational/FootprintResults.jsx +114 −99 Original line number Diff line number Diff line 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"; import Stack from "@mui/material/Stack"; import {Card, CardContent, CardActions, Skeleton, Chip, Stack} from "@mui/material"; // icons import PreviewIcon from "@mui/icons-material/Preview"; Loading @@ -13,13 +9,8 @@ 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"; // geotiff thumbnail viewer import DisplayGeoTiff from "../presentational/DisplayGeoTiff.jsx"; // geotiff thumbnail viewer... Should we be using DisplayGeoTiff.jsx instead? import GeoTiffViewer from "../../js/geoTiffViewer.js"; import { Skeleton } from "@mui/material"; /** Loading Loading @@ -73,6 +64,68 @@ function NoFootprints(){ ); } function FootprintCard(props){ // Metadata Popup const geoTiffViewer = new GeoTiffViewer("GeoTiffAsset"); const showMetadata = (value) => () => { geoTiffViewer.displayGeoTiff(value.assets.thumbnail.href); geoTiffViewer.changeMetaData( value.collection, value.id, value.properties.datetime, value.assets ); geoTiffViewer.openModal(); }; return( <Card sx={{ width: 250, margin: 1}}> <CardContent sx={{padding: 1.2, paddingBottom: 0}}> <div className="resultContainer" > <div className="resultImgDiv"> <img className="resultImg" src={props.feature.assets.thumbnail.href} /> </div> <div className="resultData"> {/* <div className="resultSub"> <strong>Collection:</strong> {props.feature.collection} </div> */} <div className="resultSub"> <strong>ID:</strong> {props.feature.id} </div> <div className="resultSub"> <strong>Date:</strong> {props.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(props.feature)} variant="outlined" clickable /> <Chip label="STAC Browser" icon={<LaunchIcon />} size="small" component="a" href={`https://stac.astrogeology.usgs.gov/browser-dev/#/collections/${props.feature.collection}/items/${props.feature.id}`} target="_blank" variant="outlined" clickable /> </Stack> </div> </CardActions> </Card> ); } /** Loading Loading @@ -100,24 +153,11 @@ let css = { */ export default function FootprintResults(props) { const [features, setFeatures] = React.useState([]); const [featureCollections, setFeatureCollections] = React.useState([]); 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( value.collection, value.id, value.properties.datetime, value.assets ); geoTiffViewer.openModal(); }; useEffect(() => { // If target has collections (of footprints) Loading @@ -133,19 +173,26 @@ export default function FootprintResults(props) { // Result let jsonRes = {}; let itemCollectionUrls = []; let itemCollectionData = []; 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); let itemsUrl = collection.links.find(obj => obj.rel === "items").href; itemCollectionData.push({ "itemsUrl" : itemsUrl, "itemsUrlWithQuery" : itemsUrl + props.queryString, "id" : collection.id, "title" : collection.title }); } for(const itemCollectionUrl of itemCollectionUrls) { fetchPromise[itemCollectionUrl] = "Not Started"; jsonPromise[itemCollectionUrl] = "Not Started"; jsonRes[itemCollectionUrl] = []; // For Query Console props.setCollectionUrls(itemCollectionData); // Get ready to fetch for(const itemCollectionUrl of itemCollectionData) { fetchPromise[itemCollectionUrl.itemsUrlWithQuery] = "Not Started"; jsonPromise[itemCollectionUrl.itemsUrlWithQuery] = "Not Started"; jsonRes[itemCollectionUrl.itemsUrlWithQuery] = []; } // Fetch JSON and read into object Loading @@ -163,6 +210,7 @@ export default function FootprintResults(props) { }); } // To be executed after Fetch has been started, wait for fetch to finish async function awaitFetch(targetUrl) { await fetchPromise[targetUrl]; await jsonPromise[targetUrl]; Loading @@ -170,34 +218,43 @@ export default function FootprintResults(props) { async function fetchAndWait() { // Start fetching for(const itemCollectionUrl of itemCollectionUrls) { startFetch(itemCollectionUrl); for(const itemCollectionUrl of itemCollectionData) { startFetch(itemCollectionUrl.itemsUrlWithQuery); } // Wait for completion for(const itemCollectionUrl of itemCollectionUrls) { await awaitFetch(itemCollectionUrl); for(const itemCollectionUrl of itemCollectionData) { await awaitFetch(itemCollectionUrl.itemsUrlWithQuery); } // 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) let myCollections = []; for(const itemCollectionUrl of itemCollectionData) { // Add info to returned item collection from top level collection data. jsonRes[itemCollectionUrl.itemsUrlWithQuery].id = itemCollectionUrl.id; jsonRes[itemCollectionUrl.itemsUrlWithQuery].title = itemCollectionUrl.title; jsonRes[itemCollectionUrl.itemsUrlWithQuery].itemsUrl = itemCollectionUrl.itemsUrl; jsonRes[itemCollectionUrl.itemsUrlWithQuery].itemsUrlWithQuery = itemCollectionUrl.itemsUrlWithQuery; myCollections.push(jsonRes[itemCollectionUrl.itemsUrlWithQuery]); } return resultsArr; return myCollections; } // Send to Leaflet window.postMessage(["setQuery", props.queryString], "*"); (async () => { // Wait let myFeatures = await fetchAndWait() setFeatures(myFeatures); setHasFootprints(myFeatures.length > 0); // Wait for features to be fetched and parsed let myFeatureCollections = await fetchAndWait() // Set relevant properties based on features received setFeatureCollections(myFeatureCollections); setHasFootprints(myFeatureCollections.length > 0); setIsLoading(false); // Send to Leaflet window.postMessage(["setFeatureCollections", myFeatureCollections], "*"); })(); } else { Loading @@ -205,10 +262,6 @@ export default function FootprintResults(props) { setHasFootprints(false); } // setTimeout(() => { // setFeatures(getFeatures); // }, 1000); }, [props.target.name, props.queryString]); return ( Loading @@ -233,51 +286,13 @@ export default function FootprintResults(props) { <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> {featureCollections.map((collection) => ( <React.Fragment key={collection.id}> {collection.features.length > 0 ? <p style={{maxWidth: 250, paddingLeft: 10, fontSize: "14px", fontWeight: "bold"}}>{collection.title}</p> : null } {collection.features.map((feature) => ( <FootprintCard feature={feature} title={collection.title} key={feature.id}/> ))} </React.Fragment> ))} </div> : Loading src/components/presentational/QueryConsole.jsx +101 −30 Original line number Diff line number Diff line import React from "react"; import React, { useEffect } from "react"; import { alpha } from "@mui/material/styles"; import Button from "@mui/material/Button"; import ButtonGroup from "@mui/material/ButtonGroup"; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import SwitchLeftIcon from '@mui/icons-material/SwitchLeft'; import SwitchRightIcon from '@mui/icons-material/SwitchRight'; import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle'; import Checkbox from "@mui/material/Checkbox"; import ExpandLessIcon from "@mui/icons-material/ExpandLess"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { Collapse, FormControl, InputLabel, MenuItem, Select } from "@mui/material"; let css = { button: { consoleButton: { width: "auto", color: "#000", backgroundColor: "#fff", Loading @@ -19,49 +18,121 @@ let css = { "&:hover": { backgroundColor: alpha("#eee", 0.9) } }, consoleSelectLabel: { color: "#000", "&.MuiInputLabel-shrink": { color: "#555", backgroundColor: "#FFF", paddingLeft: 1, paddingRight: 1, borderRadius: 2, }, "&.Mui-focused": { color: "#FFF", backgroundColor: "#1971c2", paddingLeft: 1, paddingRight: 1, borderRadius: 2, border: "2px solid white" } }, consoleSelect: { color: "#000", backgroundColor: "#fff", marginBottom: 1, maxWidth: "165px" }, }; /** * Component lets user view and use stac queries * @component * @example * <StacQueryConsole /> * <QueryConsole /> */ export default function QueryConsole() { export default function QueryConsole(props) { const [showConsole, setShowConsole] = React.useState(false); const [selectedUrl, setSelectedUrl] = React.useState(""); const [queryUrl, setQueryUrl] = React.useState(props.queryString); const handleTextChange = (event) => { setQueryUrl(event.target.value); } const [consoleAuto, setConsoleAuto] = React.useState(true); const [consoleAutoWkt, setConsoleAutoWkt] = React.useState(false); const handleOpenCloseConsole = () => { setShowConsole(!showConsole); } const handleConsoleAutoChange = (event) => { setConsoleAuto(event.target.checked); const handleSelectedUrlChange = (event) => { setSelectedUrl(event.target.value); } const handleConsoleAutoWktChange = (event) => { setConsoleAutoWkt(event.target.checked); const handleRunQueryClick = () => { const newQuery = queryUrl.split("?")[1]; if(typeof newQuery !== 'undefined'){ props.setQueryString("?" + queryUrl.split("?")[1]); } } useEffect(() => { setQueryUrl(selectedUrl + props.queryString); }, [selectedUrl, props.queryString]); return ( <details id="query-console-container"> <summary id="query-console-collapsed"> <div id="query-console-container"> <div id="query-console-collapsed" onClick={handleOpenCloseConsole}> <span id="query-console-title"> <ArrowDropDownCircleIcon sx={{marginRight:1}}/> Query Console {showConsole ? <ExpandMoreIcon /> : <ExpandLessIcon />} Query Console </span> </summary> <div id="query-console-expanded"> </div> <Collapse id="query-console-expanded" in={showConsole}> <div id="query-textarea-container"> <textarea id="query-textarea" placeholder="> Type Query Here"></textarea> <textarea value={queryUrl} onChange={handleTextChange} id="query-textarea" placeholder="> Type Query Here"> </textarea> <div id="query-command-bar"> <FormControl> <InputLabel size="small" sx={css.consoleSelectLabel}>Set Collection...</InputLabel> <Select labelId="collection-select" size="small" sx={css.consoleSelect} value={selectedUrl} onChange={handleSelectedUrlChange} > <MenuItem value=""> <em>None</em> </MenuItem> {props.collectionUrls.map((myUrl) => ( <MenuItem value={myUrl.itemsUrl} key={myUrl.title}>{myUrl.title}</MenuItem> ))} </Select> <ButtonGroup orientation="vertical" size="small" variant="contained"> <Button id="copyCodeButton" sx={css.button} startIcon={<ContentCopyIcon />}>Copy Code</Button> <Button id="runQueryButton" sx={css.button} startIcon={<PlayArrowIcon />}>Run STAC Query</Button> <Button id="copyCodeButton" sx={css.consoleButton} startIcon={<ContentCopyIcon />}>Copy Code</Button> <Button onClick={handleRunQueryClick} id="runQueryButton" sx={css.consoleButton} startIcon={<PlayArrowIcon />}> Run STAC Query </Button> </ButtonGroup> </FormControl> </div> </div> </Collapse> </div> </details> ); } src/components/presentational/SearchAndFilterInput.jsx +40 −61 Original line number Diff line number Diff line Loading @@ -24,8 +24,6 @@ import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; // No. of Footprints, pagination import Slider from "@mui/material/Slider"; import Pagination from "@mui/material/Pagination"; import Chip from "@mui/material/Chip"; import FlagIcon from "@mui/icons-material/Flag"; import { getMaxNumberPages, Loading Loading @@ -76,14 +74,7 @@ let css = { color: "#343a40", fontSize: 18, fontWeight: 600, }, chipHidden: { visibility: "hidden", }, chipShown: { visibility: "visible", textAlign: "center", }, } }; /** Loading Loading @@ -124,24 +115,13 @@ export default function SearchAndFilterInput(props) { const [numberReturned, setNumberReturned] = React.useState(10); 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) => { setChipMessage("Apply to Show Footprints on Map"); setApplyChipVisStyle(css.chipShown); }; const handleApply = () => { setTimeout(() => { setMaxPages(getMaxNumberPages); setNumberReturned(getNumberReturned); setMaxNumberFootprints(getNumberMatched); props.footprintNavClick(); }, 3000); setApplyChipVisStyle(css.chipHidden); }; // const handleApply = () => { // setTimeout(() => { // setMaxPages(getMaxNumberPages); // setNumberReturned(getNumberReturned); // setMaxNumberFootprints(getNumberMatched); // }, 3000); // }; // Clear all values const handleClear = () => { Loading @@ -157,7 +137,6 @@ export default function SearchAndFilterInput(props) { setMaxPages(1); setMaxNumberFootprints(0); setNumberReturned(0); setApplyChip("Apply to Show Footprints on Map"); //// Uncomment to close details on clear // keywordDetails.current.open = false; // dateDetails.current.open = false; Loading @@ -174,17 +153,33 @@ export default function SearchAndFilterInput(props) { // 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 const lowestYear = 1970; const highestYear = d.getFullYear(); let fromDate = lowestYear + "-01-01T00:00:00Z"; let toDate = highestYear + "-12-31T23:59:59Z"; const isGoodDate = (dateToCheck) => { if(dateToCheck) { const isValid = !isNaN(dateToCheck.valueOf()); const isDate = dateToCheck instanceof Date; let yearToCheck = 0; if (isDate & isValid){ yearToCheck = dateToCheck.getFullYear(); return lowestYear < yearToCheck && yearToCheck < highestYear; } } return false } // From if(dateFromVal instanceof Date && !isNaN(dateFromVal.valueOf())) { if(isGoodDate(dateFromVal)) { fromDate = dateFromVal.toISOString(); } // To if(dateToVal instanceof Date && !isNaN(dateToVal.valueOf())) { if(isGoodDate(dateToVal)) { toDate = dateToVal.toISOString(); } Loading @@ -194,9 +189,6 @@ export default function SearchAndFilterInput(props) { // 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") { Loading @@ -204,6 +196,9 @@ export default function SearchAndFilterInput(props) { // myQueryString += 'sort=[{field:datetime,direction:' + sortAscDesc + '}]&' } // Area if(areaCheckVal && areaTextVal !== "") myQueryString += areaTextVal; // Add an & if not last props.setQueryString(myQueryString); } Loading @@ -218,16 +213,12 @@ export default function SearchAndFilterInput(props) { // Polygon const handleAreaCheckChange = (event) => { setAreaCheckVal(event.target.checked); if (event.target.checked === true) { setApplyChip("Apply to filter footprints"); } }; // Keyword const handleKeywordCheckChange = (event) => { setKeywordCheckVal(event.target.checked); if (event.target.checked === true) { setApplyChip("Apply to filter footprints"); if (keywordDetails.current.open === false) { keywordDetails.current.open = true; } Loading @@ -242,7 +233,6 @@ export default function SearchAndFilterInput(props) { const handleDateCheckChange = (event) => { setDateCheckVal(event.target.checked); if (event.target.checked === true) { setApplyChip("Apply to filter footprints"); if (dateDetails.current.open === false) { dateDetails.current.open = true; } Loading @@ -261,14 +251,12 @@ export default function SearchAndFilterInput(props) { const handleLimitChange = (event, value) => { setLimitVal(value); setLimit(value); 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 Loading @@ -281,7 +269,6 @@ export default function SearchAndFilterInput(props) { setLimitVal(10); setLimit(10); setMaxPages(getMaxNumberPages); props.footprintNavClick(); }, 2000); }, [props.target.name]); Loading @@ -291,8 +278,10 @@ export default function SearchAndFilterInput(props) { }, [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); console.info("Window meassage received from: ", event.origin); // For production, check if messages coming from prod url if(typeof event.data == "object" && event.data[0] === "setWkt"){ const receivedWkt = event.data[1]; setAreaTextVal(receivedWkt); setAreaCheckVal(true); } } Loading Loading @@ -320,8 +309,8 @@ export default function SearchAndFilterInput(props) { return ( <div style={css.container}> <div className="panelSection panelHeader">Sort and Filter</div> <div className="panelSection"> <div className="panelSection panelHeader">Filter Results</div> {/* <div className="panelSection"> <ButtonGroup> <Button id="applyButton" Loading Loading @@ -381,7 +370,7 @@ export default function SearchAndFilterInput(props) { </div> </div> <div className="panelSection panelHeader">Filter By...</div> <div className="panelSection panelHeader">Filter By...</div> */} <div className="panelSection"> <div className="panelSectionHeader"> Loading @@ -396,7 +385,7 @@ export default function SearchAndFilterInput(props) { </div> </div> <div className="panelSection"> {/* <div className="panelSection"> <details ref={keywordDetails}> <summary> <div className="panelSectionHeader"> Loading Loading @@ -427,7 +416,7 @@ export default function SearchAndFilterInput(props) { /> </div> </details> </div> </div> */} <div className="panelSection"> <details ref={dateDetails}> Loading Loading @@ -502,16 +491,6 @@ export default function SearchAndFilterInput(props) { Displaying {numberReturned} of {maxNumberFootprints} Results </div> </div> <div style={applyChipVisStyle}> <Chip id="applyChip" label={chipMessage} icon={<FlagIcon />} onClick={handleApply} variant="outlined" clickable /> </div> </div> ); } Loading
src/components/container/App.jsx +4 −5 Original line number Diff line number Diff line Loading @@ -6,7 +6,7 @@ import SplashScreen from "../presentational/SplashScreen.jsx"; /** * App is the parent component for all of the other components in the project. * It loads the data needed to initialize GeoStac. * It fetches and parses the data needed to initialize GeoStac. * It includes the main GeoStacApp and OCAP compliant headers and footers. * * @component Loading @@ -21,11 +21,11 @@ export default function App() { useEffect(() => { // Astro Web Maps, has the tile data // Astro Web Maps, has the tile base data for the map of each planetary body const astroWebMaps = "https://astrowebmaps.wr.usgs.gov/webmapatlas/Layers/maps.json"; // STAC API, has footprint data // STAC API, has footprint data for select planetary bodies const stacApiCollections = "https://stac.astrogeology.usgs.gov/api/collections"; Loading Loading @@ -227,7 +227,6 @@ export default function App() { setMainComponent(<GeoStacApp mapList={aggregateMapList}/>); })(); }, []) return ( Loading
src/components/container/GeoStacApp.jsx +10 −8 Original line number Diff line number Diff line Loading @@ -2,7 +2,6 @@ import React from "react"; import ConsoleAppBar from "../presentational/ConsoleAppBar.jsx"; import MapContainer from "./MapContainer.jsx"; import QueryConsole from "../presentational/QueryConsole.jsx"; import { getFeatures } from "../../js/ApiJsonCollection"; import DisplayGeoTiff from "../presentational/DisplayGeoTiff.jsx"; import Sidebar from "../presentational/Sidebar.jsx"; import MenuBar from "../presentational/Menubar.jsx"; Loading Loading @@ -33,6 +32,9 @@ export default function GeoStacApp(props) { const [footprintData, setFootprintData] = React.useState([]); const [queryString, setQueryString] = React.useState("?"); const [collectionUrls, setCollectionUrls] = React.useState([]); const [appFullWindow, setAppFullWindow] = React.useState(true); const [appViewStyle, setAppViewStyle] = React.useState(css.appFlex); Loading @@ -49,11 +51,6 @@ export default function GeoStacApp(props) { setTargetPlanet(value); }; const handleFootprintClick = () => { setFootprintData(getFeatures); //console.log(footprintData); }; return ( <div style={appViewStyle} className="flex col scroll-parent"> <MenuBar Loading @@ -70,11 +67,16 @@ export default function GeoStacApp(props) { <div id="map-area"> <MapContainer target={targetPlanet.name} /> </div> <QueryConsole /> <QueryConsole queryString={queryString} setQueryString={setQueryString} collectionUrls={collectionUrls}/> </div> <Sidebar queryString={queryString} setQueryString={setQueryString} setCollectionUrls={setCollectionUrls} target={targetPlanet} footprintNavClick={handleFootprintClick} /> </div> <DisplayGeoTiff /> Loading
src/components/presentational/FootprintResults.jsx +114 −99 Original line number Diff line number Diff line 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"; import Stack from "@mui/material/Stack"; import {Card, CardContent, CardActions, Skeleton, Chip, Stack} from "@mui/material"; // icons import PreviewIcon from "@mui/icons-material/Preview"; Loading @@ -13,13 +9,8 @@ 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"; // geotiff thumbnail viewer import DisplayGeoTiff from "../presentational/DisplayGeoTiff.jsx"; // geotiff thumbnail viewer... Should we be using DisplayGeoTiff.jsx instead? import GeoTiffViewer from "../../js/geoTiffViewer.js"; import { Skeleton } from "@mui/material"; /** Loading Loading @@ -73,6 +64,68 @@ function NoFootprints(){ ); } function FootprintCard(props){ // Metadata Popup const geoTiffViewer = new GeoTiffViewer("GeoTiffAsset"); const showMetadata = (value) => () => { geoTiffViewer.displayGeoTiff(value.assets.thumbnail.href); geoTiffViewer.changeMetaData( value.collection, value.id, value.properties.datetime, value.assets ); geoTiffViewer.openModal(); }; return( <Card sx={{ width: 250, margin: 1}}> <CardContent sx={{padding: 1.2, paddingBottom: 0}}> <div className="resultContainer" > <div className="resultImgDiv"> <img className="resultImg" src={props.feature.assets.thumbnail.href} /> </div> <div className="resultData"> {/* <div className="resultSub"> <strong>Collection:</strong> {props.feature.collection} </div> */} <div className="resultSub"> <strong>ID:</strong> {props.feature.id} </div> <div className="resultSub"> <strong>Date:</strong> {props.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(props.feature)} variant="outlined" clickable /> <Chip label="STAC Browser" icon={<LaunchIcon />} size="small" component="a" href={`https://stac.astrogeology.usgs.gov/browser-dev/#/collections/${props.feature.collection}/items/${props.feature.id}`} target="_blank" variant="outlined" clickable /> </Stack> </div> </CardActions> </Card> ); } /** Loading Loading @@ -100,24 +153,11 @@ let css = { */ export default function FootprintResults(props) { const [features, setFeatures] = React.useState([]); const [featureCollections, setFeatureCollections] = React.useState([]); 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( value.collection, value.id, value.properties.datetime, value.assets ); geoTiffViewer.openModal(); }; useEffect(() => { // If target has collections (of footprints) Loading @@ -133,19 +173,26 @@ export default function FootprintResults(props) { // Result let jsonRes = {}; let itemCollectionUrls = []; let itemCollectionData = []; 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); let itemsUrl = collection.links.find(obj => obj.rel === "items").href; itemCollectionData.push({ "itemsUrl" : itemsUrl, "itemsUrlWithQuery" : itemsUrl + props.queryString, "id" : collection.id, "title" : collection.title }); } for(const itemCollectionUrl of itemCollectionUrls) { fetchPromise[itemCollectionUrl] = "Not Started"; jsonPromise[itemCollectionUrl] = "Not Started"; jsonRes[itemCollectionUrl] = []; // For Query Console props.setCollectionUrls(itemCollectionData); // Get ready to fetch for(const itemCollectionUrl of itemCollectionData) { fetchPromise[itemCollectionUrl.itemsUrlWithQuery] = "Not Started"; jsonPromise[itemCollectionUrl.itemsUrlWithQuery] = "Not Started"; jsonRes[itemCollectionUrl.itemsUrlWithQuery] = []; } // Fetch JSON and read into object Loading @@ -163,6 +210,7 @@ export default function FootprintResults(props) { }); } // To be executed after Fetch has been started, wait for fetch to finish async function awaitFetch(targetUrl) { await fetchPromise[targetUrl]; await jsonPromise[targetUrl]; Loading @@ -170,34 +218,43 @@ export default function FootprintResults(props) { async function fetchAndWait() { // Start fetching for(const itemCollectionUrl of itemCollectionUrls) { startFetch(itemCollectionUrl); for(const itemCollectionUrl of itemCollectionData) { startFetch(itemCollectionUrl.itemsUrlWithQuery); } // Wait for completion for(const itemCollectionUrl of itemCollectionUrls) { await awaitFetch(itemCollectionUrl); for(const itemCollectionUrl of itemCollectionData) { await awaitFetch(itemCollectionUrl.itemsUrlWithQuery); } // 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) let myCollections = []; for(const itemCollectionUrl of itemCollectionData) { // Add info to returned item collection from top level collection data. jsonRes[itemCollectionUrl.itemsUrlWithQuery].id = itemCollectionUrl.id; jsonRes[itemCollectionUrl.itemsUrlWithQuery].title = itemCollectionUrl.title; jsonRes[itemCollectionUrl.itemsUrlWithQuery].itemsUrl = itemCollectionUrl.itemsUrl; jsonRes[itemCollectionUrl.itemsUrlWithQuery].itemsUrlWithQuery = itemCollectionUrl.itemsUrlWithQuery; myCollections.push(jsonRes[itemCollectionUrl.itemsUrlWithQuery]); } return resultsArr; return myCollections; } // Send to Leaflet window.postMessage(["setQuery", props.queryString], "*"); (async () => { // Wait let myFeatures = await fetchAndWait() setFeatures(myFeatures); setHasFootprints(myFeatures.length > 0); // Wait for features to be fetched and parsed let myFeatureCollections = await fetchAndWait() // Set relevant properties based on features received setFeatureCollections(myFeatureCollections); setHasFootprints(myFeatureCollections.length > 0); setIsLoading(false); // Send to Leaflet window.postMessage(["setFeatureCollections", myFeatureCollections], "*"); })(); } else { Loading @@ -205,10 +262,6 @@ export default function FootprintResults(props) { setHasFootprints(false); } // setTimeout(() => { // setFeatures(getFeatures); // }, 1000); }, [props.target.name, props.queryString]); return ( Loading @@ -233,51 +286,13 @@ export default function FootprintResults(props) { <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> {featureCollections.map((collection) => ( <React.Fragment key={collection.id}> {collection.features.length > 0 ? <p style={{maxWidth: 250, paddingLeft: 10, fontSize: "14px", fontWeight: "bold"}}>{collection.title}</p> : null } {collection.features.map((feature) => ( <FootprintCard feature={feature} title={collection.title} key={feature.id}/> ))} </React.Fragment> ))} </div> : Loading
src/components/presentational/QueryConsole.jsx +101 −30 Original line number Diff line number Diff line import React from "react"; import React, { useEffect } from "react"; import { alpha } from "@mui/material/styles"; import Button from "@mui/material/Button"; import ButtonGroup from "@mui/material/ButtonGroup"; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import SwitchLeftIcon from '@mui/icons-material/SwitchLeft'; import SwitchRightIcon from '@mui/icons-material/SwitchRight'; import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle'; import Checkbox from "@mui/material/Checkbox"; import ExpandLessIcon from "@mui/icons-material/ExpandLess"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { Collapse, FormControl, InputLabel, MenuItem, Select } from "@mui/material"; let css = { button: { consoleButton: { width: "auto", color: "#000", backgroundColor: "#fff", Loading @@ -19,49 +18,121 @@ let css = { "&:hover": { backgroundColor: alpha("#eee", 0.9) } }, consoleSelectLabel: { color: "#000", "&.MuiInputLabel-shrink": { color: "#555", backgroundColor: "#FFF", paddingLeft: 1, paddingRight: 1, borderRadius: 2, }, "&.Mui-focused": { color: "#FFF", backgroundColor: "#1971c2", paddingLeft: 1, paddingRight: 1, borderRadius: 2, border: "2px solid white" } }, consoleSelect: { color: "#000", backgroundColor: "#fff", marginBottom: 1, maxWidth: "165px" }, }; /** * Component lets user view and use stac queries * @component * @example * <StacQueryConsole /> * <QueryConsole /> */ export default function QueryConsole() { export default function QueryConsole(props) { const [showConsole, setShowConsole] = React.useState(false); const [selectedUrl, setSelectedUrl] = React.useState(""); const [queryUrl, setQueryUrl] = React.useState(props.queryString); const handleTextChange = (event) => { setQueryUrl(event.target.value); } const [consoleAuto, setConsoleAuto] = React.useState(true); const [consoleAutoWkt, setConsoleAutoWkt] = React.useState(false); const handleOpenCloseConsole = () => { setShowConsole(!showConsole); } const handleConsoleAutoChange = (event) => { setConsoleAuto(event.target.checked); const handleSelectedUrlChange = (event) => { setSelectedUrl(event.target.value); } const handleConsoleAutoWktChange = (event) => { setConsoleAutoWkt(event.target.checked); const handleRunQueryClick = () => { const newQuery = queryUrl.split("?")[1]; if(typeof newQuery !== 'undefined'){ props.setQueryString("?" + queryUrl.split("?")[1]); } } useEffect(() => { setQueryUrl(selectedUrl + props.queryString); }, [selectedUrl, props.queryString]); return ( <details id="query-console-container"> <summary id="query-console-collapsed"> <div id="query-console-container"> <div id="query-console-collapsed" onClick={handleOpenCloseConsole}> <span id="query-console-title"> <ArrowDropDownCircleIcon sx={{marginRight:1}}/> Query Console {showConsole ? <ExpandMoreIcon /> : <ExpandLessIcon />} Query Console </span> </summary> <div id="query-console-expanded"> </div> <Collapse id="query-console-expanded" in={showConsole}> <div id="query-textarea-container"> <textarea id="query-textarea" placeholder="> Type Query Here"></textarea> <textarea value={queryUrl} onChange={handleTextChange} id="query-textarea" placeholder="> Type Query Here"> </textarea> <div id="query-command-bar"> <FormControl> <InputLabel size="small" sx={css.consoleSelectLabel}>Set Collection...</InputLabel> <Select labelId="collection-select" size="small" sx={css.consoleSelect} value={selectedUrl} onChange={handleSelectedUrlChange} > <MenuItem value=""> <em>None</em> </MenuItem> {props.collectionUrls.map((myUrl) => ( <MenuItem value={myUrl.itemsUrl} key={myUrl.title}>{myUrl.title}</MenuItem> ))} </Select> <ButtonGroup orientation="vertical" size="small" variant="contained"> <Button id="copyCodeButton" sx={css.button} startIcon={<ContentCopyIcon />}>Copy Code</Button> <Button id="runQueryButton" sx={css.button} startIcon={<PlayArrowIcon />}>Run STAC Query</Button> <Button id="copyCodeButton" sx={css.consoleButton} startIcon={<ContentCopyIcon />}>Copy Code</Button> <Button onClick={handleRunQueryClick} id="runQueryButton" sx={css.consoleButton} startIcon={<PlayArrowIcon />}> Run STAC Query </Button> </ButtonGroup> </FormControl> </div> </div> </Collapse> </div> </details> ); }
src/components/presentational/SearchAndFilterInput.jsx +40 −61 Original line number Diff line number Diff line Loading @@ -24,8 +24,6 @@ import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; // No. of Footprints, pagination import Slider from "@mui/material/Slider"; import Pagination from "@mui/material/Pagination"; import Chip from "@mui/material/Chip"; import FlagIcon from "@mui/icons-material/Flag"; import { getMaxNumberPages, Loading Loading @@ -76,14 +74,7 @@ let css = { color: "#343a40", fontSize: 18, fontWeight: 600, }, chipHidden: { visibility: "hidden", }, chipShown: { visibility: "visible", textAlign: "center", }, } }; /** Loading Loading @@ -124,24 +115,13 @@ export default function SearchAndFilterInput(props) { const [numberReturned, setNumberReturned] = React.useState(10); 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) => { setChipMessage("Apply to Show Footprints on Map"); setApplyChipVisStyle(css.chipShown); }; const handleApply = () => { setTimeout(() => { setMaxPages(getMaxNumberPages); setNumberReturned(getNumberReturned); setMaxNumberFootprints(getNumberMatched); props.footprintNavClick(); }, 3000); setApplyChipVisStyle(css.chipHidden); }; // const handleApply = () => { // setTimeout(() => { // setMaxPages(getMaxNumberPages); // setNumberReturned(getNumberReturned); // setMaxNumberFootprints(getNumberMatched); // }, 3000); // }; // Clear all values const handleClear = () => { Loading @@ -157,7 +137,6 @@ export default function SearchAndFilterInput(props) { setMaxPages(1); setMaxNumberFootprints(0); setNumberReturned(0); setApplyChip("Apply to Show Footprints on Map"); //// Uncomment to close details on clear // keywordDetails.current.open = false; // dateDetails.current.open = false; Loading @@ -174,17 +153,33 @@ export default function SearchAndFilterInput(props) { // 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 const lowestYear = 1970; const highestYear = d.getFullYear(); let fromDate = lowestYear + "-01-01T00:00:00Z"; let toDate = highestYear + "-12-31T23:59:59Z"; const isGoodDate = (dateToCheck) => { if(dateToCheck) { const isValid = !isNaN(dateToCheck.valueOf()); const isDate = dateToCheck instanceof Date; let yearToCheck = 0; if (isDate & isValid){ yearToCheck = dateToCheck.getFullYear(); return lowestYear < yearToCheck && yearToCheck < highestYear; } } return false } // From if(dateFromVal instanceof Date && !isNaN(dateFromVal.valueOf())) { if(isGoodDate(dateFromVal)) { fromDate = dateFromVal.toISOString(); } // To if(dateToVal instanceof Date && !isNaN(dateToVal.valueOf())) { if(isGoodDate(dateToVal)) { toDate = dateToVal.toISOString(); } Loading @@ -194,9 +189,6 @@ export default function SearchAndFilterInput(props) { // 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") { Loading @@ -204,6 +196,9 @@ export default function SearchAndFilterInput(props) { // myQueryString += 'sort=[{field:datetime,direction:' + sortAscDesc + '}]&' } // Area if(areaCheckVal && areaTextVal !== "") myQueryString += areaTextVal; // Add an & if not last props.setQueryString(myQueryString); } Loading @@ -218,16 +213,12 @@ export default function SearchAndFilterInput(props) { // Polygon const handleAreaCheckChange = (event) => { setAreaCheckVal(event.target.checked); if (event.target.checked === true) { setApplyChip("Apply to filter footprints"); } }; // Keyword const handleKeywordCheckChange = (event) => { setKeywordCheckVal(event.target.checked); if (event.target.checked === true) { setApplyChip("Apply to filter footprints"); if (keywordDetails.current.open === false) { keywordDetails.current.open = true; } Loading @@ -242,7 +233,6 @@ export default function SearchAndFilterInput(props) { const handleDateCheckChange = (event) => { setDateCheckVal(event.target.checked); if (event.target.checked === true) { setApplyChip("Apply to filter footprints"); if (dateDetails.current.open === false) { dateDetails.current.open = true; } Loading @@ -261,14 +251,12 @@ export default function SearchAndFilterInput(props) { const handleLimitChange = (event, value) => { setLimitVal(value); setLimit(value); 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 Loading @@ -281,7 +269,6 @@ export default function SearchAndFilterInput(props) { setLimitVal(10); setLimit(10); setMaxPages(getMaxNumberPages); props.footprintNavClick(); }, 2000); }, [props.target.name]); Loading @@ -291,8 +278,10 @@ export default function SearchAndFilterInput(props) { }, [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); console.info("Window meassage received from: ", event.origin); // For production, check if messages coming from prod url if(typeof event.data == "object" && event.data[0] === "setWkt"){ const receivedWkt = event.data[1]; setAreaTextVal(receivedWkt); setAreaCheckVal(true); } } Loading Loading @@ -320,8 +309,8 @@ export default function SearchAndFilterInput(props) { return ( <div style={css.container}> <div className="panelSection panelHeader">Sort and Filter</div> <div className="panelSection"> <div className="panelSection panelHeader">Filter Results</div> {/* <div className="panelSection"> <ButtonGroup> <Button id="applyButton" Loading Loading @@ -381,7 +370,7 @@ export default function SearchAndFilterInput(props) { </div> </div> <div className="panelSection panelHeader">Filter By...</div> <div className="panelSection panelHeader">Filter By...</div> */} <div className="panelSection"> <div className="panelSectionHeader"> Loading @@ -396,7 +385,7 @@ export default function SearchAndFilterInput(props) { </div> </div> <div className="panelSection"> {/* <div className="panelSection"> <details ref={keywordDetails}> <summary> <div className="panelSectionHeader"> Loading Loading @@ -427,7 +416,7 @@ export default function SearchAndFilterInput(props) { /> </div> </details> </div> </div> */} <div className="panelSection"> <details ref={dateDetails}> Loading Loading @@ -502,16 +491,6 @@ export default function SearchAndFilterInput(props) { Displaying {numberReturned} of {maxNumberFootprints} Results </div> </div> <div style={applyChipVisStyle}> <Chip id="applyChip" label={chipMessage} icon={<FlagIcon />} onClick={handleApply} variant="outlined" clickable /> </div> </div> ); }