From 536b98a2d26b3444a95d116a0a7e4d056c78fb7f Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Dec 2021 15:07:58 -0800 Subject: [PATCH 01/44] Modify date filtering --- src/utils/portals-common/portals-common.js | 164 +++++++++++++++------ 1 file changed, 121 insertions(+), 43 deletions(-) diff --git a/src/utils/portals-common/portals-common.js b/src/utils/portals-common/portals-common.js index 034401e5..0eadc040 100644 --- a/src/utils/portals-common/portals-common.js +++ b/src/utils/portals-common/portals-common.js @@ -1,5 +1,5 @@ import contains from 'lodash/fp/contains'; -import every from 'lodash/fp/every'; +import compact from 'lodash/fp/compact'; import filter from 'lodash/fp/filter'; import flatten from 'lodash/fp/flatten'; import flow from 'lodash/fp/flow'; @@ -7,6 +7,9 @@ import intersection from 'lodash/fp/intersection'; import map from 'lodash/fp/map'; import some from 'lodash/fp/some'; import uniq from 'lodash/fp/uniq'; +import isNil from 'lodash/fp/isNil'; +import union from 'lodash/fp/union'; +import reduce from 'lodash/fp/reduce'; import { isPointInPolygonWn } from '../geometry-algorithms'; @@ -21,13 +24,110 @@ const getY = point => point[1]; export const isPointInGeoJSONPolygon = isPointInPolygonWn(getX, getY); +export const utcDateOnly = d => { + // Return a new Date object which is suitable for comparing UTC date only + // (fixed zero time component). One might suppose that the comparisons in + // the filter below also would work with local time (Date.setHours()), but + // they don't; only UTC works. + if (!d) { + return d; + } + const r = new Date(d); + r.setUTCHours(0, 0, 0, 0); + return r; +}; + + +export const isNilOrAscending = (a, b, c) => { + // Allowing for nils (undefined, null), check that a <= b <= c. + return isNil(b) || ((isNil(a) || a <= b) && (isNil(c) || b <= c)); +}; + + +export const historyDateMatch = ( + history, startDate, endDate, strict = true +) => { + // Return true if history observation dates match start and end date. + // Compare using dates only. + const minObsDate = utcDateOnly(history.min_obs_time); + const maxObsDate = utcDateOnly(history.max_obs_time); + startDate = utcDateOnly(startDate); + endDate = utcDateOnly(endDate); + + if (strict) { + // Allowing for nils, check that + // minObsDate <= startDate <= endDate <= maxObsDate. + return ( + isNilOrAscending(startDate, endDate) + && isNilOrAscending(minObsDate, startDate, maxObsDate) + && isNilOrAscending(minObsDate, endDate, maxObsDate) + ); + } + + // This is a variant of the looser legacy comparison. Allows nil start, end. + return ( + !isNil(minObsDate) && !isNil(maxObsDate) + && isNilOrAscending(startDate, maxObsDate) + && isNilOrAscending(minObsDate, endDate) + ); + + // This is the looser comparison used by the legacy PDP portal. + // return ( + // !isNil(minObsDate) && !isNil(maxObsDate) + // && !isNil(startDate) && !isNil(endDate) + // && maxObsDate > startDate && minObsDate < endDate + // ); +}; + + +export const stationDateMatch = ( + station, startDate, endDate, strict = true +) => { + // TODO: Coalesce adjacent histories from a station. NB: tricky. + // If we don't do this, and we use strict date matching, then a station with + // several histories fully covering an interval will not be selected, even + // though it should be. The question of what "adjacent" means is a bit tricky + // ... would depend in part on history.freq to distinguish too-large gaps, + // but we already know that attribute isn't always an accurate reflection of + // actual observations. + return flow( + map(hx => historyDateMatch(hx, startDate, endDate, strict)), + some(Boolean), + )(station.histories) +}; + + +export const unionAll = reduce(union, []); + +export const atLeastOne = items => items.length > 0; + + +export const stationReportsSomeVariables = (station, variableUris) => { + return flow( + map("variable_uris"), + compact, + unionAll, + intersection(variableUris), + atLeastOne, + )(station.histories); +}; + + +export const stationReportsAnyFreqs = (station, freqs) => { + return flow( + map("freq"), + compact, + intersection(freqs), + atLeastOne, + )(station.histories); +}; + + export const stationFilter = ( startDate, endDate, selectedNetworks, selectedVariables, selectedFrequencies, onlyWithClimatology, area, allNetworks, allVariables, allStations ) => { // console.log('filteredStations allStations', allStations) - // console.log('filteredStations allVariables', this.state.allVariables) - console.log('test date', new Date()) const selectedVariableUris = flow( map(selectedVariable => selectedVariable.contexts), flatten, @@ -35,57 +135,30 @@ export const stationFilter = ( uniq, )(selectedVariables); // console.log('filteredStations selectedVariableUris', selectedVariableUris) + const selectedFrequencyValues = map(option => option.value)(selectedFrequencies); // console.log('filteredStations selectedVariableUris', selectedVariableUris) - return flow( + + console.group("stationFilter") + const r = flow( + // tap(s => console.log("all stations", s?.length)), + filter(station => { return ( - // Filter for selected date range covered entirely by station - // records date range. - ( - startDate === null || - !station.min_obs_time || // not supposed to happen; optimistic - startDate >= station.min_obs_time - ) && - ( - endDate === null || - !station.max_obs_time || // observations to current date - endDate <= station.max_obs_time - ) && - - // Filter for selected date range with any overlap at all with - // station records date range. Probably not wanted. - // ( - // endDate === null || - // !station.min_obs_time || - // endDate >= station.min_obs_time - // ) && - // ( - // startDate === null || - // !station.max_obs_time || - // startDate <= station.max_obs_time - // ) && + stationDateMatch(station, startDate, endDate, false) // Station is part of one of selected networks - contains( + && contains( station.network_uri, map(nw => nw.value.uri)(selectedNetworks) - ) && - - // Station reports one of selected variables - ( - station.histories && station.histories[0] && - intersection(station.histories[0].variable_uris, selectedVariableUris).length > 0 - ) && - - // Station recording frequency is one of selected frequencies - ( - station.histories && station.histories[0] && - contains(station.histories[0].freq, selectedFrequencyValues) ) - ) + + && stationReportsSomeVariables(station, selectedVariableUris) + && stationReportsAnyFreqs(station, selectedFrequencyValues) + ); }), + // tap(s => console.log("after date etc filtering", s?.length)), // Stations match `onlyWithClimatology`: // If `onlyWithClimatology`, station reports a climatology variable. @@ -101,6 +174,7 @@ export const stationFilter = ( some(({ cell_method }) => /(within|over)/.test(cell_method)) )(allVariables) }), + // tap(s => console.log("after onlyWithClimatology filtering", s?.length)), // Stations are inside `area` filter(station => { @@ -115,5 +189,9 @@ export const stationFilter = ( [station.histories[0].lon, station.histories[0].lat] ))(area.coordinates); }), + // tap(s => console.log("after area filtering", s?.length)), )(allStations); + + console.groupEnd() + return r; }; From ad9a349d981a2939d658f05ce37c36fd85466d07 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 21 Dec 2021 09:48:31 -0800 Subject: [PATCH 02/44] Fix freq filtering --- src/utils/portals-common/portals-common.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/utils/portals-common/portals-common.js b/src/utils/portals-common/portals-common.js index 0eadc040..75c1684c 100644 --- a/src/utils/portals-common/portals-common.js +++ b/src/utils/portals-common/portals-common.js @@ -90,10 +90,14 @@ export const stationDateMatch = ( // ... would depend in part on history.freq to distinguish too-large gaps, // but we already know that attribute isn't always an accurate reflection of // actual observations. - return flow( + const r = flow( map(hx => historyDateMatch(hx, startDate, endDate, strict)), some(Boolean), - )(station.histories) + )(station.histories); + // if (!r) { + // console.log(`Station ${station.id} filtered out on date`) + // } + return r; }; @@ -103,23 +107,30 @@ export const atLeastOne = items => items.length > 0; export const stationReportsSomeVariables = (station, variableUris) => { - return flow( + const r = flow( map("variable_uris"), compact, unionAll, intersection(variableUris), atLeastOne, )(station.histories); + // if (!r) { + // console.log(`Station ${station.id} filtered out on variables`) + // } + return r; }; export const stationReportsAnyFreqs = (station, freqs) => { - return flow( + const r = flow( map("freq"), - compact, intersection(freqs), atLeastOne, )(station.histories); + // if (!r) { + // console.log(`Station ${station.id} filtered out on freqs`) + // } + return r; }; From 9a2f805fb7ea5215d49d8eb40a0007b18c490d22 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 21 Dec 2021 09:48:58 -0800 Subject: [PATCH 03/44] Add logging to station-data-service --- src/data-services/station-data-service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/data-services/station-data-service.js b/src/data-services/station-data-service.js index 82a4cf69..dc8eccb1 100644 --- a/src/data-services/station-data-service.js +++ b/src/data-services/station-data-service.js @@ -4,6 +4,7 @@ import flow from 'lodash/fp/flow'; import getOr from 'lodash/fp/getOr'; import isFinite from 'lodash/fp/isFinite'; import isString from 'lodash/fp/isString'; +import tap from 'lodash/fp/tap'; import { mapDeep } from '../utils/fp'; import { filterExpressionsParser, @@ -84,6 +85,7 @@ export function getStations(config) { stride: envVarNumber('REACT_APP_STATION_STRIDE', undefined), }, transformResponse: axios.defaults.transformResponse.concat( + tap(x => console.log("raw station count", x.length)), filterStations, mapDeep(transformIso8601Date) ), From f2b4c20a93d711a6e497b68e418dcea919ec7307 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 21 Dec 2021 09:50:03 -0800 Subject: [PATCH 04/44] Add station counts to Station Filters tab --- src/components/main/VersionA/VersionA.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/main/VersionA/VersionA.js b/src/components/main/VersionA/VersionA.js index 966c592d..5ad576db 100644 --- a/src/components/main/VersionA/VersionA.js +++ b/src/components/main/VersionA/VersionA.js @@ -6,6 +6,7 @@ import get from 'lodash/fp/get'; import map from 'lodash/fp/map'; import filter from 'lodash/fp/filter'; import join from 'lodash/fp/join'; +import difference from 'lodash/fp/difference'; import css from '../common.module.css'; @@ -231,6 +232,15 @@ class Portal extends Component { + + +

+ {filteredStations?.length} stations selected + of {this.state.allStations?.length} (see + Station Data tab for details) +

+ +
{/**/} @@ -241,7 +251,7 @@ class Portal extends Component { label={'Start Date'} /> - + Date: Tue, 21 Dec 2021 15:13:52 -0800 Subject: [PATCH 05/44] Improve station metadata display --- .../info/StationMetadata/StationMetadata.css | 11 ++ .../info/StationMetadata/StationMetadata.js | 129 ++++++++++++++---- 2 files changed, 112 insertions(+), 28 deletions(-) diff --git a/src/components/info/StationMetadata/StationMetadata.css b/src/components/info/StationMetadata/StationMetadata.css index 90ce4f64..d2b2ae3b 100644 --- a/src/components/info/StationMetadata/StationMetadata.css +++ b/src/components/info/StationMetadata/StationMetadata.css @@ -5,4 +5,15 @@ .ReactTable .rt-thead { overflow-y: scroll; +} + +ul.compact { + padding-top: 0; + padding-bottom: 0; + padding-left: 15px; + padding-right: 0; +} + +ul.compact li { + margin-left: 0; } \ No newline at end of file diff --git a/src/components/info/StationMetadata/StationMetadata.js b/src/components/info/StationMetadata/StationMetadata.js index 5991ffa9..9c76e6c5 100644 --- a/src/components/info/StationMetadata/StationMetadata.js +++ b/src/components/info/StationMetadata/StationMetadata.js @@ -5,9 +5,16 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import ReactTable from 'react-table'; + +import flow from 'lodash/fp/flow'; import find from 'lodash/fp/find'; +import map from 'lodash/fp/map'; +import uniq from 'lodash/fp/uniq'; +import uniqWith from 'lodash/fp/uniqWith'; + import FrequencySelector from '../../selectors/FrequencySelector'; import logger from '../../../logger'; +import { utcDateOnly } from '../../../utils/portals-common'; import 'react-table/react-table.css'; import './StationMetadata.css'; @@ -46,49 +53,115 @@ export default class StationMetadata extends Component { accessor: 'native_id' }, { - id: 'Station Name', - Header: 'Station Name', + id: 'Unique Name(s)', + Header: 'Unique Name(s)', minWidth: 120, maxWidth: 200, - accessor: station => station.histories[0].station_name, + accessor: station => ( +
    + { + flow( + map('station_name'), + uniq, + map(name => (
  • {name}
  • )), + )(station.histories) + } +
+ ), + // accessor: station => station.histories[0].station_name, }, { - id: 'Location', - Header: 'Location', + id: 'Unique Location(s)', + Header: 'Unique Location(s)', minWidth: 120, maxWidth: 200, - accessor: station => { - const hx = station.histories[0]; - return ( -
- {-hx.lon} W
- {hx.lat} N
- Elev. {hx.elevation} m -
- ); - } + accessor: station => ( +
    + { + flow( + uniqWith( + (hx1, hx2) => hx1.lon === hx2.lon && hx1.lat === hx2.lat + ), + map(hx => ( +
  • + {-hx.lon} W
    + {hx.lat} N
    + Elev. {hx.elevation} m +
  • + )), + )(station.histories) + } +
+ ), + // accessor: station => { + // const hx = station.histories[0]; + // return ( + //
+ // {-hx.lon} W
+ // {hx.lat} N
+ // Elev. {hx.elevation} m + //
+ // ); + // } }, { - id: 'Record', - Header: 'Record', + id: 'Unique Record(s)', + Header: 'Unique Record(s)', minWidth: 100, maxWidth: 200, // accessor: station => 'record', - accessor: station => { - return ( -
- {formatDate(station.min_obs_time)} to
- {formatDate(station.max_obs_time)} -
- )} + accessor: station => ( +
    + { + flow( + uniqWith( + (hx1, hx2) => + utcDateOnly(hx1.min_obs_time).getTime() + === utcDateOnly(hx2.min_obs_time).getTime() + && utcDateOnly(hx1.max_obs_time).getTime() + === utcDateOnly(hx2.max_obs_time).getTime() + ), + map(hx => ( +
  • + {formatDate(hx.min_obs_time)} to
    + {formatDate(hx.max_obs_time)} +
  • + )), + )(station.histories) + } +
+ ), + // accessor: station => { + // return ( + //
+ // {formatDate(station.min_obs_time)} to
+ // {formatDate(station.max_obs_time)} + //
+ // )} }, { minWidth: 80, maxWidth: 100, - id: 'Obs Freq', - Header: 'Obs Freq', - accessor: station => - FrequencySelector.valueToLabel(station.histories[0].freq), + id: 'Uniq Obs Freq', + Header: 'Uniq Obs Freq', + accessor: station => ( +
    + { + flow( + map('freq'), + uniq, + map(freq => (
  • {FrequencySelector.valueToLabel(freq)}
  • )), + )(station.histories) + } +
+ ), + }, + { + id: '# Hx', + Header: '# Hx', + minWidth: 50, + maxWidth: 100, + accessor: station => station.histories.length, }, ]; From c0da0aa185853481ca7935543803b44da79eed13 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 21 Dec 2021 15:15:41 -0800 Subject: [PATCH 06/44] Improve station popup contents --- .../maps/StationPopup/StationPopup.css | 10 ++++ .../maps/StationPopup/StationPopup.js | 47 +++++++++++-------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/components/maps/StationPopup/StationPopup.css b/src/components/maps/StationPopup/StationPopup.css index 9fad10e0..bbff27d3 100644 --- a/src/components/maps/StationPopup/StationPopup.css +++ b/src/components/maps/StationPopup/StationPopup.css @@ -1,3 +1,13 @@ h1 { font-size: 1.2em; +} + +.histories { + max-height: 10em; + overflow-y: scroll; + padding: 0px; +} + +.variables { + padding: 0; } \ No newline at end of file diff --git a/src/components/maps/StationPopup/StationPopup.js b/src/components/maps/StationPopup/StationPopup.js index 8feb4c70..0bfdd22c 100644 --- a/src/components/maps/StationPopup/StationPopup.js +++ b/src/components/maps/StationPopup/StationPopup.js @@ -9,7 +9,6 @@ import FrequencySelector from '../../selectors/FrequencySelector'; import logger from '../../../logger'; import './StationPopup.css'; -import getOr from 'lodash/fp/getOr'; logger.configure({ active: true }); @@ -32,12 +31,13 @@ class StationPopup extends Component { render() { const { station, network, variables, defaultNetworkColor } = this.props; - const history = station.histories[0]; + const histories = station.histories; + const history0 = histories[0]; const networkColor = chroma(network.color ?? defaultNetworkColor).alpha(0.5).css(); return ( -

Station: {history.station_name} ({network.name})

+

Station: {history0.station_name} ({network.name})

@@ -58,37 +58,44 @@ class StationPopup extends Component { - + - + - + - - - - - + + - + - + + - { - map(variable => ( - - - - ))(variables) - }
Longitude{history.lon}{history0.lon}
Latitude{history.lat}{history0.lat}
Elevation{history.elevation}{history0.elevation}
Recordsfrom {formatDate(station.min_obs_time)}
to {formatDate(station.max_obs_time)}Record span (histories: {histories.length}) +
    + { + map(hx => ( +
  • {formatDate(hx.min_obs_time)} to {formatDate(hx.max_obs_time)}
  • + ))(histories) + } +
+
Observation frequency{FrequencySelector.valueToLabel(history.freq)}{FrequencySelector.valueToLabel(history0.freq)}
Recorded variablesRecorded variables +
    + { + map(variable => ( +
  • {variable.display_name}
  • + ))(variables) + } +
+
{variable.display_name}
From ceb479d057fc67ed255a3c4b08c0b52d040581b0 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 21 Dec 2021 15:48:00 -0800 Subject: [PATCH 07/44] Add tooltip to station markers --- .../maps/StationMarkers/StationMarkers.js | 8 ++++++- .../maps/StationTooltip/StationTooltip.css | 0 .../maps/StationTooltip/StationTooltip.js | 21 +++++++++++++++++++ .../maps/StationTooltip/package.json | 6 ++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/components/maps/StationTooltip/StationTooltip.css create mode 100644 src/components/maps/StationTooltip/StationTooltip.js create mode 100644 src/components/maps/StationTooltip/package.json diff --git a/src/components/maps/StationMarkers/StationMarkers.js b/src/components/maps/StationMarkers/StationMarkers.js index fed2e783..6eff3d11 100644 --- a/src/components/maps/StationMarkers/StationMarkers.js +++ b/src/components/maps/StationMarkers/StationMarkers.js @@ -7,10 +7,12 @@ import find from 'lodash/fp/find'; import flow from 'lodash/fp/flow'; import tap from 'lodash/fp/tap'; +import StationTooltip from '../StationTooltip'; +import StationPopup from '../StationPopup'; + import logger from '../../../logger'; import './StationMarkers.css'; -import StationPopup from '../StationPopup'; logger.configure({ active: true }); @@ -70,6 +72,10 @@ class StationMarkers extends Component { {...this.props.markerOptions} color={network && network.color} > + + {history0.station_name} ({network.name}) + + ); +} + +StationTooltip.propTypes = { + station: PropTypes.object.isRequired, + network: PropTypes.object.isRequired, +}; + +export default StationTooltip; diff --git a/src/components/maps/StationTooltip/package.json b/src/components/maps/StationTooltip/package.json new file mode 100644 index 00000000..9e84088f --- /dev/null +++ b/src/components/maps/StationTooltip/package.json @@ -0,0 +1,6 @@ +{ + "name": "StationTooltip", + "version": "0.0.0", + "private": true, + "main": "./StationTooltip.js" +} \ No newline at end of file From 6d359d516357431dccc78f36d6daddbe6d575dd5 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 21 Dec 2021 17:21:42 -0800 Subject: [PATCH 08/44] Factor out station-info functions --- .../info/StationMetadata/StationMetadata.js | 65 ++++++------------- src/utils/station-info.js | 37 +++++++++++ 2 files changed, 56 insertions(+), 46 deletions(-) create mode 100644 src/utils/station-info.js diff --git a/src/components/info/StationMetadata/StationMetadata.js b/src/components/info/StationMetadata/StationMetadata.js index 9c76e6c5..132936f0 100644 --- a/src/components/info/StationMetadata/StationMetadata.js +++ b/src/components/info/StationMetadata/StationMetadata.js @@ -9,12 +9,14 @@ import ReactTable from 'react-table'; import flow from 'lodash/fp/flow'; import find from 'lodash/fp/find'; import map from 'lodash/fp/map'; -import uniq from 'lodash/fp/uniq'; -import uniqWith from 'lodash/fp/uniqWith'; import FrequencySelector from '../../selectors/FrequencySelector'; import logger from '../../../logger'; -import { utcDateOnly } from '../../../utils/portals-common'; +import { + uniqStationFreqs, + uniqStationLocations, + uniqStationNames, uniqStationObsPeriods +} from '../../../utils/station-info'; import 'react-table/react-table.css'; import './StationMetadata.css'; @@ -61,14 +63,12 @@ export default class StationMetadata extends Component {
    { flow( - map('station_name'), - uniq, + uniqStationNames, map(name => (
  • {name}
  • )), - )(station.histories) + )(station) }
), - // accessor: station => station.histories[0].station_name, }, { id: 'Unique Location(s)', @@ -79,65 +79,39 @@ export default class StationMetadata extends Component {
    { flow( - uniqWith( - (hx1, hx2) => hx1.lon === hx2.lon && hx1.lat === hx2.lat - ), - map(hx => ( + uniqStationLocations, + map(loc => (
  • - {-hx.lon} W
    - {hx.lat} N
    - Elev. {hx.elevation} m + {-loc.lon} W
    + {loc.lat} N
    + Elev. {loc.elevation} m
  • )), - )(station.histories) + )(station) }
), - // accessor: station => { - // const hx = station.histories[0]; - // return ( - //
- // {-hx.lon} W
- // {hx.lat} N
- // Elev. {hx.elevation} m - //
- // ); - // } }, { id: 'Unique Record(s)', Header: 'Unique Record(s)', minWidth: 100, maxWidth: 200, - // accessor: station => 'record', accessor: station => (
    { flow( - uniqWith( - (hx1, hx2) => - utcDateOnly(hx1.min_obs_time).getTime() - === utcDateOnly(hx2.min_obs_time).getTime() - && utcDateOnly(hx1.max_obs_time).getTime() - === utcDateOnly(hx2.max_obs_time).getTime() - ), - map(hx => ( + uniqStationObsPeriods, + map(period => (
  • - {formatDate(hx.min_obs_time)} to
    - {formatDate(hx.max_obs_time)} + {formatDate(period.min_obs_time)} to
    + {formatDate(period.max_obs_time)}
  • )), - )(station.histories) + )(station) }
), - // accessor: station => { - // return ( - //
- // {formatDate(station.min_obs_time)} to
- // {formatDate(station.max_obs_time)} - //
- // )} }, { minWidth: 80, @@ -148,8 +122,7 @@ export default class StationMetadata extends Component {
    { flow( - map('freq'), - uniq, + uniqStationFreqs, map(freq => (
  • {FrequencySelector.valueToLabel(freq)}
  • )), )(station.histories) } diff --git a/src/utils/station-info.js b/src/utils/station-info.js new file mode 100644 index 00000000..c0cf07c8 --- /dev/null +++ b/src/utils/station-info.js @@ -0,0 +1,37 @@ +import flow from 'lodash/fp/flow'; +import map from 'lodash/fp/map'; +import uniq from 'lodash/fp/uniq'; +import React from 'react'; +import uniqWith from 'lodash/fp/uniqWith'; +import { utcDateOnly } from './portals-common'; + +export const uniqStationNames = station => + flow( + map('station_name'), + uniq, + )(station.histories); + + +export const uniqStationLocations = station => + uniqWith( + (hx1, hx2) => hx1.lon === hx2.lon + && hx1.lat === hx2.lat + && hx1.elevation === hx2.elevation + )(station.histories); + + +export const uniqStationObsPeriods = station => + uniqWith( + (hx1, hx2) => + utcDateOnly(hx1.min_obs_time).getTime() + === utcDateOnly(hx2.min_obs_time).getTime() + && utcDateOnly(hx1.max_obs_time).getTime() + === utcDateOnly(hx2.max_obs_time).getTime() + )(station.histories); + + +export const uniqStationFreqs = station => + flow( + map('freq'), + uniq, + )(station.histories); From 29c738834657e5161bd19b4fdde1deac20917415 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 21 Dec 2021 17:22:11 -0800 Subject: [PATCH 09/44] Improve station popup contents --- .../maps/StationPopup/StationPopup.js | 88 +++++++++++++------ 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/src/components/maps/StationPopup/StationPopup.js b/src/components/maps/StationPopup/StationPopup.js index 0bfdd22c..fb5aaa06 100644 --- a/src/components/maps/StationPopup/StationPopup.js +++ b/src/components/maps/StationPopup/StationPopup.js @@ -2,13 +2,20 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { Table } from 'react-bootstrap'; import { Popup } from 'react-leaflet' +import flow from 'lodash/fp/flow'; import map from 'lodash/fp/map'; +import join from 'lodash/fp/join'; import chroma from 'chroma-js'; import FrequencySelector from '../../selectors/FrequencySelector'; import logger from '../../../logger'; import './StationPopup.css'; +import { + uniqStationFreqs, + uniqStationLocations, + uniqStationNames, uniqStationObsPeriods +} from '../../../utils/station-info'; logger.configure({ active: true }); @@ -31,20 +38,65 @@ class StationPopup extends Component { render() { const { station, network, variables, defaultNetworkColor } = this.props; - const histories = station.histories; - const history0 = histories[0]; const networkColor = chroma(network.color ?? defaultNetworkColor).alpha(0.5).css(); + + + const stationNames = flow( + uniqStationNames, + join(", "), + )(station); + + const stationLocations = ( +
      + { + flow( + uniqStationLocations, + map(loc => ( +
    • + {-loc.lon} W
      + {loc.lat} N
      + Elev. {loc.elevation} m +
    • + )), + )(station) + } +
    + ); + + const stationObsPeriods = ( +
      + { + flow( + uniqStationObsPeriods, + map(hx => ( +
    • {formatDate(hx.min_obs_time)} to {formatDate(hx.max_obs_time)}
    • + )) + )(station) + } +
    + ); + const stationObsFreqs = ( +
      + { + flow( + uniqStationFreqs, + map(freq => (
    • {FrequencySelector.valueToLabel(freq)}
    • )), + )(station) + } +
    + ); + return ( -

    Station: {history0.station_name} ({network.name})

    +

    Station: {stationNames}

    @@ -57,32 +109,16 @@ class StationPopup extends Component { - - + + - - - - - - - - - - + + - - + + From 0487bccadd2180e7b40f196399c2400ae8cf025b Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 21 Dec 2021 17:24:38 -0800 Subject: [PATCH 10/44] Draw a station marker for every history WIP --- .../maps/StationMarkers/StationMarkers.js | 179 +++++++++++++----- 1 file changed, 129 insertions(+), 50 deletions(-) diff --git a/src/components/maps/StationMarkers/StationMarkers.js b/src/components/maps/StationMarkers/StationMarkers.js index 6eff3d11..a929b758 100644 --- a/src/components/maps/StationMarkers/StationMarkers.js +++ b/src/components/maps/StationMarkers/StationMarkers.js @@ -1,12 +1,14 @@ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { CircleMarker } from 'react-leaflet'; +import React from 'react'; +import { CircleMarker, Polygon } from 'react-leaflet'; import compact from 'lodash/fp/compact'; import map from 'lodash/fp/map'; import find from 'lodash/fp/find'; import flow from 'lodash/fp/flow'; import tap from 'lodash/fp/tap'; +import uniqWith from 'lodash/fp/uniqWith'; +import { utcDateOnly } from '../../../utils/portals-common'; import StationTooltip from '../StationTooltip'; import StationPopup from '../StationPopup'; @@ -36,57 +38,134 @@ const variables_for = (history, variables) => ( ); -class StationMarkers extends Component { - static propTypes = { - stations: PropTypes.array.isRequired, - allNetworks: PropTypes.array.isRequired, - allVariables: PropTypes.array.isRequired, - markerOptions: PropTypes.object, - }; - - static defaultProps = { - markerOptions: { - radius: 4, - weight: 1, - fillOpacity: 0.75, - color: '#000000', - }, - }; - - render() { +function StationMarker({ station, allNetworks, allVariables, markerOptions }) { + const histories = station.histories; + const history = histories[0]; + const network = network_for(station, allNetworks); + const variables = variables_for(history, allVariables); + + const uniqLatLngs = flow( + uniqWith( + (hx1, hx2) => ( + utcDateOnly(hx1.min_obs_time).getTime() + === utcDateOnly(hx2.min_obs_time).getTime() + && utcDateOnly(hx1.max_obs_time).getTime() + === utcDateOnly(hx2.max_obs_time).getTime() + ) + ), + map(hx => ({ lng: hx.lon, lat: hx.lat })) + )(station.histories); + + return map( + latLng => ( + + + + + ), + uniqLatLngs + ); + + + if (histories.length === 0) { + return null; + } + + if (histories.length === 1) { return ( - flow( - tap(stations => console.log('stations', stations)), - map(station => { - const history = station.histories[0]; - const network = network_for(station, this.props.allNetworks); - const variables = variables_for(history, this.props.allVariables); - return ( - history && - - - - - ) - }) - )(this.props.stations || noStations) + + + + ); } + + return ( + + + + + ) } +StationMarker.propTypes = { + stations: PropTypes.array.isRequired, + allNetworks: PropTypes.array.isRequired, + allVariables: PropTypes.array.isRequired, + markerOptions: PropTypes.object, +}; + +StationMarker.defaultProps = { + markerOptions: { + radius: 4, + weight: 1, + fillOpacity: 0.75, + color: '#000000', + }, +}; + + +function StationMarkers({ + stations, ...rest +}) { + return ( + map( + station => ( + + ), + stations || noStations + ) + ); +} + +StationMarkers.propTypes = { + stations: PropTypes.array.isRequired, + allNetworks: PropTypes.array.isRequired, + allVariables: PropTypes.array.isRequired, + markerOptions: PropTypes.object, +}; + +StationMarkers.defaultProps = { + markerOptions: { + radius: 4, + weight: 1, + fillOpacity: 0.75, + color: '#000000', + }, +}; + export default StationMarkers; From fb3fc982f0d4e5cea3710ab38f13291c3c0877db Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 22 Dec 2021 11:05:55 -0800 Subject: [PATCH 11/44] Extract more into station-info utils --- src/utils/station-info.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/utils/station-info.js b/src/utils/station-info.js index c0cf07c8..8563ff84 100644 --- a/src/utils/station-info.js +++ b/src/utils/station-info.js @@ -4,6 +4,17 @@ import uniq from 'lodash/fp/uniq'; import React from 'react'; import uniqWith from 'lodash/fp/uniqWith'; import { utcDateOnly } from './portals-common'; +import find from 'lodash/fp/find'; +import flatten from 'lodash/fp/flatten'; +import compact from 'lodash/fp/compact'; +import tap from 'lodash/fp/tap'; +import sortBy from 'lodash/fp/sortBy'; +import identity from 'lodash/fp/identity'; +import sortedUniq from 'lodash/fp/sortedUniq'; + +export const stationNetwork = (networks, station) => + find({ uri: station.network_uri })(networks); + export const uniqStationNames = station => flow( @@ -35,3 +46,24 @@ export const uniqStationFreqs = station => map('freq'), uniq, )(station.histories); + + +export const uniqStationVariableNames = (variables, station) => + flow( + map(history => + map( + variable_uri => find({ uri: variable_uri }, variables), + history.variable_uris + ) + ), + flatten, + // compacting this array should not be necessary, but the API delivers + // erroneous data (due ultimately to erroneous database records, I believe) + // that causes some of the variables to be "missing". + compact, + // tap(x => console.log("### compacted vars", x)), + map('display_name'), + sortBy(identity), + uniq, + sortedUniq, + )(station.histories) \ No newline at end of file From 4d286a4a8726b19202251370b859b6c7737bbb2e Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 22 Dec 2021 11:07:01 -0800 Subject: [PATCH 12/44] Refactor and improve StationTooltip --- .../maps/StationTooltip/StationTooltip.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/maps/StationTooltip/StationTooltip.js b/src/components/maps/StationTooltip/StationTooltip.js index 74bb10df..64f1dd5e 100644 --- a/src/components/maps/StationTooltip/StationTooltip.js +++ b/src/components/maps/StationTooltip/StationTooltip.js @@ -1,21 +1,28 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Tooltip } from 'react-leaflet' +import { stationNetwork, uniqStationNames } from '../../../utils/station-info'; +import flow from 'lodash/fp/flow'; +import join from 'lodash/fp/join'; import './StationTooltip.css'; -function StationTooltip({ station, network }) { - const histories = station.histories; - const history0 = histories[0]; +function StationTooltip({ station, allNetworks }) { + const network = stationNetwork(allNetworks, station); + const stationNames = flow( + uniqStationNames, + join(", "), + )(station); + return ( - {history0.station_name} ({network.name}) + {stationNames} ({network.name}) ); } StationTooltip.propTypes = { station: PropTypes.object.isRequired, - network: PropTypes.object.isRequired, + allNetworks: PropTypes.array.isRequired, }; export default StationTooltip; From 767dc2c534ddf08bf06e91751ed7a1deb211c420 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 22 Dec 2021 11:08:24 -0800 Subject: [PATCH 13/44] Refactor and improve StationPopup --- .../maps/StationPopup/StationPopup.js | 240 +++++++++--------- 1 file changed, 124 insertions(+), 116 deletions(-) diff --git a/src/components/maps/StationPopup/StationPopup.js b/src/components/maps/StationPopup/StationPopup.js index fb5aaa06..0b5a30cd 100644 --- a/src/components/maps/StationPopup/StationPopup.js +++ b/src/components/maps/StationPopup/StationPopup.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React from 'react'; import { Table } from 'react-bootstrap'; import { Popup } from 'react-leaflet' import flow from 'lodash/fp/flow'; @@ -12,9 +12,12 @@ import logger from '../../../logger'; import './StationPopup.css'; import { + stationNetwork, uniqStationFreqs, uniqStationLocations, - uniqStationNames, uniqStationObsPeriods + uniqStationNames, + uniqStationObsPeriods, + uniqStationVariableNames, } from '../../../utils/station-info'; logger.configure({ active: true }); @@ -23,120 +26,125 @@ logger.configure({ active: true }); const formatDate = d => d ? d.toISOString().substr(0,10) : 'unknown'; -class StationPopup extends Component { - static propTypes = { - station: PropTypes.object.isRequired, - network: PropTypes.object.isRequired, - variables: PropTypes.array.isRequired, - defaultNetworkColor: PropTypes.string, - }; - - static defaultProps = { - defaultNetworkColor: - process.env.REACT_APP_DEFAULT_NETWORK_COLOR ?? '#000000', - } - - render() { - const { station, network, variables, defaultNetworkColor } = this.props; - const networkColor = - chroma(network.color ?? defaultNetworkColor).alpha(0.5).css(); - - - const stationNames = flow( - uniqStationNames, - join(", "), - )(station); - - const stationLocations = ( -
      - { - flow( - uniqStationLocations, - map(loc => ( -
    • - {-loc.lon} W
      - {loc.lat} N
      - Elev. {loc.elevation} m -
    • - )), - )(station) - } -
    - ); - - const stationObsPeriods = ( -
      - { - flow( - uniqStationObsPeriods, - map(hx => ( -
    • {formatDate(hx.min_obs_time)} to {formatDate(hx.max_obs_time)}
    • - )) - )(station) - } -
    - ); - const stationObsFreqs = ( -
      - { - flow( - uniqStationFreqs, - map(freq => (
    • {FrequencySelector.valueToLabel(freq)}
    • )), - )(station) - } -
    - ); - - return ( - -

    Station: {stationNames}

    -
    Network - {`${network.name} – ${network.long_name}`} + {`${network.name}`}
    {station.id}
    Longitude{history0.lon}Locations{stationLocations}
    Latitude{history0.lat}
    Elevation{history0.elevation}
    Record span (histories: {histories.length}) -
      - { - map(hx => ( -
    • {formatDate(hx.min_obs_time)} to {formatDate(hx.max_obs_time)}
    • - ))(histories) - } -
    -
    Record spans{stationObsPeriods}
    Observation frequency{FrequencySelector.valueToLabel(history0.freq)}Observation freqs{stationObsFreqs}
    Recorded variables
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Network - - {`${network.name}`} - -
    Native ID{station.native_id}
    Database ID{station.id}
    Locations{stationLocations}
    Record spans{stationObsPeriods}
    Observation freqs{stationObsFreqs}
    Recorded variables -
      - { - map(variable => ( -
    • {variable.display_name}
    • - ))(variables) - } -
    -
    -
    - ); - } +function StationPopup({ + station, allNetworks, allVariables, defaultNetworkColor +}) { + const network = stationNetwork(allNetworks, station); + const networkColor = + chroma(network.color ?? defaultNetworkColor).alpha(0.5).css(); + + const stationNames = flow( + uniqStationNames, + join(", "), + )(station); + + const stationLocations = ( +
      + { + flow( + uniqStationLocations, + map(loc => ( +
    • + {-loc.lon} W
      + {loc.lat} N
      + Elev. {loc.elevation} m +
    • + )), + )(station) + } +
    + ); + + const stationObsPeriods = ( +
      + { + flow( + uniqStationObsPeriods, + map(hx => ( +
    • {formatDate(hx.min_obs_time)} to {formatDate(hx.max_obs_time)}
    • + )) + )(station) + } +
    + ); + + const stationObsFreqs = ( +
      + { + flow( + uniqStationFreqs, + map(freq => (
    • {FrequencySelector.valueToLabel(freq)}
    • )), + )(station) + } +
    + ); + + const variableNames = ( +
      + {map( + name => (
    • {name}
    • ), + uniqStationVariableNames(allVariables, station) + )} +
    + ); + + return ( + +

    Station: {stationNames}

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Network + + {`${network.name}`} + +
    Native ID{station.native_id}
    Database ID{station.id}
    History count{station.histories.length}
    Locations{stationLocations}
    Record spans{stationObsPeriods}
    Observation freqs{stationObsFreqs}
    Recorded variables{variableNames}
    +
    + ); +} + +StationPopup.propTypes = { + station: PropTypes.object.isRequired, + allNetworks: PropTypes.array.isRequired, + allVariables: PropTypes.array.isRequired, + defaultNetworkColor: PropTypes.string, +}; + +StationPopup.defaultProps = { + defaultNetworkColor: + process.env.REACT_APP_DEFAULT_NETWORK_COLOR ?? '#000000', } export default StationPopup; From c8b8d13e04e82d3f6c6b68491cc503add35044f0 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 22 Dec 2021 11:09:33 -0800 Subject: [PATCH 14/44] Refactor StationMarkers; add multi-location polygon --- .../maps/StationMarkers/StationMarkers.js | 174 +++++++----------- 1 file changed, 65 insertions(+), 109 deletions(-) diff --git a/src/components/maps/StationMarkers/StationMarkers.js b/src/components/maps/StationMarkers/StationMarkers.js index a929b758..a3f0c3c5 100644 --- a/src/components/maps/StationMarkers/StationMarkers.js +++ b/src/components/maps/StationMarkers/StationMarkers.js @@ -1,20 +1,19 @@ import PropTypes from 'prop-types'; import React from 'react'; import { CircleMarker, Polygon } from 'react-leaflet'; -import compact from 'lodash/fp/compact'; import map from 'lodash/fp/map'; -import find from 'lodash/fp/find'; import flow from 'lodash/fp/flow'; -import tap from 'lodash/fp/tap'; -import uniqWith from 'lodash/fp/uniqWith'; -import { utcDateOnly } from '../../../utils/portals-common'; import StationTooltip from '../StationTooltip'; import StationPopup from '../StationPopup'; import logger from '../../../logger'; import './StationMarkers.css'; +import { + stationNetwork, + uniqStationLocations +} from '../../../utils/station-info'; logger.configure({ active: true }); @@ -22,111 +21,74 @@ logger.configure({ active: true }); const noStations = []; -const network_for = (station, networks) => ( - find({ uri: station.network_uri })(networks) -); - - -const variables_for = (history, variables) => ( - flow( - map(variable_uri => find({ uri: variable_uri })(variables)), - // compacting this array should not be necessary, but the API delivers - // erroneous data (due ultimately to erroneous database records, I believe) - // that causes some of the variables to be "missing". - compact, - )(history.variable_uris) -); - - -function StationMarker({ station, allNetworks, allVariables, markerOptions }) { - const histories = station.histories; - const history = histories[0]; - const network = network_for(station, allNetworks); - const variables = variables_for(history, allVariables); +function StationMarker({ + station, allNetworks, allVariables, markerOptions, polygonOptions +}) { + const network = stationNetwork(allNetworks, station); const uniqLatLngs = flow( - uniqWith( - (hx1, hx2) => ( - utcDateOnly(hx1.min_obs_time).getTime() - === utcDateOnly(hx2.min_obs_time).getTime() - && utcDateOnly(hx1.max_obs_time).getTime() - === utcDateOnly(hx2.max_obs_time).getTime() - ) - ), + uniqStationLocations, map(hx => ({ lng: hx.lon, lat: hx.lat })) - )(station.histories); - - return map( - latLng => ( - - - - - ), - uniqLatLngs - ); + )(station); + const stationTooltip = ( + + ); - if (histories.length === 0) { - return null; - } - - if (histories.length === 1) { - return ( - - - - - ); - } + const stationPopup = ( + + ); return ( - - - - - ) + + { + map( + latLng => ( + + {stationTooltip} + {stationPopup} + + ), + uniqLatLngs + ) + } + { + uniqLatLngs.length > 1 && ( + + {stationTooltip} + {stationPopup} + + ) + } + + ); } -StationMarker.propTypes = { - stations: PropTypes.array.isRequired, +const commonStationMarkerPropTypes = { allNetworks: PropTypes.array.isRequired, allVariables: PropTypes.array.isRequired, markerOptions: PropTypes.object, + polygonOptions: PropTypes.object, +}; + +StationMarker.propTypes = { + station: PropTypes.object.isRequired, + ...commonStationMarkerPropTypes, }; StationMarker.defaultProps = { @@ -136,6 +98,9 @@ StationMarker.defaultProps = { fillOpacity: 0.75, color: '#000000', }, + polygonOptions: { + color: "purple", + }, }; @@ -154,18 +119,9 @@ function StationMarkers({ StationMarkers.propTypes = { stations: PropTypes.array.isRequired, - allNetworks: PropTypes.array.isRequired, - allVariables: PropTypes.array.isRequired, - markerOptions: PropTypes.object, + ...commonStationMarkerPropTypes, }; -StationMarkers.defaultProps = { - markerOptions: { - radius: 4, - weight: 1, - fillOpacity: 0.75, - color: '#000000', - }, -}; +StationMarkers.defaultProps = StationMarker.defaultProps; export default StationMarkers; From 902b9f083e55528ad3111270353c0e41946e53f3 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 22 Dec 2021 11:30:37 -0800 Subject: [PATCH 15/44] Refactor StationMarkers; add multi-location polygon --- src/components/maps/StationMarkers/StationMarkers.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/maps/StationMarkers/StationMarkers.js b/src/components/maps/StationMarkers/StationMarkers.js index a3f0c3c5..3f9b4cf9 100644 --- a/src/components/maps/StationMarkers/StationMarkers.js +++ b/src/components/maps/StationMarkers/StationMarkers.js @@ -14,6 +14,7 @@ import { stationNetwork, uniqStationLocations } from '../../../utils/station-info'; +import chroma from 'chroma-js'; logger.configure({ active: true }); @@ -25,6 +26,8 @@ function StationMarker({ station, allNetworks, allVariables, markerOptions, polygonOptions }) { const network = stationNetwork(allNetworks, station); + const polygonColor = + chroma(network.color ?? polygonOptions.color).alpha(0.3).css(); const uniqLatLngs = flow( uniqStationLocations, @@ -67,7 +70,8 @@ function StationMarker({ { uniqLatLngs.length > 1 && ( {stationTooltip} @@ -99,7 +103,7 @@ StationMarker.defaultProps = { color: '#000000', }, polygonOptions: { - color: "purple", + color: "green", }, }; From 99879e78c9cfafb2a0d5707bb8192faa3e488d68 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 22 Dec 2021 11:31:01 -0800 Subject: [PATCH 16/44] Add local backend to .env --- .env | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env b/.env index 623cbfa9..1e4c942b 100644 --- a/.env +++ b/.env @@ -4,7 +4,8 @@ REACT_APP_BASE_MAP=BC #REACT_APP_BASE_MAP=YNWT REACT_APP_BC_BASE_MAP_TILES_URL=https://services.pacificclimate.org/tiles/bc-albers-lite/{z}/{x}/{y}.png REACT_APP_YNWT_BASE_MAP_TILES_URL=https://services.pacificclimate.org/tiles/yukon-albers-lite/{z}/{x}/{y}.png -REACT_APP_SDS_URL=http://docker-dev02.pcic.uvic.ca:30512 +REACT_APP_SDS_URL=http://localhost:5000 +#REACT_APP_SDS_URL=http://docker-dev02.pcic.uvic.ca:30512 REACT_APP_PDP_DATA_URL=https://services.pacificclimate.org/data REACT_APP_DEFAULT_NETWORK_COLOR=#000000 # Values must be single-quoted to prevent errors created by processing dbl quotes. From a3066e8f22053bb32ed3f5ca4700ec752d71fefc Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 22 Dec 2021 11:33:03 -0800 Subject: [PATCH 17/44] Make station tooltip sticky --- src/components/maps/StationTooltip/StationTooltip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/maps/StationTooltip/StationTooltip.js b/src/components/maps/StationTooltip/StationTooltip.js index 64f1dd5e..e9e9b25a 100644 --- a/src/components/maps/StationTooltip/StationTooltip.js +++ b/src/components/maps/StationTooltip/StationTooltip.js @@ -14,7 +14,7 @@ function StationTooltip({ station, allNetworks }) { )(station); return ( - + {stationNames} ({network.name}) ); From 3bdfee6dba9ceed24c96fd6d854246d34cc616e4 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 22 Dec 2021 13:41:30 -0800 Subject: [PATCH 18/44] Curry some station-info fns --- src/utils/station-info.js | 46 +++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/utils/station-info.js b/src/utils/station-info.js index 8563ff84..86c4ff6a 100644 --- a/src/utils/station-info.js +++ b/src/utils/station-info.js @@ -11,14 +11,17 @@ import tap from 'lodash/fp/tap'; import sortBy from 'lodash/fp/sortBy'; import identity from 'lodash/fp/identity'; import sortedUniq from 'lodash/fp/sortedUniq'; +import curry from 'lodash/fp/curry'; -export const stationNetwork = (networks, station) => - find({ uri: station.network_uri })(networks); +export const stationNetwork = curry( + (networks, station) => find({ uri: station.network_uri }, networks) +); export const uniqStationNames = station => flow( map('station_name'), + sortBy(identity), uniq, )(station.histories); @@ -48,22 +51,23 @@ export const uniqStationFreqs = station => )(station.histories); -export const uniqStationVariableNames = (variables, station) => - flow( - map(history => - map( - variable_uri => find({ uri: variable_uri }, variables), - history.variable_uris - ) - ), - flatten, - // compacting this array should not be necessary, but the API delivers - // erroneous data (due ultimately to erroneous database records, I believe) - // that causes some of the variables to be "missing". - compact, - // tap(x => console.log("### compacted vars", x)), - map('display_name'), - sortBy(identity), - uniq, - sortedUniq, - )(station.histories) \ No newline at end of file +export const uniqStationVariableNames = curry( + (variables, station) => + flow( + map(history => + map( + variable_uri => find({ uri: variable_uri }, variables), + history.variable_uris + ) + ), + flatten, + // compacting this array should not be necessary, but the API delivers + // erroneous data (due ultimately to erroneous database records, I believe) + // that causes some of the variables to be "missing". + compact, + // tap(x => console.log("### compacted vars", x)), + map('display_name'), + sortBy(identity), + sortedUniq, + )(station.histories) +); \ No newline at end of file From dc00504980fe2f03bb80d698f99887f34724583e Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 22 Dec 2021 13:43:47 -0800 Subject: [PATCH 19/44] Fix sorting on StationMetadata table --- .../info/StationMetadata/StationMetadata.js | 262 ++++++++++-------- src/components/main/VersionA/VersionA.js | 2 +- 2 files changed, 143 insertions(+), 121 deletions(-) diff --git a/src/components/info/StationMetadata/StationMetadata.js b/src/components/info/StationMetadata/StationMetadata.js index 132936f0..c9adefeb 100644 --- a/src/components/info/StationMetadata/StationMetadata.js +++ b/src/components/info/StationMetadata/StationMetadata.js @@ -3,19 +3,21 @@ // passed into React Table. import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React from 'react'; import ReactTable from 'react-table'; import flow from 'lodash/fp/flow'; -import find from 'lodash/fp/find'; import map from 'lodash/fp/map'; import FrequencySelector from '../../selectors/FrequencySelector'; import logger from '../../../logger'; import { + stationNetwork, uniqStationFreqs, uniqStationLocations, - uniqStationNames, uniqStationObsPeriods + uniqStationNames, + uniqStationObsPeriods, + uniqStationVariableNames } from '../../../utils/station-info'; import 'react-table/react-table.css'; @@ -24,127 +26,147 @@ import './StationMetadata.css'; logger.configure({ active: true }); +// TODO: Put these in a util module const formatDate = d => d ? d.toISOString().substr(0,10) : 'unknown'; +const lexCompare = (a, b) => { + const n = Math.min(a.length, b.length); + for (let i = 0; i < n; i += 1) { + if (a[i] < b[i]) { + return -1; + } + if (a[i] > b[i]) { + return 1; + } + } + return a.length - b.length; +} -export default class StationMetadata extends Component { - static propTypes = { - stations: PropTypes.array, - allNetworks: PropTypes.array.isRequired, - }; - - render() { - const { stations, allNetworks, ...restProps } = this.props; - const columns = [ - { - id: 'Network', - Header: 'Network', - minWidth: 80, - maxWidth: 100, - accessor: station => { - const network = find({ uri: station.network_uri })(allNetworks); - return network ? network.name : '?'; - }, - }, - { - id: 'Native ID', - Header: 'Native ID', - minWidth: 80, - maxWidth: 100, - accessor: 'native_id' - }, - { - id: 'Unique Name(s)', - Header: 'Unique Name(s)', - minWidth: 120, - maxWidth: 200, - accessor: station => ( -
      - { - flow( - uniqStationNames, - map(name => (
    • {name}
    • )), - )(station) - } -
    - ), - }, - { - id: 'Unique Location(s)', - Header: 'Unique Location(s)', - minWidth: 120, - maxWidth: 200, - accessor: station => ( -
      - { - flow( - uniqStationLocations, - map(loc => ( -
    • - {-loc.lon} W
      - {loc.lat} N
      - Elev. {loc.elevation} m -
    • - )), - )(station) - } -
    - ), +function StationMetadata({ + stations, allNetworks, allVariables, ...restProps +}) { + const columns = [ + { + id: 'Network', + Header: 'Network', + minWidth: 80, + maxWidth: 100, + accessor: station => { + const network = stationNetwork(allNetworks, station); + return network ? network.name : '?'; }, - { - id: 'Unique Record(s)', - Header: 'Unique Record(s)', - minWidth: 100, - maxWidth: 200, - accessor: station => ( -
      - { - flow( - uniqStationObsPeriods, - map(period => ( -
    • - {formatDate(period.min_obs_time)} to
      - {formatDate(period.max_obs_time)} -
    • - )), - )(station) - } -
    - ), - }, - { - minWidth: 80, - maxWidth: 100, - id: 'Uniq Obs Freq', - Header: 'Uniq Obs Freq', - accessor: station => ( -
      - { - flow( - uniqStationFreqs, - map(freq => (
    • {FrequencySelector.valueToLabel(freq)}
    • )), - )(station.histories) - } -
    - ), - }, - { - id: '# Hx', - Header: '# Hx', - minWidth: 50, - maxWidth: 100, - accessor: station => station.histories.length, - }, - ]; + }, + { + id: 'Native ID', + Header: 'Native ID', + minWidth: 80, + maxWidth: 100, + accessor: 'native_id' + }, + { + id: 'Unique Names', + Header: 'Unique Names', + minWidth: 120, + maxWidth: 200, + accessor: uniqStationNames, + sortMethod: lexCompare, + Cell: row => ( +
      + {map(name => (
    • {name}
    • ), row.value)} +
    + ), + }, + { + id: 'Unique Locations', + Header: 'Unique Locations', + minWidth: 120, + maxWidth: 200, + sortable: false, + accessor: uniqStationLocations, + Cell: row => ( +
      + { + map(loc => ( +
    • + {-loc.lon} W
      + {loc.lat} N
      + Elev. {loc.elevation} m +
    • + ), row.value) + } +
    + ), + }, + { + id: 'Unique Records', + Header: 'Unique Records', + minWidth: 100, + maxWidth: 100, + sortable: false, + accessor: uniqStationObsPeriods, + Cell: row => ( +
      + { + map(period => ( +
    • + {formatDate(period.min_obs_time)} to
      + {formatDate(period.max_obs_time)} +
    • + ), row.value) + } +
    + ), + }, + { + minWidth: 80, + maxWidth: 100, + id: 'Uniq Obs Freqs', + Header: 'Uniq Obs Freqs', + accessor: flow(uniqStationFreqs, map(FrequencySelector.valueToLabel)), + sortMethod: lexCompare, + Cell: row => ( +
      + {map(freq => (
    • {freq}
    • ), row.value)} +
    + ), + }, + { + minWidth: 100, + maxWidth: 250, + id: 'Variables', + Header: 'Variables', + accessor: uniqStationVariableNames(allVariables), + sortable: false, + Cell: row => ( +
      + {map(name => (
    • {name}
    • ), row.value)} +
    + ), + }, + { + id: '# Hx', + Header: '# Hx', + minWidth: 30, + maxWidth: 30, + accessor: station => station.histories.length, + }, + ]; - return ( - - ); - } + return ( + + ); } + +StationMetadata.propTypes = { + stations: PropTypes.array, + allNetworks: PropTypes.array.isRequired, + allVariables: PropTypes.array.isRequired, +}; + +export default StationMetadata; diff --git a/src/components/main/VersionA/VersionA.js b/src/components/main/VersionA/VersionA.js index 5ad576db..430a762f 100644 --- a/src/components/main/VersionA/VersionA.js +++ b/src/components/main/VersionA/VersionA.js @@ -6,7 +6,6 @@ import get from 'lodash/fp/get'; import map from 'lodash/fp/map'; import filter from 'lodash/fp/filter'; import join from 'lodash/fp/join'; -import difference from 'lodash/fp/difference'; import css from '../common.module.css'; @@ -317,6 +316,7 @@ class Portal extends Component { From 4ee1204363be860976fa4c810c6a50e0b6e65871 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 22 Dec 2021 16:06:03 -0800 Subject: [PATCH 20/44] Fix layout of StationPopup --- src/components/maps/StationPopup/StationPopup.css | 7 +------ src/components/maps/StationPopup/StationPopup.js | 6 +++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/maps/StationPopup/StationPopup.css b/src/components/maps/StationPopup/StationPopup.css index bbff27d3..52154a60 100644 --- a/src/components/maps/StationPopup/StationPopup.css +++ b/src/components/maps/StationPopup/StationPopup.css @@ -2,12 +2,7 @@ h1 { font-size: 1.2em; } -.histories { +.scroll-y { max-height: 10em; overflow-y: scroll; - padding: 0px; } - -.variables { - padding: 0; -} \ No newline at end of file diff --git a/src/components/maps/StationPopup/StationPopup.js b/src/components/maps/StationPopup/StationPopup.js index 0b5a30cd..4335db66 100644 --- a/src/components/maps/StationPopup/StationPopup.js +++ b/src/components/maps/StationPopup/StationPopup.js @@ -39,7 +39,7 @@ function StationPopup({ )(station); const stationLocations = ( -
      +
        { flow( uniqStationLocations, @@ -56,7 +56,7 @@ function StationPopup({ ); const stationObsPeriods = ( -
          +
            { flow( uniqStationObsPeriods, @@ -80,7 +80,7 @@ function StationPopup({ ); const variableNames = ( -
              +
                {map( name => (
              • {name}
              • ), uniqStationVariableNames(allVariables, station) From 7068d5174262bff4116a1e88bc31234ee8c1d78f Mon Sep 17 00:00:00 2001 From: rod-glover Date: Thu, 23 Dec 2021 11:38:58 -0800 Subject: [PATCH 21/44] Add unionAll to utils/fp --- src/utils/fp/fp.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/utils/fp/fp.js b/src/utils/fp/fp.js index 220c2a79..4922cd25 100644 --- a/src/utils/fp/fp.js +++ b/src/utils/fp/fp.js @@ -1,6 +1,14 @@ -import { - reduce, assign, map, mapValues, toPairs, flow, curry, groupBy, isArray, isObject, -} from 'lodash/fp'; +import reduce from 'lodash/fp/reduce'; +import assign from 'lodash/fp/assign'; +import map from 'lodash/fp/map'; +import mapValues from 'lodash/fp/mapValues'; +import toPairs from 'lodash/fp/toPairs'; +import flow from 'lodash/fp/flow'; +import curry from 'lodash/fp/curry'; +import groupBy from 'lodash/fp/groupBy'; +import isArray from 'lodash/fp/isArray'; +import isObject from 'lodash/fp/isObject'; +import union from 'lodash/fp/union'; // TODO: There is a better fp way to do this. Find it and do it. @@ -58,4 +66,8 @@ export const mapWithKey = map.convert({ cap: false }); // properties of the objects to a single, initially empty, result object. // If a property occurs in more than one object in the list, the last // occurrence wins (as in `assign`). -export const objUnion = reduce((result, value) => assign(result, value), {}); \ No newline at end of file +export const objUnion = reduce((result, value) => assign(result, value), {}); + + +// Return the _.union of all arrays in the (array) argument. +export const unionAll = reduce(union, []); From fc0ea551b026dc69cea15cdc5e12403898944e27 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Thu, 23 Dec 2021 11:41:45 -0800 Subject: [PATCH 22/44] Refactor station filtering; handle histories everywhere --- src/utils/portals-common/portals-common.js | 166 +++++++++++++++------ src/utils/station-info.js | 9 ++ 2 files changed, 126 insertions(+), 49 deletions(-) diff --git a/src/utils/portals-common/portals-common.js b/src/utils/portals-common/portals-common.js index 75c1684c..da19a62a 100644 --- a/src/utils/portals-common/portals-common.js +++ b/src/utils/portals-common/portals-common.js @@ -1,24 +1,64 @@ import contains from 'lodash/fp/contains'; import compact from 'lodash/fp/compact'; +import every from 'lodash/fp/every'; import filter from 'lodash/fp/filter'; import flatten from 'lodash/fp/flatten'; import flow from 'lodash/fp/flow'; import intersection from 'lodash/fp/intersection'; import map from 'lodash/fp/map'; +import min from 'lodash/fp/min'; +import max from 'lodash/fp/max'; import some from 'lodash/fp/some'; import uniq from 'lodash/fp/uniq'; import isNil from 'lodash/fp/isNil'; -import union from 'lodash/fp/union'; -import reduce from 'lodash/fp/reduce'; +import flattenDepth from 'lodash/fp/flattenDepth'; import { isPointInPolygonWn } from '../geometry-algorithms'; - - -const checkGeoJSONPolygon = geometry => { +import { unionAll } from '../fp'; +import { stationVariableUris, uniqStationLocations } from '../station-info'; + + +// GeoJSON MultiPolygon format. Taken from +// https://conservancy.umn.edu/bitstream/handle/11299/210208/GeoJSON_Primer_2019.pdf +// +// { +// "type": "MultiPolygon", +// "coordinates": [ +// // one or more Polygon coordinate array: +// [ +// // one or more Linear ring coordinate arrays: +// [ +// // at least four Points; first point = last point: +// [x0, y0], +// [x1, y1], +// [x2, y2], +// // ... +// [x0, y0] +// ], +// // ... +// ], +// // ... +// ], +// // ... +// }; + + +const checkGeoJSONMultiPolygon = geometry => { if (geometry['type'] !== 'MultiPolygon') { throw new Error(`Invalid geometry type: ${geometry['type']}`) } }; + +const gJMultiPolygonBoundingBox = geometry => { + const points = flattenDepth(3, geometry["coordinates"]); + const xs = map(p => p[0], points); + const ys = map(p => p[1], points); + return [ + [min(xs), max(ys)], // top left + [max(xs), min(ys)], // bottom right + ] +}; + const getX = point => point[0]; const getY = point => point[1]; export const isPointInGeoJSONPolygon = isPointInPolygonWn(getX, getY); @@ -80,7 +120,7 @@ export const historyDateMatch = ( }; -export const stationDateMatch = ( +export const stationMatchesDates = ( station, startDate, endDate, strict = true ) => { // TODO: Coalesce adjacent histories from a station. NB: tricky. @@ -100,8 +140,13 @@ export const stationDateMatch = ( return r; }; +export const stationInAnyNetwork = (station, networks) => { + return contains( + station.network_uri, + map(nw => nw.value.uri, networks) + ); +}; -export const unionAll = reduce(union, []); export const atLeastOne = items => items.length > 0; @@ -134,6 +179,61 @@ export const stationReportsAnyFreqs = (station, freqs) => { }; +export const stationReportsClimatologyVariable = (station, variables) => { + return flow( + // Select variables that station reports + filter(({ uri }) => contains(uri, stationVariableUris(station))), + // Test that some reported variable is a climatology -- criterion from + // PDP PCDS backend + some(({ cell_method }) => /(within|over)/.test(cell_method)) + )(variables); +}; + + +// Checker for station inside polygon. Slightly optimized. +// Intended use is one polygon and many stations. +// Returns a function that checks a station against the given polygon. +export const stationInsideMultiPolygon = multiPolygon => { + // polygon should always be a geoJSON MultiPolygon (even if there's only + // one polygon) + if (!multiPolygon) { + return () => true; + } + + checkGeoJSONMultiPolygon(multiPolygon); + // TODO: Is it worth checking bounding box? + const [[minLon, maxLat], [maxLon, minLat]] = + gJMultiPolygonBoundingBox(multiPolygon); + + return station => { + const stationCoords = flow( + uniqStationLocations, + map(history => [history.lon, history.lat]), + )(station); + + // Check bounding box + if ( + every( + ([lon, lat]) => + lon < minLon || lon > maxLon || lat < minLat || lat > maxLat, + stationCoords + ) + ) { + return false; + } + + //return true if any station coordinate is in any selected polygon + return some( + point => some( + polygon => isPointInGeoJSONPolygon(polygon[0], point), + multiPolygon.coordinates + ), + stationCoords + ); + } +}; + + export const stationFilter = ( startDate, endDate, selectedNetworks, selectedVariables, selectedFrequencies, onlyWithClimatology, area, allNetworks, allVariables, allStations @@ -151,56 +251,24 @@ export const stationFilter = ( map(option => option.value)(selectedFrequencies); // console.log('filteredStations selectedVariableUris', selectedVariableUris) + const stationInsideArea = stationInsideMultiPolygon(area); + console.group("stationFilter") + // TODO: Remove flow wrapper const r = flow( - // tap(s => console.log("all stations", s?.length)), - filter(station => { return ( - stationDateMatch(station, startDate, endDate, false) - - // Station is part of one of selected networks - && contains( - station.network_uri, - map(nw => nw.value.uri)(selectedNetworks) - ) - + stationMatchesDates(station, startDate, endDate, false) + && stationInAnyNetwork(station, selectedNetworks) && stationReportsSomeVariables(station, selectedVariableUris) && stationReportsAnyFreqs(station, selectedFrequencyValues) + && ( + !onlyWithClimatology || + stationReportsClimatologyVariable(station, allVariables) + ) + && stationInsideArea(station) ); }), - // tap(s => console.log("after date etc filtering", s?.length)), - - // Stations match `onlyWithClimatology`: - // If `onlyWithClimatology`, station reports a climatology variable. - filter(station => { - if (!onlyWithClimatology) { - return true; - } - return flow( - // Select variables that station reports - filter(({ uri }) => contains(uri, station.histories[0].variable_uris)), - // Test that some reported variable is a climatology -- criterion from - // PDP PCDS backend - some(({ cell_method }) => /(within|over)/.test(cell_method)) - )(allVariables) - }), - // tap(s => console.log("after onlyWithClimatology filtering", s?.length)), - - // Stations are inside `area` - filter(station => { - if (!area) { - return true; - } - //area will always be a geoJSON MultiPolygon (even if there's only one polygon) - checkGeoJSONPolygon(area); - - //return true if the station is in any selected polygon - return some(poly => isPointInGeoJSONPolygon(poly[0], - [station.histories[0].lon, station.histories[0].lat] ))(area.coordinates); - - }), - // tap(s => console.log("after area filtering", s?.length)), )(allStations); console.groupEnd() diff --git a/src/utils/station-info.js b/src/utils/station-info.js index 86c4ff6a..ba3a9c5e 100644 --- a/src/utils/station-info.js +++ b/src/utils/station-info.js @@ -12,12 +12,21 @@ import sortBy from 'lodash/fp/sortBy'; import identity from 'lodash/fp/identity'; import sortedUniq from 'lodash/fp/sortedUniq'; import curry from 'lodash/fp/curry'; +import { unionAll } from './fp' + export const stationNetwork = curry( (networks, station) => find({ uri: station.network_uri }, networks) ); +export const stationVariableUris = station => + flow( + map('variable_uris'), + unionAll, + )(station.histories); + + export const uniqStationNames = station => flow( map('station_name'), From 28a0776c3ccf818989aaad664257838af4f935a0 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Fri, 24 Dec 2021 14:32:00 -0800 Subject: [PATCH 23/44] Add timing module --- src/utils/timing.js | 90 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/utils/timing.js diff --git a/src/utils/timing.js b/src/utils/timing.js new file mode 100644 index 00000000..537aa651 --- /dev/null +++ b/src/utils/timing.js @@ -0,0 +1,90 @@ +// Module for timing execution of functions. +// +// Typical usage: +// +// timer = getTimer(""); +// +// Use function `timeThis` like a decorator, as follows: +// +// const f = timer.timeThis("")( +// function f(...) { ... }; +// ); +// +// Execution times of invocations of function `f` are accumulated under the +// key "". Accumulation of execution times is reset when `reset("")` +// or, more commonly, `resetAll()`, is called, normally from a caller of `f`. +// +// Typically timing results are logged immediately after reset: +// +// timer.resetAll(); +// timer.log(); +// +// This is typically done immediately before `reset` is called. + +import flow from 'lodash/fp/flow'; +import map from 'lodash/fp/map'; +import sum from 'lodash/fp/sum'; +import sortBy from 'lodash/fp/sortBy'; + +export class Timer { + constructor(name) { + this.name = name; + this.timings = {}; + } + + reset(key) { + this.timings[key] = {}; + } + + resetAll() { + this.timings = {}; + } + + start(key, log = false) { + if (log) { + console.log(`${key} timing start`) + } + this.timings[key] = this.timings[key] || {}; + this.timings[key].start = Date.now(); + this.timings[key].duration = this.timings[key].duration ?? 0; + } + + stop(key, log = false) { + const more = Date.now() - this.timings[key].start; + if (log) { + console.log(`${key} timing stop: ${more}`) + } + this.timings[key].duration += more; + this.timings[key].start = null; + } + + timeThis = (key, log = false) => f => { + return (...args) => { + this.start(key, log); + const r = f(...args); + this.stop(key, log); + return r; + } + } + + log() { + console.group(this.name); + const totalDuration = flow(map('duration'), sum)(this.timings); + console.log(`Total duration: ${totalDuration / 1000} s`) + for(const [key, value] of sortBy('[0]', Object.entries(this.timings))) { + console.log(`${key}: ${value.duration / 1000} s (${Math.round(100 * value.duration / totalDuration)}%) [${value.start ? "running" : "stopped"}]`); + } + console.groupEnd(); + } + +} + + +const registry = {}; + +export const getTimer = name => { + if (!registry[name]) { + registry[name] = new Timer(name); + } + return registry[name]; +}; From 3ab6890d0ed1678a11268cf7ee012ee2364f02bc Mon Sep 17 00:00:00 2001 From: rod-glover Date: Fri, 24 Dec 2021 15:06:58 -0800 Subject: [PATCH 24/44] Apply timing to filtering fns; optimize some --- src/utils/portals-common/portals-common.js | 140 +++++++++++---------- 1 file changed, 72 insertions(+), 68 deletions(-) diff --git a/src/utils/portals-common/portals-common.js b/src/utils/portals-common/portals-common.js index da19a62a..d73894d9 100644 --- a/src/utils/portals-common/portals-common.js +++ b/src/utils/portals-common/portals-common.js @@ -1,10 +1,8 @@ import contains from 'lodash/fp/contains'; -import compact from 'lodash/fp/compact'; import every from 'lodash/fp/every'; import filter from 'lodash/fp/filter'; import flatten from 'lodash/fp/flatten'; import flow from 'lodash/fp/flow'; -import intersection from 'lodash/fp/intersection'; import map from 'lodash/fp/map'; import min from 'lodash/fp/min'; import max from 'lodash/fp/max'; @@ -13,8 +11,11 @@ import uniq from 'lodash/fp/uniq'; import isNil from 'lodash/fp/isNil'; import flattenDepth from 'lodash/fp/flattenDepth'; import { isPointInPolygonWn } from '../geometry-algorithms'; -import { unionAll } from '../fp'; import { stationVariableUris, uniqStationLocations } from '../station-info'; +import { getTimer } from '../timing'; + +const ft = getTimer("Station filtering timing") +ft.log(); // GeoJSON MultiPolygon format. Taken from @@ -97,6 +98,7 @@ export const historyDateMatch = ( if (strict) { // Allowing for nils, check that // minObsDate <= startDate <= endDate <= maxObsDate. + // TODO: Optimize return ( isNilOrAscending(startDate, endDate) && isNilOrAscending(minObsDate, startDate, maxObsDate) @@ -106,80 +108,80 @@ export const historyDateMatch = ( // This is a variant of the looser legacy comparison. Allows nil start, end. return ( - !isNil(minObsDate) && !isNil(maxObsDate) - && isNilOrAscending(startDate, maxObsDate) - && isNilOrAscending(minObsDate, endDate) + (isNil(startDate) || isNil(maxObsDate) || startDate <= maxObsDate) + && (isNil(endDate) || isNil(minObsDate) || endDate >= minObsDate) ); - - // This is the looser comparison used by the legacy PDP portal. - // return ( - // !isNil(minObsDate) && !isNil(maxObsDate) - // && !isNil(startDate) && !isNil(endDate) - // && maxObsDate > startDate && minObsDate < endDate - // ); }; -export const stationMatchesDates = ( - station, startDate, endDate, strict = true -) => { - // TODO: Coalesce adjacent histories from a station. NB: tricky. - // If we don't do this, and we use strict date matching, then a station with - // several histories fully covering an interval will not be selected, even - // though it should be. The question of what "adjacent" means is a bit tricky - // ... would depend in part on history.freq to distinguish too-large gaps, - // but we already know that attribute isn't always an accurate reflection of - // actual observations. - const r = flow( - map(hx => historyDateMatch(hx, startDate, endDate, strict)), - some(Boolean), - )(station.histories); - // if (!r) { - // console.log(`Station ${station.id} filtered out on date`) - // } - return r; -}; +export const stationMatchesDates = ft.timeThis("stationMatchesDates")( + (station, startDate, endDate, strict = true) => { + // TODO: Coalesce adjacent histories from a station. NB: tricky. + // If we don't do this, and we use strict date matching, then a station with + // several histories fully covering an interval will not be selected, even + // though it should be. The question of what "adjacent" means is a bit tricky + // ... would depend in part on history.freq to distinguish too-large gaps, + // but we already know that attribute isn't always an accurate reflection of + // actual observations. + const r = some( + hx => historyDateMatch(hx, startDate, endDate, strict), + station.histories + ); + // if (!r) { + // console.log(`Station ${station.id} filtered out on date`) + // } + return r; + } +); -export const stationInAnyNetwork = (station, networks) => { - return contains( - station.network_uri, - map(nw => nw.value.uri, networks) - ); -}; +export const stationInAnyNetwork = ft.timeThis("stationInAnyNetwork")( + (station, networks) => { + return contains( + station.network_uri, + map(nw => nw.value.uri, networks) + ); + } +); export const atLeastOne = items => items.length > 0; -export const stationReportsSomeVariables = (station, variableUris) => { - const r = flow( - map("variable_uris"), - compact, - unionAll, - intersection(variableUris), - atLeastOne, - )(station.histories); - // if (!r) { - // console.log(`Station ${station.id} filtered out on variables`) - // } - return r; -}; +export const stationReportsSomeVariables = + ft.timeThis("stationReportsSomeVariables")( + (station, variableUris) => { + const stationVariableUris = ft.timeThis("stationVariableUris")(flow( + map("variable_uris"), + flatten, + ))(station.histories); + const r = ft.timeThis("variableUri in stationVariableUris")( + some(uri => contains(uri, stationVariableUris)) + )(variableUris); + // if (!r) { + // console.log(`Station ${station.id} filtered out on variables`) + // } + return r; + } + ); -export const stationReportsAnyFreqs = (station, freqs) => { - const r = flow( - map("freq"), - intersection(freqs), - atLeastOne, - )(station.histories); - // if (!r) { - // console.log(`Station ${station.id} filtered out on freqs`) - // } - return r; -}; +export const stationReportsAnyFreqs = ft.timeThis("stationReportsAnyFreqs")( + (station, freqs) => { + const stationFreqs = ft.timeThis("stationFreqs")( + map("freq"), + )(station.histories); + const r = ft.timeThis("freq in stationFreqs")( + some(freq => contains(freq, stationFreqs)) + )(freqs); + if (!r) { + console.log(`Station ${station.id} filtered out on freqs`) + } + return r; + } +); -export const stationReportsClimatologyVariable = (station, variables) => { +export const stationReportsClimatologyVariable = ft.timeThis("stationReportsClimatologyVariable")((station, variables) => { return flow( // Select variables that station reports filter(({ uri }) => contains(uri, stationVariableUris(station))), @@ -187,13 +189,13 @@ export const stationReportsClimatologyVariable = (station, variables) => { // PDP PCDS backend some(({ cell_method }) => /(within|over)/.test(cell_method)) )(variables); -}; +}); // Checker for station inside polygon. Slightly optimized. // Intended use is one polygon and many stations. // Returns a function that checks a station against the given polygon. -export const stationInsideMultiPolygon = multiPolygon => { +export const stationInsideMultiPolygon = ft.timeThis("stationInsideMultiPolygon")(multiPolygon => { // polygon should always be a geoJSON MultiPolygon (even if there's only // one polygon) if (!multiPolygon) { @@ -231,20 +233,21 @@ export const stationInsideMultiPolygon = multiPolygon => { stationCoords ); } -}; +}); export const stationFilter = ( startDate, endDate, selectedNetworks, selectedVariables, selectedFrequencies, onlyWithClimatology, area, allNetworks, allVariables, allStations ) => { + ft.resetAll(); // console.log('filteredStations allStations', allStations) - const selectedVariableUris = flow( + const selectedVariableUris = ft.timeThis("selectedVariableUris")(flow( map(selectedVariable => selectedVariable.contexts), flatten, map(context => context.uri), uniq, - )(selectedVariables); + ))(selectedVariables); // console.log('filteredStations selectedVariableUris', selectedVariableUris) const selectedFrequencyValues = @@ -271,6 +274,7 @@ export const stationFilter = ( }), )(allStations); + ft.log(); console.groupEnd() return r; }; From d83ca51afeb5d1c8a8cf11fa275eef2f9c911c02 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Fri, 24 Dec 2021 15:10:21 -0800 Subject: [PATCH 25/44] Apply timing to StationMap (not helpful) --- src/components/maps/StationMap/StationMap.js | 4 ++++ .../maps/StationMarkers/StationMarkers.js | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/maps/StationMap/StationMap.js b/src/components/maps/StationMap/StationMap.js index 76709acb..98c25e9d 100644 --- a/src/components/maps/StationMap/StationMap.js +++ b/src/components/maps/StationMap/StationMap.js @@ -44,8 +44,11 @@ import { geoJSONToLeafletLayers, layersToGeoJSONMultipolygon } import logger from '../../../logger'; import './StationMap.css'; +import { getTimer } from '../../../utils/timing'; logger.configure({ active: true }); +const smtimer = getTimer("StationMarker timing") +smtimer.log(); export default class StationMap extends Component { static propTypes = { @@ -144,6 +147,7 @@ export default class StationMap extends Component { this.props; const { geometryLayers } = this.state; const allowGeometryDraw = true || geometryLayers.length === 0; + smtimer.log(); return ( diff --git a/src/components/maps/StationMarkers/StationMarkers.js b/src/components/maps/StationMarkers/StationMarkers.js index 3f9b4cf9..c854ff3e 100644 --- a/src/components/maps/StationMarkers/StationMarkers.js +++ b/src/components/maps/StationMarkers/StationMarkers.js @@ -15,16 +15,19 @@ import { uniqStationLocations } from '../../../utils/station-info'; import chroma from 'chroma-js'; +import { getTimer } from '../../../utils/timing'; + logger.configure({ active: true }); +const timer = getTimer("StationMarker timing"); const noStations = []; -function StationMarker({ +const StationMarker = timer.timeThis("StationMarker")(({ station, allNetworks, allVariables, markerOptions, polygonOptions -}) { +}) => { const network = stationNetwork(allNetworks, station); const polygonColor = chroma(network.color ?? polygonOptions.color).alpha(0.3).css(); @@ -49,7 +52,7 @@ function StationMarker({ /> ); - return ( + const r = ( { map( @@ -81,7 +84,8 @@ function StationMarker({ } ); -} + return r; +}); const commonStationMarkerPropTypes = { allNetworks: PropTypes.array.isRequired, @@ -108,10 +112,10 @@ StationMarker.defaultProps = { }; -function StationMarkers({ +const StationMarkers = ({ stations, ...rest -}) { - return ( +}) => { + const r = ( map( station => ( @@ -119,6 +123,7 @@ function StationMarkers({ stations || noStations ) ); + return r; } StationMarkers.propTypes = { From d4b886e0f6424c5f7d3255c65fa131c8bd90016a Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 4 Jan 2022 13:40:11 -0800 Subject: [PATCH 26/44] Comment out debug code --- src/utils/portals-common/portals-common.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/portals-common/portals-common.js b/src/utils/portals-common/portals-common.js index d73894d9..436441a4 100644 --- a/src/utils/portals-common/portals-common.js +++ b/src/utils/portals-common/portals-common.js @@ -173,9 +173,9 @@ export const stationReportsAnyFreqs = ft.timeThis("stationReportsAnyFreqs")( const r = ft.timeThis("freq in stationFreqs")( some(freq => contains(freq, stationFreqs)) )(freqs); - if (!r) { - console.log(`Station ${station.id} filtered out on freqs`) - } + // if (!r) { + // console.log(`Station ${station.id} filtered out on freqs`) + // } return r; } ); From 2a252f94856a73efee2ece447513e5e1adb3e4ad Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 4 Jan 2022 13:41:00 -0800 Subject: [PATCH 27/44] Factor out StationData component --- .../info/StationData/StationData.css | 0 .../info/StationData/StationData.js | 60 +++++++++++++++++++ src/components/info/StationData/package.json | 6 ++ 3 files changed, 66 insertions(+) create mode 100644 src/components/info/StationData/StationData.css create mode 100644 src/components/info/StationData/StationData.js create mode 100644 src/components/info/StationData/package.json diff --git a/src/components/info/StationData/StationData.css b/src/components/info/StationData/StationData.css new file mode 100644 index 00000000..e69de29b diff --git a/src/components/info/StationData/StationData.js b/src/components/info/StationData/StationData.js new file mode 100644 index 00000000..af3ba02a --- /dev/null +++ b/src/components/info/StationData/StationData.js @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types'; +import React, { useState } from 'react'; +import ButtonToolbar from 'react-bootstrap/lib/ButtonToolbar'; +import capitalize from 'react-bootstrap/lib/utils/capitalize'; +import map from 'lodash/fp/map'; + +import FileFormatSelector from '../../selectors/FileFormatSelector'; +import ClipToDateControl from '../../controls/ClipToDateControl'; +import ObservationCounts from '../../info/ObservationCounts'; + +import logger from '../../../logger'; + +import './StationData.css'; + +logger.configure({ active: true }); + + +function StationData({ + selectedStations, dataDownloadUrl, dataDownloadFilename +}) { + const [fileFormat, setFileFormat] = useState(); + const [clipToDate, setClipToDate] = useState(false); + const toggleClipToDate = () => setClipToDate(!clipToDate); + + return ( + + + + + + + + + { + map( + id => ( + + Download {capitalize(id)} + + ), + ['timeseries', 'climatology'] + ) + } + + + + ); +} + +StationData.propTypes = { + selectedStations: PropTypes.array.isRequired, + dataDownloadUrl: PropTypes.func.isRequired, + dataDownloadFilename: PropTypes.func.isRequired, +}; + +export default StationData; diff --git a/src/components/info/StationData/package.json b/src/components/info/StationData/package.json new file mode 100644 index 00000000..ac1a7240 --- /dev/null +++ b/src/components/info/StationData/package.json @@ -0,0 +1,6 @@ +{ + "name": "StationData", + "version": "0.0.0", + "private": true, + "main": "./StationData.js" +} \ No newline at end of file From cf18cb1513f2fb70c84d36f32e0c720e6f291644 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 4 Jan 2022 13:42:25 -0800 Subject: [PATCH 28/44] Make marker keys unique --- src/components/maps/StationMarkers/StationMarkers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/maps/StationMarkers/StationMarkers.js b/src/components/maps/StationMarkers/StationMarkers.js index c854ff3e..2aa656fb 100644 --- a/src/components/maps/StationMarkers/StationMarkers.js +++ b/src/components/maps/StationMarkers/StationMarkers.js @@ -58,7 +58,7 @@ const StationMarker = timer.timeThis("StationMarker")(({ map( latLng => ( Date: Tue, 4 Jan 2022 13:50:57 -0800 Subject: [PATCH 29/44] Don't hide unselected stations in map --- src/components/maps/StationMap/StationMap.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/maps/StationMap/StationMap.js b/src/components/maps/StationMap/StationMap.js index 98c25e9d..69c7bfd5 100644 --- a/src/components/maps/StationMap/StationMap.js +++ b/src/components/maps/StationMap/StationMap.js @@ -54,7 +54,8 @@ export default class StationMap extends Component { static propTypes = { BaseMap: PropTypes.object.isRequired, initialViewport: PropTypes.object.isRequired, - stations: PropTypes.array.isRequired, + allStations: PropTypes.array.isRequired, + // selectedStations: PropTypes.array.isRequired, allNetworks: PropTypes.array.isRequired, allVariables: PropTypes.array.isRequired, onSetArea: PropTypes.func.isRequired, @@ -143,11 +144,18 @@ export default class StationMap extends Component { }; render() { - const { BaseMap, initialViewport, stations, allNetworks, allVariables } = - this.props; + const { + BaseMap, + initialViewport, + allStations, + // selectedStations, + allNetworks, + allVariables + } = this.props; const { geometryLayers } = this.state; const allowGeometryDraw = true || geometryLayers.length === 0; smtimer.log(); + smtimer.resetAll(); return ( @@ -177,7 +185,7 @@ export default class StationMap extends Component { From 8cc447c11e4c98ec7dfa07f057b8096f994f5f69 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 4 Jan 2022 13:51:55 -0800 Subject: [PATCH 30/44] Tweak station marker code --- src/components/maps/StationMarkers/StationMarkers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/maps/StationMarkers/StationMarkers.js b/src/components/maps/StationMarkers/StationMarkers.js index 2aa656fb..14aae586 100644 --- a/src/components/maps/StationMarkers/StationMarkers.js +++ b/src/components/maps/StationMarkers/StationMarkers.js @@ -30,7 +30,7 @@ const StationMarker = timer.timeThis("StationMarker")(({ }) => { const network = stationNetwork(allNetworks, station); const polygonColor = - chroma(network.color ?? polygonOptions.color).alpha(0.3).css(); + chroma(network?.color ?? polygonOptions.color).alpha(0.3).css(); const uniqLatLngs = flow( uniqStationLocations, @@ -61,7 +61,7 @@ const StationMarker = timer.timeThis("StationMarker")(({ key={latLng.id} center={latLng} {...markerOptions} - color={network && network.color} + color={network?.color} > {stationTooltip} {stationPopup} From 3ba06a5260bcb3af4d98ab4022decef3cb66ca83 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 4 Jan 2022 13:53:49 -0800 Subject: [PATCH 31/44] Adjust VersionA portal for changes above --- src/components/main/VersionA/VersionA.js | 66 ++++++++---------------- 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/src/components/main/VersionA/VersionA.js b/src/components/main/VersionA/VersionA.js index 430a762f..86d851a0 100644 --- a/src/components/main/VersionA/VersionA.js +++ b/src/components/main/VersionA/VersionA.js @@ -21,19 +21,15 @@ import VariableSelector from '../../selectors/VariableSelector'; import FrequencySelector from '../../selectors/FrequencySelector/FrequencySelector'; import DateSelector from '../../selectors/DateSelector'; -import FileFormatSelector from '../../selectors/FileFormatSelector'; -import ClipToDateControl from '../../controls/ClipToDateControl'; -import ObservationCounts from '../../info/ObservationCounts'; import { stationFilter } from '../../../utils/portals-common'; -import ButtonToolbar from 'react-bootstrap/es/ButtonToolbar'; -import StationMetadata from '../../info/StationMetadata'; import OnlyWithClimatologyControl from '../../controls/OnlyWithClimatologyControl'; import StationMap from '../../maps/StationMap'; +import StationMetadata from '../../info/StationMetadata'; +import StationData from '../../info/StationData'; +import NetworksMetadata from '../../info/NetworksMetadata'; import AdjustableColumns from '../../util/AdjustableColumns'; -import capitalize from 'react-bootstrap/lib/utils/capitalize'; import baseMaps from '../../maps/baseMaps'; -import NetworksMetadata from '../../info/NetworksMetadata'; logger.configure({ active: true }); @@ -221,7 +217,8 @@ class Portal extends Component { contents={[

                { - this.state.allStations ? + this.state.allStations ?( `${filteredStations.length} stations selected of - ${this.state.allStations ? this.state.allStations.length : 0} available` : - `Loading station info ... (this may take a couple of minutes)` + ${this.state.allStations?.length} available` + ) :( + `Loading station info ... ` + ) }

                {` - Available stations are filtered by - the network they are part of, - the variable(s) they observe, - and the frequency of obervation. - Stations matching selected criteria are displayed on the map. - `}

                + Available stations are filtered by + the network they are part of, + the variable(s) they observe, + and the frequency of obervation. + Stations matching selected criteria are displayed + on the map. + `}

                { unselectedThings &&

                You haven't selected any {unselectedThings}.

                } - - - - - - - - { - map( - id => ( - - Download {capitalize(id)} - - ), - ['timeseries', 'climatology'] - ) - } - - From 781a98f521fa966d967ff2d1b2beb4bde1d1a042 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 4 Jan 2022 15:17:15 -0800 Subject: [PATCH 32/44] Add timing to ObservationCounts --- .../ObservationCounts/ObservationCounts.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/info/ObservationCounts/ObservationCounts.js b/src/components/info/ObservationCounts/ObservationCounts.js index ff724045..417a266c 100644 --- a/src/components/info/ObservationCounts/ObservationCounts.js +++ b/src/components/info/ObservationCounts/ObservationCounts.js @@ -1,15 +1,23 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { Table } from 'react-bootstrap'; -import { flow, map, reduce } from 'lodash/fp'; +import { reduce } from 'lodash/fp'; import { getObservationCounts } from '../../../data-services/station-data-service'; import logger from '../../../logger'; +import { getTimer } from '../../../utils/timing'; import './ObservationCounts.css'; logger.configure({ active: true }); +const timer = getTimer("Observation count timing", { disable: true }) + + +const totalCounts = timer.timeThis("totalCounts")( + (counts, stations) => + reduce((sum, station) => sum + (counts[station.id] || 0), 0)(stations) +); class ObservationCounts extends Component { static propTypes = { @@ -67,14 +75,12 @@ class ObservationCounts extends Component { return

                Loading counts...

                } - const totalCounts = (counts, stations) => ( - reduce((sum, station) => sum + (counts[station.id] || 0), 0)(stations) - ); - + timer.resetAll(); const totalObservationCountsForStations = totalCounts(countData.observationCounts, this.props.stations); const totalClimatologyCountsForStations = totalCounts(countData.climatologyCounts, this.props.stations); + timer.log(); return ( @@ -104,9 +110,6 @@ class ObservationCounts extends Component {
                - ) - return ( -

                Total observations for selected stations: {totalObservationCountsForStations}

                ); } } From 2701c30b464586f2f8d107329cc7fc36df4a09b1 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 4 Jan 2022 15:17:54 -0800 Subject: [PATCH 33/44] Improve timing util --- src/utils/timing.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/utils/timing.js b/src/utils/timing.js index 537aa651..5da2739e 100644 --- a/src/utils/timing.js +++ b/src/utils/timing.js @@ -25,6 +25,7 @@ import flow from 'lodash/fp/flow'; import map from 'lodash/fp/map'; import sum from 'lodash/fp/sum'; import sortBy from 'lodash/fp/sortBy'; +import isNil from 'lodash/fp/isNil'; export class Timer { constructor(name) { @@ -33,11 +34,15 @@ export class Timer { } reset(key) { + if (isNil(key)) { + this.timings = {}; + return; + } this.timings[key] = {}; } resetAll() { - this.timings = {}; + this.reset(); } start(key, log = false) { @@ -58,7 +63,11 @@ export class Timer { this.timings[key].start = null; } - timeThis = (key, log = false) => f => { + timeThis = (key, options = {}) => f => { + const { disable = false, log = false } = options; + if (disable) { + return f; + } return (...args) => { this.start(key, log); const r = f(...args); From 1184f534ef53fbfeac1617fd4a7aaa3abfcbe67f Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 4 Jan 2022 17:09:34 -0800 Subject: [PATCH 34/44] Fix map display of filtered vs selected stations --- src/components/main/VersionA/VersionA.js | 41 +++++++++++--------- src/components/maps/StationMap/StationMap.js | 9 +++-- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/components/main/VersionA/VersionA.js b/src/components/main/VersionA/VersionA.js index 86d851a0..bfb645d6 100644 --- a/src/components/main/VersionA/VersionA.js +++ b/src/components/main/VersionA/VersionA.js @@ -21,7 +21,10 @@ import VariableSelector from '../../selectors/VariableSelector'; import FrequencySelector from '../../selectors/FrequencySelector/FrequencySelector'; import DateSelector from '../../selectors/DateSelector'; -import { stationFilter } from '../../../utils/portals-common'; +import { + stationFilter, + stationInsideMultiPolygon +} from '../../../utils/portals-common'; import OnlyWithClimatologyControl from '../../controls/OnlyWithClimatologyControl'; import StationMap from '../../maps/StationMap'; @@ -139,7 +142,7 @@ class Portal extends Component { stationFilter = memoize(stationFilter); - dataDownloadUrl = dataCategory => { + dataDownloadUrl = ({ dataCategory, clipToDate, fileFormat }) => { // Check whether state has settled. Each selector calls an onReady callback // to export information (e.g., all its options) that it has set up // internally. In retrospect, this is a too-clever solution to the problem @@ -159,18 +162,18 @@ class Portal extends Component { variables: this.state.selectedVariables, frequencies: this.state.selectedFrequencies, polygon: this.state.area, - clipToDate: this.state.clipToDate, onlyWithClimatology: this.state.onlyWithClimatology, - dataCategory, - dataFormat: this.state.fileFormat, allNetworks: this.state.networkActions.getAllOptions(), allVariables: this.state.variableActions.getAllOptions(), allFrequencies: this.state.frequencyActions.getAllOptions(), + dataCategory, + clipToDate, + dataFormat: fileFormat, }); }; - dataDownloadFilename = id => { - return `${id}.${get('value', this.state.fileFormat)}`; + dataDownloadFilename = ({ dataCategory, fileFormat }) => { + return `${{ dataCategory, fileFormat }}.${get('value', fileFormat)}`; } render() { @@ -188,6 +191,9 @@ class Portal extends Component { this.state.allStations, ); + const stationInsideArea = stationInsideMultiPolygon(this.state.area); + const selectedStations = filter(stationInsideArea, filteredStations); + const selections = [ { name: 'networks', @@ -217,8 +223,7 @@ class Portal extends Component { contents={[

                - {filteredStations?.length} stations selected + {selectedStations?.length} stations selected of {this.state.allStations?.length} (see Station Data tab for details)

                @@ -309,9 +314,9 @@ class Portal extends Component { Download Metadata -

                {filteredStations.length} stations selected

                +

                {selectedStations.length} stations selected

                @@ -320,12 +325,12 @@ class Portal extends Component {

                { - this.state.allStations ?( - `${filteredStations.length} stations selected of + this.state.allStations ? ( + `${selectedStations.length} stations selected of ${this.state.allStations?.length} available` - ) :( - `Loading station info ... ` - ) + ) : ( + `Loading station info ... ` + ) }

                {` Available stations are filtered by @@ -341,7 +346,7 @@ class Portal extends Component { } diff --git a/src/components/maps/StationMap/StationMap.js b/src/components/maps/StationMap/StationMap.js index 69c7bfd5..814c076c 100644 --- a/src/components/maps/StationMap/StationMap.js +++ b/src/components/maps/StationMap/StationMap.js @@ -54,8 +54,7 @@ export default class StationMap extends Component { static propTypes = { BaseMap: PropTypes.object.isRequired, initialViewport: PropTypes.object.isRequired, - allStations: PropTypes.array.isRequired, - // selectedStations: PropTypes.array.isRequired, + stations: PropTypes.array.isRequired, allNetworks: PropTypes.array.isRequired, allVariables: PropTypes.array.isRequired, onSetArea: PropTypes.func.isRequired, @@ -147,7 +146,7 @@ export default class StationMap extends Component { const { BaseMap, initialViewport, - allStations, + stations, // selectedStations, allNetworks, allVariables @@ -157,6 +156,8 @@ export default class StationMap extends Component { smtimer.log(); smtimer.resetAll(); + // alert("StationMap render") + return ( From 0acbc974eabb3c974d940de9e5a3998501dc84f6 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 4 Jan 2022 17:21:08 -0800 Subject: [PATCH 35/44] Fix map display of filtered vs selected stations --- src/utils/portals-common/portals-common.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/utils/portals-common/portals-common.js b/src/utils/portals-common/portals-common.js index 436441a4..d811c4b0 100644 --- a/src/utils/portals-common/portals-common.js +++ b/src/utils/portals-common/portals-common.js @@ -241,26 +241,17 @@ export const stationFilter = ( onlyWithClimatology, area, allNetworks, allVariables, allStations ) => { ft.resetAll(); - // console.log('filteredStations allStations', allStations) const selectedVariableUris = ft.timeThis("selectedVariableUris")(flow( map(selectedVariable => selectedVariable.contexts), flatten, map(context => context.uri), uniq, ))(selectedVariables); - // console.log('filteredStations selectedVariableUris', selectedVariableUris) const selectedFrequencyValues = map(option => option.value)(selectedFrequencies); - // console.log('filteredStations selectedVariableUris', selectedVariableUris) - const stationInsideArea = stationInsideMultiPolygon(area); - - console.group("stationFilter") - // TODO: Remove flow wrapper - const r = flow( - filter(station => { - return ( + const r = filter(station => ( stationMatchesDates(station, startDate, endDate, false) && stationInAnyNetwork(station, selectedNetworks) && stationReportsSomeVariables(station, selectedVariableUris) @@ -269,12 +260,9 @@ export const stationFilter = ( !onlyWithClimatology || stationReportsClimatologyVariable(station, allVariables) ) - && stationInsideArea(station) - ); - }), - )(allStations); + ) + )(allStations); ft.log(); - console.groupEnd() return r; }; From c36ee1e4149f65a24b127a04d77aa8cd5e77a333 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 4 Jan 2022 17:21:48 -0800 Subject: [PATCH 36/44] Fix data download urls --- src/components/info/StationData/StationData.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/info/StationData/StationData.js b/src/components/info/StationData/StationData.js index af3ba02a..146f8851 100644 --- a/src/components/info/StationData/StationData.js +++ b/src/components/info/StationData/StationData.js @@ -33,13 +33,13 @@ function StationData({ { map( - id => ( + dataCategory => ( - Download {capitalize(id)} + Download {capitalize(dataCategory)} ), ['timeseries', 'climatology'] From 76299b0e45150ff8f2e7b1c8646038a55f08d37d Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 5 Jan 2022 09:30:31 -0800 Subject: [PATCH 37/44] Update component template to functional style --- src/components/0Template/Template.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/components/0Template/Template.js b/src/components/0Template/Template.js index 73ba8b84..79c354ce 100644 --- a/src/components/0Template/Template.js +++ b/src/components/0Template/Template.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React from 'react'; import logger from '../../logger'; @@ -7,16 +7,13 @@ import './Template.css'; logger.configure({ active: true }); -export default class Template extends Component { - static propTypes = { - }; - - state = {}; - - render() { - return ( -

                Template
                - ); - } +function Template({ a }) { + return ( +
                Template
                + ); } +Template.propTypes = { +}; + +export default Template; From 7f63908511b340052f2f5e91de2f50699d45d2ac Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 5 Jan 2022 11:35:54 -0800 Subject: [PATCH 38/44] Add component SelectionCounts --- .../info/SelectionCounts/SelectionCounts.css | 0 .../info/SelectionCounts/SelectionCounts.js | 49 +++++++++++++++++++ .../info/SelectionCounts/package.json | 6 +++ 3 files changed, 55 insertions(+) create mode 100644 src/components/info/SelectionCounts/SelectionCounts.css create mode 100644 src/components/info/SelectionCounts/SelectionCounts.js create mode 100644 src/components/info/SelectionCounts/package.json diff --git a/src/components/info/SelectionCounts/SelectionCounts.css b/src/components/info/SelectionCounts/SelectionCounts.css new file mode 100644 index 00000000..e69de29b diff --git a/src/components/info/SelectionCounts/SelectionCounts.js b/src/components/info/SelectionCounts/SelectionCounts.js new file mode 100644 index 00000000..73822cd4 --- /dev/null +++ b/src/components/info/SelectionCounts/SelectionCounts.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import flow from 'lodash/fp/flow'; +import map from 'lodash/fp/map'; +import flatten from 'lodash/fp/flatten'; +import sum from 'lodash/fp/sum'; + +import logger from '../../../logger'; + +import './SelectionCounts.css'; + +logger.configure({ active: true }); + +const numHistories = flow( + map(station => station.histories.length), + flatten, + sum, +); + +function SelectionCounts({ + allStations, selectedStations, Container = "p" +}) { + if (!allStations) { + return ( + Loading station info ... + ) + } + if (!selectedStations) { + return ( + Loading selection info ... + ) + } + const numAllHistories = numHistories(allStations); + const numSelectedHistories = numHistories(selectedStations); + return ( + + {selectedStations.length} stations selected of {' '} + {allStations.length} available; {' '} + {numSelectedHistories} histories of {numAllHistories} + + ); +} + +SelectionCounts.propTypes = { + allStations: PropTypes.array, + selectedStations: PropTypes.array, +}; + +export default SelectionCounts; diff --git a/src/components/info/SelectionCounts/package.json b/src/components/info/SelectionCounts/package.json new file mode 100644 index 00000000..c592136e --- /dev/null +++ b/src/components/info/SelectionCounts/package.json @@ -0,0 +1,6 @@ +{ + "name": "SelectionCounts", + "version": "0.0.0", + "private": true, + "main": "./SelectionCounts.js" +} \ No newline at end of file From 34844759aa0a81aba21eab47e1ec15dee5f74e51 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 5 Jan 2022 11:36:09 -0800 Subject: [PATCH 39/44] Add component SelectionCriteria --- .../info/SelectionCriteria/SelectionCriteria.css | 0 .../info/SelectionCriteria/SelectionCriteria.js | 16 ++++++++++++++++ .../info/SelectionCriteria/package.json | 6 ++++++ 3 files changed, 22 insertions(+) create mode 100644 src/components/info/SelectionCriteria/SelectionCriteria.css create mode 100644 src/components/info/SelectionCriteria/SelectionCriteria.js create mode 100644 src/components/info/SelectionCriteria/package.json diff --git a/src/components/info/SelectionCriteria/SelectionCriteria.css b/src/components/info/SelectionCriteria/SelectionCriteria.css new file mode 100644 index 00000000..e69de29b diff --git a/src/components/info/SelectionCriteria/SelectionCriteria.js b/src/components/info/SelectionCriteria/SelectionCriteria.js new file mode 100644 index 00000000..5648f061 --- /dev/null +++ b/src/components/info/SelectionCriteria/SelectionCriteria.js @@ -0,0 +1,16 @@ +import React from 'react'; + +function SelectionCriteria({ Container = "p" }) { + return ( + + Stations displayed on the map are filtered by the dates of + available observations, the network they are part of, the variable(s) + they observe,and the frequency of observation. + Stations selected for data and metadata download are further + limited by the polygon you draw on the map (no polygon: all + displayed stations). + + ); +} + +export default SelectionCriteria; diff --git a/src/components/info/SelectionCriteria/package.json b/src/components/info/SelectionCriteria/package.json new file mode 100644 index 00000000..d17713d3 --- /dev/null +++ b/src/components/info/SelectionCriteria/package.json @@ -0,0 +1,6 @@ +{ + "name": "SelectionCriteria", + "version": "0.0.0", + "private": true, + "main": "./SelectionCriteria.js" +} \ No newline at end of file From db1a860925a7db5abc2d858c40ba77525876534d Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 5 Jan 2022 11:36:39 -0800 Subject: [PATCH 40/44] Use components SelectionCounts, SelectionCriteria --- src/components/main/VersionA/VersionA.js | 37 +++++++++++------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/components/main/VersionA/VersionA.js b/src/components/main/VersionA/VersionA.js index bfb645d6..e5d574c6 100644 --- a/src/components/main/VersionA/VersionA.js +++ b/src/components/main/VersionA/VersionA.js @@ -31,6 +31,8 @@ import StationMap from '../../maps/StationMap'; import StationMetadata from '../../info/StationMetadata'; import StationData from '../../info/StationData'; import NetworksMetadata from '../../info/NetworksMetadata'; +import SelectionCounts from '../../info/SelectionCounts'; +import SelectionCriteria from '../../info/SelectionCriteria'; import AdjustableColumns from '../../util/AdjustableColumns'; import baseMaps from '../../maps/baseMaps'; @@ -235,10 +237,12 @@ class Portal extends Component { +

                - {selectedStations?.length} stations selected - of {this.state.allStations?.length} (see - Station Data tab for details) + (See Station Metadata and Station Data tabs for details)

                @@ -310,11 +314,15 @@ class Portal extends Component {
                + {/* TODO: Button belongs in StationMetadata */} -

                {selectedStations.length} stations selected

                + -

                { - this.state.allStations ? ( - `${selectedStations.length} stations selected of - ${this.state.allStations?.length} available` - ) : ( - `Loading station info ... ` - ) - }

                -

                {` - Available stations are filtered by - the network they are part of, - the variable(s) they observe, - and the frequency of obervation. - Stations matching selected criteria are displayed - on the map. - `}

                + + { unselectedThings &&

                You haven't selected any {unselectedThings}.

                From a1b6252f3fba62f3095af4185b1b16111819e5a2 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 5 Jan 2022 12:15:17 -0800 Subject: [PATCH 41/44] Code cleanup: StationMetadata --- .../DownloadMetadata/DownloadMetadata.css | 0 .../DownloadMetadata/DownloadMetadata.js | 24 +++++++++++++++++++ .../controls/DownloadMetadata/package.json | 6 +++++ .../info/StationMetadata/StationMetadata.js | 22 ++++++++++++----- src/components/main/VersionA/VersionA.js | 8 +------ 5 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 src/components/controls/DownloadMetadata/DownloadMetadata.css create mode 100644 src/components/controls/DownloadMetadata/DownloadMetadata.js create mode 100644 src/components/controls/DownloadMetadata/package.json diff --git a/src/components/controls/DownloadMetadata/DownloadMetadata.css b/src/components/controls/DownloadMetadata/DownloadMetadata.css new file mode 100644 index 00000000..e69de29b diff --git a/src/components/controls/DownloadMetadata/DownloadMetadata.js b/src/components/controls/DownloadMetadata/DownloadMetadata.js new file mode 100644 index 00000000..8b2a485d --- /dev/null +++ b/src/components/controls/DownloadMetadata/DownloadMetadata.js @@ -0,0 +1,24 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import logger from '../../../logger'; + +import './DownloadMetadata.css'; +import { Button } from 'react-bootstrap'; + +logger.configure({ active: true }); + +function DownloadMetadata({ data, columns }) { + return ( + + ); +} + +DownloadMetadata.propTypes = { + data: PropTypes.array.isRequired, + columns: PropTypes.array.isRequired, +}; + +export default DownloadMetadata; diff --git a/src/components/controls/DownloadMetadata/package.json b/src/components/controls/DownloadMetadata/package.json new file mode 100644 index 00000000..3ae7e614 --- /dev/null +++ b/src/components/controls/DownloadMetadata/package.json @@ -0,0 +1,6 @@ +{ + "name": "DownloadMetadata", + "version": "0.0.0", + "private": true, + "main": "./DownloadMetadata.js" +} \ No newline at end of file diff --git a/src/components/info/StationMetadata/StationMetadata.js b/src/components/info/StationMetadata/StationMetadata.js index c9adefeb..1f10f0b4 100644 --- a/src/components/info/StationMetadata/StationMetadata.js +++ b/src/components/info/StationMetadata/StationMetadata.js @@ -9,6 +9,7 @@ import ReactTable from 'react-table'; import flow from 'lodash/fp/flow'; import map from 'lodash/fp/map'; +import DownloadMetadata from '../../controls/DownloadMetadata'; import FrequencySelector from '../../selectors/FrequencySelector'; import logger from '../../../logger'; import { @@ -153,13 +154,22 @@ function StationMetadata({ }, ]; + // Note: Download button is placed here because it should use the same + // formatting as React Table, i.e., what is defined in `columns`. It's too + // bad that React Table doesn't provide an export feature. return ( - + + + + ); } diff --git a/src/components/main/VersionA/VersionA.js b/src/components/main/VersionA/VersionA.js index e5d574c6..fc9e7aa8 100644 --- a/src/components/main/VersionA/VersionA.js +++ b/src/components/main/VersionA/VersionA.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { Button, Col, Panel, Row, Tab, Tabs } from 'react-bootstrap'; +import { Col, Panel, Row, Tab, Tabs } from 'react-bootstrap'; import memoize from 'memoize-one'; import flow from 'lodash/fp/flow'; import get from 'lodash/fp/get'; @@ -314,11 +314,6 @@ class Portal extends Component {
                - {/* TODO: Button belongs in StationMetadata */} - - - From 06fe051cd6df0fb04e3008f48f66dcd19df31641 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 5 Jan 2022 13:20:21 -0800 Subject: [PATCH 42/44] Code cleanup: StationMap --- src/components/maps/StationMap/StationMap.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/maps/StationMap/StationMap.js b/src/components/maps/StationMap/StationMap.js index 814c076c..c192b58d 100644 --- a/src/components/maps/StationMap/StationMap.js +++ b/src/components/maps/StationMap/StationMap.js @@ -5,7 +5,7 @@ // Terminology // // - Leaflet uses the term 'layer' for all single polygons, markers, etc. -// Leaflet uses the term 'layer group' for an object (iteself also a +// Leaflet uses the term 'layer group' for an object (itself also a // layer, i.e, a subclass of `Layer`) that groups layers together. // // Purpose @@ -17,8 +17,8 @@ // Behaviour // // - The geometry layer group is initially empty. Geometry can be added to -// it by any combination of drawing (on the map), uploading (e.g., a -// from GeoJSON file), and editing and/or deleting existing geometry. +// it by any combination of drawing (on the map) and editing and/or +// deleting existing geometry. // // `onSetArea` callback // @@ -26,8 +26,8 @@ // communicated by the `DataMap` callback prop `onSetArea`. This callback // is more or less the whole point of the geometry layer group. // -// - `onSetArea` is called with a single GeoJSON object representing the the -// contents of the layer group. But see next point. +// - `onSetArea` is called with a single GeoJSON object representing the +// contents of the layer group. import PropTypes from 'prop-types'; From e4ed68f59ecadffd6776ae5e08514bc0b2120e64 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 5 Jan 2022 13:21:07 -0800 Subject: [PATCH 43/44] Code cleanup: Separate utilities --- src/components/main/VersionA/VersionA.js | 2 +- src/components/main/VersionB/VersionB.js | 2 +- src/utils/dates.js | 12 +++ src/utils/geoJSON.js | 51 ++++++++++++ src/utils/portals-common/package.json | 6 -- src/utils/station-filtering/package.json | 6 ++ .../station-filtering.js} | 83 ++----------------- src/utils/station-info.js | 4 +- 8 files changed, 79 insertions(+), 87 deletions(-) create mode 100644 src/utils/dates.js create mode 100644 src/utils/geoJSON.js delete mode 100644 src/utils/portals-common/package.json create mode 100644 src/utils/station-filtering/package.json rename src/utils/{portals-common/portals-common.js => station-filtering/station-filtering.js} (74%) diff --git a/src/components/main/VersionA/VersionA.js b/src/components/main/VersionA/VersionA.js index fc9e7aa8..26dd462c 100644 --- a/src/components/main/VersionA/VersionA.js +++ b/src/components/main/VersionA/VersionA.js @@ -24,7 +24,7 @@ import DateSelector from '../../selectors/DateSelector'; import { stationFilter, stationInsideMultiPolygon -} from '../../../utils/portals-common'; +} from '../../../utils/station-filtering'; import OnlyWithClimatologyControl from '../../controls/OnlyWithClimatologyControl'; import StationMap from '../../maps/StationMap'; diff --git a/src/components/main/VersionB/VersionB.js b/src/components/main/VersionB/VersionB.js index ea09a387..2c83eb44 100644 --- a/src/components/main/VersionB/VersionB.js +++ b/src/components/main/VersionB/VersionB.js @@ -27,7 +27,7 @@ import DateSelector from '../../selectors/DateSelector'; import FileFormatSelector from '../../selectors/FileFormatSelector'; import ClipToDateControl from '../../controls/ClipToDateControl'; import ObservationCounts from '../../info/ObservationCounts'; -import { stationFilter } from '../../../utils/portals-common'; +import { stationFilter } from '../../../utils/station-filtering'; import ButtonToolbar from 'react-bootstrap/es/ButtonToolbar'; import StationMetadata from '../../info/StationMetadata'; import OnlyWithClimatologyControl diff --git a/src/utils/dates.js b/src/utils/dates.js new file mode 100644 index 00000000..eedcea71 --- /dev/null +++ b/src/utils/dates.js @@ -0,0 +1,12 @@ +export const utcDateOnly = d => { + // Return a new Date object which is suitable for comparing UTC date only + // (fixed zero time component). One might suppose that the comparisons in + // the filter below also would work with local time (Date.setHours()), but + // they don't; only UTC works. + if (!d) { + return d; + } + const r = new Date(d); + r.setUTCHours(0, 0, 0, 0); + return r; +}; diff --git a/src/utils/geoJSON.js b/src/utils/geoJSON.js new file mode 100644 index 00000000..c415e111 --- /dev/null +++ b/src/utils/geoJSON.js @@ -0,0 +1,51 @@ +import flattenDepth from 'lodash/fp/flattenDepth'; +import map from 'lodash/fp/map'; +import min from 'lodash/fp/min'; +import max from 'lodash/fp/max'; +import { isPointInPolygonWn } from './geometry-algorithms'; + +// GeoJSON MultiPolygon format. Taken from +// https://conservancy.umn.edu/bitstream/handle/11299/210208/GeoJSON_Primer_2019.pdf +// +// { +// "type": "MultiPolygon", +// "coordinates": [ +// // one or more Polygon coordinate array: +// [ +// // one or more Linear ring coordinate arrays: +// [ +// // at least four Points; first point = last point: +// [x0, y0], +// [x1, y1], +// [x2, y2], +// // ... +// [x0, y0] +// ], +// // ... +// ], +// // ... +// ], +// // ... +// }; + + +export const checkGeoJSONMultiPolygon = geometry => { + if (geometry['type'] !== 'MultiPolygon') { + throw new Error(`Invalid geometry type: ${geometry['type']}`) + } +}; + + +export const gJMultiPolygonBoundingBox = geometry => { + const points = flattenDepth(3, geometry["coordinates"]); + const xs = map(p => p[0], points); + const ys = map(p => p[1], points); + return [ + [min(xs), max(ys)], // top left + [max(xs), min(ys)], // bottom right + ] +}; + +const getX = point => point[0]; +const getY = point => point[1]; +export const isPointInGeoJSONPolygon = isPointInPolygonWn(getX, getY); diff --git a/src/utils/portals-common/package.json b/src/utils/portals-common/package.json deleted file mode 100644 index b1088c43..00000000 --- a/src/utils/portals-common/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "portals-common", - "version": "0.0.0", - "private": true, - "main": "./portals-common.js" -} \ No newline at end of file diff --git a/src/utils/station-filtering/package.json b/src/utils/station-filtering/package.json new file mode 100644 index 00000000..710ad3a8 --- /dev/null +++ b/src/utils/station-filtering/package.json @@ -0,0 +1,6 @@ +{ + "name": "station-filtering", + "version": "0.0.0", + "private": true, + "main": "station-filtering.js" +} \ No newline at end of file diff --git a/src/utils/portals-common/portals-common.js b/src/utils/station-filtering/station-filtering.js similarity index 74% rename from src/utils/portals-common/portals-common.js rename to src/utils/station-filtering/station-filtering.js index d811c4b0..049f6abf 100644 --- a/src/utils/portals-common/portals-common.js +++ b/src/utils/station-filtering/station-filtering.js @@ -4,13 +4,13 @@ import filter from 'lodash/fp/filter'; import flatten from 'lodash/fp/flatten'; import flow from 'lodash/fp/flow'; import map from 'lodash/fp/map'; -import min from 'lodash/fp/min'; -import max from 'lodash/fp/max'; import some from 'lodash/fp/some'; import uniq from 'lodash/fp/uniq'; import isNil from 'lodash/fp/isNil'; -import flattenDepth from 'lodash/fp/flattenDepth'; -import { isPointInPolygonWn } from '../geometry-algorithms'; +import { utcDateOnly } from '../dates'; +import { + checkGeoJSONMultiPolygon, gJMultiPolygonBoundingBox, isPointInGeoJSONPolygon, +} from '../geoJSON'; import { stationVariableUris, uniqStationLocations } from '../station-info'; import { getTimer } from '../timing'; @@ -18,73 +18,6 @@ const ft = getTimer("Station filtering timing") ft.log(); -// GeoJSON MultiPolygon format. Taken from -// https://conservancy.umn.edu/bitstream/handle/11299/210208/GeoJSON_Primer_2019.pdf -// -// { -// "type": "MultiPolygon", -// "coordinates": [ -// // one or more Polygon coordinate array: -// [ -// // one or more Linear ring coordinate arrays: -// [ -// // at least four Points; first point = last point: -// [x0, y0], -// [x1, y1], -// [x2, y2], -// // ... -// [x0, y0] -// ], -// // ... -// ], -// // ... -// ], -// // ... -// }; - - -const checkGeoJSONMultiPolygon = geometry => { - if (geometry['type'] !== 'MultiPolygon') { - throw new Error(`Invalid geometry type: ${geometry['type']}`) - } -}; - - -const gJMultiPolygonBoundingBox = geometry => { - const points = flattenDepth(3, geometry["coordinates"]); - const xs = map(p => p[0], points); - const ys = map(p => p[1], points); - return [ - [min(xs), max(ys)], // top left - [max(xs), min(ys)], // bottom right - ] -}; - -const getX = point => point[0]; -const getY = point => point[1]; -export const isPointInGeoJSONPolygon = isPointInPolygonWn(getX, getY); - - -export const utcDateOnly = d => { - // Return a new Date object which is suitable for comparing UTC date only - // (fixed zero time component). One might suppose that the comparisons in - // the filter below also would work with local time (Date.setHours()), but - // they don't; only UTC works. - if (!d) { - return d; - } - const r = new Date(d); - r.setUTCHours(0, 0, 0, 0); - return r; -}; - - -export const isNilOrAscending = (a, b, c) => { - // Allowing for nils (undefined, null), check that a <= b <= c. - return isNil(b) || ((isNil(a) || a <= b) && (isNil(c) || b <= c)); -}; - - export const historyDateMatch = ( history, startDate, endDate, strict = true ) => { @@ -96,14 +29,10 @@ export const historyDateMatch = ( endDate = utcDateOnly(endDate); if (strict) { + // TODO , or remove strict // Allowing for nils, check that // minObsDate <= startDate <= endDate <= maxObsDate. - // TODO: Optimize - return ( - isNilOrAscending(startDate, endDate) - && isNilOrAscending(minObsDate, startDate, maxObsDate) - && isNilOrAscending(minObsDate, endDate, maxObsDate) - ); + return true; } // This is a variant of the looser legacy comparison. Allows nil start, end. diff --git a/src/utils/station-info.js b/src/utils/station-info.js index ba3a9c5e..dcea290c 100644 --- a/src/utils/station-info.js +++ b/src/utils/station-info.js @@ -3,7 +3,7 @@ import map from 'lodash/fp/map'; import uniq from 'lodash/fp/uniq'; import React from 'react'; import uniqWith from 'lodash/fp/uniqWith'; -import { utcDateOnly } from './portals-common'; +import { utcDateOnly } from './dates'; import find from 'lodash/fp/find'; import flatten from 'lodash/fp/flatten'; import compact from 'lodash/fp/compact'; @@ -79,4 +79,4 @@ export const uniqStationVariableNames = curry( sortBy(identity), sortedUniq, )(station.histories) -); \ No newline at end of file +); From 579adc9469f6a7c0566f9d1bffa0feac443b1d3f Mon Sep 17 00:00:00 2001 From: rod-glover Date: Wed, 5 Jan 2022 13:30:19 -0800 Subject: [PATCH 44/44] Doc tweak --- src/components/info/StationMetadata/StationMetadata.js | 6 +++++- src/components/main/VersionB/VersionB.js | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/info/StationMetadata/StationMetadata.js b/src/components/info/StationMetadata/StationMetadata.js index 1f10f0b4..64a798a9 100644 --- a/src/components/info/StationMetadata/StationMetadata.js +++ b/src/components/info/StationMetadata/StationMetadata.js @@ -27,9 +27,13 @@ import './StationMetadata.css'; logger.configure({ active: true }); -// TODO: Put these in a util module const formatDate = d => d ? d.toISOString().substr(0,10) : 'unknown'; +// Comparator for standard lexicographic ordering of arrays of values. +// Returns negative, zero, or positive integer as usual for comparators. +// Uses the natural (<, >) ordering of the array elements. No guarantees if you +// supply it with arrays with different element types (pairwise by index). You +// have been warned. const lexCompare = (a, b) => { const n = Math.min(a.length, b.length); for (let i = 0; i < n; i += 1) { diff --git a/src/components/main/VersionB/VersionB.js b/src/components/main/VersionB/VersionB.js index 2c83eb44..e803bd9f 100644 --- a/src/components/main/VersionB/VersionB.js +++ b/src/components/main/VersionB/VersionB.js @@ -1,3 +1,6 @@ +// TODO: Components and functions used in this code have changed significantly. +// See VersionA for the new usages. + import React, { Component } from 'react'; import { Row, Col,