Skip to content

Commit

Permalink
Merge branch 'main' into 186752905-filter-function-fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
eireland committed Jan 26, 2024
2 parents 5010573 + ddcb8df commit a41591a
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 59 deletions.
4 changes: 4 additions & 0 deletions src/components/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
background-color: #fff;
}

.info-icon {
cursor: pointer;
}

.header-divider {
width: 313px;
border: solid 1px #979797;
Expand Down
31 changes: 20 additions & 11 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const kInitialDimensions = {

export const App = () => {
const { state, setState } = useStateContext();
const { createNOAAItems } = useCODAPApi();
const { filterItems, createNOAAItems } = useCODAPApi();
const [statusMessage, setStatusMessage] = useState("");
const [isFetching, setIsFetching] = useState(false);
const { showModal } = state;
Expand All @@ -52,23 +52,29 @@ export const App = () => {
};

const fetchSuccessHandler = async (data: any) => {
const {stationTimezoneOffset, weatherStation, selectedFrequency, startDate, endDate, units} = state;
if (data && weatherStation) {
const {startDate, endDate, units, selectedFrequency,
weatherStation, timezone} = state;
const allDefined = (startDate && endDate && units && selectedFrequency &&
weatherStation && timezone);

if (data && allDefined) {
const formatDataProps = {
data,
stationTimezoneOffset,
timezone,
weatherStation,
frequency: selectedFrequency,
startDate,
endDate,
units
};
const dataRecords = formatData(formatDataProps);
const items = Array.isArray(dataRecords) ? dataRecords : [dataRecords];
const filteredItems = filterItems(items);
setStatusMessage("Sending weather records to CODAP");
await createNOAAItems(dataRecords, getSelectedDataTypes()).then(
await createNOAAItems(filteredItems, getSelectedDataTypes()).then(
function (result: any) {
setIsFetching(false);
setStatusMessage(`Retrieved ${dataRecords.length} cases`);
setStatusMessage(`Retrieved ${filteredItems.length} cases`);
return result;
},
function (msg: string) {
Expand Down Expand Up @@ -100,9 +106,12 @@ export const App = () => {
};

const handleGetData = async () => {
const { location, startDate, endDate, selectedFrequency, weatherStation, stationTimezoneOffset } = state;
const attributes = state.frequencies[selectedFrequency].attrs.map(attr => attr.name);
if (location && attributes && startDate && endDate && weatherStation && selectedFrequency) {
const { location, startDate, endDate, weatherStation, frequencies,
selectedFrequency, timezone } = state;
const attributes = frequencies[selectedFrequency].attrs.map(attr => attr.name);
const allDefined = (startDate && endDate && location && weatherStation && timezone);

if (allDefined) {
const isEndDateAfterStartDate = endDate.getTime() >= startDate.getTime();
if (isEndDateAfterStartDate) {
setStatusMessage("Fetching weather records from NOAA");
Expand All @@ -112,7 +121,7 @@ export const App = () => {
frequency: selectedFrequency,
weatherStation,
attributes,
stationTimezoneOffset
gmtOffset: timezone.gmtOffset
});
try {
const tRequest = new Request(tURL);
Expand Down Expand Up @@ -145,7 +154,7 @@ export const App = () => {
<div className="App">
<div className="header">
<span>Retrieve weather data from observing stations.</span>
<InfoIcon title="Get further information about this CODAP plugin" onClick={handleOpenInfo}/>
<InfoIcon className="info-icon" title="Get further information about this CODAP plugin" onClick={handleOpenInfo}/>
</div>
<div className="header-divider" />
<LocationPicker />
Expand Down
7 changes: 5 additions & 2 deletions src/components/attribute-filter.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ $filter-background-green: rgba(90, 249, 90, 0.25);
$filter-green: #2dbe5e;

.attribute-filter-container {

padding-bottom: 16px;
.table-header {
color: rgba(0, 0, 0, 0.48);
font-size: 10px;
Expand All @@ -26,6 +26,7 @@ $filter-green: #2dbe5e;
&.units-header {
min-width: 36px;
background-color: rgba(126, 126, 126, 0.3);
cursor: pointer;
}
&.filter-header {
width: 41px;
Expand Down Expand Up @@ -60,6 +61,7 @@ $filter-green: #2dbe5e;
color: #177991;
font-size: 10px;
box-sizing: border-box;
cursor: pointer;

&.filtering {
background-color: $filter-background-green;
Expand All @@ -86,7 +88,7 @@ $filter-green: #2dbe5e;
}
}

table tr:nth-child(odd) {
table tr:nth-child(even) {
background-color: rgba(216, 216, 216, 0.3);
}

Expand Down Expand Up @@ -130,6 +132,7 @@ table tr:nth-child(odd) {
text-align: right;
color: #177991;
margin: 0 3px;
cursor: pointer;
}
svg {
margin-right: 3px;
Expand Down
1 change: 1 addition & 0 deletions src/components/attribute-selector.scss
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
font-size: 12px;
font-weight: 500;
color: #a2a2a2;
cursor: pointer;

&:hover {
border: solid 1px rgba(0, 144, 164, 0.25);
Expand Down
10 changes: 9 additions & 1 deletion src/components/attribute-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import classnames from "classnames";
import { useStateContext } from "../hooks/use-state";
import { dailyMonthlyAttrMap, hourlyAttrMap } from "../types";
Expand All @@ -15,6 +15,14 @@ export const AttributesSelector = () => {
const attributeNamesList = selectedFrequency === "hourly" ? hourlyAttributeNames : dailyMonthlyAttributeNames;
const selectedAttrsAndFiltersForFrequency = frequencies[selectedFrequency];

useEffect(() => {
if (frequencies[selectedFrequency].attrs.length === attributeList.length) {
setAllSelected(true);
} else {
setAllSelected(false);
}
}, [attributeList.length, frequencies, selectedFrequency]);

const handleUnitsClicked = () => {
setState(draft => {
draft.units = draft.units === "standard" ? "metric" : "standard";
Expand Down
3 changes: 3 additions & 0 deletions src/components/location-picker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@
margin-top: 2px;
margin-left: -37px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

&.geoname-candidate {
background-color: #ddeff1;
Expand Down
73 changes: 52 additions & 21 deletions src/components/location-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { useEffect, useRef, useState } from "react";
import classnames from "classnames";
import { createMap, selectStations } from "../utils/codapHelpers";
import { autoComplete, geoLocSearch } from "../utils/geonameSearch";
import { kStationsCollectionName, geonamesUser, kOffsetMap, timezoneServiceURL } from "../constants";
import { useStateContext } from "../hooks/use-state";
import { IPlace } from "../types";
import { findNearestActiveStation } from "../utils/getWeatherStations";
Expand All @@ -12,7 +14,8 @@ import CurrentLocationIcon from "../assets/images/icon-current-location.svg";
import "./location-picker.scss";

export const LocationPicker = () => {
const {state, setState} = useStateContext();
const { state, setState } = useStateContext();
const { location, units, weatherStation, weatherStationDistance } = state;
const [showMapButton, setShowMapButton] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [locationPossibilities, setLocationPossibilities] = useState<IPlace[]>([]);
Expand All @@ -22,21 +25,14 @@ export const LocationPicker = () => {
const locationDivRef = useRef<HTMLDivElement>(null);
const locationInputEl = useRef<HTMLInputElement>(null);
const locationSelectionListEl = useRef<HTMLUListElement>(null);
const selectedLocation = state.location;
const unit = state.units;
const unitDistanceText = unit === "standard" ? "mi" : "km";
const stationDistance = state.weatherStationDistance && unit === "standard"
? Math.round((state.weatherStationDistance * 0.6 * 10) / 10)
:state.weatherStationDistance && Math.round(state.weatherStationDistance * 10) / 10;

const handleOpenMap = () => {
//send request to CODAP to open map with available weather stations
};
const unitDistanceText = units === "standard" ? "mi" : "km";
const stationDistance = weatherStationDistance && units === "standard"
? Math.round((weatherStationDistance * 0.6 * 10) / 10)
: weatherStationDistance && Math.round(weatherStationDistance * 10) / 10;

useEffect(() => {
if (locationInputEl.current?.value === "") {
setShowSelectionList(false);
// setSelectedLocation(undefined);
}
}, [locationInputEl.current?.value]);

Expand All @@ -47,19 +43,45 @@ export const LocationPicker = () => {
}, [isEditing]);

useEffect(() => {
if (selectedLocation) {
findNearestActiveStation(selectedLocation.latitude, selectedLocation.longitude, 80926000, "present")
if (location) {
findNearestActiveStation(location.latitude, location.longitude, 80926000, "present")
.then(({station, distance}) => {
if (station) {
setState((draft) => {
draft.weatherStation = station;
draft.weatherStationDistance = distance;
});

const fetchTimezone = async (lat: number, long: number) => {
let url = `${timezoneServiceURL}?lat=${lat}&lng=${long}&username=${geonamesUser}`;
let res = await fetch(url);
if (res) {
if (res.ok) {
const timezoneData = await res.json();
const { gmtOffset } = timezoneData as { gmtOffset: keyof typeof kOffsetMap };
setState((draft) => {
draft.timezone = {
gmtOffset,
name: kOffsetMap[gmtOffset]
};
});
} else {
console.warn(res.statusText);
}
} else {
console.warn(`Failed to fetch timezone data for ${location}`);
}
};
fetchTimezone(location.latitude, location.longitude);
}
});
} else {
setState((draft) => {
draft.timezone = undefined;
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},[selectedLocation]);
}, [location]);

const getLocationList = () => {
if (locationInputEl.current) {
Expand Down Expand Up @@ -178,17 +200,26 @@ export const LocationPicker = () => {
setIsEditing(true);
};

const handleOpenMap = () => {
if (weatherStation) {
createMap(kStationsCollectionName, {width: 500, height: 350}, [weatherStation.latitude, weatherStation.longitude], 7);
selectStations([weatherStation.name]);
} else if (location) {
createMap(kStationsCollectionName, {width: 500, height: 350}, [location.latitude, location.longitude], 7);
}
};

return (
<div className="location-picker-container">
<div className="location-header">
<span className="location-title">Location</span>
{ selectedLocation && !isEditing &&
{ location && !isEditing &&
<div className="selected-weather-station">
{ state.weatherStation &&
{ weatherStation &&
<>
{state.weatherStationDistance &&
{weatherStationDistance &&
<span className="station-distance">({stationDistance} {unitDistanceText}) </span>}
<span className="station-name">{state.weatherStation?.name}</span>
<span className="station-name">{weatherStation?.name}</span>
{/* <EditIcon /> hide this for now until implemented*/}
</>
}
Expand All @@ -200,10 +231,10 @@ export const LocationPicker = () => {
<div ref={locationDivRef} className={classnames("location-input-wrapper", {"short" : showMapButton, "editing": isEditing})}
onClick={handleLocationInputClick}>
<LocationIcon />
{ selectedLocation && !isEditing
{ location && !isEditing
? <div>
<span className="selected-loc-intro">Stations near </span>
<span className="selected-loc-name">{state.location?.name}</span>
<span className="selected-loc-name">{location?.name}</span>
</div>
: <input ref={locationInputEl} className="location-input" type="text" placeholder={"Enter location or identifier here"}
onChange={handleLocationInputChange} onKeyDown={handleInputKeyDown} onBlur={handleLocationInputBlur}/>
Expand Down
10 changes: 10 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,13 @@ export const kWeatherStationCollectionAttrs = [
},
}
];

export const kOffsetMap = {
"-4": "AST",
"-5": "EST",
"-6": "CST",
"-7": "MST",
"-8": "PST",
"-9": "AKST",
"-10": "HST"
};
53 changes: 46 additions & 7 deletions src/hooks/use-codap-api.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Attribute, Collection, DataContext, IDataType } from "../types";
import { Attribute, Collection, DataContext, IDataType, IItem } from "../types";
import { IResult, codapInterface, createItems, getDataContext } from "@concord-consortium/codap-plugin-api";
import { DSCollection1, DSCollection2, DSName } from "../constants";
import { useStateContext } from "./use-state";
Expand Down Expand Up @@ -138,17 +138,55 @@ export const useCODAPApi = () => {
return Promise.all(promises);
};

const arrayify = (value: any) => {
return Array.isArray(value) ? value : [value];
const filterItems = (items: IItem[]) => {
const { selectedFrequency, frequencies } = state;
const { attrs, filters } = frequencies[selectedFrequency];
const filteredItems = items.filter((item: IItem) => {
const allFiltersMatch: boolean[] = [];
filters.forEach((filter) => {
const { attribute, operator } = filter;
const attrKey = attrs.find((attr) => attr.name === attribute)?.abbr;
if (attrKey) {
const itemValue = Number(item[attrKey]);
if (operator === "equals") {
allFiltersMatch.push(itemValue === filter.value);
} else if (operator === "doesNotEqual") {
allFiltersMatch.push(itemValue !== filter.value);
} else if (operator === "greaterThan") {
allFiltersMatch.push(itemValue > filter.value);
} else if (operator === "lessThan") {
allFiltersMatch.push(itemValue < filter.value);
} else if (operator === "greaterThanOrEqualTo") {
allFiltersMatch.push(itemValue >= filter.value);
} else if (operator === "lessThanOrEqualTo") {
allFiltersMatch.push(itemValue <= filter.value);
} else if (operator === "between") {
const { lowerValue, upperValue } = filter;
allFiltersMatch.push(itemValue > lowerValue && itemValue < upperValue);
} else if (operator === "top" || operator === "bottom") {
const sortedItems = items.sort((a, b) => {
return Number(b[attrKey]) - Number(a[attrKey]);
});
const end = operator === "top" ? filter.value : sortedItems.length;
const itemsToCheck = sortedItems.slice(end - filter.value, end);
allFiltersMatch.push(itemsToCheck.includes(item));
} else if (operator === "aboveMean" || operator === "belowMean") {
const mean = items.reduce((acc, i) => acc + Number(i[attrKey]), 0) / items.length;
const expression = operator === "aboveMean" ? itemValue > mean : itemValue < mean;
allFiltersMatch.push(expression);
}
}
});
return allFiltersMatch.every((match) => match === true);
});
return filteredItems;
};

const createNOAAItems = async (dataRecords: any, dataTypes: IDataType[]) => {
const createNOAAItems = async (items: IItem[], dataTypes: IDataType[]) => {
await updateWeatherDataset(dataTypes);
const items = arrayify(dataRecords);
// eslint-disable-next-line no-console
console.log("noaa-cdo ... createNOAAItems with " + dataRecords.length + " case(s)");
console.log("noaa-cdo ... createNOAAItems with " + items.length + " case(s)");
await createItems(DSName, items);

await codapInterface.sendRequest({
"action": "create",
"resource": "component",
Expand All @@ -161,6 +199,7 @@ export const useCODAPApi = () => {
};

return {
filterItems,
createNOAAItems
};
};
Loading

0 comments on commit a41591a

Please sign in to comment.