diff --git a/src/components/App.scss b/src/components/App.scss index fcb1f10..e7ff949 100755 --- a/src/components/App.scss +++ b/src/components/App.scss @@ -1,6 +1,6 @@ .App { padding: 12px; - box-sizing: content-box; + box-sizing: border-box; display: flex; flex-direction: column; align-items: center; diff --git a/src/components/App.tsx b/src/components/App.tsx index 72844bf..9e0adda 100755 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -10,7 +10,6 @@ import { adjustStationDataset, getWeatherStations } from "../utils/getWeatherSta import { addNotificationHandler, createStationsDataset, guaranteeGlobal } from "../utils/codapHelpers"; import InfoIcon from "../assets/images/icon-info.svg"; import { useCODAPApi } from "../hooks/use-codap-api"; -import { dataTypeStore } from "../utils/noaaDataTypes"; import { composeURL, formatData } from "../utils/noaaApiHelper"; import { IDataType } from "../types"; import { StationDSName, globalMaxDate, globalMinDate } from "../constants"; @@ -23,7 +22,7 @@ const kPluginName = "NOAA Weather Station Data"; const kVersion = "0014"; const kInitialDimensions = { width: 360, - height: 650 + height: 670 }; export const App = () => { @@ -49,6 +48,7 @@ export const App = () => { draft.weatherStation = station; draft.location = {name: locationName, latitude, longitude}; draft.weatherStationDistance = 0; + draft.zoomMap = false; }); } } @@ -76,14 +76,6 @@ export const App = () => { }); }; - const getSelectedDataTypes = () => { - const { selectedFrequency } = state; - const attributes = state.frequencies[selectedFrequency].attrs.map(attr => attr.name); - return attributes.map((attr) => { - return dataTypeStore.findByName(attr); - }) as IDataType[]; - }; - const fetchSuccessHandler = async (data: any) => { const {startDate, endDate, units, selectedFrequency, weatherStation, timezone} = state; @@ -104,7 +96,7 @@ export const App = () => { const items = Array.isArray(dataRecords) ? dataRecords : [dataRecords]; const filteredItems = filterItems(items); setStatusMessage("Sending weather records to CODAP"); - await createNOAAItems(filteredItems, getSelectedDataTypes()).then( + await createNOAAItems(filteredItems).then( function (result: any) { setIsFetching(false); setStatusMessage(`Retrieved ${filteredItems.length} cases`); diff --git a/src/components/attribute-filter.tsx b/src/components/attribute-filter.tsx index f63002c..adaea95 100644 --- a/src/components/attribute-filter.tsx +++ b/src/components/attribute-filter.tsx @@ -289,9 +289,13 @@ const FilterModal = ({attr, position, targetFilterBottom, setShowFilterModal, se } else if (operator === "aboveMean" || operator === "belowMean") { return null; } else if (operator === "top" || operator === "bottom") { - return ; + return + ; } else { - return ; + return + ; } }; diff --git a/src/components/attribute-selector.scss b/src/components/attribute-selector.scss index d12147a..8ffea65 100644 --- a/src/components/attribute-selector.scss +++ b/src/components/attribute-selector.scss @@ -60,10 +60,10 @@ } } - .attribute-selection{ + .attribute-selection { display: flex; flex-wrap: wrap; - margin: 8px 0 11px 0; + margin: 8px 12px 11px 12px; .attribute-button { display: flex; diff --git a/src/components/date-range/calendars.tsx b/src/components/date-range/calendars.tsx index 2813d65..fda7f89 100644 --- a/src/components/date-range/calendars.tsx +++ b/src/components/date-range/calendars.tsx @@ -1,8 +1,9 @@ -import React from "react"; -import "./calendars.scss"; +import React, { useEffect, useState } from "react"; import { Calendar } from "./calendar"; import { useStateContext } from "../../hooks/use-state"; +import "./calendars.scss"; + interface ICalendarsProps { selectedCalendar: string | undefined; handleSelectCalendar: (calendar: string) => void; @@ -10,8 +11,22 @@ interface ICalendarsProps { } export const Calendars = ({selectedCalendar, handleSelectCalendar, closeCalendars}: ICalendarsProps) => { - const {state} = useStateContext(); - const {weatherStation} = state; + const { state } = useStateContext(); + const { weatherStation } = state; + const [activeDates, setActiveDates] = useState<{from: string, to: string}>({from: "", to: ""}); + + useEffect(() => { + if (weatherStation) { + const {mindate, maxdate} = weatherStation; //"1973-01-01" + const formatDate = (date: string) => { + const [year, month, day] = date.split("-"); + return `${month}/${day}/${year}`; + }; + const from = formatDate(mindate); + const to = maxdate === "present" ? "present" : formatDate(maxdate); + setActiveDates({from, to}); + } + }, [weatherStation]); return (
@@ -24,12 +39,16 @@ export const Calendars = ({selectedCalendar, handleSelectCalendar, closeCalendar
-
-
{weatherStation?.name || "WEATHER STATION"}
-
- MM/DD/YYYY - MM/DD/YYYY +
+ { weatherStation && + <> +
{weatherStation?.name || "WEATHER STATION"}
+
+ {activeDates.from} - {activeDates.to} +
+ + }
-
diff --git a/src/components/date-range/date-range.scss b/src/components/date-range/date-range.scss index b939851..8447197 100644 --- a/src/components/date-range/date-range.scss +++ b/src/components/date-range/date-range.scss @@ -3,7 +3,7 @@ .date-range-container { height: 73px; padding: 8px 0px; - width:s $inner-container-width; + width: $inner-container-width; background-color: #fff; .date-range-header { diff --git a/src/hooks/use-codap-api.tsx b/src/hooks/use-codap-api.tsx index efb4e9b..f6fb53b 100644 --- a/src/hooks/use-codap-api.tsx +++ b/src/hooks/use-codap-api.tsx @@ -1,20 +1,76 @@ -import { Attribute, Collection, DataContext, IDataType, IItem } from "../types"; -import { IResult, codapInterface, createItems, getDataContext } from "@concord-consortium/codap-plugin-api"; -import { DSCollection1, DSCollection2, DSName, kStationsDatasetName } from "../constants"; +import { useEffect, useState } from "react"; import { useStateContext } from "./use-state"; -import { useEffect } from "react"; -import { createMap, selectStations } from "../utils/codapHelpers"; +import { Attribute, Collection, DataContext, ICODAPItem, IDataType, IItem } from "../types"; +import { IResult, codapInterface, createItems, getAllItems, getDataContext } from "@concord-consortium/codap-plugin-api"; +import { DSCollection1, DSCollection2, DSName, kStationsCollectionName } from "../constants"; +import { clearData, createMap, selectStations } from "../utils/codapHelpers"; +import { dataTypeStore } from "../utils/noaaDataTypes"; export const useCODAPApi = () => { const {state} = useStateContext(); + const [ selectedDataTypes, setSelectedDataTypes ] = useState([]); + const { frequencies, selectedFrequency, weatherStation, units, isMapOpen, zoomMap } = state; + const { attrs } = frequencies[selectedFrequency]; useEffect(() => { - if (state.weatherStation && state.isMapOpen) { - const zoom = state.zoomMap ? 7 : null; - createMap(kStationsDatasetName, {width: 500, height: 350}, [state.weatherStation.latitude, state.weatherStation.longitude], zoom); - selectStations([state.weatherStation.name]); + if (weatherStation && isMapOpen) { + const zoom = zoomMap ? 7 : null; + createMap(kStationsCollectionName, {width: 500, height: 350}, [weatherStation.latitude, weatherStation.longitude], zoom); + selectStations([weatherStation.name]); } - }, [state.isMapOpen, state.weatherStation, state.zoomMap]); + }, [isMapOpen, weatherStation, zoomMap]); + + useEffect(() => { + const attributes = frequencies[selectedFrequency].attrs.map(attr => attr.name); + const dataTypes = attributes.map((attr) => { + return dataTypeStore.findByName(attr); + }) as IDataType[]; + setSelectedDataTypes(dataTypes); + }, [selectedFrequency, frequencies, attrs]); + + const createNOAAItems = async (items: IItem[]) => { + await updateWeatherDataset(selectedDataTypes); + // eslint-disable-next-line no-console + console.log("noaa-cdo ... createNOAAItems with " + items.length + " case(s)"); + await createItems(DSName, items); + await codapInterface.sendRequest({ + "action": "create", + "resource": "component", + "values": { + "type": "caseTable", + "dataContext": DSName, + "horizontalScrollOffset": 500 + } + }); + }; + + useEffect(() => { + const updateUnitsInCODAP = async () => { + let doesDataCtxExist = await getDataContext(DSName); + if (!doesDataCtxExist || !doesDataCtxExist.success) { + return; + } + const oldUnits = units === "metric" ? "standard" : "metric"; + // fetch existing items in existing dataset + let allItemsRes = await getAllItems(DSName); + let allItems = allItemsRes.values.map((item: {id: string, values: ICODAPItem}) => item.values); + // convert from old units to new units + allItems.forEach(function (item: ICODAPItem) { + Object.keys(item).forEach(function (attrName) { + let dataType = dataTypeStore.findByAbbr(attrName); + if (dataType && dataType.convertUnits) { + item[attrName] = dataType.convertUnits(dataType.units[oldUnits], dataType.units[units], item[attrName]); + } + }); + }); + // clear dataset + await clearData(DSName); + // insert items + await createNOAAItems(allItems); + }; + updateUnitsInCODAP(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [units]); const getNoaaDataContextSetupObject = () => { return { @@ -69,14 +125,13 @@ export const useCODAPApi = () => { }; const createAttribute = (datasetName: string, collectionName: string, dataType: IDataType) => { - const {units} = state; return codapInterface.sendRequest({ - action: "create", - resource: "dataContext[" + datasetName + "].collection[" + collectionName + "].attribute", - values: { - name: dataType.name, - unit: dataType.units[units], - description: dataType.description + action: "create", + resource: "dataContext[" + datasetName + "].collection[" + collectionName + "].attribute", + values: { + name: dataType.name, + unit: dataType.units[units], + description: dataType.description } }); }; @@ -101,7 +156,6 @@ export const useCODAPApi = () => { }; const updateWeatherDataset = async (dataTypes: IDataType[]) => { - const {units} = state; let result = await getDataContext(DSName); if (!result || !result.success) { @@ -123,34 +177,33 @@ export const useCODAPApi = () => { const attrDefs: Attribute[] = []; dataSetDef.collections.forEach(function (collection: Collection) { - collection.attrs.forEach(function (attr) { - attrDefs.push(attr); - }); + collection.attrs.forEach(function (attr) { + attrDefs.push(attr); + }); }); const lastCollection = dataSetDef.collections[dataSetDef.collections.length - 1]; const promises = dataTypes.map(function (dataType) { - const attrName = dataType.name; - const attrDef = attrDefs.find(function (ad) { - return ad.name === attrName; - }); - if (!attrDef) { - return createAttribute(DSName, lastCollection.name, dataType); - } else { - let unit = dataType.units[units]; - if (attrDef.unit !== unit) { - return updateAttributeUnit(dataSetDef, attrName, unit); - } else { - return Promise.resolve("Unknown attribute."); - } - } + const attrName = dataType.name; + const attrDef = attrDefs.find(function (ad) { + return ad.name === attrName; + }); + if (!attrDef) { + return createAttribute(DSName, lastCollection.name, dataType); + } else { + let unit = dataType.units[units]; + if (attrDef.unit !== unit) { + return updateAttributeUnit(dataSetDef, attrName, unit); + } else { + return Promise.resolve("Unknown attribute."); + } + } }); return Promise.all(promises); }; const filterItems = (items: IItem[]) => { - const { selectedFrequency, frequencies } = state; - const { attrs, filters } = frequencies[selectedFrequency]; + const { filters } = frequencies[selectedFrequency]; const filteredItems = items.filter((item: IItem) => { const allFiltersMatch: boolean[] = []; filters.forEach((filter) => { @@ -192,22 +245,6 @@ export const useCODAPApi = () => { return filteredItems; }; - const createNOAAItems = async (items: IItem[], dataTypes: IDataType[]) => { - await updateWeatherDataset(dataTypes); - // eslint-disable-next-line no-console - console.log("noaa-cdo ... createNOAAItems with " + items.length + " case(s)"); - await createItems(DSName, items); - await codapInterface.sendRequest({ - "action": "create", - "resource": "component", - "values": { - "type": "caseTable", - "dataContext": DSName, - "horizontalScrollOffset": 500 - } - }); - }; - return { filterItems, createNOAAItems diff --git a/src/types.ts b/src/types.ts index 4690e1e..8a9c74d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -255,3 +255,7 @@ export interface IRecord { export interface IItem { [key: string]: string; } + +export interface ICODAPItem { + [key: string]: any; +} diff --git a/src/utils/codapHelpers.ts b/src/utils/codapHelpers.ts index 2fde76e..5989ec7 100644 --- a/src/utils/codapHelpers.ts +++ b/src/utils/codapHelpers.ts @@ -33,50 +33,50 @@ const createMap = async (name: string, dimensions: IDimensions, center: ILatLong } if (!map) { - let result = await codapInterface.sendRequest({ - action: "create", resource: "component", values: { - type: "map", - name, - dimensions, - dataContextName: name, - legendAttributeName: "isActive" - } - }) as IResult; - if (result.success) { - map = result.values; - } + let result = await codapInterface.sendRequest({ + action: "create", resource: "component", values: { + type: "map", + name, + dimensions, + dataContextName: name, + legendAttributeName: "isActive" + } + }) as IResult; + if (result.success) { + map = result.values; + } } if (map && center && (zoom != null)) { - return centerAndZoomMap(map.id, center, zoom); + return centerAndZoomMap(map.id, center, zoom); } else { - return selectComponent(map.id); + return selectComponent(map.id); } }; const centerAndZoomMap = (mapName: string, center: ILatLong, zoom: number) => { - return new Promise((resolve) => { - setTimeout(function () { - codapInterface.sendRequest({ - action: "update", - resource: `component[${mapName}]`, - values: { - center, - zoom: 4 - } - }); - setTimeout(function () { - codapInterface.sendRequest({ - action: "update", - resource: `component[${mapName}]`, - values: { - zoom - } - }); - - }, 500); - }, 2000); - resolve(); - }); + return new Promise((resolve) => { + setTimeout(function () { + codapInterface.sendRequest({ + action: "update", + resource: `component[${mapName}]`, + values: { + center, + zoom: 4 + } + }); + setTimeout(function () { + codapInterface.sendRequest({ + action: "update", + resource: `component[${mapName}]`, + values: { + zoom + } + }); + + }, 500); + }, 2000); + resolve(); + }); }; const hasMap = async () => { diff --git a/src/utils/noaaApiHelper.ts b/src/utils/noaaApiHelper.ts index 31a1cbf..06ebfe9 100644 --- a/src/utils/noaaApiHelper.ts +++ b/src/utils/noaaApiHelper.ts @@ -3,20 +3,6 @@ import { IFrequency, IRecord, ITimeZone, IUnits, IWeatherStation } from "../type import { frequencyToReportTypeMap, nceiBaseURL } from "../constants"; import { dataTypeStore } from "./noaaDataTypes"; -export const convertUnits = (fromUnitSystem: IUnits, toUnitSystem: IUnits, data: any) => { - if (fromUnitSystem === toUnitSystem) { - return; - } - data.forEach(function (item: any) { - Object.keys(item).forEach(function (prop) { - let dataType = dataTypeStore.findByName(prop); - if (dataType && dataType.convertUnits) { - item[prop] = dataType.convertUnits(dataType.units[fromUnitSystem], dataType.units[toUnitSystem], item[prop]); - } - }); - }); -}; - interface IFormatData { data: IRecord[]; units: IUnits; @@ -26,7 +12,7 @@ interface IFormatData { } export const formatData = (props: IFormatData) => { - const {data, timezone, units, frequency, weatherStation} = props; + const {data, timezone, frequency, weatherStation} = props; const database = frequencyToReportTypeMap[frequency]; let dataRecords: any[] = []; data.forEach((r: any) => { @@ -39,12 +25,11 @@ export const formatData = (props: IFormatData) => { aValue["report type"] = frequency; dataRecords.push(aValue); }); - convertUnits("metric", units, dataRecords); return dataRecords; }; export const decodeData = (iField: string, iValue: any, database: string) => { - let dataType = dataTypeStore.findByName(iField); + let dataType = dataTypeStore.findByAbbr(iField); let decoder = dataType && dataType.decode && dataType.decode[database]; return decoder ? decoder(iValue) : iValue; }; diff --git a/src/utils/noaaDataTypes.ts b/src/utils/noaaDataTypes.ts index f8eabb6..feeca1b 100644 --- a/src/utils/noaaDataTypes.ts +++ b/src/utils/noaaDataTypes.ts @@ -8,8 +8,9 @@ import { } from "../constants"; import { Unit, unitMap } from "../types"; +type ConvertUnitsFunc = (fromUnit: Unit, toUnit: Unit, value: string) => number; interface ConverterMap { - [key: string]: null | ((fromUnit: Unit, toUnit: Unit, value: number) => number); + [key: string]: null | ConvertUnitsFunc; } const converterMap: ConverterMap = { @@ -21,42 +22,45 @@ const converterMap: ConverterMap = { pressure: null }; -function convertPrecip(fromUnit: Unit, toUnit: Unit, value: number) { - let k = 25.4; - if (!convertible(value)) { - return value; - } else if (fromUnit === "mm" && toUnit === "in") { - return value / k; - } else if (fromUnit === "in" && toUnit === "mm") { - return value * k; - } else { - return value; - } +function convertPrecip(fromUnit: Unit, toUnit: Unit, value: string) { + let k = 25.4; + const numValue = Number(value); + if (!convertible(value)) { + return numValue; + } else if (fromUnit === "mm" && toUnit === "in") { + return numValue / k; + } else if (fromUnit === "in" && toUnit === "mm") { + return numValue * k; + } else { + return numValue; + } } -function convertTemp(fromUnit: Unit, toUnit: Unit, value: number) { - if (!convertible(value)) { - return value; - } else if (fromUnit === "°C" && toUnit === "°F") { - return 1.8*value + 32; - } else if (fromUnit === "°F" && toUnit === "°C") { - return (value - 32) / 1.8; - } else { - return value; - } +function convertTemp(fromUnit: Unit, toUnit: Unit, value: string) { + const numValue = Number(value); + if (!convertible(value)) { + return numValue; + } else if (fromUnit === "°C" && toUnit === "°F") { + return (1.8 * numValue) + 32; + } else if (fromUnit === "°F" && toUnit === "°C") { + return (numValue - 32) / 1.8; + } else { + return numValue; + } } -function convertWindspeed(fromUnit: Unit, toUnit: Unit, value: number) { - let k=0.44704; - if (!convertible(value)) { - return value; - } else if (fromUnit === "m/s" && toUnit === "mph") { - return value / k; - } else if (fromUnit === "mph" && toUnit === "m/s") { - return value * k; - } else { - return value; - } +function convertWindspeed(fromUnit: Unit, toUnit: Unit, value: string) { + const numValue = Number(value); + let k=0.44704; + if (!convertible(value)) { + return numValue; + } else if (fromUnit === "m/s" && toUnit === "mph") { + return numValue / k; + } else if (fromUnit === "mph" && toUnit === "m/s") { + return numValue * k; + } else { + return numValue; + } } function formatNthCurry(n: number, separator: string, multiplier: number) { @@ -100,7 +104,7 @@ class NoaaType { description: string; datasetList: string[]; decode: undefined | ({ [key: string]: (value: any) => number|undefined }); - convertUnits: null | ((fromUnit: Unit, toUnit: Unit, value: number) => number); + convertUnits: null | ConvertUnitsFunc; constructor( sourceName: string, @@ -167,6 +171,12 @@ function findAllBySourceName(targetName: string) { }); } +function findByAbbr (abbr: string) { + return dataTypes.find(function (dataType) { + return abbr === dataType.name; + }); +} + function findByName(targetName: string) { return dataTypes.find(function (dataType) { return targetName === dataType.description; @@ -188,6 +198,7 @@ const dataTypeStore = { findByName, findAllByNoaaDataset, findAll, + findByAbbr }; export {NoaaType, dataTypeStore};