From e94c3f08c7bb86303bfd0ce119d39516cd5d8df5 Mon Sep 17 00:00:00 2001 From: Thibault Reidy <147397675+ReidyT@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:24:33 +0100 Subject: [PATCH] feat: remove code complexity (#113) * feat(refactor): simplify heatloss and season * feat(refactor): define simulation duration in years * feat(refactor): remove measurement frequency to have one temperature per day * feat(refactor): regroup simulation context properties by domain --- src/config/simulation.ts | 12 +- src/context/SeasonContext.tsx | 7 +- src/context/SimulationContext.tsx | 281 ++++++++++-------- src/hooks/useHouseMaterial.tsx | 6 +- src/models/SimulationHeatLoss.ts | 9 +- src/modules/common/SimulationCanvas.tsx | 2 +- src/modules/common/SimulationControl.tsx | 23 +- .../SimulationInformations.tsx | 14 +- .../useSimulationInformations.tsx | 9 +- .../ElectricityCostSettings.tsx | 4 +- .../SimulationSettingsPanel/HouseSettings.tsx | 8 +- .../useMaterialSettingsDialog.tsx | 8 +- .../SimulationDurationSettings.tsx | 7 +- .../TemperatureSettings.tsx | 26 +- .../WindowControlSettings.tsx | 11 +- src/modules/common/SpeedButton.tsx | 8 +- src/modules/main/PlayerView.tsx | 2 +- .../models/House/ResidentialHouse/Floor.tsx | 6 +- .../models/House/ResidentialHouse/Roof.tsx | 6 +- .../models/House/ResidentialHouse/Wall.tsx | 6 +- .../House/ResidentialHouse/WindowFrame.tsx | 10 +- src/types/time.ts | 13 - src/utils/heatLoss.test.ts | 79 +---- src/utils/heatLoss.ts | 34 +-- src/utils/seasons.test.ts | 48 +-- src/utils/seasons.ts | 57 +--- src/utils/time.ts | 22 +- 27 files changed, 272 insertions(+), 446 deletions(-) delete mode 100644 src/types/time.ts diff --git a/src/config/simulation.ts b/src/config/simulation.ts index 4bec28b..f2c0121 100644 --- a/src/config/simulation.ts +++ b/src/config/simulation.ts @@ -1,5 +1,4 @@ import { HouseComponentInsulation } from '@/types/houseComponentInsulation'; -import { TimeUnit } from '@/types/time'; import { HOUSE_INSULATIONS, @@ -7,16 +6,9 @@ import { } from './houseInsulations'; export const SIMULATION_FRAME_MS = 150; -export const SIMULATION_SLIDING_WINDOW = { window: 1, unit: TimeUnit.Days }; export const SIMULATION_CSV_FILES = { - 1: { - path: 'temperatures/predictions_1_year.csv', - measurementFrequency: TimeUnit.Days, - }, - 25: { - path: 'temperatures/predictions_25_year.csv', - measurementFrequency: TimeUnit.Days, - }, + 1: 'temperatures/predictions_1_year.csv', + 25: 'temperatures/predictions_25_year.csv', } as const; export const SIMULATION_INDOOR_TEMPERATURE_CELCIUS = { diff --git a/src/context/SeasonContext.tsx b/src/context/SeasonContext.tsx index 02a298a..abbfde6 100644 --- a/src/context/SeasonContext.tsx +++ b/src/context/SeasonContext.tsx @@ -1,7 +1,7 @@ import { ReactNode, createContext, useContext, useMemo } from 'react'; import { Season, Seasons } from '@/types/seasons'; -import { getMostPresentSeason } from '@/utils/seasons'; +import { getSeason } from '@/utils/seasons'; import { useSimulation } from './SimulationContext'; @@ -18,9 +18,8 @@ type Props = { }; export const SeasonProvider = ({ children }: Props): ReactNode => { - const { date } = useSimulation(); - // TODO: juste use the date... - const season = getMostPresentSeason(date, date); + const { date } = useSimulation('simulation'); + const season = getSeason(date); const contextValue = useMemo( () => ({ diff --git a/src/context/SimulationContext.tsx b/src/context/SimulationContext.tsx index 64529fc..5ce3bd7 100644 --- a/src/context/SimulationContext.tsx +++ b/src/context/SimulationContext.tsx @@ -27,7 +27,6 @@ import { FormattedHeatLoss } from '@/types/heatLoss'; import { HeatLossPerComponent } from '@/types/houseComponent'; import { SimulationStatus } from '@/types/simulation'; import { TemperatureRow, UserOutdoorTemperature } from '@/types/temperatures'; -import { FormattedTime, TimeUnit } from '@/types/time'; import { WindowScaleSize, WindowSizeType } from '@/types/window'; import { undefinedContextErrorFactory } from '@/utils/context'; import { formatHeatLossRate } from '@/utils/heatLoss'; @@ -47,41 +46,53 @@ const SPEED_STATES: SpeedState[] = [ { text: 'x5', multiply: 5 }, ]; -// TODO: regroup by type like windowSize: { value, update }... -type SimulationContextType = UseHouseComponentsReturnType & { - status: SimulationStatus; - heatLossPerComponent: HeatLossPerComponent; - heatLoss: number; - totalHeatLoss: FormattedHeatLoss; - electricityCost: number; - pricekWh: number; - setPricekWh: (newPrice: number) => void; - indoorTemperature: number; - updateIndoorTemperature: (newTemperature: number) => void; - outdoorTemperature: UserOutdoorTemperature; - updateOutdoorTemperature: (props: { - override: boolean; - value: number; - }) => void; - date: Date; - duration: FormattedTime; - numberOfDays: number; - updateSimulationDuration: ( - duration: Pick & { unit: typeof TimeUnit.Years }, - ) => void; - startSimulation: () => void; - pauseSimulation: () => void; - currDayIdx: number; - gotToDay: (idx: number) => void; - getDateOf: (idx: number) => Date; - windowScaleSize: Vector3; - windowSize: WindowSizeType; - updateWindowSize: (newSize: WindowSizeType) => void; - numberOfFloors: number; - updateNumberOfFloors: (numberOfFloors: number) => void; - houseComponentsConfigurator: HouseComponentsConfigurator; - speed: string; - nextSpeed: () => void; +type SimulationContextType = { + simulation: { + status: SimulationStatus; + start: () => void; + pause: () => void; + date: Date; + duration: { + years: number; + update: ({ durationInYears }: { durationInYears: number }) => void; + }; + days: { + total: number; + currentIdx: number; + goToDay: (idx: number) => void; + getDateOf: (idx: number) => Date; + }; + speed: { + current: string; + next: () => void; + }; + }; + heatLoss: { + global: number; + perComponent: HeatLossPerComponent; + total: FormattedHeatLoss; + }; + electricity: { + cost: number; + pricekWh: number; + updatePricekWh: (newPrice: number) => void; + }; + temperatures: { + indoor: number; + updateIndoor: (newTemperature: number) => void; + outdoor: UserOutdoorTemperature; + updateOutdoor: (props: { override: boolean; value: number }) => void; + }; + house: UseHouseComponentsReturnType & { + window: { + scaleSize: Vector3; + size: WindowSizeType; + updateSize: (newSize: WindowSizeType) => void; + }; + numberOfFloors: number; + updateNumberOfFloors: (numberOfFloors: number) => void; + componentsConfigurator: HouseComponentsConfigurator; + }; }; const SimulationContext = createContext(null); @@ -105,10 +116,8 @@ export const SimulationProvider = ({ ]); // States - const [simulationDuration, setSimulationDuration] = useState({ - value: 1, - unit: TimeUnit.Years, - }); + const [simulationDurationInYears, setSimulationDurationInYears] = + useState(1); const [simulationStatus, setSimulationStatus] = useState( SimulationStatus.INITIAL_LOADING, // waiting for the temperatures... ); @@ -117,7 +126,7 @@ export const SimulationProvider = ({ // Computed states const csv = SIMULATION_CSV_FILES[ - simulationDuration.value as keyof typeof SIMULATION_CSV_FILES + simulationDurationInYears as keyof typeof SIMULATION_CSV_FILES ]; const numberOfDays = temperatures.current.length; // We assume it is one temperature per day. const [ @@ -140,11 +149,11 @@ export const SimulationProvider = ({ useEffect(() => { if (!csv) { throw new Error( - `The CSV was not found for the duration of ${simulationDuration.value}`, + `The CSV was not found for the duration of ${simulationDurationInYears}`, ); } - loadTemperaturesFromCSV(csv.path).then((rows) => { + loadTemperaturesFromCSV(csv).then((rows) => { temperatures.current = rows; dispatchHistory({ type: 'load', @@ -152,7 +161,7 @@ export const SimulationProvider = ({ }); setSimulationStatus(SimulationStatus.LOADING); }); - }, [csv, csv.measurementFrequency, csv.path, simulationDuration.value]); + }, [csv, simulationDurationInYears]); useEffect(() => { // Only set the status from LOADING to IDLE when the heat loss is computed. @@ -199,7 +208,7 @@ export const SimulationProvider = ({ // The useDebouncedCallback function is used to avoid modifying days too quickly // and creating too many new days. - const gotToDay = useDebouncedCallback((idx: number): void => { + const goToDay = useDebouncedCallback((idx: number): void => { pauseSimulation(); if (idx >= numberOfDays - 1) { @@ -278,12 +287,8 @@ export const SimulationProvider = ({ }, []); const updateSimulationDuration = useCallback( - ( - duration: Pick & { - unit: typeof TimeUnit.Years; - }, - ): void => { - setSimulationDuration(duration); + ({ durationInYears }: { durationInYears: number }): void => { + setSimulationDurationInYears(durationInYears); }, [], ); @@ -295,74 +300,98 @@ export const SimulationProvider = ({ }); }, []); - const contextValue = useMemo( - () => ({ - indoorTemperature: simulationSettings.indoorTemperature, - updateIndoorTemperature, - outdoorTemperature: { - ...simulationSettings.outdoorTemperature, - value: getOutdoorTemperature({ - userTemperature: simulationSettings.outdoorTemperature, - weather: currentDay.weatherTemperature, - }), + const contextValue = useMemo(() => { + const { + weatherTemperature, + heatLoss, + totalHeatLoss, + totalElectricityCost, + } = currentDay; + + const { + indoorTemperature, + outdoorTemperature, + pricekWh, + windowSize, + numberOfFloors, + houseConfigurator, + } = simulationSettings; + + return { + simulation: { + status: simulationStatus, + start: startSimulation, + pause: pauseSimulation, + date: new Date(temperatures.current[currentDayIdx]?.time), + duration: { + years: simulationDurationInYears, + update: updateSimulationDuration, + }, + days: { + total: numberOfDays, + currentIdx: currentDayIdx, + goToDay, + getDateOf: (idx: number) => new Date(temperatures.current[idx]?.time), + }, + speed: { + current: SPEED_STATES[simulationSpeedIdx].text, + next: () => + setSimulationSpeedIdx((curr) => (curr + 1) % SPEED_STATES.length), + }, }, - updateOutdoorTemperature, - date: new Date(temperatures.current[currentDayIdx]?.time), - getDateOf: (idx: number) => new Date(temperatures.current[idx]?.time), - duration: simulationDuration, - numberOfDays, - updateSimulationDuration, - status: simulationStatus, - heatLossPerComponent: currentDay.heatLoss.perComponent ?? 0, - heatLoss: currentDay.heatLoss.global, - totalHeatLoss: formatHeatLossRate(currentDay.totalHeatLoss), - electricityCost: currentDay.totalElectricityCost, - pricekWh: simulationSettings.pricekWh, - setPricekWh: updatePricekWh, - startSimulation, - pauseSimulation, - currDayIdx: currentDayIdx, - gotToDay, - windowSize: simulationSettings.windowSize, - windowScaleSize: WindowScaleSize[simulationSettings.windowSize], - updateWindowSize, - numberOfFloors: simulationSettings.numberOfFloors, - updateNumberOfFloors, - houseComponentsConfigurator: simulationSettings.houseConfigurator, - speed: SPEED_STATES[simulationSpeedIdx].text, - nextSpeed: () => - setSimulationSpeedIdx((curr) => (curr + 1) % SPEED_STATES.length), - ...houseComponentsHook, - }), - [ - simulationSettings.indoorTemperature, - simulationSettings.outdoorTemperature, - simulationSettings.pricekWh, - simulationSettings.windowSize, - simulationSettings.numberOfFloors, - simulationSettings.houseConfigurator, - updateIndoorTemperature, - currentDay.weatherTemperature, - currentDay.heatLoss.perComponent, - currentDay.heatLoss.global, - currentDay.totalHeatLoss, - currentDay.totalElectricityCost, - updateOutdoorTemperature, - currentDayIdx, - simulationDuration, - numberOfDays, - updateSimulationDuration, - simulationStatus, - updatePricekWh, - startSimulation, - pauseSimulation, - gotToDay, - updateWindowSize, - updateNumberOfFloors, - simulationSpeedIdx, - houseComponentsHook, - ], - ); + heatLoss: { + global: heatLoss.global, + perComponent: heatLoss.perComponent ?? 0, + total: formatHeatLossRate(totalHeatLoss), + }, + electricity: { + cost: totalElectricityCost, + pricekWh, + updatePricekWh, + }, + temperatures: { + indoor: indoorTemperature, + updateIndoor: updateIndoorTemperature, + outdoor: { + ...outdoorTemperature, + value: getOutdoorTemperature({ + userTemperature: outdoorTemperature, + weather: weatherTemperature, + }), + }, + updateOutdoor: updateOutdoorTemperature, + }, + house: { + window: { + size: windowSize, + scaleSize: WindowScaleSize[windowSize], + updateSize: updateWindowSize, + }, + numberOfFloors, + updateNumberOfFloors, + componentsConfigurator: houseConfigurator, + ...houseComponentsHook, + }, + }; + }, [ + currentDay, + simulationSettings, + simulationStatus, + startSimulation, + pauseSimulation, + currentDayIdx, + simulationDurationInYears, + updateSimulationDuration, + numberOfDays, + goToDay, + simulationSpeedIdx, + updatePricekWh, + updateIndoorTemperature, + updateOutdoorTemperature, + updateWindowSize, + updateNumberOfFloors, + houseComponentsHook, + ]); return ( @@ -371,12 +400,24 @@ export const SimulationProvider = ({ ); }; -export const useSimulation = (): SimulationContextType => { +type SimulationContextKey = keyof SimulationContextType | undefined; +type UseSimulationReturnType = + K extends undefined + ? SimulationContextType + : SimulationContextType[NonNullable]; + +export const useSimulation = ( + prefix?: K, +): UseSimulationReturnType => { const context = useContext(SimulationContext); if (!context) { throw undefinedContextErrorFactory('Simulation'); } - return context; + if (prefix) { + return context[prefix] as UseSimulationReturnType; + } + + return context as UseSimulationReturnType; }; diff --git a/src/hooks/useHouseMaterial.tsx b/src/hooks/useHouseMaterial.tsx index 5cfed21..e1eac14 100644 --- a/src/hooks/useHouseMaterial.tsx +++ b/src/hooks/useHouseMaterial.tsx @@ -18,12 +18,12 @@ export const useHouseMaterial = ({ colors, defaultColor, }: Props): Material => { - const { houseComponentsConfigurator } = useSimulation(); + const { componentsConfigurator } = useSimulation('house'); // Use memo to avoid too many renrenders and optimize performances const houseComponentMaterials = useMemo( - () => houseComponentsConfigurator.getFirstOfType(houseComponent), - [houseComponent, houseComponentsConfigurator], + () => componentsConfigurator.getFirstOfType(houseComponent), + [houseComponent, componentsConfigurator], ); const copiedMaterial = new MeshStandardMaterial().copy(houseMaterial); diff --git a/src/models/SimulationHeatLoss.ts b/src/models/SimulationHeatLoss.ts index 1a60aec..53d6940 100644 --- a/src/models/SimulationHeatLoss.ts +++ b/src/models/SimulationHeatLoss.ts @@ -1,8 +1,7 @@ import { HeatLossPerComponent } from '@/types/houseComponent'; -import { TimeUnit } from '@/types/time'; import { calculateHeatLossConstantFactor, - sumHeatLossRate, + sumHeatLossRateForDay, } from '@/utils/heatLoss'; import { HouseComponentsConfigurator } from './HouseComponentsConfigurator'; @@ -23,7 +22,6 @@ export class SimulationHeatLoss { outdoorTemperature, houseConfigurator, }: Constructor) { - // TODO: improve this code? const heatLossConstantFactors = houseConfigurator .getAll() .reduce( @@ -42,11 +40,10 @@ export class SimulationHeatLoss { ).reduce( (acc, [componentId, heatLossConstantFactor]) => ({ ...acc, - [componentId]: sumHeatLossRate({ - temperatures: [outdoorTemperature], + [componentId]: sumHeatLossRateForDay({ + temperature: outdoorTemperature, constantFactor: heatLossConstantFactor, indoorTemperature, - timeUnit: TimeUnit.Days, // TODO: to adapt or always set per day?, }), }), {}, diff --git a/src/modules/common/SimulationCanvas.tsx b/src/modules/common/SimulationCanvas.tsx index 9879e4d..5dc00f8 100644 --- a/src/modules/common/SimulationCanvas.tsx +++ b/src/modules/common/SimulationCanvas.tsx @@ -11,7 +11,7 @@ import { ResidentialHouse } from '../models/House/ResidentialHouse/ResidentialHo import { Tree } from '../models/Tree/Tree'; export const SimulationCanvas = (): JSX.Element => { - const { numberOfFloors } = useSimulation(); + const { numberOfFloors } = useSimulation('house'); const theme = useTheme(); const md = useMediaQuery(theme.breakpoints.up('sm')); diff --git a/src/modules/common/SimulationControl.tsx b/src/modules/common/SimulationControl.tsx index aec4040..f147e37 100644 --- a/src/modules/common/SimulationControl.tsx +++ b/src/modules/common/SimulationControl.tsx @@ -10,18 +10,15 @@ import { SpeedButton } from './SpeedButton'; export const SimulationControl = (): JSX.Element => { const { - currDayIdx, - numberOfDays, + days: { currentIdx, total, getDateOf, goToDay }, + pause, + start, status, - getDateOf, - gotToDay, - pauseSimulation, - startSimulation, - } = useSimulation(); + } = useSimulation('simulation'); const handleGoToDay = (idx: number | number[]): void => { if (typeof idx === 'number') { - gotToDay(idx); + goToDay(idx); } }; @@ -31,11 +28,11 @@ export const SimulationControl = (): JSX.Element => { handleGoToDay(v)} min={0} - max={numberOfDays - 1} + max={total - 1} hideValue hideLabels={status === SimulationStatus.LOADING} formatValue={(v) => getDateOf(v).toLocaleDateString()} @@ -46,11 +43,7 @@ export const SimulationControl = (): JSX.Element => { data-testid={`simulation-control-button-${status === SimulationStatus.RUNNING ? 'pause' : 'start'}`} color="primary" disabled={status === SimulationStatus.LOADING} - onClick={ - status === SimulationStatus.RUNNING - ? pauseSimulation - : startSimulation - } + onClick={status === SimulationStatus.RUNNING ? pause : start} > {status === SimulationStatus.RUNNING ? : } diff --git a/src/modules/common/SimulationInformations/SimulationInformations.tsx b/src/modules/common/SimulationInformations/SimulationInformations.tsx index f222c21..612f045 100644 --- a/src/modules/common/SimulationInformations/SimulationInformations.tsx +++ b/src/modules/common/SimulationInformations/SimulationInformations.tsx @@ -32,12 +32,10 @@ export const SimulationInformations = (): JSX.Element => { const { season } = useSeason(); const { - status, - indoorTemperature, - outdoorTemperature, - date, - totalHeatLoss, - electricityCost, + electricity: { cost: electricityCost }, + heatLoss: { total: totalHeatLoss }, + temperatures: { indoor, outdoor }, + simulation: { date, status }, } = useSimulation(); const { seasonIcon, heatLoss, formattedWallSize, wallsPrice } = @@ -86,7 +84,7 @@ export const SimulationInformations = (): JSX.Element => { {tInformations('CURRENT_PERIOD.OUTDOOR')} - {outdoorTemperature.value} °C + {outdoor.value} °C @@ -96,7 +94,7 @@ export const SimulationInformations = (): JSX.Element => { {tInformations('CURRENT_PERIOD.INDOOR')} - {indoorTemperature} °C + {indoor} °C diff --git a/src/modules/common/SimulationInformations/useSimulationInformations.tsx b/src/modules/common/SimulationInformations/useSimulationInformations.tsx index 1ed9a8d..f455911 100644 --- a/src/modules/common/SimulationInformations/useSimulationInformations.tsx +++ b/src/modules/common/SimulationInformations/useSimulationInformations.tsx @@ -28,13 +28,16 @@ export const useSimulationInformations = (): UseSimulationInformationsReturnType => { const { season } = useSeason(); - const { heatLoss, houseComponentsConfigurator } = useSimulation(); + const { + heatLoss: { global: heatLoss }, + house: { componentsConfigurator }, + } = useSimulation(); - const wallComponent = houseComponentsConfigurator.getFirstOfType( + const wallComponent = componentsConfigurator.getFirstOfType( HouseComponent.Wall, ); - const wallPrices = houseComponentsConfigurator + const wallPrices = componentsConfigurator .getByType(HouseComponent.Wall) .reduce( (totCost, houseComponent) => diff --git a/src/modules/common/SimulationSettingsPanel/ElectricityCostSettings.tsx b/src/modules/common/SimulationSettingsPanel/ElectricityCostSettings.tsx index 54dfe41..dc3bd74 100644 --- a/src/modules/common/SimulationSettingsPanel/ElectricityCostSettings.tsx +++ b/src/modules/common/SimulationSettingsPanel/ElectricityCostSettings.tsx @@ -5,13 +5,13 @@ import { FormControlValidator } from '@/modules/common/FormControlValidator'; export const ElectricityCostSettings = (): JSX.Element => { const { t } = useTranslation('SIMULATION_SETTINGS_PANEL'); - const { pricekWh, setPricekWh } = useSimulation(); + const { pricekWh, updatePricekWh } = useSimulation('electricity'); const handleElectricityCostChange = (newValue: string): void => { const newPrice = Number.parseFloat(newValue); if (!Number.isNaN(newPrice)) { - setPricekWh(newPrice); + updatePricekWh(newPrice); } else { console.error(`Invalid price for ${newValue}`); } diff --git a/src/modules/common/SimulationSettingsPanel/HouseSettings.tsx b/src/modules/common/SimulationSettingsPanel/HouseSettings.tsx index 3bc4996..140b73a 100644 --- a/src/modules/common/SimulationSettingsPanel/HouseSettings.tsx +++ b/src/modules/common/SimulationSettingsPanel/HouseSettings.tsx @@ -30,11 +30,11 @@ export const HouseSettings = (): JSX.Element => { const { t } = useTranslation('SIMULATION_SETTINGS_PANEL'); const { t: tInsulations } = useTranslation('INSULATIONS'); const { - houseComponentsConfigurator, + componentsConfigurator, changeComponentInsulation, numberOfFloors, updateNumberOfFloors, - } = useSimulation(); + } = useSimulation('house'); const wallInsulations = Object.keys( HOUSE_INSULATIONS.Wall, @@ -45,12 +45,12 @@ export const HouseSettings = (): JSX.Element => { ) as (keyof typeof HOUSE_INSULATIONS.Window)[]; const currWallInsulation = - houseComponentsConfigurator.getFirstOfType(HouseComponent.Wall) + componentsConfigurator.getFirstOfType(HouseComponent.Wall) ?.insulationName ?? SIMULATION_DEFAULT_WALL_COMPONENT_INSULATION.insulationName; const currWindowInsulation = - houseComponentsConfigurator.getFirstOfType(HouseComponent.Window) + componentsConfigurator.getFirstOfType(HouseComponent.Window) ?.insulationName ?? SIMULATION_DEFAULT_WINDOW_COMPONENT_INSULATION.insulationName; diff --git a/src/modules/common/SimulationSettingsPanel/MaterialSettingsDialog/useMaterialSettingsDialog.tsx b/src/modules/common/SimulationSettingsPanel/MaterialSettingsDialog/useMaterialSettingsDialog.tsx index 82967c2..5efdbd2 100644 --- a/src/modules/common/SimulationSettingsPanel/MaterialSettingsDialog/useMaterialSettingsDialog.tsx +++ b/src/modules/common/SimulationSettingsPanel/MaterialSettingsDialog/useMaterialSettingsDialog.tsx @@ -17,13 +17,13 @@ type UseMaterialSettingsDialogReturnType = { export const useMaterialSettingsDialog = (): UseMaterialSettingsDialogReturnType => { - const { houseComponentsConfigurator, updateCompositionOfInsulation } = - useSimulation(); + const { componentsConfigurator, updateCompositionOfInsulation } = + useSimulation('house'); const [currTab, setCurrTab] = useState(''); const wallComponents = useMemo( - () => houseComponentsConfigurator.getFirstOfType(HouseComponent.Wall), - [houseComponentsConfigurator], + () => componentsConfigurator.getFirstOfType(HouseComponent.Wall), + [componentsConfigurator], ); const wallMaterials = wallComponents?.buildingMaterials; const wallInsulation = wallComponents?.insulationName; diff --git a/src/modules/common/SimulationSettingsPanel/SimulationDurationSettings.tsx b/src/modules/common/SimulationSettingsPanel/SimulationDurationSettings.tsx index fb4b501..1e5cdf7 100644 --- a/src/modules/common/SimulationSettingsPanel/SimulationDurationSettings.tsx +++ b/src/modules/common/SimulationSettingsPanel/SimulationDurationSettings.tsx @@ -5,7 +5,6 @@ import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'; import { SIMULATION_CSV_FILES } from '@/config/simulation'; import { useSimulation } from '@/context/SimulationContext'; import { SimulationStatus } from '@/types/simulation'; -import { TimeUnit } from '@/types/time'; const OPTIONS = Object.keys(SIMULATION_CSV_FILES); @@ -16,7 +15,7 @@ export const SimulationDurationSettings = (): JSX.Element => { const { t: tDates } = useTranslation('DATES'); - const { duration, updateSimulationDuration, status } = useSimulation(); + const { duration, status } = useSimulation('simulation'); const selectIsDisabled = [ SimulationStatus.LOADING, @@ -34,7 +33,7 @@ export const SimulationDurationSettings = (): JSX.Element => { : Number.parseInt(newDuration, 10); if (!Number.isNaN(value)) { - updateSimulationDuration({ value, unit: TimeUnit.Years }); + duration.update({ durationInYears: value }); } else { console.error(`The duration ${newDuration} is not a valid number!`); } @@ -47,7 +46,7 @@ export const SimulationDurationSettings = (): JSX.Element => { labelId="house-duration-select-label" id="house-duration-select" label={t('LABEL')} - value={duration.value} + value={duration.years} onChange={(e) => handleChange(e.target.value)} type="number" disabled={selectIsDisabled} diff --git a/src/modules/common/SimulationSettingsPanel/TemperatureSettings.tsx b/src/modules/common/SimulationSettingsPanel/TemperatureSettings.tsx index d5661dc..7502e20 100644 --- a/src/modules/common/SimulationSettingsPanel/TemperatureSettings.tsx +++ b/src/modules/common/SimulationSettingsPanel/TemperatureSettings.tsx @@ -14,12 +14,8 @@ export const TemperatureSettings = (): JSX.Element => { keyPrefix: 'TEMPERATURES_SETTINGS_PANEL', }); - const { - indoorTemperature, - updateIndoorTemperature, - outdoorTemperature, - updateOutdoorTemperature, - } = useSimulation(); + const { indoor, updateIndoor, outdoor, updateOutdoor } = + useSimulation('temperatures'); const { MIN: minIndoorTemperature, MAX: maxIndoorTemperature } = SIMULATION_INDOOR_TEMPERATURE_CELCIUS; @@ -35,10 +31,10 @@ export const TemperatureSettings = (): JSX.Element => { @@ -46,26 +42,26 @@ export const TemperatureSettings = (): JSX.Element => { - updateOutdoorTemperature({ - override: outdoorTemperature.userOverride, + updateOutdoor({ + override: outdoor.userOverride, value: v, }) } formatValue={formatTemperature} - disabled={!outdoorTemperature.userOverride} + disabled={!outdoor.userOverride} /> - updateOutdoorTemperature({ + updateOutdoor({ override: checked, - value: outdoorTemperature.value, + value: outdoor.value, }) } /> diff --git a/src/modules/common/SimulationSettingsPanel/WindowControlSettings.tsx b/src/modules/common/SimulationSettingsPanel/WindowControlSettings.tsx index 2f552d5..16b94de 100644 --- a/src/modules/common/SimulationSettingsPanel/WindowControlSettings.tsx +++ b/src/modules/common/SimulationSettingsPanel/WindowControlSettings.tsx @@ -43,9 +43,8 @@ export const WindowControlSettings = ({ const { t: tInsulations } = useTranslation('INSULATIONS'); const { t: tMaterials } = useTranslation('MATERIALS'); - const { updateWindowSize, windowSize, houseComponentsConfigurator } = - useSimulation(); - const windowComponent = houseComponentsConfigurator.getFirstOfType( + const { window, componentsConfigurator } = useSimulation('house'); + const windowComponent = componentsConfigurator.getFirstOfType( HouseComponent.Window, ); @@ -56,7 +55,7 @@ export const WindowControlSettings = ({ const windowInsulation = tInsulations(windowComponent.insulationName); const handleSizeChange = (newSize: string): void => { - updateWindowSize(newSize as WindowSizeType); + window.updateSize(newSize as WindowSizeType); }; return ( @@ -80,7 +79,7 @@ export const WindowControlSettings = ({ labelId="window-size-select-label" id="window-size-select" label={t('SIZE_LABEL')} - value={windowSize} + value={window.size} onChange={(v) => handleSizeChange(v.target.value)} > {WindowSizes.map((s) => ( @@ -90,7 +89,7 @@ export const WindowControlSettings = ({ ))} {t('CURRENT_SIZE_LABEL')}{' '} {formatComponentSize({ componentSize: windowComponent.size })} diff --git a/src/modules/common/SpeedButton.tsx b/src/modules/common/SpeedButton.tsx index 158b12c..60ecd67 100644 --- a/src/modules/common/SpeedButton.tsx +++ b/src/modules/common/SpeedButton.tsx @@ -3,15 +3,15 @@ import { Button, Typography } from '@mui/material'; import { useSimulation } from '@/context/SimulationContext'; export const SpeedButton = (): JSX.Element => { - const { speed, nextSpeed } = useSimulation(); + const { speed } = useSimulation('simulation'); return ( ); }; diff --git a/src/modules/main/PlayerView.tsx b/src/modules/main/PlayerView.tsx index 291d77f..2689a17 100644 --- a/src/modules/main/PlayerView.tsx +++ b/src/modules/main/PlayerView.tsx @@ -14,7 +14,7 @@ import { SimulationSettingsPanel } from '../common/SimulationSettingsPanel/Simul const PlayerViewComponent = (): JSX.Element => { const { t } = useTranslation('INITIAL_LOADING'); - const { status } = useSimulation(); + const { status } = useSimulation('simulation'); if (status === SimulationStatus.INITIAL_LOADING) { return ( diff --git a/src/modules/models/House/ResidentialHouse/Floor.tsx b/src/modules/models/House/ResidentialHouse/Floor.tsx index cd0a1b5..5072764 100644 --- a/src/modules/models/House/ResidentialHouse/Floor.tsx +++ b/src/modules/models/House/ResidentialHouse/Floor.tsx @@ -15,15 +15,15 @@ export const Floor = ({ floor: number; }): JSX.Element => { const { wallGeometries } = useWallGeometries(); - const { houseComponentsConfigurator } = useSimulation(); + const { componentsConfigurator } = useSimulation('house'); if (floor < 0) { throw new Error('The floor number can be < 0!'); } const wallHeight = - houseComponentsConfigurator.getFirstOfType(HouseComponent.Wall)?.size - .height ?? 0; + componentsConfigurator.getFirstOfType(HouseComponent.Wall)?.size.height ?? + 0; const offSetY = wallHeight * floor; diff --git a/src/modules/models/House/ResidentialHouse/Roof.tsx b/src/modules/models/House/ResidentialHouse/Roof.tsx index 02d4c5c..a3850c3 100644 --- a/src/modules/models/House/ResidentialHouse/Roof.tsx +++ b/src/modules/models/House/ResidentialHouse/Roof.tsx @@ -60,15 +60,15 @@ export const Roof = ({ nFloors: number; }): JSX.Element => { const wallMaterial = useWallMaterial({ wallMaterial: materials.Wall }); - const { houseComponentsConfigurator } = useSimulation(); + const { componentsConfigurator } = useSimulation('house'); if (nFloors <= 0) { throw new Error('The house must at least have one floor!'); } const wallHeight = - houseComponentsConfigurator.getFirstOfType(HouseComponent.Wall)?.size - .height ?? 0; + componentsConfigurator.getFirstOfType(HouseComponent.Wall)?.size.height ?? + 0; const offsetY = wallHeight * (nFloors - 1); diff --git a/src/modules/models/House/ResidentialHouse/Wall.tsx b/src/modules/models/House/ResidentialHouse/Wall.tsx index ba924f8..437bf8b 100644 --- a/src/modules/models/House/ResidentialHouse/Wall.tsx +++ b/src/modules/models/House/ResidentialHouse/Wall.tsx @@ -24,8 +24,10 @@ const WallComponent = ({ hasWindows?: boolean; wallProps: WallProps; }): JSX.Element => { - const { heatLossPerComponent, registerComponent, unregisterComponent } = - useSimulation(); + const { + heatLoss: { perComponent: heatLossPerComponent }, + house: { registerComponent, unregisterComponent }, + } = useSimulation(); const heatLoss = heatLossPerComponent[id] ?? 0; const material = useWallMaterial({ wallMaterial: materials.Wall }); diff --git a/src/modules/models/House/ResidentialHouse/WindowFrame.tsx b/src/modules/models/House/ResidentialHouse/WindowFrame.tsx index 7fce4f6..965e7ae 100644 --- a/src/modules/models/House/ResidentialHouse/WindowFrame.tsx +++ b/src/modules/models/House/ResidentialHouse/WindowFrame.tsx @@ -23,10 +23,12 @@ export const WindowFrame = ({ }: Props): JSX.Element => { const id = `${wallId}-Window-${windowIdx}`; const { - heatLossPerComponent, - windowScaleSize, - registerComponent, - unregisterComponent, + heatLoss: { perComponent: heatLossPerComponent }, + house: { + window: { scaleSize: windowScaleSize }, + registerComponent, + unregisterComponent, + }, } = useSimulation(); const { frameMaterial } = useWindowMaterial({ windowMaterial: materials.Wood, diff --git a/src/types/time.ts b/src/types/time.ts deleted file mode 100644 index 07fd8f7..0000000 --- a/src/types/time.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const TimeUnit = { - Hours: 'Hours', - Days: 'Days', - Weeks: 'Weeks', - Months: 'Months', - Years: 'Years', -} as const; -export type TimeUnitType = keyof typeof TimeUnit; - -export type FormattedTime = { - value: number; - unit: TimeUnitType; -}; diff --git a/src/utils/heatLoss.test.ts b/src/utils/heatLoss.test.ts index 9f0ae9c..5f094cc 100644 --- a/src/utils/heatLoss.test.ts +++ b/src/utils/heatLoss.test.ts @@ -1,15 +1,12 @@ import { describe, expect, it } from 'vitest'; import { FormattedHeatLoss, HeatLossUnit } from '@/types/heatLoss'; -import { TimeUnit, TimeUnitType } from '@/types/time'; import { calculateHeatLossConstantFactor, formatHeatLossRate, - sumHeatLossRate, + sumHeatLossRateForDay, } from '@/utils/heatLoss'; -import { timeConversionFactors } from './time'; - describe('Heat Loss Utils', () => { describe('calculateHeatLossConstantFactor', () => { it('should calculate the correct heat loss constant factor', () => { @@ -94,90 +91,32 @@ describe('Heat Loss Utils', () => { }); }); - describe('sumHeatLossRate', () => { - it('should return 0 when there are no temperatures', () => { - const result = sumHeatLossRate({ - temperatures: [], - constantFactor: 10, - indoorTemperature: 20, - timeUnit: TimeUnit.Hours, - }); - expect(result).toBe(0); - }); - + describe('sumHeatLossRateForDay', () => { it('should calculate the correct total heat loss for positive heat loss values', () => { - const temperatures = [10, 12, 15]; + const temperature = 10; const constantFactor = 2; const indoorTemperature = 20; - const timeUnit = TimeUnit.Hours; const expectedHeatLoss = - constantFactor * (indoorTemperature - temperatures[0]) + - constantFactor * (indoorTemperature - temperatures[1]) + - constantFactor * (indoorTemperature - temperatures[2]); + constantFactor * (indoorTemperature - temperature) * 24; - const result = sumHeatLossRate({ - temperatures, + const result = sumHeatLossRateForDay({ + temperature, constantFactor, indoorTemperature, - timeUnit, }); expect(result).toBe(expectedHeatLoss); }); it('should handle negative heat loss values (cooling) by treating them as 0', () => { - const temperatures = [25, 28, 30]; // Outdoor temps higher than indoor + const temperature = 30; // Outdoor temperature higher than indoor const constantFactor = 2; const indoorTemperature = 20; - const timeUnit = TimeUnit.Days; const expectedHeatLoss = 0; // Expected heat loss should be 0 as we are cooling - const result = sumHeatLossRate({ - temperatures, - constantFactor, - indoorTemperature, - timeUnit, - }); - expect(result).toBe(expectedHeatLoss); - }); - - it('should calculate the correct total heat loss with different time units', () => { - const temperatures = [10, 12, 15]; - const constantFactor = 2; - const indoorTemperature = 20; - - const timeUnits: TimeUnitType[] = Object.keys(TimeUnit) as TimeUnitType[]; - - timeUnits.forEach((timeUnit) => { - const expectedHeatLoss = - (constantFactor * (indoorTemperature - temperatures[0]) + - constantFactor * (indoorTemperature - temperatures[1]) + - constantFactor * (indoorTemperature - temperatures[2])) * - timeConversionFactors[timeUnit]; - const result = sumHeatLossRate({ - temperatures, - constantFactor, - indoorTemperature, - timeUnit, - }); - expect(result).toBe(expectedHeatLoss); - }); - }); - - it('should handle a mix of positive and negative heat loss values', () => { - const temperatures = [10, 25, 15]; // Mix of outdoor temps below and above indoor - const constantFactor = 2; - const indoorTemperature = 20; - const timeUnit = TimeUnit.Hours; - const expectedHeatLoss = - constantFactor * (indoorTemperature - temperatures[0]) + - 0 + - constantFactor * (indoorTemperature - temperatures[2]); // Only positive heat loss contributes - - const result = sumHeatLossRate({ - temperatures, + const result = sumHeatLossRateForDay({ + temperature, constantFactor, indoorTemperature, - timeUnit, }); expect(result).toBe(expectedHeatLoss); }); diff --git a/src/utils/heatLoss.ts b/src/utils/heatLoss.ts index 761706b..5bdab1d 100644 --- a/src/utils/heatLoss.ts +++ b/src/utils/heatLoss.ts @@ -1,10 +1,7 @@ import { BuildingMaterial } from '@/models/BuildingMaterial'; import { FormattedHeatLoss, HeatLossUnit } from '@/types/heatLoss'; -import { TimeUnitType } from '@/types/time'; import { NonEmptyArray } from '@/types/utils'; -import { timeConversionFactors } from './time'; - /** * Calculates the overall heat transfer coefficient (U-value) for a composite material. * The U-value represents the rate of heat transfer through a unit area of a structure divided by the temperature difference across that structure. @@ -67,38 +64,27 @@ export const calculateHeatLossRate = ({ }; /** - * Calculates the total heat loss rate based on an array of outdoor temperatures. + * Calculates the total heat loss rate per day based on an array of outdoor temperatures. * * @param temperatures - An array of outdoor temperatures (°C). * @param constantFactor - A constant factor used in the heat loss calculation (W/K). * @param indoorTemperature - The indoor temperature for comparison in the heat loss rate calculation (°C). - * @param timeUnit - The time unit corresponding to the frequency of the temperature measurements. - * Its indicate whether the temperatures are recorded once per hour, once per day, etc. - * This affects the total heat loss calculation by scaling the rate according to the measurement frequency. - * @returns The total heat loss rate summed over all specified outdoor temperatures (W or J/s). + * @returns The total heat loss rate per day summed over all specified outdoor temperatures (W or J/s). */ -export const sumHeatLossRate = ({ - temperatures, +export const sumHeatLossRateForDay = ({ + temperature, constantFactor, indoorTemperature, - timeUnit, }: { - temperatures: number[]; + temperature: number; constantFactor: number; indoorTemperature: number; - timeUnit: TimeUnitType; }): number => - temperatures.reduce( - (sum, temperature) => - sum + - calculateHeatLossRate({ - constantFactor, - indoorTemperature, - outdoorTemperature: temperature, - }) * - timeConversionFactors[timeUnit], - 0, - ); + calculateHeatLossRate({ + constantFactor, + indoorTemperature, + outdoorTemperature: temperature, + }) * 24; // multiply by 24 because its for the whole day. /** * Conversion factors for different units of power. diff --git a/src/utils/seasons.test.ts b/src/utils/seasons.test.ts index fe79751..28913fe 100644 --- a/src/utils/seasons.test.ts +++ b/src/utils/seasons.test.ts @@ -3,7 +3,7 @@ import { UnionOfConst } from '@graasp/sdk'; import { describe, expect, it } from 'vitest'; import { Seasons } from '@/types/seasons'; -import { getMostPresentSeason, getSeason } from '@/utils/seasons'; +import { getSeason } from '@/utils/seasons'; describe('Tests seasons utils', () => { describe('getSeason', () => { @@ -51,50 +51,4 @@ describe('Tests seasons utils', () => { }); }); }); - - describe('getMostPresentSeason', () => { - it('should return the correct most present season', () => { - const testCases: [Date, Date, (typeof Seasons)[keyof typeof Seasons]][] = - [ - [new Date('2024-03-15'), new Date('2024-04-15'), Seasons.Spring], - [new Date('2024-06-15'), new Date('2024-09-25'), Seasons.Summer], - [new Date('2024-09-15'), new Date('2024-12-25'), Seasons.Autumn], - [new Date('2024-03-20'), new Date('2024-06-20'), Seasons.Spring], // Exact season boundaries - [new Date('2024-12-15'), new Date('2025-03-25'), Seasons.Winter], // Spans across year end - [new Date('2024-03-20'), new Date('2024-07-20'), Seasons.Spring], // Full spring and half of summer - ]; - - testCases.forEach(([from, to, expectedSeason]) => { - expect(getMostPresentSeason(from, to)).toBe(expectedSeason); - }); - }); - - it('should throw an error if "from" is after "to"', () => { - const from = new Date('2024-04-01'); - const to = new Date('2024-03-15'); - expect(() => getMostPresentSeason(from, to)).toThrowError( - 'The date from must be smaller than date to.', - ); - }); - - it('should throw an error if "from" or "to" is null', () => { - const from = new Date('2024-03-15'); - const to = null as unknown as Date; // Need type assertion for null test - - expect(() => getMostPresentSeason(from, to)).toThrowError( - 'The dates must be defined.', - ); - expect(() => - getMostPresentSeason(null as unknown as Date, from), - ).toThrowError('The dates must be defined.'); - expect(() => - getMostPresentSeason(null as unknown as Date, null as unknown as Date), - ).toThrowError('The dates must be defined.'); - }); - - it('should handle a single day correctly', () => { - const date = new Date('2024-04-15'); - expect(getMostPresentSeason(date, date)).toBe(Seasons.Spring); - }); - }); }); diff --git a/src/utils/seasons.ts b/src/utils/seasons.ts index e3b5572..0fec0e5 100644 --- a/src/utils/seasons.ts +++ b/src/utils/seasons.ts @@ -2,8 +2,15 @@ import { UnionOfConst } from '@graasp/sdk'; import { Seasons } from '@/types/seasons'; -import { dateDiffInDays, getDayOfYear, isLeapYear } from './time'; +import { getDayOfYear, isLeapYear } from './time'; +/** + * Determines the season of a given date. + * + * @param date The given date. + * @param to The end date of the range (inclusive). + * @returns The season of the specified date. + */ export const getSeason = (date: Date): UnionOfConst => { const dayOfYear = getDayOfYear(date); const leapYearOffset = isLeapYear(date.getFullYear()) ? 1 : 0; // Offset for leap years @@ -25,51 +32,3 @@ export const getSeason = (date: Date): UnionOfConst => { return Seasons.Winter; } }; - -/** - * Determines the most present season within a given date range. - * - * @param from The start date of the range (inclusive). - * @param to The end date of the range (inclusive). - * @returns The most present season within the specified date range. If the date range is empty or invalid (e.g., 'from' is after 'to'), it throws an error. If multiple seasons have the same highest count, the function returns the first one encountered. - */ -export const getMostPresentSeason = ( - from: Date, - to: Date, -): UnionOfConst => { - if (!from || !to) { - throw new Error('The dates must be defined.'); - } - - if (from > to) { - throw new Error('The date from must be smaller than date to.'); - } - - const seasonCounts = { - [Seasons.Spring]: 0, - [Seasons.Summer]: 0, - [Seasons.Autumn]: 0, - [Seasons.Winter]: 0, - }; - - let mostPresent: { season: UnionOfConst; count: number } = { - season: Seasons.Spring, - count: -1, - }; - - for (let i = 0; i < dateDiffInDays(from, to) + 1; i += 1) { - const currentDate = new Date(from); - currentDate.setDate(from.getDate() + i); - const currentSeason = getSeason(currentDate); - seasonCounts[currentSeason] += 1; - - if (seasonCounts[currentSeason] > mostPresent.count) { - mostPresent = { - season: currentSeason, - count: seasonCounts[currentSeason], - }; - } - } - - return mostPresent.season; -}; diff --git a/src/utils/time.ts b/src/utils/time.ts index 1099c4e..47282b4 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -1,24 +1,4 @@ -import { FormattedTime, TimeUnit, TimeUnitType } from '@/types/time'; - -export const timeConversionFactors: { [key in TimeUnitType]: number } = { - [TimeUnit.Hours]: 1, - [TimeUnit.Days]: 24, - [TimeUnit.Weeks]: 24 * 7, - [TimeUnit.Months]: 24 * 30, // Approximate as 30 days - [TimeUnit.Years]: 24 * 365, // Approximate as 365 days -} as const; - -export const formatHoursToDays = (hours: number): FormattedTime => { - const days = Math.floor(hours / timeConversionFactors.Days); - - if (days > 0) { - return { value: days, unit: TimeUnit.Days }; - } - - return { value: hours, unit: TimeUnit.Hours }; -}; - -export const dateDiffInDays = (from: Date, to: Date): number => { +const dateDiffInDays = (from: Date, to: Date): number => { const diffInMs = to.getTime() - from.getTime(); const oneDay = 1000 * 60 * 60 * 24; return Math.floor(diffInMs / oneDay);