diff --git a/src/components/App.scss b/src/components/App.scss
index 62053a7..fcb1f10 100755
--- a/src/components/App.scss
+++ b/src/components/App.scss
@@ -20,6 +20,10 @@
background-color: #fff;
}
+ .info-icon {
+ cursor: pointer;
+ }
+
.header-divider {
width: 313px;
border: solid 1px #979797;
diff --git a/src/components/App.tsx b/src/components/App.tsx
index 22acb9d..655b531 100755
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -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;
@@ -55,11 +55,15 @@ 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,
@@ -67,11 +71,13 @@ export const App = () => {
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) {
@@ -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");
@@ -115,7 +124,7 @@ export const App = () => {
frequency: selectedFrequency,
weatherStation,
attributes,
- stationTimezoneOffset
+ gmtOffset: timezone.gmtOffset
});
try {
const tRequest = new Request(tURL);
@@ -148,7 +157,7 @@ export const App = () => {
Retrieve weather data from observing stations.
-
+
diff --git a/src/components/attribute-filter.scss b/src/components/attribute-filter.scss
index 9720fc8..c60dd2a 100644
--- a/src/components/attribute-filter.scss
+++ b/src/components/attribute-filter.scss
@@ -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;
@@ -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;
@@ -60,6 +61,7 @@ $filter-green: #2dbe5e;
color: #177991;
font-size: 10px;
box-sizing: border-box;
+ cursor: pointer;
&.filtering {
background-color: $filter-background-green;
@@ -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);
}
@@ -130,6 +132,7 @@ table tr:nth-child(odd) {
text-align: right;
color: #177991;
margin: 0 3px;
+ cursor: pointer;
}
svg {
margin-right: 3px;
diff --git a/src/components/attribute-selector.scss b/src/components/attribute-selector.scss
index 55ab762..d12147a 100644
--- a/src/components/attribute-selector.scss
+++ b/src/components/attribute-selector.scss
@@ -78,6 +78,7 @@
font-size: 12px;
font-weight: 500;
color: #a2a2a2;
+ cursor: pointer;
&:hover {
border: solid 1px rgba(0, 144, 164, 0.25);
diff --git a/src/components/attribute-selector.tsx b/src/components/attribute-selector.tsx
index 4995f1e..1723933 100644
--- a/src/components/attribute-selector.tsx
+++ b/src/components/attribute-selector.tsx
@@ -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";
@@ -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";
diff --git a/src/components/location-picker.scss b/src/components/location-picker.scss
index 7043b35..8369a43 100644
--- a/src/components/location-picker.scss
+++ b/src/components/location-picker.scss
@@ -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;
diff --git a/src/components/location-picker.tsx b/src/components/location-picker.tsx
index a233658..e7826f6 100644
--- a/src/components/location-picker.tsx
+++ b/src/components/location-picker.tsx
@@ -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";
@@ -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) {
@@ -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
@@ -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 (
@@ -295,7 +330,7 @@ export const LocationPicker = () => {
{ location && !isEditing
?
Stations near
- {state.location?.name}
+ {location?.name}
:
diff --git a/src/constants.ts b/src/constants.ts
index 72cee98..b833808 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -145,3 +145,13 @@ export const kWeatherStationCollectionAttrs = [
},
}
];
+
+export const kOffsetMap = {
+ "-4": "AST",
+ "-5": "EST",
+ "-6": "CST",
+ "-7": "MST",
+ "-8": "PST",
+ "-9": "AKST",
+ "-10": "HST"
+};
diff --git a/src/hooks/use-codap-api.tsx b/src/hooks/use-codap-api.tsx
index a1348f2..3cc3073 100644
--- a/src/hooks/use-codap-api.tsx
+++ b/src/hooks/use-codap-api.tsx
@@ -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";
@@ -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",
@@ -161,6 +199,7 @@ export const useCODAPApi = () => {
};
return {
+ filterItems,
createNOAAItems
};
};
diff --git a/src/types.ts b/src/types.ts
index 7c351ae..a0e02f2 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -100,6 +100,11 @@ interface IWeatherStationID {
id: string;
}
+export interface ITimeZone {
+ gmtOffset: string;
+ name: string;
+}
+
export interface IState {
location?: IPlace;
weatherStation?: IWeatherStation;
@@ -112,8 +117,7 @@ export interface IState {
endDate?: Date;
units: IUnits;
showModal?: "info" | "data-return-warning";
- stationTimezoneOffset?: number;
- stationTimezoneName?: string;
+ timezone?: ITimeZone;
didUserSelectDate: boolean;
}
@@ -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[] = [
@@ -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,
};
@@ -243,3 +247,7 @@ export interface UnitMap {
export interface IRecord {
[key: string]: number | string | Date | IWeatherStation | IFrequency;
}
+
+export interface IItem {
+ [key: string]: string;
+}
diff --git a/src/utils/noaaApiHelper.ts b/src/utils/noaaApiHelper.ts
index 2e5cb96..1d5aa59 100644
--- a/src/utils/noaaApiHelper.ts
+++ b/src/utils/noaaApiHelper.ts
@@ -1,5 +1,5 @@
import dayjs from "dayjs";
-import { IFrequency, IRecord, IUnits, IWeatherStation } from "../types";
+import { IFrequency, IRecord, ITimeZone, IUnits, IWeatherStation } from "../types";
import { frequencyToReportTypeMap, nceiBaseURL } from "../constants";
import { dataTypeStore } from "./noaaDataTypes";
@@ -19,23 +19,22 @@ export const convertUnits = (fromUnitSystem: IUnits, toUnitSystem: IUnits, data:
interface IFormatData {
data: IRecord[];
- stationTimezoneOffset?: number;
- stationTimezoneName?: string;
units: IUnits;
frequency: IFrequency;
weatherStation: IWeatherStation;
+ timezone: ITimeZone;
}
export const formatData = (props: IFormatData) => {
- const {data, stationTimezoneOffset, stationTimezoneName, units, frequency, weatherStation} = props;
+ const {data, timezone, units, frequency, weatherStation} = props;
const database = frequencyToReportTypeMap[frequency];
let dataRecords: any[] = [];
data.forEach((r: any) => {
const aValue = convertNOAARecordToValue(r, weatherStation, database);
aValue.latitude = weatherStation.latitude;
aValue.longitude = weatherStation.longitude;
- aValue["UTC offset"] = stationTimezoneOffset || "";
- aValue.timezone = stationTimezoneName || "";
+ aValue["UTC offset"] = timezone.gmtOffset;
+ aValue.timezone = timezone.name;
aValue.elevation = weatherStation.elevation;
aValue["report type"] = frequency;
dataRecords.push(aValue);
@@ -51,7 +50,7 @@ export const decodeData = (iField: string, iValue: any, database: string) => {
};
export const convertNOAARecordToValue = (iRecord: IRecord, weatherStation: IWeatherStation, database: string) => {
- let out: IRecord = {}; // to-do: add interface / type
+ let out: IRecord = {};
Object.keys(iRecord).forEach(function (key: any) {
let value = iRecord[key];
let dataTypeName;
@@ -84,19 +83,20 @@ interface IComposeURL {
frequency: IFrequency;
attributes: string[];
weatherStation: IWeatherStation;
- stationTimezoneOffset?: number;
+ gmtOffset: string;
}
export const composeURL = (props: IComposeURL) => {
- const { startDate, endDate, frequency, attributes, weatherStation, stationTimezoneOffset } = props;
+ const { startDate, endDate, frequency, attributes, weatherStation, gmtOffset } = props;
const database = frequencyToReportTypeMap[frequency];
const format = "YYYY-MM-DDThh:mm:ss";
let sDate = dayjs(startDate);
let eDate = dayjs(endDate);
+
// adjust for local station time
- if (database === "global-hourly" && stationTimezoneOffset) {
- sDate = dayjs(startDate).subtract(stationTimezoneOffset, "hour");
- eDate = dayjs(endDate).subtract(stationTimezoneOffset, "hour").add(1, "day");
+ if (database === "global-hourly") {
+ sDate = dayjs(startDate).subtract(Number(gmtOffset), "hour");
+ eDate = dayjs(endDate).subtract(Number(gmtOffset), "hour").add(1, "day");
}
const startDateString = dayjs(sDate).format(format);
const endDateString = dayjs(eDate).format(format);