diff --git a/api/alembic/versions/5845f568a975_adds_grass_curing_to_morecast_forecast.py b/api/alembic/versions/5845f568a975_adds_grass_curing_to_morecast_forecast.py new file mode 100644 index 000000000..5837c80c9 --- /dev/null +++ b/api/alembic/versions/5845f568a975_adds_grass_curing_to_morecast_forecast.py @@ -0,0 +1,29 @@ +"""adds grass_curing to morecast_forecast + +Revision ID: 5845f568a975 +Revises: 403586c146ae +Create Date: 2024-02-02 08:16:57.605162 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '5845f568a975' +down_revision = '403586c146ae' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### start Alembic commands ### + op.add_column('morecast_forecast', sa.Column('grass_curing', sa.Float(), nullable=True)) + op.create_index(op.f('ix_morecast_forecast_grass_curing'), 'morecast_forecast', ['grass_curing'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### start Alembic commands ### + op.drop_index(op.f('ix_morecast_forecast_grass_curing'), table_name='morecast_forecast') + op.drop_column('morecast_forecast', 'grass_curing') + # ### end Alembic commands ### diff --git a/api/app/db/models/morecast_v2.py b/api/app/db/models/morecast_v2.py index 2c1883eb8..36a6a224d 100644 --- a/api/app/db/models/morecast_v2.py +++ b/api/app/db/models/morecast_v2.py @@ -16,6 +16,7 @@ class MorecastForecastRecord(Base): precip = Column(Float, nullable=False, index=True) wind_speed = Column(Float, nullable=False, index=True) wind_direction = Column(Integer, nullable=True, index=True) + grass_curing = Column(Float, nullable=True, index=True) create_timestamp = Column(TZTimeStamp, nullable=False, index=True) create_user = Column(String, nullable=False) update_timestamp = Column(TZTimeStamp, nullable=False, index=True) diff --git a/api/app/morecast_v2/forecasts.py b/api/app/morecast_v2/forecasts.py index 5153c32f0..02e0360c0 100644 --- a/api/app/morecast_v2/forecasts.py +++ b/api/app/morecast_v2/forecasts.py @@ -29,6 +29,7 @@ def get_forecasts(db_session: Session, start_time: Optional[datetime], end_time: precip=forecast.precip, wind_speed=forecast.wind_speed, wind_direction=forecast.wind_direction, + grass_curing=forecast.grass_curing, update_timestamp=int(forecast.update_timestamp.timestamp())) for forecast in result] return forecasts diff --git a/api/app/routers/morecast_v2.py b/api/app/routers/morecast_v2.py index 11986a36a..bcfcbcca0 100644 --- a/api/app/routers/morecast_v2.py +++ b/api/app/routers/morecast_v2.py @@ -77,6 +77,7 @@ async def get_forecasts_by_date_range(start_date: date, end_date: date, request: precip=forecast.precip, wind_speed=forecast.wind_speed, wind_direction=forecast.wind_direction, + grass_curing=forecast.grass_curing, update_timestamp=forecast.update_timestamp.timestamp()) for forecast in result] return MorecastForecastResponse(forecasts=morecast_forecast_outputs) @@ -101,6 +102,7 @@ async def save_forecasts(forecasts: MoreCastForecastRequest, precip=forecast.precip, wind_speed=forecast.wind_speed, wind_direction=forecast.wind_direction, + grass_curing=forecast.grass_curing, create_user=username, create_timestamp=now, update_user=username, @@ -127,6 +129,7 @@ async def save_forecasts(forecasts: MoreCastForecastRequest, precip=forecast.precip, wind_speed=forecast.wind_speed, wind_direction=forecast.wind_direction, + grass_curing=forecast.grass_curing, update_timestamp=int(now.timestamp() * 1000)) for forecast in forecasts_list] return MorecastForecastResponse(forecasts=morecast_forecast_outputs) diff --git a/web/src/api/moreCast2API.test.ts b/web/src/api/moreCast2API.test.ts index 07b4cf404..8f836c3b6 100644 --- a/web/src/api/moreCast2API.test.ts +++ b/web/src/api/moreCast2API.test.ts @@ -24,7 +24,7 @@ describe('moreCast2API', () => { temp: { choice: 'FORECAST', value: 0 }, windDirection: { choice: 'FORECAST', value: 0 }, windSpeed: { choice: 'FORECAST', value: 0 }, - grassCuring: 0 + grassCuring: { choice: 'FORECAST', value: 0 } }) it('should marshall forecast records correctly', async () => { const res = marshalMoreCast2ForecastRecords([ diff --git a/web/src/api/moreCast2API.ts b/web/src/api/moreCast2API.ts index a05d69a3b..03a850f83 100644 --- a/web/src/api/moreCast2API.ts +++ b/web/src/api/moreCast2API.ts @@ -187,7 +187,7 @@ export const marshalMoreCast2ForecastRecords = (forecasts: MoreCast2ForecastRow[ temp: forecast.temp.value, wind_direction: forecast.windDirection.value, wind_speed: forecast.windSpeed.value, - grass_curing: forecast.grassCuring + grass_curing: forecast.grassCuring.value } }) return forecastRecords @@ -277,8 +277,8 @@ export const mapMoreCast2RowsToIndeterminates = (rows: MoreCast2Row[]): WeatherI initial_spread_index: isForecast ? r.isiCalcForecast!.value : r.isiCalcActual, build_up_index: isForecast ? r.buiCalcForecast!.value : r.buiCalcActual, fire_weather_index: isForecast ? r.fwiCalcForecast!.value : r.fwiCalcActual, - danger_rating: isForecast ? null : r.rhActual, - grass_curing: r.grassCuring + danger_rating: isForecast ? null : r.dgrCalcActual, + grass_curing: isForecast ? r.grassCuringForecast!.value : r.grassCuringActual } }) return mappedIndeterminates diff --git a/web/src/features/moreCast2/components/ColumnDefBuilder.tsx b/web/src/features/moreCast2/components/ColumnDefBuilder.tsx index d1a5f17c0..55e7097e9 100644 --- a/web/src/features/moreCast2/components/ColumnDefBuilder.tsx +++ b/web/src/features/moreCast2/components/ColumnDefBuilder.tsx @@ -49,13 +49,11 @@ export class ColumnDefBuilder implements ColDefGenerator, ForecastColDefGenerato } public generateForecastColDef = (headerName?: string) => { - const isCalcField = this.field.includes('Calc') - return this.generateForecastColDefWith( `${this.field}${WeatherDeterminate.FORECAST}`, headerName ? headerName : this.headerName, this.precision, - isCalcField ? DEFAULT_COLUMN_WIDTH : DEFAULT_FORECAST_COLUMN_WIDTH + DEFAULT_FORECAST_COLUMN_WIDTH ) } @@ -103,7 +101,11 @@ export class ColumnDefBuilder implements ColDefGenerator, ForecastColDefGenerato } public generateForecastColDefWith = (field: string, headerName: string, precision: number, width?: number) => { + const isGrassField = field.includes('grass') const isCalcField = field.includes('Calc') + if (isGrassField || isCalcField) { + width = DEFAULT_COLUMN_WIDTH + } return { field: field, disableColumnMenu: true, @@ -112,14 +114,16 @@ export class ColumnDefBuilder implements ColDefGenerator, ForecastColDefGenerato headerName: headerName, sortable: false, type: 'number', - width: width || 120, + width: width ?? DEFAULT_FORECAST_COLUMN_WIDTH, renderHeader: (params: GridColumnHeaderParams) => { - return isCalcField + return isCalcField || isGrassField ? this.gridComponentRenderer.renderHeaderWith(params) : this.gridComponentRenderer.renderForecastHeaderWith(params) }, renderCell: (params: Pick) => { - return this.gridComponentRenderer.renderForecastCellWith(params, field) + return isCalcField + ? this.gridComponentRenderer.renderCellWith(params) + : this.gridComponentRenderer.renderForecastCellWith(params, field) }, valueFormatter: (params: Pick) => { return this.valueFormatterWith(params, precision) diff --git a/web/src/features/moreCast2/components/ForecastSummaryDataGrid.tsx b/web/src/features/moreCast2/components/ForecastSummaryDataGrid.tsx index a6002557d..14d8efdab 100644 --- a/web/src/features/moreCast2/components/ForecastSummaryDataGrid.tsx +++ b/web/src/features/moreCast2/components/ForecastSummaryDataGrid.tsx @@ -10,6 +10,7 @@ import { storeUserEditedRows, getSimulatedIndices } from 'features/moreCast2/sli import { AppDispatch } from 'app/store' import { useDispatch } from 'react-redux' import { filterRowsForSimulationFromEdited } from 'features/moreCast2/rowFilters' +import { fillStationGrassCuringForward } from 'features/moreCast2/util' const PREFIX = 'ForecastSummaryDataGrid' @@ -48,15 +49,17 @@ const ForecastSummaryDataGrid = ({ handleClose }: ForecastSummaryDataGridProps) => { const dispatch: AppDispatch = useDispatch() - const processRowUpdate = async (editedRow: MoreCast2Row) => { - dispatch(storeUserEditedRows([editedRow])) - const rowsForSimulation = filterRowsForSimulationFromEdited(editedRow, rows) + const processRowUpdate = async (newRow: MoreCast2Row) => { + const filledRows = fillStationGrassCuringForward(newRow, rows) + dispatch(storeUserEditedRows(filledRows)) + + const rowsForSimulation = filterRowsForSimulationFromEdited(newRow, filledRows) if (rowsForSimulation) { dispatch(getSimulatedIndices(rowsForSimulation)) } - return editedRow + return newRow } return ( diff --git a/web/src/features/moreCast2/components/GridComponentRenderer.tsx b/web/src/features/moreCast2/components/GridComponentRenderer.tsx index 86cef0692..ed3877994 100644 --- a/web/src/features/moreCast2/components/GridComponentRenderer.tsx +++ b/web/src/features/moreCast2/components/GridComponentRenderer.tsx @@ -64,14 +64,15 @@ export class GridComponentRenderer { // We need the prefix to help us grab the correct 'actual' field (eg. tempACTUAL, precipACTUAL, etc.) const actualField = this.getActualField(field) - const isCalcField = field.includes('Calc') + const isGrassField = field.includes('grass') const isActual = !isNaN(params.row[actualField]) + return ( ) @@ -83,7 +84,7 @@ export class GridComponentRenderer { precision: number ) => { const oldValue = params.row[field].value - const newValue = Number(params.value) + const newValue = params.value ? Number(params.value) : NaN if (isNaN(oldValue) && isNaN(newValue)) { return { ...params.row } diff --git a/web/src/features/moreCast2/components/MoreCast2Column.tsx b/web/src/features/moreCast2/components/MoreCast2Column.tsx index 9e3888b5d..c6af31faf 100644 --- a/web/src/features/moreCast2/components/MoreCast2Column.tsx +++ b/web/src/features/moreCast2/components/MoreCast2Column.tsx @@ -119,6 +119,7 @@ export const ffmcField = new IndeterminateField('ffmcCalc', 'FFMC', 'number', 1, export const dmcField = new IndeterminateField('dmcCalc', 'DMC', 'number', 0, false) export const dcField = new IndeterminateField('dcCalc', 'DC', 'number', 0, false) export const dgrField = new IndeterminateField('dgrCalc', 'DGR', 'number', 0, false) +export const gcField = new IndeterminateField('grassCuring', 'GC', 'number', 0, false) export const MORECAST2_STATION_DATE_FIELDS: ColDefGenerator[] = [ StationForecastField.getInstance(), @@ -140,7 +141,8 @@ export const MORECAST2_FORECAST_FIELDS: ForecastColDefGenerator[] = [ rhForecastField, windDirForecastField, windSpeedForecastField, - precipForecastField + precipForecastField, + gcField ] export const MORECAST2_INDEX_FIELDS: ForecastColDefGenerator[] = [ diff --git a/web/src/features/moreCast2/components/TabbedDataGrid.tsx b/web/src/features/moreCast2/components/TabbedDataGrid.tsx index 7e20b42c3..fe530429d 100644 --- a/web/src/features/moreCast2/components/TabbedDataGrid.tsx +++ b/web/src/features/moreCast2/components/TabbedDataGrid.tsx @@ -88,7 +88,8 @@ const TabbedDataGrid = ({ morecast2Rows, fromTo, setFromTo }: TabbedDataGridProp if ( !isEqual(params.colDef.field, 'stationName') && !isEqual(params.colDef.field, 'forDate') && - !params.colDef.field.includes('Calc') + !params.colDef.field.includes('Calc') && + !params.colDef.field.includes('grass') ) { setClickedColDef(params.colDef) setContextMenu(contextMenu === null ? { mouseX: event.clientX, mouseY: event.clientY } : null) diff --git a/web/src/features/moreCast2/interfaces.ts b/web/src/features/moreCast2/interfaces.ts index d3245801e..b54d389d3 100644 --- a/web/src/features/moreCast2/interfaces.ts +++ b/web/src/features/moreCast2/interfaces.ts @@ -16,7 +16,7 @@ export interface MoreCast2ForecastRow { temp: PredictionItem windDirection: PredictionItem windSpeed: PredictionItem - grassCuring: number + grassCuring: PredictionItem } export interface BaseRow { @@ -46,8 +46,9 @@ export interface MoreCast2Row extends BaseRow { fwiCalcForecast?: PredictionItem dgrCalcForecast?: PredictionItem - // Grass curing carryover - grassCuring: number + // Grass curing + grassCuringActual: number + grassCuringForecast?: PredictionItem // Forecast properties precipForecast?: PredictionItem diff --git a/web/src/features/moreCast2/rowFilters.test.ts b/web/src/features/moreCast2/rowFilters.test.ts index 1f2b0dbc3..bed2b0ecb 100644 --- a/web/src/features/moreCast2/rowFilters.test.ts +++ b/web/src/features/moreCast2/rowFilters.test.ts @@ -18,6 +18,7 @@ export const buildValidForecastRow = ( forecastRow.tempForecast = { choice: choice, value: 2 } forecastRow.rhForecast = { choice: choice, value: 2 } forecastRow.windSpeedForecast = { choice: choice, value: 2 } + forecastRow.grassCuringForecast = { choice: choice, value: NaN } forecastRow.id = id return forecastRow diff --git a/web/src/features/moreCast2/saveForecast.test.ts b/web/src/features/moreCast2/saveForecast.test.ts index 6bf02db01..6f72cca29 100644 --- a/web/src/features/moreCast2/saveForecast.test.ts +++ b/web/src/features/moreCast2/saveForecast.test.ts @@ -67,7 +67,7 @@ const baseRow = { buiCalcActual: 0, fwiCalcActual: 0, dgrCalcActual: 0, - grassCuring: 0 + grassCuringActual: NaN } const baseRowWithActuals = { @@ -76,7 +76,8 @@ const baseRowWithActuals = { rhActual: 0, tempActual: 0, windDirectionActual: 0, - windSpeedActual: 0 + windSpeedActual: 0, + grassCuringActual: 0 } const mockForDate = DateTime.fromISO('2023-02-16T20:00:00+00:00') @@ -100,7 +101,8 @@ const buildCompleteForecast = ( rhForecast: { choice: ModelChoice.GDPS, value: 0 }, tempForecast: { choice: ModelChoice.GDPS, value: 0 }, windDirectionForecast: { choice: ModelChoice.GDPS, value: 0 }, - windSpeedForecast: { choice: ModelChoice.GDPS, value: 0 } + windSpeedForecast: { choice: ModelChoice.GDPS, value: 0 }, + grassCuringForecast: { choice: ModelChoice.NULL, value: 0 } }) const buildForecastMissingWindDirection = ( @@ -123,7 +125,7 @@ const buildForecastMissingWindDirection = ( tempForecast: { choice: ModelChoice.GDPS, value: 0 }, windDirectionForecast: { choice: ModelChoice.NULL, value: NaN }, windSpeedForecast: { choice: ModelChoice.GDPS, value: 0 }, - grassCuring: 0 + grassCuringForecast: { choice: ModelChoice.NULL, value: 0 } }) const buildInvalidForecast = ( @@ -162,7 +164,8 @@ const buildNAForecast = ( rhForecast: { choice: ModelChoice.NULL, value: NaN }, tempForecast: { choice: ModelChoice.NULL, value: NaN }, windDirectionForecast: { choice: ModelChoice.NULL, value: NaN }, - windSpeedForecast: { choice: ModelChoice.NULL, value: NaN } + windSpeedForecast: { choice: ModelChoice.NULL, value: NaN }, + grassCuringForecast: { choice: ModelChoice.NULL, value: NaN } }) const buildForecastWithActuals = ( @@ -184,7 +187,8 @@ const buildForecastWithActuals = ( rhForecast: { choice: ModelChoice.GDPS, value: 0 }, tempForecast: { choice: ModelChoice.GDPS, value: 0 }, windDirectionForecast: { choice: ModelChoice.GDPS, value: 0 }, - windSpeedForecast: { choice: ModelChoice.GDPS, value: 0 } + windSpeedForecast: { choice: ModelChoice.GDPS, value: 0 }, + grassCuringForecast: { choice: ModelChoice.NULL, value: 0 } }) describe('saveForecasts', () => { diff --git a/web/src/features/moreCast2/saveForecasts.ts b/web/src/features/moreCast2/saveForecasts.ts index 8aa40478c..1b8a7fb7f 100644 --- a/web/src/features/moreCast2/saveForecasts.ts +++ b/web/src/features/moreCast2/saveForecasts.ts @@ -1,6 +1,6 @@ import { ModelChoice } from 'api/moreCast2API' import { MoreCast2ForecastRow, MoreCast2Row } from 'features/moreCast2/interfaces' -import { fillGrassCuring, validForecastPredicate } from 'features/moreCast2/util' +import { validForecastPredicate } from 'features/moreCast2/util' // Forecast rows contain all NaN values in their 'actual' fields export const isForecastRowPredicate = (row: MoreCast2Row) => @@ -8,7 +8,8 @@ export const isForecastRowPredicate = (row: MoreCast2Row) => isNaN(row.rhActual) && isNaN(row.tempActual) && isNaN(row.windDirectionActual) && - isNaN(row.windSpeedActual) + isNaN(row.windSpeedActual) && + isNaN(row.grassCuringActual) export const getForecastRows = (rows: MoreCast2Row[]): MoreCast2Row[] => { return rows ? rows.filter(isForecastRowPredicate) : [] @@ -21,8 +22,7 @@ export const isForecastValid = (rows: MoreCast2Row[]) => { } export const getRowsToSave = (rows: MoreCast2Row[]): MoreCast2ForecastRow[] => { - const filledRows = fillGrassCuring(rows) - const forecastRows = getForecastRows(filledRows) + const forecastRows = getForecastRows(rows) const rowsToSave = forecastRows.filter(validForecastPredicate) return rowsToSave.map(r => ({ id: r.id, @@ -34,6 +34,6 @@ export const getRowsToSave = (rows: MoreCast2Row[]): MoreCast2ForecastRow[] => { temp: r.tempForecast ?? { choice: ModelChoice.NULL, value: NaN }, windDirection: r.windDirectionForecast ?? { choice: ModelChoice.NULL, value: NaN }, windSpeed: r.windSpeedForecast ?? { choice: ModelChoice.NULL, value: NaN }, - grassCuring: r.grassCuring + grassCuring: r.grassCuringForecast ?? { choice: ModelChoice.NULL, value: NaN } })) } diff --git a/web/src/features/moreCast2/slices/dataSlice.ts b/web/src/features/moreCast2/slices/dataSlice.ts index 0634d6f49..4918d4670 100644 --- a/web/src/features/moreCast2/slices/dataSlice.ts +++ b/web/src/features/moreCast2/slices/dataSlice.ts @@ -12,7 +12,7 @@ import { fetchCalculatedIndices } from 'api/moreCast2API' import { AppThunk } from 'app/store' -import { createDateInterval, rowIDHasher } from 'features/moreCast2/util' +import { createDateInterval, rowIDHasher, fillGrassCuring } from 'features/moreCast2/util' import { DateTime } from 'luxon' import { logError } from 'utils/error' import { MoreCast2Row } from 'features/moreCast2/interfaces' @@ -189,7 +189,6 @@ export const createMoreCast2Rows = ( firstItem.latitude, firstItem.longitude ) - row.grassCuring = getNumberOrNaN(firstItem.grass_curing) for (const value of values) { switch (value.determinate) { @@ -206,6 +205,7 @@ export const createMoreCast2Rows = ( row.buiCalcActual = getNumberOrNaN(value.build_up_index) row.fwiCalcActual = getNumberOrNaN(value.fire_weather_index) row.dgrCalcActual = getNumberOrNaN(value.danger_rating) + row.grassCuringActual = getNumberOrNaN(value.grass_curing) break case WeatherDeterminate.FORECAST: case WeatherDeterminate.NULL: @@ -253,6 +253,10 @@ export const createMoreCast2Rows = ( choice: forecastOrNull(ModelChoice.NULL), value: getNumberOrNaN(value.fire_weather_index) } + row.grassCuringForecast = { + choice: forecastOrNull(ModelChoice.NULL), + value: getNumberOrNaN(value.grass_curing) + } break case WeatherDeterminate.GDPS: row.precipGDPS = getNumberOrNaN(value.precipitation) @@ -342,8 +346,9 @@ export const createMoreCast2Rows = ( row.precipForecast.value = 0 } } + const newRows = fillGrassCuring(rows) - return rows + return newRows } const forecastOrNull = (determinate: WeatherDeterminateType): ModelChoice.FORECAST | ModelChoice.NULL => { @@ -615,7 +620,7 @@ export const createEmptyMoreCast2Row = ( dgrCalcActual: NaN, // - grassCuring: NaN, + grassCuringActual: NaN, // GDPS model predictions precipGDPS: NaN, diff --git a/web/src/features/moreCast2/util.test.ts b/web/src/features/moreCast2/util.test.ts index 1bce0e12d..892c1ca9e 100644 --- a/web/src/features/moreCast2/util.test.ts +++ b/web/src/features/moreCast2/util.test.ts @@ -4,6 +4,7 @@ import { createDateInterval, createWeatherModelLabel, fillGrassCuring, + fillStationGrassCuringForward, mapForecastChoiceLabels, parseForecastsHelper, rowIDHasher, @@ -104,7 +105,7 @@ describe('parseForecastsHelper', () => { temp: { choice: ModelChoice.FORECAST, value: 1 }, windDirection: { choice: ModelChoice.FORECAST, value: 1 }, windSpeed: { choice: ModelChoice.FORECAST, value: 1 }, - grassCuring: 1 + grassCuring: { choice: ModelChoice.FORECAST, value: 1 } }, { id: '22022-01-02', @@ -116,7 +117,7 @@ describe('parseForecastsHelper', () => { temp: { choice: ModelChoice.FORECAST, value: 1 }, windDirection: { choice: ModelChoice.FORECAST, value: 1 }, windSpeed: { choice: ModelChoice.FORECAST, value: 1 }, - grassCuring: 1 + grassCuring: { choice: ModelChoice.FORECAST, value: 1 } } ]) }) @@ -133,7 +134,7 @@ describe('parseForecastsHelper', () => { temp: { choice: ModelChoice.FORECAST, value: 1 }, windDirection: { choice: ModelChoice.FORECAST, value: 1 }, windSpeed: { choice: ModelChoice.FORECAST, value: 1 }, - grassCuring: 1 + grassCuring: { choice: ModelChoice.FORECAST, value: 1 } } ]) }) @@ -163,7 +164,7 @@ describe('parseForecastsHelper', () => { temp: { choice: ModelChoice.FORECAST, value: NaN }, windDirection: { choice: ModelChoice.FORECAST, value: NaN }, windSpeed: { choice: ModelChoice.FORECAST, value: NaN }, - grassCuring: NaN + grassCuring: { choice: ModelChoice.FORECAST, value: NaN } } ]) }) @@ -221,27 +222,50 @@ describe('mapForecastChoiceLabels', () => { expect(labelledRows[1].rhForecast!.choice).toBe('MANUAL') }) }) -describe('fillGrassCuring', () => { - const forecast1A = buildValidForecastRow(123, TEST_DATETIME, 'FORECAST') - const forecast2A = buildValidForecastRow(321, TEST_DATETIME, 'FORECAST') - const forecast3A = buildValidForecastRow(111, TEST_DATETIME, 'FORECAST') - const actual1A = buildValidActualRow(123, TEST_DATETIME.minus({ days: 1 })) - const actual2A = buildValidActualRow(321, TEST_DATETIME.minus({ days: 1 })) - const actual3A = buildValidActualRow(111, TEST_DATETIME.minus({ days: 1 })) - actual1A.grassCuring = 80 - actual2A.grassCuring = 70 - const actual1B = buildValidActualRow(123, TEST_DATETIME.minus({ days: 2 })) - const actual2B = buildValidActualRow(321, TEST_DATETIME.minus({ days: 2 })) - actual1B.grassCuring = 8 - actual2B.grassCuring = 7 +const forecast1A = buildValidForecastRow(123, TEST_DATETIME, 'FORECAST') +const forecast1B = buildValidForecastRow(123, TEST_DATETIME.plus({ days: 1 }), 'FORECAST') +const forecast1C = buildValidForecastRow(123, TEST_DATETIME.plus({ days: 2 }), 'FORECAST') +const forecast2A = buildValidForecastRow(321, TEST_DATETIME, 'FORECAST') +const forecast3A = buildValidForecastRow(111, TEST_DATETIME, 'FORECAST') +const actual1A = buildValidActualRow(123, TEST_DATETIME.minus({ days: 1 })) +const actual2A = buildValidActualRow(321, TEST_DATETIME.minus({ days: 1 })) +const actual3A = buildValidActualRow(111, TEST_DATETIME.minus({ days: 1 })) +actual1A.grassCuringActual = 80 +actual2A.grassCuringActual = 70 + +const actual1B = buildValidActualRow(123, TEST_DATETIME.minus({ days: 2 })) +const actual2B = buildValidActualRow(321, TEST_DATETIME.minus({ days: 2 })) +actual1B.grassCuringActual = 8 +actual2B.grassCuringActual = 7 - const rows = [forecast1A, forecast2A, forecast3A, actual1A, actual1B, actual2A, actual2B, actual3A] +const rows = [ + forecast1A, + forecast1B, + forecast1C, + forecast2A, + forecast3A, + actual1A, + actual1B, + actual2A, + actual2B, + actual3A +] +describe('fillGrassCuring', () => { it('should map the most recent grass curing value for each station to each forecast', () => { - const filledRows = fillGrassCuring(rows) - expect(filledRows[0].grassCuring).toBe(80) - expect(filledRows[1].grassCuring).toBe(70) - expect(filledRows[2].grassCuring).toBe(NaN) + fillGrassCuring(rows) + expect(forecast1A.grassCuringForecast!.value).toBe(80) + expect(forecast2A.grassCuringForecast!.value).toBe(70) + expect(forecast3A.grassCuringForecast!.value).toBe(NaN) + }) +}) +describe('fillStationGrassCuringForward', () => { + it('should fill grass curing forward for each station if a row is edited', () => { + forecast1B.grassCuringForecast!.value = 43 + fillStationGrassCuringForward(forecast1B, rows) + expect(forecast1C.grassCuringForecast!.value).toBe(43) + expect(forecast1A.grassCuringForecast!.value).toBe(80) + expect(forecast2A.grassCuringForecast!.value).toBe(70) }) }) diff --git a/web/src/features/moreCast2/util.ts b/web/src/features/moreCast2/util.ts index cd9ef3a67..71ec26c2a 100644 --- a/web/src/features/moreCast2/util.ts +++ b/web/src/features/moreCast2/util.ts @@ -36,7 +36,10 @@ export const parseForecastsHelper = ( choice: ModelChoice.FORECAST, value: forecast.wind_speed }, - grassCuring: forecast.grass_curing + grassCuring: { + choice: ModelChoice.FORECAST, + value: forecast.grass_curing + } } rows.push(row) }) @@ -114,17 +117,26 @@ export const mapForecastChoiceLabels = (newRows: MoreCast2Row[], storedRows: Mor row.rhForecast = matchingRow.rhForecast row.windDirectionForecast = matchingRow.windDirectionForecast row.windSpeedForecast = matchingRow.windSpeedForecast + row.grassCuringForecast = matchingRow.grassCuringForecast } } return newRows } +/** + * Fills all stations grass curing values with the last entered value for each station + * @param rows - MoreCast2Row[] + * @returns MoreCast2Row[] + */ export const fillGrassCuring = (rows: MoreCast2Row[]): MoreCast2Row[] => { const stationGrassMap = new Map() // iterate through all rows first so we know we have all the grass curing values for each station // regardless of row order for (const row of rows) { - const { stationCode, forDate, grassCuring } = row + const { stationCode, forDate, grassCuringForecast, grassCuringActual } = row + + const grassCuring = + grassCuringForecast && !isNaN(grassCuringForecast.value) ? grassCuringForecast.value : grassCuringActual if (!isNaN(grassCuring)) { const existingStation = stationGrassMap.get(stationCode) @@ -136,13 +148,31 @@ export const fillGrassCuring = (rows: MoreCast2Row[]): MoreCast2Row[] => { } for (const row of rows) { - if (validForecastPredicate(row)) { - const stationInfo = stationGrassMap.get(row.stationCode) + const stationInfo = stationGrassMap.get(row.stationCode) - if (stationInfo) { - row.grassCuring = stationInfo.grassCuring - } + if (stationInfo && row.grassCuringForecast) { + row.grassCuringForecast.value = stationInfo.grassCuring } } return rows } + +/** + * Provided a single row for a station is edited, fills all grass curing values for that station + * forward in time. + * @param editedRow - MoreCast2Row + * @param allRows - MoreCast2Row[] + * @returns MoreCast2Row[] + */ +export const fillStationGrassCuringForward = (editedRow: MoreCast2Row, allRows: MoreCast2Row[]) => { + const editedStation = editedRow.stationCode + const editedDate = editedRow.forDate + const newGrassCuringValue = editedRow.grassCuringForecast!.value + + for (const row of allRows) { + if (row.stationCode === editedStation && row.forDate > editedDate) { + row.grassCuringForecast!.value = newGrassCuringValue + } + } + return allRows +}