Commit ee7212d5 authored by Cain, Jacob Ryan's avatar Cain, Jacob Ryan
Browse files

Merge branch 'query-console' into 'main'

Load Leaflet layers from data fetched in react

Closes #37, #12, #18, #20, #23, #33, and #34

See merge request asc/geostac!13
parents d6d2b822 a0f516fd
Loading
Loading
Loading
Loading
+24 −8
Original line number Diff line number Diff line
import React, { useEffect, useState } from "react";
import UsgsHeader from "../presentational/UsgsHeader.jsx";
import UsgsFooter from "../presentational/UsgsFooter.jsx";
import Menubar from "../presentational/Menubar.jsx";
import GeoStacApp from "./GeoStacApp.jsx";
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
 */
export default function App() {

  const [showHeaderFooter, setShowHeaderFooter] = React.useState(true);

  const handleOpenCloseHeader = () => {
    setShowHeaderFooter(!showHeaderFooter);
  }

  const [mainComponent, setMainComponent] = useState(() => {
    return(
      <SplashScreen />
@@ -21,11 +28,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";

@@ -224,17 +231,26 @@ export default function App() {

    (async () => {
        aggregateMapList = await getStacAndAstroWebMapsData();
        setMainComponent(<GeoStacApp mapList={aggregateMapList}/>);
        setMainComponent(
            <GeoStacApp 
                mapList={aggregateMapList}
                astroWebMaps={mapsJson[astroWebMaps]}
                showHeaderFooter={showHeaderFooter}
                setShowHeaderFooter={setShowHeaderFooter}
            />);
    })();

    
  }, [])

  return (
    <>
      <UsgsHeader />
      <UsgsHeader visible={showHeaderFooter}/>
      <Menubar
        showHeaderFooter={showHeaderFooter}
        handleOpenCloseHeader={handleOpenCloseHeader}
      />
      {mainComponent}
      <UsgsFooter />
      <UsgsFooter visible={showHeaderFooter}/>
    </>
  );
}
+11 −38
Original line number Diff line number Diff line
@@ -2,24 +2,8 @@ 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";

/**
 * Controls css styling for this component using js to css
 */
let css = {
  appFlex: {
    position: "relative",
  },
  appFull: {
    position: "fixed",
    height: "100%",
    width: "100%",
  },
};

/**
 * GeoStacApp is the parent component for all of the other components of the main app.
@@ -31,15 +15,8 @@ let css = {
export default function GeoStacApp(props) {
  const [targetPlanet, setTargetPlanet] = React.useState(props.mapList.systems[4].bodies[0]);

  const [footprintData, setFootprintData] = React.useState([]);

  const [appFullWindow, setAppFullWindow] = React.useState(true);
  const [appViewStyle, setAppViewStyle] = React.useState(css.appFlex);

  const handleAppViewChange = () => {
    setAppFullWindow(!appFullWindow);
    setAppViewStyle(appFullWindow ? css.appFull : css.appFlex);
  };
  const [queryString, setQueryString] = React.useState("?");
  const [collectionUrls, setCollectionUrls] = React.useState([]);

  /**
   * Handles target body selection
@@ -49,17 +26,8 @@ export default function GeoStacApp(props) {
    setTargetPlanet(value);
  };

  const handleFootprintClick = () => {
    setFootprintData(getFeatures);
    //console.log(footprintData);
  };

  return (
    <div style={appViewStyle} className="flex col scroll-parent">
      <MenuBar
        handleAppViewChange={handleAppViewChange}
        appFullWindow={appFullWindow}
      />
    <div className="flex col scroll-parent">
      <div className="flex row scroll-parent">
        <div className="flex col">
          <ConsoleAppBar
@@ -68,13 +36,18 @@ export default function GeoStacApp(props) {
            bodyChange={handleTargetBodyChange}
          />
          <div id="map-area">
            <MapContainer target={targetPlanet.name} />
            <MapContainer target={targetPlanet.name} astroWebMaps={props.astroWebMaps}/>
          </div>
          <QueryConsole />
          <QueryConsole
            queryString={queryString}
            setQueryString={setQueryString}
            collectionUrls={collectionUrls}/>
        </div>
        <Sidebar
          queryString={queryString}
          setQueryString={setQueryString}
          setCollectionUrls={setCollectionUrls}
          target={targetPlanet}
          footprintNavClick={handleFootprintClick}
        />
      </div>
      <DisplayGeoTiff />
+2 −2
Original line number Diff line number Diff line
@@ -20,7 +20,7 @@ export default function MapContainer(props) {
   * handles all of the map intialization and creation.
   */
  useEffect( () => {
    let map = new AstroMap("map-container", props.target, {});
    let map = new AstroMap("map-container", props.target, props.astroWebMaps, {});
    let controlManager = new AstroControlManager(map);
    controlManager.addTo(map);
    setOldTarget(props.target)
@@ -48,7 +48,7 @@ export default function MapContainer(props) {
      document.getElementById("projectionSouthPole").classList.remove("disabled");

      // create new map with updated target
      let map = new AstroMap("map-container", props.target, {});
      let map = new AstroMap("map-container", props.target, props.astroWebMaps, {});
      let controlManager = new AstroControlManager(map);
      controlManager.addTo(map);
      setOldTarget(props.target)
+0 −43
Original line number Diff line number Diff line
@@ -22,13 +22,8 @@ import PublicIcon from "@mui/icons-material/Public"; // Planets
import DarkModeIcon from "@mui/icons-material/DarkMode"; // Moons
import CookieIcon from '@mui/icons-material/Cookie'; // Asteroids
import TravelExploreIcon from '@mui/icons-material/TravelExplore'; // Footprints.
// import PetsIcon from '@mui/icons-material/Pets';                 // Other
// import SatelliteAltIcon from '@mui/icons-material/SatelliteAlt'; // possible
// import ViewTimelineIcon from '@mui/icons-material/ViewTimeline'; // footprint
// import WhereToVoteIcon from '@mui/icons-material/WhereToVote';   // icons.
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import { textTransform } from "@mui/system";

/**
 * Controls css styling for this component using js to css
@@ -53,44 +48,6 @@ let css = {
  },
};


// Delete if new data loading works
// Unless we add images here
// Why is Puck/Titania not on this list?

const planets = [
  ["Mercury"],
  ["Venus"],
  ["Earth"],
  ["Mars"],
  ["Jupiter"],
  ["Saturn"],
  ["Uranus"],
  ["Neptune"],
  ["Pluto"],
];
const moons = [
  "Moon",
  "Ceres",
  "Mimas",
  "Titan",
  "Deimos",
  "Tethys",
  "Phoebe",
  "Iapetus",
  "Dione",
  "Enceladus",
  "Hyperion",
  "Io",
  "Callisto",
  "Europa",
  "Ganymede",
  "Rhea",
  "Phobos",
  "Vesta",
  "Charon",
];

/**
 * Dialog for selecting planets
 * @param {open, onClose, selectedValue} props
+202 −104
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, List, ListItemButton, ListItemText, Collapse, Divider, ListSubheader} from "@mui/material";

// icons
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.]
import TravelExploreIcon from '@mui/icons-material/TravelExplore'; // Footprints

// object with results
import { getFeatures } from "../../js/ApiJsonCollection";
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';

// 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";


/**
 * Skeleton to show when footprints are loading
@@ -29,7 +22,7 @@ function LoadingFootprints() {
  
  return (
    <div className="resultsList">
      { Array(5).fill(null).map((_, i) => (
      { Array(8).fill(null).map((_, i) => (
        <Card sx={{ width: 250, margin: 1}} key={i}>
          <CardContent sx={{padding: 0.9, paddingBottom: 0}}>
            <div className="resultContainer">
@@ -40,8 +33,6 @@ function LoadingFootprints() {
                <Skeleton/>
                <Skeleton/>
                <Skeleton/>
                <Skeleton/>
                <Skeleton/>
              </div>
            </div>
          </CardContent>
@@ -64,7 +55,7 @@ function NoFootprints(){
  return(
    <div style={{padding: 10, maxWidth: 268}}>
      <p>
        This target has no footprints. To see 
        This target has no footprints. To find 
        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.
@@ -73,6 +64,86 @@ function NoFootprints(){
  );
}

function FilterTooStrict(){
  return(
    <div style={{padding: 10, maxWidth: 268}}>
      <p>
        No footprints match this filter.
      </p>
      <p>
        To find more footprints: 
      </p>
      <ul>
        <li>Uncheck current filters</li>
        <li>Draw a larger search area</li>
        <li>Enter a wider date range to filter by</li>
      </ul>
    </div>
  );
}

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>&nbsp;{props.feature.collection}
            </div> */}
            <div className="resultSub">
              <strong>ID:</strong>&nbsp;{props.feature.id}
            </div>
            <div className="resultSub">
              <strong>Date:</strong>&nbsp;{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>
  );
}


/**
@@ -100,23 +171,22 @@ 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();
  };
  const [openCollection, setOpenCollection] = React.useState([]);

  function handleOpenCollection(index){
    const nextOpenCollection = openCollection.map((isOpen, curIndex) => {
      if (index === curIndex) {
        return !isOpen;
      }
      return isOpen;
    });
    setOpenCollection(nextOpenCollection);
  }

  useEffect(() => {

@@ -133,19 +203,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
@@ -163,6 +240,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];
@@ -170,34 +248,54 @@ 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);


        if(myFeatureCollections.length > openCollection.length){
          setOpenCollection(Array(myFeatureCollections.length).fill(true));
        }        

        let myMaxFootprintsMatched = 0;
        for(const collection of myFeatureCollections) {
          myMaxFootprintsMatched = Math.max(myMaxFootprintsMatched, collection.numberMatched);
        }
        props.setMaxFootprintsMatched(myMaxFootprintsMatched);

        // Send to Leaflet
        window.postMessage(["setFeatureCollections", myFeatureCollections], "*");
      })();

    } else {
@@ -205,12 +303,13 @@ export default function FootprintResults(props) {
      setHasFootprints(false);
    }

    // setTimeout(() => {
    //   setFeatures(getFeatures);
    // }, 1000); 

  }, [props.target.name, props.queryString]);

  let noFootprintsReturned = true;
  for(const collection of featureCollections){
    if(collection.numberReturned > 0) noFootprintsReturned = false;
  }

  return (
    <div style={css.root} className="scroll-parent">
      <div className="resultHeader">
@@ -231,54 +330,53 @@ export default function FootprintResults(props) {
      </div>
      {isLoading ? 
        <LoadingFootprints/>
      : noFootprintsReturned ?
        <FilterTooStrict/>
      : 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>&nbsp;{feature.collection}
                    </div>
                    <div className="resultSub">
                      <strong>ID:</strong>&nbsp;{feature.id}
                    </div>
                    <div className="resultSub">
                      <strong>Date:</strong>&nbsp;{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>
          <List sx={{maxWidth: 265, paddingTop: 0}}>
            {featureCollections.map((collection, collectionIndex) => (
              <React.Fragment key={collection.id}>
                {collection.features.length > 0 ? 
                <>
                  <ListItemButton onClick={() => handleOpenCollection(collectionIndex)}>
                    <ListItemText
                      sx={{marginTop: 0, marginBottom: 0}}
                      primary={
                        <p style={{fontSize: "12px", lineHeight: "15px", fontWeight: "bold", marginTop: 1, marginBottom: 1}}>{collection.title}</p>
                      } secondary={
                        <span className="collectionStatExpander">
                          <span>{
                            ((props.currentPage-1)*props.currentStep + 1) + "-"
                            + ((props.currentPage-1)*props.currentStep + collection.numberReturned)
                            + " of " + collection.numberMatched + " footprints"}</span>
                          <span className="flatExpander">{openCollection[collectionIndex] ? <ExpandLess /> : <ExpandMore />}</span>
                        </span>
                      }/>
                  </ListItemButton>
                  <Collapse in={openCollection[collectionIndex]}>
                    <Divider/>
                    <ListSubheader sx={{
                        overflow:"hidden", 
                        whiteSpace:"nowrap", 
                        backgroundColor:"rgb(248, 249, 250) none repeat scroll 0% 0%",
                        fontSize: "12px",
                        lineHeight: "24px",
                        borderBottom: "1px solid lightgrey",
                        boxShadow: "0px 1px 2px lightgrey"
                      }}>
                        {collection.title}
                      </ListSubheader>
                    {collection.features.map((feature) => (
                      <FootprintCard feature={feature} title={collection.title} key={feature.id}/>
                    ))}
                  </Collapse>
                </>
                : null }
                <Divider/>
              </React.Fragment>
            ))}
          </List>
        </div>
      :
        <NoFootprints/>
Loading