From 30d322d66d6bd0ca69a1b566c7a2afad6322c48e Mon Sep 17 00:00:00 2001 From: eireland Date: Tue, 23 Jan 2024 15:38:58 -0800 Subject: [PATCH 1/8] WIP merge main --- src/components/location-picker.scss | 61 ++++++++++++++++++++----- src/components/location-picker.tsx | 70 ++++++++++++++++++++--------- src/types.ts | 2 +- src/utils/codapConnect.ts | 2 +- src/utils/getWeatherStations.ts | 26 +++++++---- 5 files changed, 120 insertions(+), 41 deletions(-) diff --git a/src/components/location-picker.scss b/src/components/location-picker.scss index 132e2e3..1f9fbb4 100644 --- a/src/components/location-picker.scss +++ b/src/components/location-picker.scss @@ -16,18 +16,57 @@ font-weight: bold; } - .selected-weather-station { - padding: 2px 2px 2px 8px; - background-color: $teal-light-25; - color: $teal-dark; - font-size: 10px; - font-weight: 500; - .station-distance { - font-style: italic; + .weather-station-wrapper { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + top: 0; + right: 0; + .selected-weather-station { + padding: 2px 2px 2px 8px; + background-color: $teal-light-25; + color: $teal-dark; + font-size: 10px; + font-weight: 500; + .station-distance { + font-style: italic; + } + svg { + height: 12px; + width: 12px; + } } - .location-edit-icon { - height: 12px; - width: 12px; + .station-selection-list { + width: 311px; + height: 86px; + margin: 25px 14px 42px 12px; + padding: 4px 18px 1px 13px; + box-shadow: 0 5px 8px 0 rgba(0, 0, 0, 0.5); + background-color: #fff; + visibility: hidden; + &.show { + visibility: visible; + z-index: 5; + } + + .station-selection { + font-family: "Montserrat", sans-serif; + font-size: 10px; + font-weight: 500; + + .station-name { + text-overflow: ellipsis; + color: #000; + } + .station-distance { + color: $teal-dark-75; + } + &.selected-station { + background-color: $teal-medium; + } + } + } } } diff --git a/src/components/location-picker.tsx b/src/components/location-picker.tsx index fcc7349..4301d76 100644 --- a/src/components/location-picker.tsx +++ b/src/components/location-picker.tsx @@ -2,8 +2,8 @@ import React, { useEffect, useRef, useState } from "react"; import classnames from "classnames"; import { autoComplete, geoLocSearch } from "../utils/geonameSearch"; import { useStateContext } from "../hooks/use-state"; -import { IPlace } from "../types"; -import { findNearestActiveStation } from "../utils/getWeatherStations"; +import { IPlace, IStation } from "../types"; +import { findNearestActiveStations } from "../utils/getWeatherStations"; import OpenMapIcon from "../assets/images/icon-map.svg"; import EditIcon from "../assets/images/icon-edit.svg"; import LocationIcon from "../assets/images/icon-location.svg"; @@ -17,17 +17,22 @@ export const LocationPicker = () => { const [isEditing, setIsEditing] = useState(false); const [locationPossibilities, setLocationPossibilities] = useState([]); const [showSelectionList, setShowSelectionList] = useState(false); + const [stationPossibilities, setStationPossibilities] = useState([]); + const [showStationSelectionList, setShowStationSelectionList] = useState(false); const [hoveredIndex, setHoveredIndex] = useState(null); const [arrowedIndex, setArrowedIndex] = useState(-1); const locationDivRef = useRef(null); const locationInputEl = useRef(null); const locationSelectionListEl = useRef(null); + const stationSelectionListEl = useRef(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 selectedStation = state.weatherStation; + const selectedStationDistance = state.weatherStationDistance; + const stationDistance = selectedStationDistance && unit === "standard" + ? Math.round((selectedStationDistance * 0.6 * 10) / 10) + :selectedStationDistance && Math.round(selectedStationDistance * 10) / 10; const handleOpenMap = () => { //send request to CODAP to open map with available weather stations @@ -48,15 +53,24 @@ export const LocationPicker = () => { useEffect(() => { if (selectedLocation) { - findNearestActiveStation(selectedLocation.latitude, selectedLocation.longitude, 80926000, "present") - .then(({station, distance}) => { - if (station) { + findNearestActiveStations(selectedLocation.latitude, selectedLocation.longitude, 80926000, "present") + .then((stationList: IStation[]) => { + if (stationList) { + setStationPossibilities(stationList); + (isEditing && stationList.length > 0) && setShowSelectionList(true); setState((draft) => { - draft.weatherStation = station; - draft.weatherStationDistance = distance; + draft.weatherStation = stationList[0].station; + draft.weatherStationDistance = stationList[0].distance; }); } - }); + }); + // if (station) { + // setState((draft) => { + // draft.weatherStation = station; + // draft.weatherStationDistance = distance; + // }); + // } + // }); } // eslint-disable-next-line react-hooks/exhaustive-deps },[selectedLocation]); @@ -181,15 +195,31 @@ export const LocationPicker = () => {
Location { selectedLocation && !isEditing && -
- { state.weatherStation && - <> - {state.weatherStationDistance && - ({stationDistance} {unitDistanceText}) } - {state.weatherStation?.name} - {/* hide this for now until implemented*/} - - } +
+
setShowStationSelectionList(true)}> + { state.weatherStation && + <> + ({stationDistance} {unitDistanceText}) + {selectedStation?.name} + + + } +
+
+
    + {stationPossibilities.map((station: IStation, idx: number) => { + if (station) { + return ( +
  • + {station.distance} from {state.location?.name} + {station.station.name} +
  • + ); + } + })} +
+
}
diff --git a/src/types.ts b/src/types.ts index c5f207a..830b4d9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -89,7 +89,7 @@ export interface IPlace { latitude: number; longitude: number; } - +export interface IStation {station: IWeatherStation, distance: number} export interface IWeatherStation { country: string; // "US" state: string; // 2 char state name diff --git a/src/utils/codapConnect.ts b/src/utils/codapConnect.ts index b7f2065..aa9ec1b 100644 --- a/src/utils/codapConnect.ts +++ b/src/utils/codapConnect.ts @@ -1,4 +1,4 @@ -import { IResult, codapInterface, createDataContext, createItems, createNewCollection } from "@concord-consortium/codap-plugin-api"; +import { codapInterface, createDataContext, createItems, createNewCollection } from "@concord-consortium/codap-plugin-api"; import { IWeatherStation, kStationsCollectionName, kStationsDatasetName, kWeatherStationCollectionAttrs } from "../types"; export const createStationsDataset = async(stations: IWeatherStation[]) => { diff --git a/src/utils/getWeatherStations.ts b/src/utils/getWeatherStations.ts index 9905c2d..5e99565 100644 --- a/src/utils/getWeatherStations.ts +++ b/src/utils/getWeatherStations.ts @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -import { IWeatherStation } from "../types"; +import { IStation, IWeatherStation } from "../types"; import weatherStations from "../assets/data/weather-stations.json"; /** @@ -34,21 +34,31 @@ export const adjustStationDataset = (dataset: IWeatherStation[]) => { } }; -export const findNearestActiveStation = async(targetLat: number, targetLong: number, fromDate: number | string, +export const findNearestActiveStations = async(targetLat: number, targetLong: number, fromDate: number | string, toDate: number | string) => { // TODO: filter out weather stations that are active - let nearestStation: IWeatherStation | null = null; - let minDistance = Number.MAX_VALUE; + // let nearestStation: IWeatherStation | null = null; + // let minDistance = Number.MAX_VALUE; + let nearestStations: IStation[] = []; for (const station of weatherStations) { const distance = calculateDistance(targetLat, targetLong, station.latitude, station.longitude); - if (distance < minDistance) { - minDistance = distance; - nearestStation = station; + const newStation = {station, distance}; + + // Insert the new station into the sorted array at the correct position + const index = nearestStations.findIndex(s => s.distance > distance); + if (index === -1) { + nearestStations.push(newStation); + } else { + nearestStations.splice(index, 0, newStation); } + // if (distance < minDistance) { + // minDistance = distance; + // nearestStation = station; + // } } - return {station: nearestStation, distance: minDistance}; + return nearestStations.slice(0, 5); }; function degreesToRadians(degrees: number): number { From 4bf15fcda4256a81891e0482a28b827ccc035312 Mon Sep 17 00:00:00 2001 From: eireland Date: Wed, 24 Jan 2024 11:44:24 -0800 Subject: [PATCH 2/8] Shows a list of the 5 closest weather stations when user clicks on the default station Styles the selection list. --- src/components/App.scss | 6 +- src/components/App.tsx | 4 +- src/components/location-picker.scss | 37 +++++++++--- src/components/location-picker.tsx | 93 ++++++++++++++++++++--------- src/types.ts | 6 +- src/utils/getWeatherStations.ts | 6 +- 6 files changed, 105 insertions(+), 47 deletions(-) diff --git a/src/components/App.scss b/src/components/App.scss index 1a31f3f..178da85 100755 --- a/src/components/App.scss +++ b/src/components/App.scss @@ -1,10 +1,6 @@ -.body { - width: 360px; -} - .App { padding: 12px; - box-sizing: border-box; + box-sizing: content-box; display: flex; flex-direction: column; align-items: center; diff --git a/src/components/App.tsx b/src/components/App.tsx index f080395..e21cf5d 100755 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -28,8 +28,8 @@ export const App = () => { useEffect(() => { initializePlugin({pluginName: kPluginName, version: kVersion, dimensions: kInitialDimensions}); - adjustStationDataset(weatherStations); //change max data to "present" - createStationsDataset(weatherStations); //send weather station data to CODAP + adjustStationDataset(weatherStations as IWeatherStation[]); //change max data to "present" + createStationsDataset(weatherStations as IWeatherStation[]); //send weather station data to CODAP }, []); const handleOpenInfo = () => { diff --git a/src/components/location-picker.scss b/src/components/location-picker.scss index 1f9fbb4..1961984 100644 --- a/src/components/location-picker.scss +++ b/src/components/location-picker.scss @@ -7,8 +7,8 @@ .location-header { display: flex; flex-direction: row; - align-items: center; justify-content: space-between; + flex-wrap: nowrap; height: 16px; margin: 9px 12px 0 0; @@ -19,32 +19,42 @@ .weather-station-wrapper { display: flex; flex-direction: column; - align-items: center; + align-items: flex-end; position: relative; - top: 0; - right: 0; + right: 40px; + max-width: 311px; + .selected-weather-station { padding: 2px 2px 2px 8px; background-color: $teal-light-25; color: $teal-dark; font-size: 10px; font-weight: 500; + display: flex; + flex-direction: row; + align-items: center; + .station-distance { font-style: italic; + margin-right: 4px; } svg { height: 12px; width: 12px; + margin-left: 6px; } } + .station-selection-list { - width: 311px; height: 86px; - margin: 25px 14px 42px 12px; - padding: 4px 18px 1px 13px; + padding: 8px 13px; box-shadow: 0 5px 8px 0 rgba(0, 0, 0, 0.5); background-color: #fff; + list-style-type: none; + margin-top: 0; visibility: hidden; + max-width: 311px; + &.show { visibility: visible; z-index: 5; @@ -54,13 +64,26 @@ font-family: "Montserrat", sans-serif; font-size: 10px; font-weight: 500; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 2px 0; + gap: 4px; .station-name { + overflow: hidden; + white-space: nowrap; text-overflow: ellipsis; color: #000; + max-width: 160px; } .station-distance { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; color: $teal-dark-75; + min-width: 120px; } &.selected-station { background-color: $teal-medium; diff --git a/src/components/location-picker.tsx b/src/components/location-picker.tsx index f797f05..aa21c77 100644 --- a/src/components/location-picker.tsx +++ b/src/components/location-picker.tsx @@ -3,9 +3,9 @@ import classnames from "classnames"; import { autoComplete, geoLocSearch } from "../utils/geonameSearch"; import { useStateContext } from "../hooks/use-state"; import { IPlace, IStation } from "../types"; -import { findNearestActiveStations } from "../utils/getWeatherStations"; +import { findNearestActiveStations, getWeatherStations } from "../utils/getWeatherStations"; import OpenMapIcon from "../assets/images/icon-map.svg"; -// import EditIcon from "../assets/images/icon-edit.svg"; +import EditIcon from "../assets/images/icon-edit.svg"; import LocationIcon from "../assets/images/icon-location.svg"; import CurrentLocationIcon from "../assets/images/icon-current-location.svg"; @@ -13,6 +13,7 @@ import "./location-picker.scss"; export const LocationPicker = () => { const {state, setState} = useStateContext(); + const {units, location, weatherStation, weatherStationDistance} = state; const [showMapButton, setShowMapButton] = useState(false); const [isEditing, setIsEditing] = useState(false); const [locationPossibilities, setLocationPossibilities] = useState([]); @@ -21,18 +22,17 @@ export const LocationPicker = () => { const [showStationSelectionList, setShowStationSelectionList] = useState(false); const [hoveredIndex, setHoveredIndex] = useState(null); const [arrowedIndex, setArrowedIndex] = useState(-1); + const [distanceWidth, setDistanceWidth] = useState(0); const locationDivRef = useRef(null); const locationInputEl = useRef(null); const locationSelectionListEl = useRef(null); const stationSelectionListEl = useRef(null); - const selectedLocation = state.location; - const unit = state.units; - const unitDistanceText = unit === "standard" ? "mi" : "km"; - const selectedStation = state.weatherStation; - const selectedStationDistance = state.weatherStationDistance; - const stationDistance = selectedStationDistance && unit === "standard" - ? Math.round((selectedStationDistance * 0.6 * 10) / 10) - :selectedStationDistance && Math.round(selectedStationDistance * 10) / 10; + const firstStationListedRef = useRef(null); + const stations: IWeatherStation[] = getWeatherStations(); + const unitDistanceText = units === "standard" ? "mi" : "km"; + const stationDistance = weatherStationDistance && units === "standard" + ? (Math.round((weatherStationDistance * 0.6 * 10) / 10)) + : weatherStationDistance && (Math.round(weatherStationDistance * 10) / 10); const handleOpenMap = () => { //send request to CODAP to open map with available weather stations @@ -52,8 +52,8 @@ export const LocationPicker = () => { }, [isEditing]); useEffect(() => { - if (selectedLocation) { - findNearestActiveStations(selectedLocation.latitude, selectedLocation.longitude, 80926000, "present") + if (location) { + findNearestActiveStations(location.latitude, location.longitude, 80926000, "present") .then((stationList: IStation[]) => { if (stationList) { setStationPossibilities(stationList); @@ -64,16 +64,19 @@ export const LocationPicker = () => { }); } }); - // if (station) { - // setState((draft) => { - // draft.weatherStation = station; - // draft.weatherStationDistance = distance; - // }); - // } - // }); } // eslint-disable-next-line react-hooks/exhaustive-deps - },[selectedLocation]); + },[location]); + + useEffect(() => { + if (showStationSelectionList) { + const listItems = stationSelectionListEl.current?.children; + if (listItems && firstStationListedRef.current) { + const firstStationWidth = firstStationListedRef.current?.getBoundingClientRect().width; + setDistanceWidth(firstStationWidth ? firstStationWidth : 120); + } + } + },[showStationSelectionList]); const getLocationList = () => { if (locationInputEl.current) { @@ -99,6 +102,15 @@ export const LocationPicker = () => { setArrowedIndex(-1); }; + const stationSelected = (station: IWeatherStation | undefined) => { + setState(draft => { + draft.weatherStation = station; + }); + setShowStationSelectionList(false); + setHoveredIndex(null); + setArrowedIndex(-1); + }; + const handleInputKeyDown = (e: React.KeyboardEvent) => { if (e.key === "ArrowDown" && locationPossibilities.length > 0) { setHoveredIndex(0); @@ -167,6 +179,28 @@ export const LocationPicker = () => { } }; + const handleStationSelection = (ev: React.MouseEvent) => { + const target = ev.currentTarget; + if (target.dataset.ix !== undefined) { + const selectedLocIdx = parseInt(target.dataset.ix, 10); + if (selectedLocIdx >= 0) { + const selectedStation = stationPossibilities[selectedLocIdx].station; + const actualWeatherStation = stations.find((station: IWeatherStation) => station.name === selectedStation.name); + stationSelected(actualWeatherStation); + setState(draft => { + draft.weatherStation = actualWeatherStation; + }); + } + } + }; + + const handleStationSelectionKeyDown = (e: React.KeyboardEvent, index: number) => { + if (e.key === "Enter") { + placeNameSelected(locationPossibilities[index-1]); + + } + }; + const handleFindCurrentLocation = async() => { navigator.geolocation.getCurrentPosition((position: GeolocationPosition) => { const lat = position.coords.latitude; @@ -194,32 +228,33 @@ export const LocationPicker = () => {
Location - { selectedLocation && !isEditing && + { location && !isEditing &&
setShowStationSelectionList(true)}> { state.weatherStation && <> - ({stationDistance} {unitDistanceText}) - {selectedStation?.name} + ({stationDistance?.toFixed(1)} {unitDistanceText}) + {weatherStation?.name} }
-
-
    +
      {stationPossibilities.map((station: IStation, idx: number) => { if (station) { return (
    • - {station.distance} from {state.location?.name} + className={classnames("station-selection", {"selected-station": station.station.name === state.weatherStation?.name})} + onMouseOver={()=>handleLocationHover(idx)} onClick={(e)=>handleStationSelection(e)} onKeyDown={(e)=>handleStationSelectionKeyDown(e,idx)}> + + {station.distance.toFixed(1)} {unitDistanceText} {idx === 0 && `from ${state.location?.name}`} + {station.station.name}
    • ); } })}
    -
}
@@ -228,7 +263,7 @@ export const LocationPicker = () => {
- { selectedLocation && !isEditing + { location && !isEditing ?
Stations near {state.location?.name} diff --git a/src/types.ts b/src/types.ts index 45e1487..f088f40 100644 --- a/src/types.ts +++ b/src/types.ts @@ -114,12 +114,12 @@ export interface IWeatherStation { } interface IWeatherStationRange { - mindate: string | number; - maxdate: string | number; + mindate: string; + maxdate: string; latitude: number; longitude: number; name: string; - elevation?: string | number; + elevation: string; ids: IWeatherStationID[]; } diff --git a/src/utils/getWeatherStations.ts b/src/utils/getWeatherStations.ts index 5e99565..dd09630 100644 --- a/src/utils/getWeatherStations.ts +++ b/src/utils/getWeatherStations.ts @@ -41,7 +41,7 @@ export const findNearestActiveStations = async(targetLat: number, targetLong: nu // let minDistance = Number.MAX_VALUE; let nearestStations: IStation[] = []; - for (const station of weatherStations) { + for (const station of weatherStations as IWeatherStation[]) { const distance = calculateDistance(targetLat, targetLong, station.latitude, station.longitude); const newStation = {station, distance}; @@ -86,3 +86,7 @@ export function calculateDistance(point1Lat: number, point1Long: number, point2L return distance; //in km } + +export function getWeatherStations() { + return weatherStations as IWeatherStation[]; +} From 43d51ec9f758cd4b459c4e38fe7e5206740b12f5 Mon Sep 17 00:00:00 2001 From: eireland Date: Wed, 24 Jan 2024 13:49:00 -0800 Subject: [PATCH 3/8] Adds station selection functionality --- src/components/location-picker.scss | 9 ++++++- src/components/location-picker.tsx | 38 ++++++++++++++++------------- src/utils/getWeatherStations.ts | 8 +++--- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/components/location-picker.scss b/src/components/location-picker.scss index 1961984..152799d 100644 --- a/src/components/location-picker.scss +++ b/src/components/location-picker.scss @@ -33,11 +33,19 @@ display: flex; flex-direction: row; align-items: center; + max-width: 245px; .station-distance { font-style: italic; margin-right: 4px; } + + .station-name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 175px; + } svg { height: 12px; width: 12px; @@ -89,7 +97,6 @@ background-color: $teal-medium; } } - } } } diff --git a/src/components/location-picker.tsx b/src/components/location-picker.tsx index aa21c77..85dec95 100644 --- a/src/components/location-picker.tsx +++ b/src/components/location-picker.tsx @@ -3,7 +3,7 @@ import classnames from "classnames"; import { autoComplete, geoLocSearch } from "../utils/geonameSearch"; import { useStateContext } from "../hooks/use-state"; import { IPlace, IStation } from "../types"; -import { findNearestActiveStations, getWeatherStations } from "../utils/getWeatherStations"; +import { convertDistanceToStandard, findNearestActiveStations } from "../utils/getWeatherStations"; import OpenMapIcon from "../assets/images/icon-map.svg"; import EditIcon from "../assets/images/icon-edit.svg"; import LocationIcon from "../assets/images/icon-location.svg"; @@ -28,11 +28,8 @@ export const LocationPicker = () => { const locationSelectionListEl = useRef(null); const stationSelectionListEl = useRef(null); const firstStationListedRef = useRef(null); - const stations: IWeatherStation[] = getWeatherStations(); const unitDistanceText = units === "standard" ? "mi" : "km"; - const stationDistance = weatherStationDistance && units === "standard" - ? (Math.round((weatherStationDistance * 0.6 * 10) / 10)) - : weatherStationDistance && (Math.round(weatherStationDistance * 10) / 10); + const stationDistance = weatherStationDistance && units === "standard" ? convertDistanceToStandard(weatherStationDistance) : weatherStationDistance; const handleOpenMap = () => { //send request to CODAP to open map with available weather stations @@ -58,11 +55,11 @@ export const LocationPicker = () => { if (stationList) { setStationPossibilities(stationList); (isEditing && stationList.length > 0) && setShowSelectionList(true); - setState((draft) => { - draft.weatherStation = stationList[0].station; - draft.weatherStationDistance = stationList[0].distance; - }); } + setState((draft) => { + draft.weatherStation = stationList[0].station; + draft.weatherStationDistance = stationList[0].distance; + }); }); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -185,19 +182,25 @@ export const LocationPicker = () => { const selectedLocIdx = parseInt(target.dataset.ix, 10); if (selectedLocIdx >= 0) { const selectedStation = stationPossibilities[selectedLocIdx].station; - const actualWeatherStation = stations.find((station: IWeatherStation) => station.name === selectedStation.name); - stationSelected(actualWeatherStation); + stationSelected(selectedStation); setState(draft => { - draft.weatherStation = actualWeatherStation; + draft.weatherStation = selectedStation; + draft.weatherStationDistance = stationPossibilities[selectedLocIdx].distance; }); } + setShowStationSelectionList(false); } }; const handleStationSelectionKeyDown = (e: React.KeyboardEvent, index: number) => { if (e.key === "Enter") { - placeNameSelected(locationPossibilities[index-1]); - + const selectedStation = stationPossibilities[index].station; + stationSelected(selectedStation); + setState(draft => { + draft.weatherStation = selectedStation; + draft.weatherStationDistance = stationPossibilities[index].distance; + }); + setShowStationSelectionList(false); } }; @@ -231,7 +234,7 @@ export const LocationPicker = () => { { location && !isEditing &&
setShowStationSelectionList(true)}> - { state.weatherStation && + { weatherStation && <> ({stationDistance?.toFixed(1)} {unitDistanceText}) {weatherStation?.name} @@ -242,12 +245,13 @@ export const LocationPicker = () => {
    {stationPossibilities.map((station: IStation, idx: number) => { if (station) { + const stationDistanceText = units === "standard" ? convertDistanceToStandard(station.distance) : station.distance; return ( -
  • handleLocationHover(idx)} onClick={(e)=>handleStationSelection(e)} onKeyDown={(e)=>handleStationSelectionKeyDown(e,idx)}> - {station.distance.toFixed(1)} {unitDistanceText} {idx === 0 && `from ${state.location?.name}`} + {stationDistanceText.toFixed(1)} {unitDistanceText} {idx === 0 && `from ${state.location?.name}`} {station.station.name}
  • diff --git a/src/utils/getWeatherStations.ts b/src/utils/getWeatherStations.ts index dd09630..c0d1a1c 100644 --- a/src/utils/getWeatherStations.ts +++ b/src/utils/getWeatherStations.ts @@ -52,10 +52,6 @@ export const findNearestActiveStations = async(targetLat: number, targetLong: nu } else { nearestStations.splice(index, 0, newStation); } - // if (distance < minDistance) { - // minDistance = distance; - // nearestStation = station; - // } } return nearestStations.slice(0, 5); @@ -90,3 +86,7 @@ export function calculateDistance(point1Lat: number, point1Long: number, point2L export function getWeatherStations() { return weatherStations as IWeatherStation[]; } + +export function convertDistanceToStandard(distance: number) { + return distance * 0.621371; +} From 61eb4317bdc7b731e16a62821ef32aaa89e4c487 Mon Sep 17 00:00:00 2001 From: eireland Date: Wed, 24 Jan 2024 13:54:41 -0800 Subject: [PATCH 4/8] Adds click outside handling --- src/components/location-picker.tsx | 42 +++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/components/location-picker.tsx b/src/components/location-picker.tsx index 85dec95..ccdf08a 100644 --- a/src/components/location-picker.tsx +++ b/src/components/location-picker.tsx @@ -25,15 +25,33 @@ export const LocationPicker = () => { const [distanceWidth, setDistanceWidth] = useState(0); const locationDivRef = useRef(null); const locationInputEl = useRef(null); - const locationSelectionListEl = useRef(null); - const stationSelectionListEl = useRef(null); + const locationSelectionListElRef = useRef(null); + const stationSelectionListElRef = useRef(null); const firstStationListedRef = useRef(null); const unitDistanceText = units === "standard" ? "mi" : "km"; const stationDistance = weatherStationDistance && units === "standard" ? convertDistanceToStandard(weatherStationDistance) : weatherStationDistance; - const handleOpenMap = () => { - //send request to CODAP to open map with available weather stations - }; + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (event.target) { + if (stationSelectionListElRef.current && !stationSelectionListElRef.current.contains(event.target as Node)) { + setShowStationSelectionList(false); + } + if (locationSelectionListElRef.current && !locationSelectionListElRef.current.contains(event.target as Node)) { + setShowSelectionList(false); + setIsEditing(false); + } + } + } + + // Bind the event listener + document.addEventListener("mousedown", handleClickOutside); + return () => { + // Unbind the event listener on clean up + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); useEffect(() => { if (locationInputEl.current?.value === "") { @@ -67,7 +85,7 @@ export const LocationPicker = () => { useEffect(() => { if (showStationSelectionList) { - const listItems = stationSelectionListEl.current?.children; + const listItems = stationSelectionListElRef.current?.children; if (listItems && firstStationListedRef.current) { const firstStationWidth = firstStationListedRef.current?.getBoundingClientRect().width; setDistanceWidth(firstStationWidth ? firstStationWidth : 120); @@ -112,12 +130,12 @@ export const LocationPicker = () => { if (e.key === "ArrowDown" && locationPossibilities.length > 0) { setHoveredIndex(0); setArrowedIndex(0); - locationSelectionListEl.current?.focus(); + locationSelectionListElRef.current?.focus(); } }; const handleListKeyDown = (e: React.KeyboardEvent) => { - const listItems = locationSelectionListEl.current?.children; + const listItems = locationSelectionListElRef.current?.children; if (e.key === "Enter") { placeNameSelected(locationPossibilities[arrowedIndex-1]); } else @@ -227,6 +245,10 @@ export const LocationPicker = () => { setIsEditing(true); }; + const handleOpenMap = () => { + //send request to CODAP to open map with available weather stations + }; + return (
    @@ -242,7 +264,7 @@ export const LocationPicker = () => { }
    -
      +
        {stationPossibilities.map((station: IStation, idx: number) => { if (station) { const stationDistanceText = units === "standard" ? convertDistanceToStandard(station.distance) : station.distance; @@ -278,7 +300,7 @@ export const LocationPicker = () => {
    { isEditing &&
      setHoveredIndex(null)}>
    • Date: Wed, 24 Jan 2024 16:26:52 -0800 Subject: [PATCH 5/8] Adds filtering for active stations --- src/components/location-picker.tsx | 9 +++---- src/utils/getWeatherStations.ts | 38 ++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/components/location-picker.tsx b/src/components/location-picker.tsx index 05d0d7b..a233658 100644 --- a/src/components/location-picker.tsx +++ b/src/components/location-picker.tsx @@ -13,7 +13,7 @@ import "./location-picker.scss"; export const LocationPicker = () => { const {state, setState} = useStateContext(); - const {units, location, weatherStation, weatherStationDistance} = state; + const {units, location, weatherStation, weatherStationDistance, startDate, endDate} = state; const [showMapButton, setShowMapButton] = useState(false); const [isEditing, setIsEditing] = useState(false); const [locationPossibilities, setLocationPossibilities] = useState([]); @@ -56,7 +56,6 @@ export const LocationPicker = () => { useEffect(() => { if (locationInputEl.current?.value === "") { setShowSelectionList(false); - // setSelectedLocation(undefined); } }, [locationInputEl.current?.value]); @@ -67,8 +66,10 @@ export const LocationPicker = () => { }, [isEditing]); useEffect(() => { + const _startDate = startDate ? startDate : new Date( -5364662060); // 1/1/1750 + const _endDate = endDate ? endDate : new Date(Date.now()); if (location) { - findNearestActiveStations(location.latitude, location.longitude, 80926000, "present") + findNearestActiveStations(location.latitude, location.longitude, _startDate, _endDate) .then((stationList: IStation[]) => { if (stationList) { setStationPossibilities(stationList); @@ -81,7 +82,7 @@ export const LocationPicker = () => { }); } // eslint-disable-next-line react-hooks/exhaustive-deps - },[location]); + },[endDate, isEditing, location, startDate]); useEffect(() => { if (showStationSelectionList) { diff --git a/src/utils/getWeatherStations.ts b/src/utils/getWeatherStations.ts index c0d1a1c..20f90bd 100644 --- a/src/utils/getWeatherStations.ts +++ b/src/utils/getWeatherStations.ts @@ -10,7 +10,6 @@ import weatherStations from "../assets/data/weather-stations.json"; */ export const adjustStationDataset = (dataset: IWeatherStation[]) => { const datasetArr = Array.from(dataset); - let maxDate: dayjs.Dayjs | null = null; if (dataset) { @@ -32,19 +31,19 @@ export const adjustStationDataset = (dataset: IWeatherStation[]) => { }); } } + return dataset; }; -export const findNearestActiveStations = async(targetLat: number, targetLong: number, fromDate: number | string, - toDate: number | string) => { - // TODO: filter out weather stations that are active - // let nearestStation: IWeatherStation | null = null; - // let minDistance = Number.MAX_VALUE; - let nearestStations: IStation[] = []; +export const findNearestActiveStations = async(targetLat: number, targetLong: number, fromDate: Date, + toDate: Date) => { + const adjustedStationDataset = adjustStationDataset(weatherStations as IWeatherStation[]); + const fromSecs = fromDate.getTime() / 1000; + const toSecs = toDate.getTime() / 1000; + const nearestStations: IStation[] = []; + console.log(adjustedStationDataset); - for (const station of weatherStations as IWeatherStation[]) { - const distance = calculateDistance(targetLat, targetLong, station.latitude, station.longitude); + const insertStation = (station: IWeatherStation, distance: number) => { const newStation = {station, distance}; - // Insert the new station into the sorted array at the correct position const index = nearestStations.findIndex(s => s.distance > distance); if (index === -1) { @@ -52,6 +51,25 @@ export const findNearestActiveStations = async(targetLat: number, targetLong: nu } else { nearestStations.splice(index, 0, newStation); } + }; + + for (const station of adjustedStationDataset) { + let shouldInsert = false; + if (station.maxdate === "present") { // If the station is still active (maxdate === "present") then we can use it + shouldInsert = true; + } else { // If the station is not active, we need to check if it has data in the date range + const stationMinSecs = new Date(station.mindate).getTime() / 1000; + const stationMaxSecs = new Date(station.maxdate).getTime() / 1000; + if (stationMinSecs <= toSecs && stationMaxSecs >= fromSecs) { + shouldInsert = true; + } + } + + if (shouldInsert) { + const distance = calculateDistance(targetLat, targetLong, station.latitude, station.longitude); + const newStation = {station, distance}; + insertStation(newStation.station, newStation.distance); + } } return nearestStations.slice(0, 5); From 1da54b4b4f90b6450260a9be5e06700da7b38795 Mon Sep 17 00:00:00 2001 From: eireland Date: Thu, 25 Jan 2024 15:54:31 -0800 Subject: [PATCH 6/8] PR Fixes --- src/utils/getWeatherStations.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/utils/getWeatherStations.ts b/src/utils/getWeatherStations.ts index 20f90bd..506c558 100644 --- a/src/utils/getWeatherStations.ts +++ b/src/utils/getWeatherStations.ts @@ -37,10 +37,9 @@ export const adjustStationDataset = (dataset: IWeatherStation[]) => { export const findNearestActiveStations = async(targetLat: number, targetLong: number, fromDate: Date, toDate: Date) => { const adjustedStationDataset = adjustStationDataset(weatherStations as IWeatherStation[]); - const fromSecs = fromDate.getTime() / 1000; - const toSecs = toDate.getTime() / 1000; + const fromMSecs = fromDate.getTime() ; + const toMSecs = toDate.getTime(); const nearestStations: IStation[] = []; - console.log(adjustedStationDataset); const insertStation = (station: IWeatherStation, distance: number) => { const newStation = {station, distance}; @@ -58,9 +57,9 @@ export const findNearestActiveStations = async(targetLat: number, targetLong: nu if (station.maxdate === "present") { // If the station is still active (maxdate === "present") then we can use it shouldInsert = true; } else { // If the station is not active, we need to check if it has data in the date range - const stationMinSecs = new Date(station.mindate).getTime() / 1000; - const stationMaxSecs = new Date(station.maxdate).getTime() / 1000; - if (stationMinSecs <= toSecs && stationMaxSecs >= fromSecs) { + const stationMinMSecs = new Date(station.mindate).getTime(); + const stationMaxMSecs = new Date(station.maxdate).getTime(); + if (stationMinMSecs <= toMSecs && stationMaxMSecs >= fromMSecs) { shouldInsert = true; } } From 8a0ae9af395c9e190fb7821ede930f8fb8628e40 Mon Sep 17 00:00:00 2001 From: eireland Date: Fri, 26 Jan 2024 09:14:35 -0800 Subject: [PATCH 7/8] MOves the weather station import to a ts file --- src/components/App.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 6f61ea5..22acb9d 100755 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -6,9 +6,8 @@ import { AttributesSelector } from "./attribute-selector"; import { AttributeFilter } from "./attribute-filter"; import { InfoModal } from "./info-modal"; import { useStateContext } from "../hooks/use-state"; -import { adjustStationDataset } from "../utils/getWeatherStations"; +import { adjustStationDataset, getWeatherStations } from "../utils/getWeatherStations"; import { createStationsDataset } from "../utils/codapHelpers"; -import weatherStations from "../assets/data/weather-stations.json"; import InfoIcon from "../assets/images/icon-info.svg"; import { useCODAPApi } from "../hooks/use-codap-api"; import { dataTypeStore } from "../utils/noaaDataTypes"; @@ -30,13 +29,17 @@ export const App = () => { const [statusMessage, setStatusMessage] = useState(""); const [isFetching, setIsFetching] = useState(false); const { showModal } = state; + const weatherStations = getWeatherStations(); useEffect(() => { initializePlugin({pluginName: kPluginName, version: kVersion, dimensions: kInitialDimensions}); - adjustStationDataset(weatherStations as IWeatherStation[]); //change max data to "present" - createStationsDataset(weatherStations as IWeatherStation[]); //send weather station data to CODAP }, []); + useEffect(() => { + adjustStationDataset(weatherStations); //change max data to "present" + createStationsDataset(weatherStations); //send weather station data to CODAP + },[weatherStations]); + const handleOpenInfo = () => { setState(draft => { draft.showModal = "info"; From eaeda88bfce390b07a5105dd9cc0f9d79180d3a4 Mon Sep 17 00:00:00 2001 From: eireland Date: Fri, 26 Jan 2024 09:50:04 -0800 Subject: [PATCH 8/8] Fixes capitalization to match types Fixes the merge conflict with select station list and time zone code --- src/components/location-picker.tsx | 75 ++++++++++++++---------------- src/utils/noaaDataTypes.ts | 8 ++-- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/src/components/location-picker.tsx b/src/components/location-picker.tsx index e7826f6..9a86307 100644 --- a/src/components/location-picker.tsx +++ b/src/components/location-picker.tsx @@ -68,49 +68,46 @@ 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) { - findNearestActiveStations(location.latitude, location.longitude, _startDate, _endDate) - .then((stationList: IStation[]) => { - if (stationList) { - setStationPossibilities(stationList); - (isEditing && stationList.length > 0) && setShowSelectionList(true); - } + if (location) { + findNearestActiveStations(location.latitude, location.longitude, _startDate, _endDate) + .then((stationList: IStation[]) => { + if (stationList) { + setStationPossibilities(stationList); + (isEditing && stationList.length > 0) && setShowSelectionList(true); + } + setState((draft) => { + 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.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}`); - } + draft.timezone = { + gmtOffset, + name: kOffsetMap[gmtOffset] }; - fetchTimezone(location.latitude, location.longitude); - } - }); - } else { - setState((draft) => { - draft.timezone = undefined; - }); - } + }); + } 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 },[endDate, isEditing, location, startDate]); diff --git a/src/utils/noaaDataTypes.ts b/src/utils/noaaDataTypes.ts index e86c284..f8eabb6 100644 --- a/src/utils/noaaDataTypes.ts +++ b/src/utils/noaaDataTypes.ts @@ -136,7 +136,7 @@ const dataTypes = [ ["daily-summaries", "global-summary-of-the-month"]), new NoaaType("SNOW", "snow", kUnitTypePrecip, "Snowfall", ["daily-summaries", "global-summary-of-the-month"]), - new NoaaType("AWND", "avgWind", kUnitTypeSpeed, "Average windspeed", + new NoaaType("AWND", "avgWind", kUnitTypeSpeed, "Average wind speed", ["daily-summaries", "global-summary-of-the-month"], { "GHCND" (v: number) {return v/10;}, "GSOM" (v) {return v/10;} }), @@ -145,13 +145,13 @@ const dataTypes = [ new NoaaType("SLP", "pressure", kUnitTypePressure, "Barometric Pressure at sea level", ["global-hourly"], {"global-hourly": extractHourlyPressure}), - new NoaaType("TMP", "temp", kUnitTypeTemp, "Air Temperature", + new NoaaType("TMP", "temp", kUnitTypeTemp, "Air temperature", ["global-hourly"], {"global-hourly": extractHourlyTemp}), new NoaaType("VIS", "vis", kUnitTypeDistance, "Visibility", ["global-hourly"], {"global-hourly": extractHourlyVisibility}), - new NoaaType("WND", "WDir", kUnitTypeAngle, "Wind Direction", + new NoaaType("WND", "WDir", kUnitTypeAngle, "Wind direction", ["global-hourly"], {"global-hourly": extractHourlyWindDirection}), - new NoaaType("WND", "wSpeed", kUnitTypeSpeed, "Wind Speed", + new NoaaType("WND", "wSpeed", kUnitTypeSpeed, "Wind speed", ["global-hourly"], {"global-hourly": extractHourlyWindspeed}), new NoaaType("AA1", "precip", kUnitTypePrecip, "Precipitation in last hour", ["global-hourly"], {"global-hourly": extractHourlyPrecipitation}),