Unverified Commit 89ec03a6 authored by Andrew Usvat's avatar Andrew Usvat Committed by GitHub
Browse files

Merge pull request #18 from Geo-Kings/updateFootprintCard

Filtering on footprint cards complete
parents 5d83e9c3 b0d1fe20
Loading
Loading
Loading
Loading
+17 −2
Original line number Diff line number Diff line
@@ -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], "*");
  };
@@ -119,6 +126,7 @@ export default function FootprintResults(props) {
        
        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>
+110 −58
Original line number Diff line number Diff line
@@ -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,34 +104,34 @@ 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 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) 
  {
  //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;

      // set boolean
      stacAPIFlag = true;

      // display meta data for STAC api
      showMetadata = (value) => () => {
        geoTiffViewer.displayGeoTiff(value.assets.thumbnail.href);
@@ -122,36 +144,24 @@ export function FootprintCard(props){
        geoTiffViewer.openModal();
      }; 

      break;

  } 
  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;

    default:
      pyGeoAPIFlag = true;
        //display different modal for PyGeo API
      showMetadata = (value) => () => {
    geoTiffViewer.displayGeoTiff(ThumbnailLink);
        //geoTiffViewer.displayGeoTiff(ThumbnailLink);
        geoTiffViewer.changeMetaData(
          value.properties.datasetid,
          value.properties.productid,
          value.properties.datetime,
          value.links
        );
    geoTiffViewer.openModal();
    };
  }

    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>&nbsp;{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
+112 −2
Original line number Diff line number Diff line
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);
@@ -269,6 +353,32 @@ 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">
+22 −1
Original line number Diff line number Diff line
@@ -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
            target={props.target} 
            filterString={filterString}
            queryAddress={props.queryAddress}
            setQueryAddress={props.setQueryAddress}
            updateSelectedTitle={updateSelectedTitle} 
            selectedQueryables = {updatedQueryableTitles}
          />
        </Collapse>
      </div>
+22 −7
Original line number Diff line number Diff line
@@ -133,15 +133,30 @@ export async function FetchFootprints(collection, page, step){
}

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 [];
        }

        newFeatures = fullResponse.features;

      // If any features are returned, add the remainder needed to the current collection
      if (newFeatures.length > 0) {
        // 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);
        }
    }
Loading