Skip to content

Commit

Permalink
Merge branch 'main' into 186719430-station-selection-list
Browse files Browse the repository at this point in the history
  • Loading branch information
eireland committed Jan 26, 2024
2 parents 8a0ae9a + ddcb8df commit ef06c13
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 40 deletions.
4 changes: 4 additions & 0 deletions src/components/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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 @@ -25,7 +25,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 Down Expand Up @@ -55,23 +55,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 @@ -103,9 +109,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 @@ -115,7 +124,7 @@ export const App = () => {
frequency: selectedFrequency,
weatherStation,
attributes,
stationTimezoneOffset
gmtOffset: timezone.gmtOffset
});
try {
const tRequest = new Request(tURL);
Expand Down Expand Up @@ -148,7 +157,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 @@ -192,6 +192,9 @@
margin-top: 2px;
margin-left: -37px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

&.geoname-candidate {
background-color: #ddeff1;
Expand Down
39 changes: 37 additions & 2 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, IStation } from "../types";
import { convertDistanceToStandard, findNearestActiveStations } from "../utils/getWeatherStations";
Expand Down Expand Up @@ -66,6 +68,7 @@ export const LocationPicker = () => {
}, [isEditing]);

useEffect(() => {
<<<<<<< HEAD
const _startDate = startDate ? startDate : new Date( -5364662060); // 1/1/1750
const _endDate = endDate ? endDate : new Date(Date.now());
if (location) {
Expand All @@ -79,6 +82,33 @@ export const LocationPicker = () => {
draft.weatherStation = stationList[0].station;
draft.weatherStationDistance = stationList[0].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
Expand Down Expand Up @@ -249,7 +279,12 @@ export const LocationPicker = () => {
};

const handleOpenMap = () => {
//send request to CODAP to open map with available weather stations
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 (
Expand Down Expand Up @@ -295,7 +330,7 @@ export const LocationPicker = () => {
{ 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
};
};
18 changes: 13 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ interface IWeatherStationID {
id: string;
}

export interface ITimeZone {
gmtOffset: string;
name: string;
}

export interface IState {
location?: IPlace;
weatherStation?: IWeatherStation;
Expand All @@ -112,8 +117,7 @@ export interface IState {
endDate?: Date;
units: IUnits;
showModal?: "info" | "data-return-warning";
stationTimezoneOffset?: number;
stationTimezoneName?: string;
timezone?: ITimeZone;
didUserSelectDate: boolean;
}

Expand All @@ -133,7 +137,7 @@ export const dailyMonthlyAttrMap: AttrType[] = [
{name: "Average temperature", abbr: "tAvg", unit: unitMap.temperature},
{name: "Precipitation", abbr: "precip", unit: unitMap.precipitation},
{name: "Snowfall", abbr: "snow", unit: unitMap.precipitation},
{name: "Average windspeed", abbr: "avgWind", unit: unitMap.speed}
{name: "Average wind speed", abbr: "avgWind", unit: unitMap.speed}
];

export const hourlyAttrMap: AttrType[] = [
Expand All @@ -148,9 +152,9 @@ export const hourlyAttrMap: AttrType[] = [

export const DefaultState: IState = {
selectedFrequency: "daily",
frequencies: {hourly: {attrs: [], filters: []},
frequencies: {hourly: {attrs: hourlyAttrMap, filters: []},
daily: {attrs: dailyMonthlyAttrMap, filters: []},
monthly: {attrs: [], filters: []}},
monthly: {attrs: dailyMonthlyAttrMap, filters: []}},
units: "standard",
didUserSelectDate: false,
};
Expand Down Expand Up @@ -243,3 +247,7 @@ export interface UnitMap {
export interface IRecord {
[key: string]: number | string | Date | IWeatherStation | IFrequency;
}

export interface IItem {
[key: string]: string;
}
Loading

0 comments on commit ef06c13

Please sign in to comment.