Loading src/components/presentational/FootprintResults.jsx +17 −2 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -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], "*"); }; Loading Loading @@ -119,6 +126,7 @@ export default function FootprintResults(props) { let isInPyAPI = collection.hasOwnProperty("itemType"); // check for pygeo api if (isInPyAPI) { Loading Loading @@ -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"> Loading Loading @@ -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> Loading src/components/presentational/ResultsAccessories.jsx +110 −58 Original line number Diff line number Diff line Loading @@ -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(){ Loading @@ -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); Loading @@ -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], "*"); Loading @@ -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" > Loading @@ -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}> Loading @@ -211,6 +262,7 @@ export function FootprintCard(props){ </Stack> </div> </CardActions> )} </Card> ); } No newline at end of file src/components/presentational/SearchAndFilterInput.jsx +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 Loading @@ -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 Loading Loading @@ -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); } Loading Loading @@ -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); Loading Loading @@ -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"> Loading src/components/presentational/Sidebar.jsx +22 −1 Original line number Diff line number Diff line Loading @@ -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"> Loading @@ -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> Loading src/js/FootprintFetcher.js +22 −7 Original line number Diff line number Diff line Loading @@ -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 Loading
src/components/presentational/FootprintResults.jsx +17 −2 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -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], "*"); }; Loading Loading @@ -119,6 +126,7 @@ export default function FootprintResults(props) { let isInPyAPI = collection.hasOwnProperty("itemType"); // check for pygeo api if (isInPyAPI) { Loading Loading @@ -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"> Loading Loading @@ -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> Loading
src/components/presentational/ResultsAccessories.jsx +110 −58 Original line number Diff line number Diff line Loading @@ -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(){ Loading @@ -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); Loading @@ -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], "*"); Loading @@ -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" > Loading @@ -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}> Loading @@ -211,6 +262,7 @@ export function FootprintCard(props){ </Stack> </div> </CardActions> )} </Card> ); } No newline at end of file
src/components/presentational/SearchAndFilterInput.jsx +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 Loading @@ -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 Loading Loading @@ -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); } Loading Loading @@ -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); Loading Loading @@ -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"> Loading
src/components/presentational/Sidebar.jsx +22 −1 Original line number Diff line number Diff line Loading @@ -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"> Loading @@ -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> Loading
src/js/FootprintFetcher.js +22 −7 Original line number Diff line number Diff line Loading @@ -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