diff --git a/src/components/presentational/FootprintResults.jsx b/src/components/presentational/FootprintResults.jsx index 396f67a714c4a25b691ca045917c7d4c60fa4ef1..800aa965aa1a2b04121b19e54b62ec7556b20f48 100644 --- a/src/components/presentational/FootprintResults.jsx +++ b/src/components/presentational/FootprintResults.jsx @@ -32,7 +32,6 @@ export default function FootprintResults(props) { const [oldTargetName, setOldTargetName] = React.useState(""); const [oldFilterString, setOldFilterString] = React.useState(""); - const addFeatures = (newFeatures, key) => { let myFeatureCollections = featureCollections; myFeatureCollections[key].features.push(...newFeatures); @@ -62,6 +61,14 @@ export default function FootprintResults(props) { setCollectionId(newCollectionId); setMatched(featureCollections[newCollectionId].numberMatched); + // Extract the selected collection title + const selectedCollection = props.target.collections.find(collection => collection.id === newCollectionId); + const selectedCollectionTitle = selectedCollection ? selectedCollection.title : ''; + + // Call the callback function to pass the selected title to the Sidebar + props.updateSelectedTitle(selectedCollectionTitle); + + // Send to Leaflet window.postMessage(["setVisibleCollections", newCollectionId], "*"); }; @@ -118,6 +125,7 @@ export default function FootprintResults(props) { let isInStacAPI = collection.hasOwnProperty("stac_version"); let isInPyAPI = collection.hasOwnProperty("itemType"); + // check for pygeo api if (isInPyAPI) @@ -203,6 +211,9 @@ export default function FootprintResults(props) { for(const key in featureCollections){ if(featureCollections[key].numberReturned > 0) noFootprintsReturned = false; } + if(numFeatures > matched) { + setNumFeatures(matched); + } return ( <div id="footprintResults" className="scroll-parent"> @@ -232,7 +243,11 @@ export default function FootprintResults(props) { <div id="resultsList"> <List sx={{maxWidth: 265, paddingTop: 0}}> {featureCollections[collectionId].features.map(feature => ( - <FootprintCard feature={feature} key={feature.id}/> + <FootprintCard + feature={feature} + key={feature.id} + selectedQueryables = {props.selectedQueryables} + /> ))} </List> </div> diff --git a/src/components/presentational/ResultsAccessories.jsx b/src/components/presentational/ResultsAccessories.jsx index 8ee013b5255af6d271e7b4621fbb361489e9c901..ce2c5ddd3a7e64692aa676d74667c26b2b5841e8 100644 --- a/src/components/presentational/ResultsAccessories.jsx +++ b/src/components/presentational/ResultsAccessories.jsx @@ -63,6 +63,28 @@ export function NoFootprints(){ ); } +// determine the DatasetType in order to properly gather seach data for a given set +function determineDatasetType(features) { + // If no features or the first feature doesn't have properties, return 'unknown' + if(!features || !features.properties) return 'unknown'; + + // Extract keys of the first feature's properties + const propertyKeys = Object.keys(features.properties); + + // Find the key that ends with "id" + const idKey = propertyKeys.find(key => key.endsWith("id")); + + if(!idKey) return 'unknown'; // If no id found + + if(features.stac_extensions) return "stac"; + + // Based on the key determine the type(in case we need to specify in the future) + switch(idKey) { + default: + return 'unknown'; + } +} + // Shown when collections are available but no footprints were returned for current filter. export function FilterTooStrict(){ @@ -82,76 +104,64 @@ export function FilterTooStrict(){ </div> ); } - - // A small card with an images and a few key data points // shown as the result for a footprint. export function FootprintCard(props){ //initialize variables let ThumbnailLink = ''; - let modifiedProductId = ''; let BrowserLink = ''; let showMetadata; - + + let stacAPIFlag = false; + let pyGeoAPIFlag = false; + // Metadata Popup const geoTiffViewer = new GeoTiffViewer("GeoTiffAsset"); - + //determine feature type + const featureType = determineDatasetType(props.feature); + //check for feature type in order to gather correct meta data + switch(featureType) { + case "stac": + // set Thumbnail link + ThumbnailLink = props.feature.assets.thumbnail.href; + BrowserLink = 'https://stac.astrogeology.usgs.gov/browser-dev/#/api/collections/' + props.feature.collection + '/items/' + props.feature.id; - // Check for pyGeo API vs raster API - - // Check if "assets" is available before accessing it - if (props.feature.assets && props.feature.assets.thumbnail && props.feature.assets.thumbnail.href) - { - // set Thumbnail link - ThumbnailLink = props.feature.assets.thumbnail.href; - - BrowserLink = 'https://stac.astrogeology.usgs.gov/browser-dev/#/api/collections/' + props.feature.collection + '/items/' + props.feature.id; - - // display meta data for STAC api - showMetadata = (value) => () => { - geoTiffViewer.displayGeoTiff(value.assets.thumbnail.href); - geoTiffViewer.changeMetaData( - value.collection, - value.id, - value.properties.datetime, - value.assets - ); - geoTiffViewer.openModal(); - }; - - - } - else - { - - // Switch the id and date and link - props.feature.id = props.feature.properties.productid; - - props.feature.properties.datetime = props.feature.properties.createdate; - - modifiedProductId = props.feature.id.replace(/_RED|_COLOR/g, ''); - - ThumbnailLink = 'https://hirise.lpl.arizona.edu/PDS/EXTRAS/RDR/ESP/ORB_012600_012699/' + modifiedProductId + '/' + props.feature.id + '.thumb.jpg'; - - BrowserLink = props.feature.properties.produrl; - - //display different modal for PyGeo API - showMetadata = (value) => () => { - geoTiffViewer.displayGeoTiff(ThumbnailLink); - geoTiffViewer.changeMetaData( - value.properties.datasetid, - value.properties.productid, - value.properties.datetime, - value.links - ); - geoTiffViewer.openModal(); - }; - } - + // set boolean + stacAPIFlag = true; + + // display meta data for STAC api + showMetadata = (value) => () => { + geoTiffViewer.displayGeoTiff(value.assets.thumbnail.href); + geoTiffViewer.changeMetaData( + value.collection, + value.id, + value.properties.datetime, + value.assets + ); + geoTiffViewer.openModal(); + }; + break; + + default: + pyGeoAPIFlag = true; + //display different modal for PyGeo API + showMetadata = (value) => () => { + //geoTiffViewer.displayGeoTiff(ThumbnailLink); + geoTiffViewer.changeMetaData( + value.properties.datasetid, + value.properties.productid, + value.properties.datetime, + value.links + ); + }; + + break; + + }; const cardClick = () => { window.postMessage(["zoomFootprint", props.feature], "*"); @@ -165,11 +175,16 @@ export function FootprintCard(props){ window.postMessage(["unhighlightFootprint"], "*"); }; - - + // get each option and put it within an array + let queryableSelection = []; + if(props.selectedQueryables) { + queryableSelection = props.selectedQueryables.map(data => data.option); + } return( <Card sx={{ width: 250, margin: 1}}> + {/* This checks for the stac API */} + {stacAPIFlag && ( <CardActionArea onMouseEnter={cardHover} onMouseLeave={eraseHover} onClick={cardClick}> <CardContent sx={{padding: 1.2, paddingBottom: 0}}> <div className="resultContainer" > @@ -187,6 +202,42 @@ export function FootprintCard(props){ </div> </CardContent> </CardActionArea> + )} + {/* This checks for the PyGeo API */} + {pyGeoAPIFlag && ( + <CardActionArea onMouseEnter={cardHover} onMouseLeave={eraseHover} onClick={cardClick}> + <CardContent sx={{padding: 1.2, paddingBottom: 0}}> + <div className="pyGeoResultContainer" > + <div className="resultData"> + <div className="resultSub"> + <strong>ID:</strong> {props.feature.id} + </div> + <div className="resultSub"> + {props.feature?.properties && + Object.entries(props.feature.properties).map(([key, value]) => { + // Check if the key exists in the selected queryables + if(!queryableSelection.includes(key)){ + return null + } + // Checking if the value is an object or array, and not rendering it if it is + if (typeof value === 'object' && value !== null) { + return null; + } + return ( + <div key={key}> + <strong>{key}:</strong> {value} + </div> + ); + }) + } + </div> + </div> + </div> + </CardContent> + </CardActionArea> + )} + {/* This checks for the stac API */} + {stacAPIFlag && ( <CardActions> <div className="resultLinks"> <Stack direction="row" spacing={1}> @@ -211,6 +262,7 @@ export function FootprintCard(props){ </Stack> </div> </CardActions> + )} </Card> ); } \ No newline at end of file diff --git a/src/components/presentational/SearchAndFilterInput.jsx b/src/components/presentational/SearchAndFilterInput.jsx index 7001d745650829e91484b1a8c552a7864d5fb8b4..34b7cb25c2597a373fa3ff8e14f19a6ca29b918d 100644 --- a/src/components/presentational/SearchAndFilterInput.jsx +++ b/src/components/presentational/SearchAndFilterInput.jsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; // Keyword Filter import TextField from "@mui/material/TextField"; // Date Range @@ -18,6 +18,7 @@ import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; import { Collapse, Divider } from "@mui/material"; +import ListItemText from "@mui/material/ListItemText"; /** * Controls css styling for this component using js to css @@ -84,6 +85,8 @@ export default function SearchAndFilterInput(props) { const [dateFromVal, setDateFromVal] = React.useState(null); // From Date const [dateToVal, setDateToVal] = React.useState(null); // To Date + //const for callback + const {UpdateQueryableTitles} = props; const handleExpandFilterClick = () => { setExpandFilter(!expandFilter); } @@ -144,6 +147,87 @@ export default function SearchAndFilterInput(props) { props.setFilterString(myFilterString); } + // initialize pyGeoAPI flag + let pyGeoAPIFlag = false; + + // New state for queryable titles + const [queryableTitles, setQueryableTitles] = useState([]); + + // all collections + const collection = props.target.collections; + + // retrieves all PyGEO collections + const isInPyAPI = collection.filter(data => data.hasOwnProperty('itemType')); + + // finds and assigns the selected collection from the PYGEO api + const selectedCollection = isInPyAPI.find(data => data.title === props.selectedTitle); + + // retrieves all pyGEO titles + const collectionTitles = isInPyAPI.map(data => data.title); + + + + // checks if correct title selected + if (collectionTitles.includes(props.selectedTitle)) + { + //set pyGeoAPI flag + pyGeoAPIFlag = true; + + // set the selected link + let QueryableDirectoryLink = selectedCollection.links.find(link => link.rel === "queryables").href; + + // creates URL to get the properties + let QueryableURL = 'https://astrogeology.usgs.gov/pygeoapi/' + QueryableDirectoryLink; + + // fetches URL to get the properties + fetch(QueryableURL) + .then(response => response.json()) + .then(data => { + + let queryableTitlesArray = []; + + // Extract the "properties" property from the JSON response + let Queryables = data.properties; + + // loop over titles + for (const property in Queryables) { + if (Queryables.hasOwnProperty(property) && Queryables[property].hasOwnProperty("title")) { + + queryableTitlesArray.push(data.properties[property].title); + + } + } + + // Set the state with the queryable titles + setQueryableTitles(queryableTitlesArray); + + + }, []) + .catch(error => { + console.error("Error fetching data:", error); + }); + } + + + + + const [selectedOptions, setSelectedOptions] = useState([]); + + const handleOptionChange = event => { + const selectedValues = event.target.value; + setSelectedOptions(selectedValues); + + // Create an array of objects with selected option and value + const selectedOptionsWithValues = selectedValues.map((option) => ({ + option, + value: queryableTitles.find((title) => title.title === option)?.value, + })); + + // Pass the selected options and values to FootprintResults + UpdateQueryableTitles(selectedOptionsWithValues); + }; + + // Sorting const handleSortChange = (event) => { setSortVal(event.target.value); @@ -268,7 +352,33 @@ export default function SearchAndFilterInput(props) { /> </span> </div> - + + {pyGeoAPIFlag && ( + <div className="panelSection panelBar"> + <span> + <FormControl sx={{ minWidth: 150 }}> + <InputLabel id="selectQueryLabel" size="small"> + Select Query + </InputLabel> + <Select + labelId="selectQueryLabel" + label="Select Query" + multiple + value={selectedOptions} + onChange={handleOptionChange} + renderValue={(selected) => selected.join(', ')} + > + {queryableTitles.map((title) => ( + <MenuItem key={title} value={title}> + <Checkbox checked={selectedOptions.includes(title)} /> + <ListItemText primary={title} /> + </MenuItem> + ))} + </Select> + </FormControl> + </span> + </div> + )} <Divider/> <div className="panelSection"> diff --git a/src/components/presentational/Sidebar.jsx b/src/components/presentational/Sidebar.jsx index 9b6a93f513de36d266e876b90199f7e5c2b0b302..d362dd990d6396d2b4ac2178b525ce9d41b1113d 100644 --- a/src/components/presentational/Sidebar.jsx +++ b/src/components/presentational/Sidebar.jsx @@ -43,6 +43,22 @@ export default function Sidebar(props) { setShowSidePanel(!showSidePanel); }; + // State to hold the selected title + const [selectedTitle, setSelectedTitle] = React.useState(""); + + // Callback function to update selected title + const updateSelectedTitle = (newTitle) => { + setSelectedTitle(newTitle); + }; + + // State to hold the seleced queryables + let [updatedQueryableTitles, setUpdatedQueryableTitles] = React.useState(""); + + // Callback to update selected queryables + const UpdateQueryableTitles = (selectedQueryables) => { + updatedQueryableTitles = selectedQueryables; + setUpdatedQueryableTitles(selectedQueryables) + } return ( <> <div id="right-bar" className="scroll-parent"> @@ -55,12 +71,17 @@ export default function Sidebar(props) { <SearchAndFilterInput setFilterString={setFilterString} targetName={props.target.name} + target={props.target} + selectedTitle={selectedTitle} + UpdateQueryableTitles = {UpdateQueryableTitles} /> - <FootprintResults + <FootprintResults target={props.target} filterString={filterString} queryAddress={props.queryAddress} setQueryAddress={props.setQueryAddress} + updateSelectedTitle={updateSelectedTitle} + selectedQueryables = {updatedQueryableTitles} /> </Collapse> </div> diff --git a/src/js/FootprintFetcher.js b/src/js/FootprintFetcher.js index 66131177d7281bcf55cf42c9763c2e9ab3c1c918..3cbdea2ec886017624adfaa21e4ae2b1d7e5275d 100644 --- a/src/js/FootprintFetcher.js +++ b/src/js/FootprintFetcher.js @@ -132,18 +132,33 @@ export async function FetchFootprints(collection, page, step){ return jsonRes.features; } -export async function FetchStepRemainder(featureCollection, myStep){ +export async function FetchStepRemainder(featureCollection, myStep) { + if (!featureCollection || !featureCollection.features) { + console.error('Invalid featureCollection:', featureCollection); + return []; + } + let myPage = Math.ceil(featureCollection.features.length / myStep); let skip = featureCollection.features.length % myStep; let newFeatures = []; + let fullResponse; if (skip !== 0) { - newFeatures = await FetchFootprints(featureCollection, myPage, myStep); + fullResponse = await FetchFootprints(featureCollection, myPage, myStep); + + if (!fullResponse || !fullResponse.features) { + console.error('Invalid fullResponse:', fullResponse); + return []; + } - // If any features are returned, add the remainder needed to the current collection - if (newFeatures.length > 0) { - return newFeatures.slice(skip, newFeatures.length); - } + newFeatures = fullResponse.features; + + // Handle edge case where you may have requested more features than still available + if (newFeatures.length < myStep) { + return newFeatures; + } else { + return newFeatures.slice(skip, newFeatures.length); + } } return newFeatures; - } \ No newline at end of file +} diff --git a/src/styles.css b/src/styles.css index 888b1900801826e1cd1e9e834152ebfbf1002820..891787fc86e1da3cd722f071c1c0daf8ebeb69d6 100644 --- a/src/styles.css +++ b/src/styles.css @@ -338,6 +338,11 @@ Controls the CSS for projection buttons when there is no available projection / 20% 80%; } +.pyGeoResultContainer { + width: 230px; + +} + .resultSub { font-family: Roboto, Arial, Helvetica, sans-serif; font-size: small;