diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c1c9a1e7..0d505a13c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org - Trainers can see the current occupation of a vehicle and cancel it. - Simulated regions are prefixed with "\[Simuliert\]" in the request target selection for the requests behavior. - The patient, whose popup is open, is now highlighted. +- There is now a behavior that transfers patients. It has configurable load times and delay between transfers. + - Its user interface can be used to transfer specific patients in specific vehicles. + - Its user interface displays what vehicles are being loaded and what vehicles are waiting for transfer. ### Changed diff --git a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/templates/simulated-region.ts b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/templates/simulated-region.ts index 8387be2a0..bba9b49ba 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/editor-panel/templates/simulated-region.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/editor-panel/templates/simulated-region.ts @@ -5,6 +5,7 @@ import type { Mutable, } from 'digital-fuesim-manv-shared'; import { + TransferBehaviorState, cloneDeepMutable, MapPosition, SimulatedRegion, @@ -76,6 +77,7 @@ const stereotypes: SimulatedRegion[] = [ AutomaticallyDistributeVehiclesBehaviorState.create(), AnswerRequestsBehaviorState.create(), ReportBehaviorState.create(), + TransferBehaviorState.create(), ], inEvents: [], position, diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/simulated-region-overview.module.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/simulated-region-overview.module.ts index 4437f6dea..d80eec5fa 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/simulated-region-overview.module.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/simulated-region-overview.module.ts @@ -48,6 +48,7 @@ import { RequestVehiclesComponent } from './tabs/behavior-tab/behaviors/request- import { SimulatedRegionOverviewPatientInteractionBarComponent } from './tabs/patients-tab/simulated-region-overview-patient-interaction-bar/simulated-region-overview-patient-interaction-bar.component'; import { SimulatedRegionOverviewVehiclesTabComponent } from './tabs/vehicles-tab/simulated-region-overview-vehicles-tab.component'; import { SimulatedRegionOverviewPatientsTableComponent } from './patients-table/simulated-region-overview-patients-table.component'; +import { SimulatedRegionOverviewBehaviorTransferVehiclesComponent } from './tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component'; @NgModule({ declarations: [ @@ -87,6 +88,7 @@ import { SimulatedRegionOverviewPatientsTableComponent } from './patients-table/ SimulatedRegionOverviewPatientInteractionBarComponent, SimulatedRegionOverviewVehiclesTabComponent, SimulatedRegionOverviewPatientsTableComponent, + SimulatedRegionOverviewBehaviorTransferVehiclesComponent, ], imports: [ FormsModule, diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts index 0357ce9a6..c58203c9b 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/request-vehicles/simulated-region-overview-behavior-request-vehicles.component.ts @@ -61,7 +61,7 @@ export class RequestVehiclesComponent implements OnChanges { `[Simuliert] ${simulatedRegion.name}`, ]) .filter(([id, _name]) => id !== this.simulatedRegionId) - .sort(([id1, name1], [id2, name2]) => + .sort(([_id1, name1], [_id2, name2]) => name1 === name2 ? 0 : name1! < name2! ? -1 : 1 ); options.unshift(['trainees', 'Die Trainierenden']); diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.html new file mode 100644 index 000000000..dadae6f21 --- /dev/null +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.html @@ -0,0 +1,421 @@ +
+
+

+ + + + Informationen +

+
+
+
+ +
Auf Transfer wartende Fahrzeuge
+ + + + + + + + + + + + + + + + + +
FahrzeugZielPatienten
+ {{ bufferedTransfer.vehicleName }} + + {{ bufferedTransfer.destination }} + + {{ bufferedTransfer.numberOfPatients }} +
+ + + Derzeit warten keine Fahrzeuge auf den Transfer. + + +
+ + +
Fahrzeuge, die gerade für den Transfer beladen werden
+ + + + + + + + + + + + + + + + + +
FahrzeugZielPatienten
+ {{ activeActivity.vehicleName }} + + {{ activeActivity.destination }} + + {{ activeActivity.numberOfPatients }} +
+ + + Derzeit werden keine Fahrzeuge für den Transfer beladen. + + +
+
+ +
+
+

+ + Einstellungen +

+
+
+
+
+
+
+ Ladezeit pro Patient +
+
+
+ + Sek +
+
+
+
+
+ Ladezeit für Personal +
+
+
+ + Sek +
+
+
+
+
+ Zeit zwischen zwei Ausfahrten +
+
+
+ + Sek +
+
+
+
+
+
+
+
+

+ + Transfer initiieren +

+
+
+ +
+

+ Es können nur so viele Patienten ausgewählt werden, wie in das + ausgewählte Fahrzeug passen. Das Fahrzeug kann nur zu einem Fahrzeug + geändert werden, in welches alle ausgewählten Patienten passen. +

+
+ +
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + +
SKGesichtetVerlaufAusgewählt
+ + + + + + + +
+ +

+ Es befinden sich keine Patienten in der Region. +

+
+ +
+ + +
+ +
+ + + + + + + + + + +
+
+
+
+
+ +
+
+
diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.scss b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.ts new file mode 100644 index 000000000..5794d3c8d --- /dev/null +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/behaviors/transfer-vehicles/simulated-region-overview-behavior-transfer-vehicles.component.ts @@ -0,0 +1,394 @@ +import type { OnDestroy, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; +import type { MemoizedSelector } from '@ngrx/store'; +import { Store, createSelector } from '@ngrx/store'; +import type { + Hospital, + PatientStatus, + TransferBehaviorState, + TransferPoint, + UUIDSet, + Vehicle, +} from 'digital-fuesim-manv-shared'; +import { + isInSpecificSimulatedRegion, + Patient, + UUID, +} from 'digital-fuesim-manv-shared'; +import type { Observable } from 'rxjs'; +import { Subject, takeUntil } from 'rxjs'; +import { ExerciseService } from 'src/app/core/exercise.service'; +import type { AppState } from 'src/app/state/app.state'; +import { + createSelectActivityStatesByType, + createSelectBehaviorState, + createSelectElementsInSimulatedRegion, + selectConfiguration, + selectHospitals, + selectPatients, + selectTransferPoints, + selectVehicles, +} from 'src/app/state/application/selectors/exercise.selectors'; +import { comparePatientsByVisibleStatus } from '../../../compare-patients'; + +let globalLastInformationCollapsed = true; +let globalLastSettingsCollapsed = true; +let globalLastTransferCollapsed = true; + +@Component({ + selector: 'app-simulated-region-overview-behavior-transfer-vehicles', + templateUrl: + './simulated-region-overview-behavior-transfer-vehicles.component.html', + styleUrls: [ + './simulated-region-overview-behavior-transfer-vehicles.component.scss', + ], +}) +export class SimulatedRegionOverviewBehaviorTransferVehiclesComponent + implements OnInit, OnDestroy +{ + @Input() simulatedRegionId!: UUID; + @Input() transferBehaviorId!: UUID; + + private readonly destroy$ = new Subject(); + public minPatients = 0; + + public clickedPatients: { [key: UUID]: boolean } = {}; + + public bufferedTransfers$!: Observable< + { vehicleName: string; destination: string; numberOfPatients: number }[] + >; + public activeActivities$!: Observable< + { vehicleName: string; destination: string; numberOfPatients: number }[] + >; + + public vehicleToSend?: Vehicle; + + public useableVehicles$!: Observable; + + public reachableTransferPoints$!: Observable; + public reachableHospitals$!: Observable; + + public selectedDestination?: { + name?: string | undefined; + externalName?: string | undefined; + } & (Hospital | TransferPoint); + + private _informationCollapsed: boolean; + transferBehaviorState$!: Observable; + patients$!: Observable< + (Patient & { + visibleStatus: PatientStatus; + })[] + >; + + public get informationCollapsed(): boolean { + return this._informationCollapsed; + } + public set informationCollapsed(value: boolean) { + this._informationCollapsed = value; + globalLastInformationCollapsed = value; + } + + private _settingsCollapsed: boolean; + + public get settingsCollapsed(): boolean { + return this._settingsCollapsed; + } + public set settingsCollapsed(value: boolean) { + this._settingsCollapsed = value; + globalLastSettingsCollapsed = value; + } + + private _transferCollapsed: boolean; + + public get transferCollapsed(): boolean { + return this._transferCollapsed; + } + public set transferCollapsed(value: boolean) { + this._transferCollapsed = value; + globalLastTransferCollapsed = value; + } + + constructor( + private readonly store: Store, + private readonly exerciseService: ExerciseService + ) { + this._informationCollapsed = globalLastInformationCollapsed; + this._settingsCollapsed = globalLastSettingsCollapsed; + this._transferCollapsed = globalLastTransferCollapsed; + } + + ngOnInit(): void { + const transferBehaviorStateSelector: MemoizedSelector< + AppState, + TransferBehaviorState + > = createSelectBehaviorState( + this.simulatedRegionId, + this.transferBehaviorId + ); + + const bufferedTransferEventQueueSelector = createSelector( + transferBehaviorStateSelector, + (transferBehaviorState) => + transferBehaviorState.startTransferEventQueue + ); + + const bufferedTransfersSelector = createSelector( + bufferedTransferEventQueueSelector, + selectVehicles, + selectHospitals, + selectTransferPoints, + (bufferedTransferEventQueue, vehicles, hospitals, transferPoints) => + bufferedTransferEventQueue.map((bufferedTransferEvent) => ({ + vehicleName: + vehicles[bufferedTransferEvent.vehicleId]?.name ?? + 'Gelöschtes Fahrzeug', + destination: + bufferedTransferEvent.transferDestinationType === + 'hospital' + ? hospitals[ + bufferedTransferEvent.transferDestinationId + ]?.name ?? 'Gelöschtes Krankenhaus' + : transferPoints[ + bufferedTransferEvent.transferDestinationId + ]?.externalName ?? 'Gelöschtes Transferziel', + numberOfPatients: Object.keys( + vehicles[bufferedTransferEvent.vehicleId]?.patientIds ?? + {} + ).length, + })) + ); + + const activeActivityStatesSelector = createSelectActivityStatesByType( + this.simulatedRegionId, + 'loadVehicleActivity' + ); + + const activeActivitiesSelector = createSelector( + activeActivityStatesSelector, + selectVehicles, + selectHospitals, + selectTransferPoints, + (activeActivityStates, vehicles, hospitals, transferPoints) => + activeActivityStates.map((activeActivityState) => ({ + vehicleName: + vehicles[activeActivityState.vehicleId]?.name ?? + 'Gelöschtes Fahrzeug', + destination: + activeActivityState.transferDestinationType === + 'hospital' + ? hospitals[ + activeActivityState.transferDestinationId + ]?.name ?? 'Gelöschtes Krankenhaus' + : transferPoints[ + activeActivityState.transferDestinationId + ]?.externalName ?? 'Gelöschtes Transferziel', + numberOfPatients: Object.keys( + activeActivityState.patientsToBeLoaded + ).length, + })) + ); + + const ownTransferPointSelector = createSelector( + selectTransferPoints, + (transferPoints) => + Object.values(transferPoints).find((transferPoint) => + isInSpecificSimulatedRegion( + transferPoint, + this.simulatedRegionId + ) + )! + ); + + const vehiclesInSimulatedRegionSelector = + createSelectElementsInSimulatedRegion( + selectVehicles, + this.simulatedRegionId + ); + + const patientsInSimulatedRegionSelector = + createSelectElementsInSimulatedRegion( + selectPatients, + this.simulatedRegionId + ); + + const reachableTransferPointsSelector = createSelector( + selectTransferPoints, + ownTransferPointSelector, + (transferPoints, transferPoint) => + Object.keys(transferPoint.reachableTransferPoints).map( + (id) => transferPoints[id]! + ) + ); + + const reachableHospitalsSelector = createSelector( + selectHospitals, + ownTransferPointSelector, + (hospitals, transferPoint) => + Object.keys(transferPoint.reachableHospitals).map( + (id) => hospitals[id]! + ) + ); + + this.patients$ = this.store.select( + createSelector( + patientsInSimulatedRegionSelector, + selectConfiguration, + (patients, configuration) => + patients + .sort((patientA, patientB) => + comparePatientsByVisibleStatus( + patientA, + patientB, + configuration + ) + ) + .map((patient) => ({ + visibleStatus: Patient.getVisibleStatus( + patient, + configuration.pretriageEnabled, + configuration.bluePatientsEnabled + ), + ...patient, + })) + ) + ); + this.bufferedTransfers$ = this.store.select(bufferedTransfersSelector); + this.activeActivities$ = this.store.select(activeActivitiesSelector); + this.transferBehaviorState$ = this.store.select( + transferBehaviorStateSelector + ); + this.useableVehicles$ = this.store.select( + vehiclesInSimulatedRegionSelector + ); + + this.reachableTransferPoints$ = this.store.select( + reachableTransferPointsSelector + ); + this.reachableHospitals$ = this.store.select( + reachableHospitalsSelector + ); + + // remove selected elements if deleted + + this.useableVehicles$ + .pipe(takeUntil(this.destroy$)) + .subscribe((useableVehicles) => { + if ( + this.vehicleToSend && + !useableVehicles.includes(this.vehicleToSend) + ) { + this.vehicleToSend = undefined; + } + }); + + this.patients$.pipe(takeUntil(this.destroy$)).subscribe((patients) => { + Object.entries(this.clickedPatients).forEach( + ([patientId, clicked]) => { + if ( + !patients + .map((patient) => patient.id) + .includes(patientId) + ) { + if (clicked) { + this.minPatients--; + } + delete this.clickedPatients[patientId]; + } + } + ); + }); + + this.reachableHospitals$ + .pipe(takeUntil(this.destroy$)) + .subscribe((reachableHospitals) => { + if ( + this.selectedDestination?.type === 'hospital' && + !reachableHospitals.includes(this.selectedDestination) + ) { + this.selectedDestination = undefined; + } + }); + + this.reachableTransferPoints$ + .pipe(takeUntil(this.destroy$)) + .subscribe((reachableTransferPoints) => { + if ( + this.selectedDestination?.type === 'transferPoint' && + !reachableTransferPoints.includes(this.selectedDestination) + ) { + this.selectedDestination = undefined; + } + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + } + + public updatePatientLoadTime(loadTimePerPatient: number) { + this.exerciseService.proposeAction({ + type: '[TransferBehavior] Update Patient Load Time', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.transferBehaviorId, + loadTimePerPatient, + }); + } + public updatePersonnelLoadTime(personnelLoadTime: number) { + this.exerciseService.proposeAction({ + type: '[TransferBehavior] Update Personnel Load Time', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.transferBehaviorId, + personnelLoadTime, + }); + } + public updateSendDelay(delayBetweenSends: number) { + this.exerciseService.proposeAction({ + type: '[TransferBehavior] Update Delay Between Sends', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.transferBehaviorId, + delayBetweenSends, + }); + } + + public togglePatientSelection(patient: Patient) { + if ( + !this.clickedPatients[patient.id] && + this.minPatients + 1 > + (this.vehicleToSend?.patientCapacity ?? + Number.POSITIVE_INFINITY) + ) { + return; + } + this.clickedPatients[patient.id] = !this.clickedPatients[patient.id]; + this.minPatients = Object.values(this.clickedPatients).filter( + (clicked) => clicked + ).length; + } + + public sendVehicle() { + if (!this.vehicleToSend || !this.selectedDestination) { + return; + } + + const patients: UUIDSet = Object.fromEntries( + Object.entries(this.clickedPatients) + .filter(([_patientId, clicked]) => clicked) + .map(([patientId, _clicked]) => [patientId, true]) + ); + + this.exerciseService.proposeAction({ + type: '[TransferBehavior] Send Transfer Request Event', + simulatedRegionId: this.simulatedRegionId, + behaviorId: this.transferBehaviorId, + vehicleId: this.vehicleToSend.id, + destinationType: this.selectedDestination.type, + destinationId: this.selectedDestination.id, + patients, + }); + + this.clickedPatients = {}; + this.vehicleToSend = undefined; + this.minPatients = 0; + } +} diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html index 27e86e4ba..c73d67d0d 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/simulated-region-overview-behavior-tab.component.html @@ -67,6 +67,11 @@ [simulatedRegionId]="simulatedRegion.id" [requestBehaviorId]="selectedBehavior!.id" > +

Es ist noch keine Verhaltensweise ausgewählt.

diff --git a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/utils/behavior-to-german-name.pipe.ts b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/utils/behavior-to-german-name.pipe.ts index f55f1dc62..ff8dd9014 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/utils/behavior-to-german-name.pipe.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/simulated-region-overview/tabs/behavior-tab/utils/behavior-to-german-name.pipe.ts @@ -13,6 +13,7 @@ const behaviorTypeToGermanNameDictionary: { answerRequestsBehavior: 'Fahrzeuganfragen beantworten', automaticallyDistributeVehiclesBehavior: 'Fahrzeuge verteilen', requestBehavior: 'Fahrzeuge anfordern', + transferBehavior: 'Fahrzeuge versenden', }; @Pipe({ name: 'behaviorTypeToGermanName', diff --git a/frontend/src/app/shared/components/vehicle-occupation-editor/vehicle-occupation-editor.component.html b/frontend/src/app/shared/components/vehicle-occupation-editor/vehicle-occupation-editor.component.html index 178c3a559..a5a70ed56 100644 --- a/frontend/src/app/shared/components/vehicle-occupation-editor/vehicle-occupation-editor.component.html +++ b/frontend/src/app/shared/components/vehicle-occupation-editor/vehicle-occupation-editor.component.html @@ -12,6 +12,12 @@ Das Fahrzeug wird gerade ausgeladen. + + Das Fahrzeug wird gerade beladen. + + + Das Fahrzeug wartet auf den Transfer. + Die Tätigkeit ist unbekannt. diff --git a/shared/src/models/simulated-region.ts b/shared/src/models/simulated-region.ts index 1a54ac306..f4e5580da 100644 --- a/shared/src/models/simulated-region.ts +++ b/shared/src/models/simulated-region.ts @@ -38,6 +38,20 @@ export class SimulatedRegion { @IsString() public readonly borderColor: string; + @Type(...simulationEventTypeOptions) + @ValidateNested() + public readonly inEvents: readonly ExerciseSimulationEvent[] = []; + + @Type(...simulationBehaviorTypeOptions) + @ValidateNested() + public readonly behaviors: readonly ExerciseSimulationBehaviorState[] = []; + + @IsMultiTypedIdMap(getSimulationActivityConstructor) + @ValidateNested() + public readonly activities: { + readonly [stateId: UUID]: ExerciseSimulationActivityState; + } = {}; + /** * @param position top-left position * @deprecated Use {@link create} instead @@ -54,20 +68,6 @@ export class SimulatedRegion { this.borderColor = borderColor; } - @Type(...simulationEventTypeOptions) - @ValidateNested() - public readonly inEvents: readonly ExerciseSimulationEvent[] = []; - - @Type(...simulationBehaviorTypeOptions) - @ValidateNested() - public readonly behaviors: readonly ExerciseSimulationBehaviorState[] = []; - - @IsMultiTypedIdMap(getSimulationActivityConstructor) - @ValidateNested() - public readonly activities: { - readonly [stateId: UUID]: ExerciseSimulationActivityState; - } = {}; - static readonly create = getCreate(this); static image: ImageProperties = { diff --git a/shared/src/models/utils/occupations/exercise-occupation.ts b/shared/src/models/utils/occupations/exercise-occupation.ts index 6f4a76eb7..2261ab1c7 100644 --- a/shared/src/models/utils/occupations/exercise-occupation.ts +++ b/shared/src/models/utils/occupations/exercise-occupation.ts @@ -3,11 +3,15 @@ import type { Constructor } from '../../../utils'; import { IntermediateOccupation } from './intermediate-occupation'; import { Occupation } from './occupation'; import { NoOccupation } from './no-occupation'; +import { LoadOccupation } from './load-occupation'; +import { WaitForTransferOccupation } from './wait-for-transfer-occupation'; import { UnloadingOccupation } from './unloading-occupation'; export const occupations = { IntermediateOccupation, NoOccupation, + LoadOccupation, + WaitForTransferOccupation, UnloadingOccupation, }; @@ -24,6 +28,8 @@ export type ExerciseOccupationType = ExerciseOccupation['type']; export const occupationDictionary: ExerciseOccupationDictionary = { intermediateOccupation: IntermediateOccupation, noOccupation: NoOccupation, + loadOccupation: LoadOccupation, + waitForTransferOccupation: WaitForTransferOccupation, unloadingOccupation: UnloadingOccupation, }; diff --git a/shared/src/models/utils/occupations/load-occupation.ts b/shared/src/models/utils/occupations/load-occupation.ts new file mode 100644 index 000000000..c4082820f --- /dev/null +++ b/shared/src/models/utils/occupations/load-occupation.ts @@ -0,0 +1,19 @@ +import { IsUUID } from 'class-validator'; +import { IsValue } from '../../../utils/validators'; +import { getCreate } from '../get-create'; +import { UUID, uuidValidationOptions } from '../../../utils'; +import type { Occupation } from './occupation'; + +export class LoadOccupation implements Occupation { + @IsValue('loadOccupation') + readonly type = 'loadOccupation'; + + @IsUUID(4, uuidValidationOptions) + readonly loadingActivityId: UUID; + + constructor(loadingActivityId: UUID) { + this.loadingActivityId = loadingActivityId; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/models/utils/occupations/wait-for-transfer-occupation.ts b/shared/src/models/utils/occupations/wait-for-transfer-occupation.ts new file mode 100644 index 000000000..afcb5b837 --- /dev/null +++ b/shared/src/models/utils/occupations/wait-for-transfer-occupation.ts @@ -0,0 +1,10 @@ +import { IsValue } from '../../../utils/validators'; +import { getCreate } from '../get-create'; +import type { Occupation } from './occupation'; + +export class WaitForTransferOccupation implements Occupation { + @IsValue('waitForTransferOccupation') + readonly type = 'waitForTransferOccupation'; + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/activities/exercise-simulation-activity.ts b/shared/src/simulation/activities/exercise-simulation-activity.ts index aa4c1141b..c7fb8c757 100644 --- a/shared/src/simulation/activities/exercise-simulation-activity.ts +++ b/shared/src/simulation/activities/exercise-simulation-activity.ts @@ -6,8 +6,10 @@ import { unloadVehicleActivity } from './unload-vehicle'; import { recurringEventActivity } from './recurring-event'; import { generateReportActivity } from './generate-report'; import { providePersonnelFromVehiclesActivity } from './provide-personnel-from-vehicles'; -import { transferVehiclesActivity } from './transfer-vehicles'; import { createRequestActivity } from './create-request'; +import { loadVehicleActivity } from './load-vehicle'; +import { sendRemoteEventActivity } from './send-remote-event'; +import { transferVehicleActivity } from './transfer-vehicle'; export const simulationActivities = { reassignTreatmentsActivity, @@ -16,8 +18,10 @@ export const simulationActivities = { recurringEventActivity, generateReportActivity, providePersonnelFromVehiclesActivity, - transferVehiclesActivity, createRequestActivity, + loadVehicleActivity, + sendRemoteEventActivity, + transferVehicleActivity, }; export type ExerciseSimulationActivity = diff --git a/shared/src/simulation/activities/index.ts b/shared/src/simulation/activities/index.ts index 0a173f713..5e047c279 100644 --- a/shared/src/simulation/activities/index.ts +++ b/shared/src/simulation/activities/index.ts @@ -4,4 +4,6 @@ export * from './reassign-treatments'; export * from './unload-vehicle'; export * from './recurring-event'; export * from './provide-personnel-from-vehicles'; -export * from './transfer-vehicles'; +export * from './load-vehicle'; +export * from './send-remote-event'; +export * from './transfer-vehicle'; diff --git a/shared/src/simulation/activities/load-vehicle.ts b/shared/src/simulation/activities/load-vehicle.ts new file mode 100644 index 000000000..c9b8c3f6d --- /dev/null +++ b/shared/src/simulation/activities/load-vehicle.ts @@ -0,0 +1,250 @@ +import { + IsBoolean, + IsInt, + IsOptional, + IsString, + IsUUID, + Min, +} from 'class-validator'; +import { + SimulatedRegionPosition, + VehiclePosition, + getCreate, + isInSpecificSimulatedRegion, +} from '../../models/utils'; +import { + UUID, + UUIDSet, + cloneDeepMutable, + uuidValidationOptions, +} from '../../utils'; +import { IsLiteralUnion, IsUUIDSet, IsValue } from '../../utils/validators'; +import { + TransferDestination, + transferDestinationTypeAllowedValues, +} from '../utils/transfer-destination'; +import { getElement } from '../../store/action-reducers/utils'; +import { sendSimulationEvent } from '../events/utils'; +import { + MaterialRemovedEvent, + NewPatientEvent, + PatientRemovedEvent, + PersonnelRemovedEvent, + StartTransferEvent, +} from '../events'; +import { completelyLoadVehicle } from '../../store/action-reducers/utils/completely-load-vehicle'; +import { IntermediateOccupation } from '../../models/utils/occupations/intermediate-occupation'; +import { changePositionWithId } from '../../models/utils/position/position-helpers-mutable'; +import type { + SimulationActivity, + SimulationActivityState, +} from './simulation-activity'; + +export class LoadVehicleActivityState implements SimulationActivityState { + @IsValue('loadVehicleActivity' as const) + public readonly type = 'loadVehicleActivity'; + + @IsUUID(4, uuidValidationOptions) + public readonly id: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly vehicleId: UUID; + + @IsLiteralUnion(transferDestinationTypeAllowedValues) + readonly transferDestinationType: TransferDestination; + + @IsUUID(4, uuidValidationOptions) + readonly transferDestinationId: UUID; + + @IsUUIDSet() + public readonly patientsToBeLoaded: UUIDSet; + + @IsInt() + @Min(0) + public readonly loadDelay: number; + + @IsOptional() + @IsString() + public readonly key?: string; + + @IsBoolean() + public readonly hasBeenStarted: boolean = false; + + @IsInt() + @Min(0) + public readonly startTime: number = 0; + + /** + * @deprecated Use {@link create} instead + */ + constructor( + id: UUID, + vehicleId: UUID, + transferDestinationType: TransferDestination, + transferDestinationId: UUID, + patientsToBeLoaded: UUIDSet, + loadDelay: number, + key?: string + ) { + this.id = id; + this.vehicleId = vehicleId; + this.transferDestinationType = transferDestinationType; + this.transferDestinationId = transferDestinationId; + this.patientsToBeLoaded = patientsToBeLoaded; + this.loadDelay = loadDelay; + this.key = key; + } + + static readonly create = getCreate(this); +} + +export const loadVehicleActivity: SimulationActivity = + { + activityState: LoadVehicleActivityState, + tick( + draftState, + simulatedRegion, + activityState, + tickInterval, + terminate + ) { + const vehicle = getElement( + draftState, + 'vehicle', + activityState.vehicleId + ); + + // Start load process only once + + if (!activityState.hasBeenStarted) { + // Send remove events + + Object.keys(vehicle.personnelIds).forEach((personnelId) => { + const personnel = getElement( + draftState, + 'personnel', + personnelId + ); + if ( + isInSpecificSimulatedRegion( + personnel, + simulatedRegion.id + ) + ) { + sendSimulationEvent( + simulatedRegion, + PersonnelRemovedEvent.create(personnelId) + ); + } + }); + Object.keys(vehicle.materialIds).forEach((materialId) => { + const material = getElement( + draftState, + 'material', + materialId + ); + if ( + isInSpecificSimulatedRegion( + material, + simulatedRegion.id + ) + ) { + sendSimulationEvent( + simulatedRegion, + MaterialRemovedEvent.create(materialId) + ); + } + }); + Object.keys(activityState.patientsToBeLoaded).forEach( + (patientId) => { + const patient = getElement( + draftState, + 'patient', + patientId + ); + if ( + isInSpecificSimulatedRegion( + patient, + simulatedRegion.id + ) + ) { + sendSimulationEvent( + simulatedRegion, + PatientRemovedEvent.create(patientId) + ); + } + } + ); + + // Load material and personnel + + completelyLoadVehicle(draftState, vehicle); + + // Load patients (and unload patients not to be loaded) + + Object.keys(vehicle.patientIds).forEach((patientId) => { + changePositionWithId( + patientId, + SimulatedRegionPosition.create(simulatedRegion.id), + 'patient', + draftState + ); + // Inform the region that a new patient has left the vehicle + // (Only if it actually left the vehicle and will not be instantly re-added) + if (!activityState.patientsToBeLoaded[patientId]) { + sendSimulationEvent( + simulatedRegion, + NewPatientEvent.create(patientId) + ); + } + }); + + vehicle.patientIds = cloneDeepMutable( + activityState.patientsToBeLoaded + ); + + Object.keys(vehicle.patientIds).forEach((patientId) => { + changePositionWithId( + patientId, + VehiclePosition.create(vehicle.id), + 'patient', + draftState + ); + }); + + activityState.hasBeenStarted = true; + activityState.startTime = draftState.currentTime; + } + + if ( + activityState.startTime + activityState.loadDelay <= + draftState.currentTime + ) { + // terminate if the occupation has changed + if ( + vehicle.occupation.type !== 'loadOccupation' || + vehicle.occupation.loadingActivityId !== activityState.id + ) { + terminate(); + return; + } + sendSimulationEvent( + simulatedRegion, + StartTransferEvent.create( + activityState.vehicleId, + activityState.transferDestinationType, + activityState.transferDestinationId, + activityState.key + ) + ); + + vehicle.occupation = cloneDeepMutable( + IntermediateOccupation.create( + draftState.currentTime + tickInterval + ) + ); + + terminate(); + } + }, + }; diff --git a/shared/src/simulation/activities/send-remote-event.ts b/shared/src/simulation/activities/send-remote-event.ts new file mode 100644 index 000000000..b8816838d --- /dev/null +++ b/shared/src/simulation/activities/send-remote-event.ts @@ -0,0 +1,62 @@ +import { Type } from 'class-transformer'; +import { IsUUID, ValidateNested } from 'class-validator'; +import { getCreate } from '../../models/utils'; +import { UUID, uuidValidationOptions } from '../../utils'; +import { IsValue } from '../../utils/validators'; +import { ExerciseSimulationEvent, simulationEventTypeOptions } from '../events'; +import { sendSimulationEvent } from '../events/utils'; +import type { + SimulationActivity, + SimulationActivityState, +} from './simulation-activity'; + +export class SendRemoteEventActivityState implements SimulationActivityState { + @IsValue('sendRemoteEventActivity' as const) + public readonly type = 'sendRemoteEventActivity'; + + @IsUUID(4, uuidValidationOptions) + public readonly id: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly targetSimulatedRegionId: UUID; + + @Type(...simulationEventTypeOptions) + @ValidateNested() + public readonly event: ExerciseSimulationEvent; + + /** + * @deprecated Use {@link create} instead + */ + constructor( + id: UUID, + targetSimulatedRegionId: UUID, + event: ExerciseSimulationEvent + ) { + this.id = id; + this.targetSimulatedRegionId = targetSimulatedRegionId; + this.event = event; + } + + static readonly create = getCreate(this); +} + +export const sendRemoteEventActivity: SimulationActivity = + { + activityState: SendRemoteEventActivityState, + tick( + draftState, + _simulatedRegion, + activityState, + _tickInterval, + terminate + ) { + const targetSimulatedRegion = + draftState.simulatedRegions[ + activityState.targetSimulatedRegionId + ]; + if (targetSimulatedRegion) { + sendSimulationEvent(targetSimulatedRegion, activityState.event); + } + terminate(); + }, + }; diff --git a/shared/src/simulation/activities/transfer-vehicle.ts b/shared/src/simulation/activities/transfer-vehicle.ts new file mode 100644 index 000000000..aff9152ba --- /dev/null +++ b/shared/src/simulation/activities/transfer-vehicle.ts @@ -0,0 +1,266 @@ +import { IsOptional, IsString, IsUUID } from 'class-validator'; +import { + MissingTransferConnectionRadiogram, + RadiogramUnpublishedStatus, +} from '../../models/radiogram'; +import { publishRadiogram } from '../../models/radiogram/radiogram-helpers-mutable'; +import { + getCreate, + isInSpecificSimulatedRegion, + isInSpecificVehicle, + NoOccupation, + TransferStartPoint, +} from '../../models/utils'; +import { VehicleResource } from '../../models/utils/rescue-resource'; +import { TransferActionReducers } from '../../store/action-reducers/transfer'; +import { + getElement, + getElementByPredicate, +} from '../../store/action-reducers/utils'; +import { cloneDeepMutable, UUID, uuidValidationOptions } from '../../utils'; +import { IsLiteralUnion, IsValue } from '../../utils/validators'; +import { + TransferConnectionMissingEvent, + VehicleTransferSuccessfulEvent, +} from '../events'; +import { sendSimulationEvent } from '../events/utils'; +import { nextUUID } from '../utils/randomness'; +import { + TransferDestination, + transferDestinationTypeAllowedValues, +} from '../utils/transfer-destination'; +import { HospitalActionReducers } from '../../store/action-reducers/hospital'; +import type { ResourceDescription } from '../../models/utils/resource-description'; +import type { + SimulationActivity, + SimulationActivityState, +} from './simulation-activity'; + +export class TransferVehicleActivityState implements SimulationActivityState { + @IsValue('transferVehicleActivity' as const) + public readonly type = 'transferVehicleActivity'; + + @IsUUID(4, uuidValidationOptions) + readonly id: UUID; + + @IsUUID(4, uuidValidationOptions) + readonly vehicleId: UUID; + + @IsLiteralUnion(transferDestinationTypeAllowedValues) + readonly transferDestinationType: TransferDestination; + + @IsUUID(4, uuidValidationOptions) + readonly transferDestinationId: UUID; + + @IsOptional() + @IsString() + readonly key?: string; + + /** + * @deprecated Use {@link create} instead + */ + constructor( + id: UUID, + vehicleId: UUID, + transferDestinationType: TransferDestination, + transferDestinationId: UUID, + key?: string + ) { + this.id = id; + this.vehicleId = vehicleId; + + this.transferDestinationType = transferDestinationType; + this.transferDestinationId = transferDestinationId; + this.key = key; + } + + static readonly create = getCreate(this); +} + +export const transferVehicleActivity: SimulationActivity = + { + activityState: TransferVehicleActivityState, + tick( + draftState, + simulatedRegion, + activityState, + _tickInterval, + terminate + ) { + const vehicle = getElement( + draftState, + 'vehicle', + activityState.vehicleId + ); + + if (activityState.transferDestinationType === 'transferPoint') { + const ownTransferPoint = getElementByPredicate( + draftState, + 'transferPoint', + (transferPoint) => + isInSpecificSimulatedRegion( + transferPoint, + simulatedRegion.id + ) + ); + + if ( + ownTransferPoint.reachableTransferPoints[ + activityState.transferDestinationId + ] === undefined + ) { + sendSimulationEvent( + simulatedRegion, + TransferConnectionMissingEvent.create( + nextUUID(draftState), + activityState.transferDestinationId, + activityState.key + ) + ); + publishRadiogram( + draftState, + cloneDeepMutable( + MissingTransferConnectionRadiogram.create( + nextUUID(draftState), + simulatedRegion.id, + RadiogramUnpublishedStatus.create(), + activityState.transferDestinationId + ) + ) + ); + + terminate(); + return; + } + + // If the vehicle is not completely loaded terminate + if ( + Object.keys(vehicle.materialIds).some((materialId) => { + const material = getElement( + draftState, + 'material', + materialId + ); + return !isInSpecificVehicle(material, vehicle.id); + }) || + Object.keys(vehicle.personnelIds).some((personnelId) => { + const personnel = getElement( + draftState, + 'personnel', + personnelId + ); + return !isInSpecificVehicle(personnel, vehicle.id); + }) + ) { + terminate(); + return; + } + + // Do transfer and send event + + vehicle.occupation = cloneDeepMutable(NoOccupation.create()); + + TransferActionReducers.addToTransfer.reducer(draftState, { + type: '[Transfer] Add to transfer', + elementType: 'vehicle', + elementId: activityState.vehicleId, + startPoint: TransferStartPoint.create(ownTransferPoint.id), + targetTransferPointId: activityState.transferDestinationId, + }); + + const vehicleResourceDescription: ResourceDescription = {}; + vehicleResourceDescription[vehicle.vehicleType] = 1; + + sendSimulationEvent( + simulatedRegion, + VehicleTransferSuccessfulEvent.create( + activityState.transferDestinationId, + activityState.key ?? '', + VehicleResource.create(vehicleResourceDescription) + ) + ); + + terminate(); + } + if (activityState.transferDestinationType === 'hospital') { + const ownTransferPoint = getElementByPredicate( + draftState, + 'transferPoint', + (transferPoint) => + isInSpecificSimulatedRegion( + transferPoint, + simulatedRegion.id + ) + ); + + if ( + ownTransferPoint.reachableHospitals[ + activityState.transferDestinationId + ] === undefined + ) { + sendSimulationEvent( + simulatedRegion, + TransferConnectionMissingEvent.create( + nextUUID(draftState), + activityState.transferDestinationId, + activityState.key + ) + ); + // TODO: Publish Radiogram (The current Radiogram does not support hospitals) + + terminate(); + return; + } + + // If the vehicle is not completely loaded terminate + if ( + Object.keys(vehicle.materialIds).some((materialId) => { + const material = getElement( + draftState, + 'material', + materialId + ); + return !isInSpecificVehicle(material, vehicle.id); + }) || + Object.keys(vehicle.personnelIds).some((personnelId) => { + const personnel = getElement( + draftState, + 'personnel', + personnelId + ); + return !isInSpecificVehicle(personnel, vehicle.id); + }) + ) { + terminate(); + return; + } + + // Do transfer and send event + + vehicle.occupation = cloneDeepMutable(NoOccupation.create()); + + HospitalActionReducers.transportPatientToHospital.reducer( + draftState, + { + type: '[Hospital] Transport patient to hospital', + vehicleId: vehicle.id, + hospitalId: activityState.transferDestinationId, + } + ); + + const vehicleResourceDescription: ResourceDescription = {}; + vehicleResourceDescription[vehicle.vehicleType] = 1; + + sendSimulationEvent( + simulatedRegion, + VehicleTransferSuccessfulEvent.create( + activityState.transferDestinationId, + activityState.key ?? '', + VehicleResource.create(vehicleResourceDescription) + ) + ); + + terminate(); + } + }, + }; diff --git a/shared/src/simulation/activities/transfer-vehicles.ts b/shared/src/simulation/activities/transfer-vehicles.ts deleted file mode 100644 index f3f082035..000000000 --- a/shared/src/simulation/activities/transfer-vehicles.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { Type } from 'class-transformer'; -import { IsString, IsUUID, ValidateNested } from 'class-validator'; -import { groupBy } from 'lodash-es'; -import { - MissingTransferConnectionRadiogram, - RadiogramUnpublishedStatus, -} from '../../models/radiogram'; -import { publishRadiogram } from '../../models/radiogram/radiogram-helpers-mutable'; -import { - currentSimulatedRegionIdOf, - getCreate, - isInSimulatedRegion, - isInSpecificSimulatedRegion, - TransferStartPoint, -} from '../../models/utils'; -import { amountOfResourcesInVehicle } from '../../models/utils/amount-of-resources-in-vehicle'; -import { VehicleResource } from '../../models/utils/rescue-resource'; -import { TransferActionReducers } from '../../store/action-reducers/transfer'; -import { - getElement, - getElementByPredicate, -} from '../../store/action-reducers/utils'; -import { completelyLoadVehicle } from '../../store/action-reducers/utils/completely-load-vehicle'; -import { cloneDeepMutable, UUID, uuidValidationOptions } from '../../utils'; -import { IsValue } from '../../utils/validators'; -import { - ResourceRequiredEvent, - TransferConnectionMissingEvent, - VehiclesSentEvent, - VehicleTransferSuccessfulEvent, -} from '../events'; -import { sendSimulationEvent } from '../events/utils'; -import { nextUUID } from '../utils/randomness'; -import type { - SimulationActivity, - SimulationActivityState, -} from './simulation-activity'; - -export class TransferVehiclesActivityState implements SimulationActivityState { - @IsValue('transferVehiclesActivity' as const) - public readonly type = 'transferVehiclesActivity'; - - @IsUUID(4, uuidValidationOptions) - public readonly id: UUID; - - @IsUUID(4, uuidValidationOptions) - public readonly targetTransferPointId: UUID; - - @IsString() - public readonly key: string; - - @ValidateNested() - @Type(() => VehicleResource) - public readonly vehiclesToBeTransferred: VehicleResource; - - /** - * @deprecated Use {@link create} instead - */ - constructor( - id: UUID, - targetTransferPointId: UUID, - key: string, - vehiclesToBeTransferred: VehicleResource - ) { - this.id = id; - this.targetTransferPointId = targetTransferPointId; - this.key = key; - this.vehiclesToBeTransferred = vehiclesToBeTransferred; - } - - static readonly create = getCreate(this); -} - -export const transferVehiclesActivity: SimulationActivity = - { - activityState: TransferVehiclesActivityState, - tick( - draftState, - simulatedRegion, - activityState, - _tickInterval, - terminate - ) { - const ownTransferPoint = getElementByPredicate( - draftState, - 'transferPoint', - (transferPoint) => - isInSpecificSimulatedRegion( - transferPoint, - simulatedRegion.id - ) - ); - - if ( - ownTransferPoint.reachableTransferPoints[ - activityState.targetTransferPointId - ] === undefined - ) { - sendSimulationEvent( - simulatedRegion, - TransferConnectionMissingEvent.create( - nextUUID(draftState), - activityState.targetTransferPointId - ) - ); - publishRadiogram( - draftState, - cloneDeepMutable( - MissingTransferConnectionRadiogram.create( - nextUUID(draftState), - simulatedRegion.id, - RadiogramUnpublishedStatus.create(), - activityState.targetTransferPointId - ) - ) - ); - terminate(); - return; - } - - const vehicles = Object.values(draftState.vehicles) - .filter((vehicle) => - isInSpecificSimulatedRegion(vehicle, simulatedRegion.id) - ) - .filter( - (vehicle) => Object.keys(vehicle.patientIds).length === 0 - ); - const groupedVehicles = groupBy( - vehicles, - (vehicle) => vehicle.vehicleType - ); - - const missingVehicles: { [key: string]: number } = {}; - Object.entries( - activityState.vehiclesToBeTransferred.vehicleCounts - ).forEach(([vehicleType, vehicleCount]) => { - if ( - (groupedVehicles[vehicleType]?.length ?? 0) < vehicleCount - ) { - missingVehicles[vehicleType] = - vehicleCount - - (groupedVehicles[vehicleType]?.length ?? 0); - } - }); - sendSimulationEvent( - simulatedRegion, - ResourceRequiredEvent.create( - nextUUID(draftState), - simulatedRegion.id, - VehicleResource.create(missingVehicles), - activityState.key - ) - ); - - const sentVehicles: { [key: string]: number } = {}; - Object.entries( - activityState.vehiclesToBeTransferred.vehicleCounts - ).forEach(([vehicleType, vehicleCount]) => { - sentVehicles[vehicleType] = Math.min( - groupedVehicles[vehicleType]?.length ?? 0, - vehicleCount - ); - // sort the vehicles by number of loaded resources descending - groupedVehicles[vehicleType]?.sort( - (a, b) => - amountOfResourcesInVehicle(draftState, b.id) - - amountOfResourcesInVehicle(draftState, a.id) - ); - for (let i = 0; i < sentVehicles[vehicleType]!; i++) { - completelyLoadVehicle( - draftState, - groupedVehicles[vehicleType]![i]! - ); - - TransferActionReducers.addToTransfer.reducer(draftState, { - type: '[Transfer] Add to transfer', - elementType: 'vehicle', - elementId: groupedVehicles[vehicleType]![i]!.id, - startPoint: TransferStartPoint.create( - ownTransferPoint.id - ), - targetTransferPointId: - activityState.targetTransferPointId, - }); - } - }); - - const targetTransferPoint = getElement( - draftState, - 'transferPoint', - activityState.targetTransferPointId - ); - if (Object.values(sentVehicles).some((value) => value !== 0)) { - sendSimulationEvent( - simulatedRegion, - VehicleTransferSuccessfulEvent.create( - nextUUID(draftState), - targetTransferPoint.id, - activityState.key, - VehicleResource.create(sentVehicles) - ) - ); - - if (isInSimulatedRegion(targetTransferPoint)) { - sendSimulationEvent( - getElement( - draftState, - 'simulatedRegion', - currentSimulatedRegionIdOf(targetTransferPoint) - ), - VehiclesSentEvent.create( - nextUUID(draftState), - VehicleResource.create(sentVehicles) - ) - ); - } - } - - terminate(); - }, - }; diff --git a/shared/src/simulation/behaviors/answer-requests.ts b/shared/src/simulation/behaviors/answer-requests.ts index d863c3707..972144a2b 100644 --- a/shared/src/simulation/behaviors/answer-requests.ts +++ b/shared/src/simulation/behaviors/answer-requests.ts @@ -1,11 +1,23 @@ -import { IsUUID } from 'class-validator'; -import { getCreate, isInSpecificSimulatedRegion } from '../../models/utils'; +import { IsInt, IsUUID, Min, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { + VehicleResource, + getCreate, + isInSpecificSimulatedRegion, +} from '../../models/utils'; import { getElementByPredicate } from '../../store/action-reducers/utils'; -import { UUID, uuid, uuidValidationOptions } from '../../utils'; +import { + UUID, + cloneDeepMutable, + uuid, + uuidValidationOptions, +} from '../../utils'; import { IsValue } from '../../utils/validators'; -import { TransferVehiclesActivityState } from '../activities'; import { addActivity } from '../activities/utils'; import { nextUUID } from '../utils/randomness'; +import { DelayEventActivityState } from '../activities'; +import { ResourceRequiredEvent, TransferVehiclesRequestEvent } from '../events'; +import type { ResourceDescription } from '../../models/utils/resource-description'; import type { SimulationBehavior, SimulationBehaviorState, @@ -18,13 +30,22 @@ export class AnswerRequestsBehaviorState implements SimulationBehaviorState { @IsUUID(4, uuidValidationOptions) public readonly id: UUID = uuid(); + @Type(() => TransferVehiclesRequestEvent) + @ValidateNested() + public readonly receivedEvents: readonly TransferVehiclesRequestEvent[] = + []; + + @IsInt() + @Min(0) + public readonly requestsHandled: number = 0; + static readonly create = getCreate(this); } export const answerRequestsBehavior: SimulationBehavior = { behaviorState: AnswerRequestsBehaviorState, - handleEvent: (draftState, simulatedRegion, _behaviorState, event) => { + handleEvent: (draftState, simulatedRegion, behaviorState, event) => { switch (event.type) { case 'resourceRequiredEvent': { if ( @@ -41,19 +62,82 @@ export const answerRequestsBehavior: SimulationBehavior + receivedEvent.key === event.key + ); + const requestEvent = + behaviorState.receivedEvents[requestEventIndex]; + let createEvent = false; + const vehiclesNotAvailable: ResourceDescription = {}; + if (requestEvent) { + Object.entries( + requestEvent.requestedVehicles + ).forEach( + ([vehicleType, requestedVehicleAmount]) => { + if ( + (event.availableVehicles[vehicleType] ?? + 0) < requestedVehicleAmount + ) { + vehiclesNotAvailable[vehicleType] = + requestedVehicleAmount - + (event.availableVehicles[ + vehicleType + ] ?? 0); + createEvent = true; + } + } + ); + behaviorState.receivedEvents.splice( + requestEventIndex, + 1 + ); + } + if (createEvent) { + addActivity( + simulatedRegion, + DelayEventActivityState.create( + nextUUID(draftState), + ResourceRequiredEvent.create( + '', + simulatedRegion.id, + VehicleResource.create( + vehiclesNotAvailable + ), + requestEvent!.key ?? '' + ), + draftState.currentTime + ) + ); + } + } + break; default: // Ignore event } diff --git a/shared/src/simulation/behaviors/automatically-distribute-vehicles.ts b/shared/src/simulation/behaviors/automatically-distribute-vehicles.ts index f7f6311de..c83eac530 100644 --- a/shared/src/simulation/behaviors/automatically-distribute-vehicles.ts +++ b/shared/src/simulation/behaviors/automatically-distribute-vehicles.ts @@ -1,16 +1,18 @@ import { IsInt, IsOptional, IsUUID, Min } from 'class-validator'; -import { getCreate, VehicleResource } from '../../models/utils'; +import { cloneDeep } from 'lodash-es'; +import { getCreate } from '../../models/utils'; import type { Mutable } from '../../utils'; import { cloneDeepMutable, UUID, uuid, UUIDSet } from '../../utils'; import { IsUUIDSet, IsUUIDSetMap, IsValue } from '../../utils/validators'; import { IsResourceDescription } from '../../utils/validators/is-resource-description'; import { + DelayEventActivityState, RecurringEventActivityState, - TransferVehiclesActivityState, } from '../activities'; import { addActivity } from '../activities/utils'; import { TryToDistributeEvent } from '../events/try-to-distribute'; import { nextUUID } from '../utils/randomness'; +import { TransferVehiclesRequestEvent } from '../events'; import type { SimulationBehavior, SimulationBehaviorState, @@ -44,7 +46,7 @@ export class AutomaticallyDistributeVehiclesBehaviorState @IsInt() @Min(1) /* - * This *MUST* be greater that the tick Duration to Ensure that we can wait for a response in the next tick + * This *MUST* be greater than about 10 tick durations to Ensure that we can wait for a response */ public readonly distributionDelay: number = 60_000; // 1 minute @@ -180,15 +182,15 @@ export const automaticallyDistributeVehiclesBehavior: SimulationBehavior { - if (vehicleAmount === 0) { - return; - } - if ( - !behaviorState.distributedLastRound[vehicleType] - ) { + Object.entries(event.availableVehicles).forEach( + ([vehicleType, vehicleAmount]) => { + if (vehicleAmount === 0) { + return; + } + if ( + !behaviorState.distributedLastRound[ + vehicleType + ] + ) { + behaviorState.distributedLastRound[ + vehicleType + ] = 0; + } behaviorState.distributedLastRound[ vehicleType - ] = 0; - } - behaviorState.distributedLastRound[vehicleType]++; + ]++; - if (behaviorState.remainingInNeed[vehicleType]) { - delete behaviorState.remainingInNeed[ - vehicleType - ]![event.targetId]; + if ( + behaviorState.remainingInNeed[vehicleType] + ) { + delete behaviorState.remainingInNeed[ + vehicleType + ]![event.transferDestinationId]; + } } - }); + ); } break; diff --git a/shared/src/simulation/behaviors/exercise-simulation-behavior.ts b/shared/src/simulation/behaviors/exercise-simulation-behavior.ts index 3398a5996..f50b0ab43 100644 --- a/shared/src/simulation/behaviors/exercise-simulation-behavior.ts +++ b/shared/src/simulation/behaviors/exercise-simulation-behavior.ts @@ -8,6 +8,7 @@ import { automaticallyDistributeVehiclesBehavior } from './automatically-distrib import { providePersonnelBehavior } from './provide-personnel'; import { answerRequestsBehavior } from './answer-requests'; import { requestBehavior } from './request'; +import { transferBehavior } from './transfer'; export const simulationBehaviors = { automaticallyDistributeVehiclesBehavior, @@ -18,6 +19,7 @@ export const simulationBehaviors = { providePersonnelBehavior, answerRequestsBehavior, requestBehavior, + transferBehavior, }; export type ExerciseSimulationBehavior = diff --git a/shared/src/simulation/behaviors/index.ts b/shared/src/simulation/behaviors/index.ts index b23d5b2d9..bda7a4981 100644 --- a/shared/src/simulation/behaviors/index.ts +++ b/shared/src/simulation/behaviors/index.ts @@ -8,3 +8,4 @@ export * from './report'; export * from './provide-personnel'; export * from './utils'; export * from './request'; +export * from './transfer'; diff --git a/shared/src/simulation/behaviors/transfer.ts b/shared/src/simulation/behaviors/transfer.ts new file mode 100644 index 000000000..3a057b05d --- /dev/null +++ b/shared/src/simulation/behaviors/transfer.ts @@ -0,0 +1,421 @@ +import { + IsInt, + IsOptional, + IsUUID, + Min, + ValidateNested, +} from 'class-validator'; +import { groupBy } from 'lodash-es'; +import { Type } from 'class-transformer'; +import { + VehicleResource, + currentSimulatedRegionOf, + getCreate, + isInSimulatedRegion, + isInSpecificSimulatedRegion, +} from '../../models/utils'; +import { + UUID, + cloneDeepMutable, + uuid, + uuidValidationOptions, +} from '../../utils'; +import { IsValue } from '../../utils/validators'; +import { addActivity, terminateActivity } from '../activities/utils'; +import { nextUUID } from '../utils/randomness'; +import { getElement } from '../../store/action-reducers/utils'; +import { + DelayEventActivityState, + LoadVehicleActivityState, + RecurringEventActivityState, + SendRemoteEventActivityState, +} from '../activities'; +import { isUnoccupied } from '../../models/utils/occupations/occupation-helpers-mutable'; +import { amountOfResourcesInVehicle } from '../../models/utils/amount-of-resources-in-vehicle'; +import type { ResourceDescription } from '../../models/utils/resource-description'; +import { + DoTransferEvent, + RequestReceivedEvent, + StartTransferEvent, + VehiclesSentEvent, +} from '../events'; +import { LoadOccupation } from '../../models/utils/occupations/load-occupation'; +import { WaitForTransferOccupation } from '../../models/utils/occupations/wait-for-transfer-occupation'; +import { TransferVehicleActivityState } from '../activities/transfer-vehicle'; +import type { + SimulationBehavior, + SimulationBehaviorState, +} from './simulation-behavior'; + +export class TransferBehaviorState implements SimulationBehaviorState { + @IsValue('transferBehavior' as const) + readonly type = 'transferBehavior'; + + @IsUUID(4, uuidValidationOptions) + public readonly id: UUID = uuid(); + + @IsInt() + @Min(0) + public readonly loadTimePerPatient: number; + + @IsInt() + @Min(0) + public readonly personnelLoadTime: number; + + @IsInt() + @Min(0) + public readonly delayBetweenSends: number; + + @Type(() => StartTransferEvent) + @ValidateNested() + public readonly startTransferEventQueue: readonly StartTransferEvent[] = []; + + @IsUUID(4, uuidValidationOptions) + @IsOptional() + public readonly recurringActivityId: UUID | undefined; + + /** + * @deprecated Use {@link create} instead + */ + constructor( + loadTimePerPatient: number = 60_000, // 1 minute + personnelLoadTime: number = 120_000, // 2 minutes + delayBetweenSends: number = 60_000 // 1 minute + ) { + this.loadTimePerPatient = loadTimePerPatient; + this.personnelLoadTime = personnelLoadTime; + this.delayBetweenSends = delayBetweenSends; + } + + static readonly create = getCreate(this); +} + +export const transferBehavior: SimulationBehavior = { + behaviorState: TransferBehaviorState, + handleEvent(draftState, simulatedRegion, behaviorState, event) { + switch (event.type) { + case 'transferPatientsRequestEvent': + { + // find a vehicle to use + + const vehicles = Object.values(draftState.vehicles) + .filter((vehicle) => + isInSpecificSimulatedRegion( + vehicle, + simulatedRegion.id + ) + ) + .filter( + (vehicle) => + Object.keys(vehicle.patientIds).length === 0 + ); + const vehiclesOfCorrectType = vehicles.filter( + (vehicle) => vehicle.type === event.vehicleType + ); + + // sort the unoccupied vehicles by number of loaded resources descending and use the one with the most + + const vehicleToLoad = vehiclesOfCorrectType + .filter((vehicle) => + isUnoccupied(vehicle, draftState.currentTime) + ) + .sort( + (vehicle1, vehicle2) => + amountOfResourcesInVehicle( + draftState, + vehicle2.id + ) - + amountOfResourcesInVehicle( + draftState, + vehicle1.id + ) + )[0]; + + if (vehicleToLoad) { + const activityId = nextUUID(draftState); + addActivity( + simulatedRegion, + LoadVehicleActivityState.create( + activityId, + vehicleToLoad.id, + event.transferDestinationType, + event.transferDestinationId, + event.patientIds, + Math.max( + Object.keys(event.patientIds).length * + behaviorState.loadTimePerPatient, + behaviorState.personnelLoadTime + ) + ) + ); + vehicleToLoad.occupation = cloneDeepMutable( + LoadOccupation.create(activityId) + ); + } + } + break; + case 'transferPatientsInSpecificVehicleRequestEvent': + { + const vehicle = getElement( + draftState, + 'vehicle', + event.vehicleId + ); + // Don't do anything if vehicle is occupied + if (!isUnoccupied(vehicle, draftState.currentTime)) { + return; + } + + const activityId = nextUUID(draftState); + addActivity( + simulatedRegion, + LoadVehicleActivityState.create( + activityId, + vehicle.id, + event.transferDestinationType, + event.transferDestinationId, + event.patientIds, + Math.max( + Object.keys(event.patientIds).length * + behaviorState.loadTimePerPatient, + behaviorState.personnelLoadTime + ) + ) + ); + vehicle.occupation = cloneDeepMutable( + LoadOccupation.create(activityId) + ); + } + break; + case 'transferSpecificVehicleRequestEvent': + { + const vehicle = getElement( + draftState, + 'vehicle', + event.vehicleId + ); + // Don't do anything if vehicle is occupied + if (!isUnoccupied(vehicle, draftState.currentTime)) { + return; + } + + const activityId = nextUUID(draftState); + addActivity( + simulatedRegion, + LoadVehicleActivityState.create( + activityId, + vehicle.id, + event.transferDestinationType, + event.transferDestinationId, + {}, + behaviorState.personnelLoadTime + ) + ); + vehicle.occupation = cloneDeepMutable( + LoadOccupation.create(activityId) + ); + } + break; + case 'transferVehiclesRequestEvent': + { + // group vehicles + + const vehicles = Object.values(draftState.vehicles) + .filter((vehicle) => + isInSpecificSimulatedRegion( + vehicle, + simulatedRegion.id + ) + ) + .filter( + (vehicle) => + Object.keys(vehicle.patientIds).length === 0 + ); + const groupedVehicles = groupBy( + vehicles, + (vehicle) => vehicle.vehicleType + ); + + const sentVehicles: ResourceDescription = {}; + + Object.entries(event.requestedVehicles).forEach( + ([vehicleType, vehicleAmount]) => { + // sort the unoccupied vehicles by number of loaded resources descending and use the one with the most + + const loadableVehicles = groupedVehicles[ + vehicleType + ] + ?.filter((vehicle) => + isUnoccupied( + vehicle, + draftState.currentTime + ) + ) + .sort( + (vehicle1, vehicle2) => + amountOfResourcesInVehicle( + draftState, + vehicle2.id + ) - + amountOfResourcesInVehicle( + draftState, + vehicle1.id + ) + ); + + sentVehicles[vehicleType] = 0; + + for ( + let index = 0; + index < + Math.min( + loadableVehicles?.length ?? 0, + vehicleAmount + ); + index++ + ) { + const activityId = nextUUID(draftState); + addActivity( + simulatedRegion, + LoadVehicleActivityState.create( + activityId, + loadableVehicles![index]!.id, + event.transferDestinationType, + event.transferDestinationId, + {}, + behaviorState.personnelLoadTime + ) + ); + loadableVehicles![index]!.occupation = + cloneDeepMutable( + LoadOccupation.create(activityId) + ); + sentVehicles[vehicleType]++; + } + } + ); + + // Send RequestReceivedEvent into own region + + addActivity( + simulatedRegion, + DelayEventActivityState.create( + nextUUID(draftState), + RequestReceivedEvent.create( + sentVehicles, + event.transferDestinationType, + event.transferDestinationId, + event.key + ), + draftState.currentTime + ) + ); + + // Send event to destination if it is a simulated region + + if ( + event.transferDestinationType === 'transferPoint' && + isInSimulatedRegion( + getElement( + draftState, + 'transferPoint', + event.transferDestinationId + ) + ) + ) { + addActivity( + simulatedRegion, + SendRemoteEventActivityState.create( + nextUUID(draftState), + currentSimulatedRegionOf( + draftState, + getElement( + draftState, + 'transferPoint', + event.transferDestinationId + ) + ).id, + VehiclesSentEvent.create( + '', + VehicleResource.create(sentVehicles) + ) + ) + ); + } + } + break; + case 'startTransferEvent': + { + const vehicle = getElement( + draftState, + 'vehicle', + event.vehicleId + ); + vehicle.occupation = cloneDeepMutable( + WaitForTransferOccupation.create() + ); + + behaviorState.startTransferEventQueue.push(event); + } + break; + case 'tickEvent': + { + if ( + behaviorState.recurringActivityId === undefined && + behaviorState.startTransferEventQueue.length !== 0 + ) { + behaviorState.recurringActivityId = + nextUUID(draftState); + addActivity( + simulatedRegion, + RecurringEventActivityState.create( + behaviorState.recurringActivityId, + DoTransferEvent.create(), + draftState.currentTime, + behaviorState.delayBetweenSends + ) + ); + } + } + break; + case 'doTransferEvent': + { + if ( + behaviorState.startTransferEventQueue.length === 0 && + behaviorState.recurringActivityId + ) { + terminateActivity( + draftState, + simulatedRegion, + behaviorState.recurringActivityId + ); + behaviorState.recurringActivityId = undefined; + return; + } + + const transferEvent = + behaviorState.startTransferEventQueue.shift(); + const vehicle = + draftState.vehicles[transferEvent!.vehicleId]; + if ( + vehicle?.occupation.type !== 'waitForTransferOccupation' + ) { + return; + } + addActivity( + simulatedRegion, + TransferVehicleActivityState.create( + nextUUID(draftState), + vehicle.id, + transferEvent!.transferDestinationType, + transferEvent!.transferDestinationId, + transferEvent!.key + ) + ); + } + break; + default: + // ignore event + } + }, +}; diff --git a/shared/src/simulation/events/do-transfer.ts b/shared/src/simulation/events/do-transfer.ts new file mode 100644 index 000000000..90c697811 --- /dev/null +++ b/shared/src/simulation/events/do-transfer.ts @@ -0,0 +1,10 @@ +import { getCreate } from '../../models/utils'; +import { IsValue } from '../../utils/validators'; +import type { SimulationEvent } from './simulation-event'; + +export class DoTransferEvent implements SimulationEvent { + @IsValue('doTransferEvent') + readonly type = 'doTransferEvent'; + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/events/exercise-simulation-event.ts b/shared/src/simulation/events/exercise-simulation-event.ts index 8962be043..9ff18a8a5 100644 --- a/shared/src/simulation/events/exercise-simulation-event.ts +++ b/shared/src/simulation/events/exercise-simulation-event.ts @@ -21,6 +21,13 @@ import { MaterialRemovedEvent } from './material-removed'; import { PersonnelRemovedEvent } from './personnel-removed'; import { PatientRemovedEvent } from './patient-removed'; import { VehicleRemovedEvent } from './vehicle-removed'; +import { TransferPatientsInSpecificVehicleRequestEvent } from './transfer-patients-in-specific-vehicle-request'; +import { TransferSpecificVehicleRequestEvent } from './transfer-specific-vehicle-request'; +import { TransferVehiclesRequestEvent } from './transfer-vehicles-request'; +import { TransferPatientsRequestEvent } from './transfer-patients-request'; +import { RequestReceivedEvent } from './request-received'; +import { StartTransferEvent } from './start-transfer'; +import { DoTransferEvent } from './do-transfer'; export const simulationEvents = { MaterialAvailableEvent, @@ -43,6 +50,13 @@ export const simulationEvents = { PersonnelRemovedEvent, PatientRemovedEvent, VehicleRemovedEvent, + TransferPatientsInSpecificVehicleRequestEvent, + TransferSpecificVehicleRequestEvent, + TransferVehiclesRequestEvent, + TransferPatientsRequestEvent, + RequestReceivedEvent, + StartTransferEvent, + DoTransferEvent, }; export type ExerciseSimulationEvent = InstanceType< @@ -75,6 +89,14 @@ export const simulationEventDictionary: ExerciseSimulationEventDictionary = { personnelRemovedEvent: PersonnelRemovedEvent, patientRemovedEvent: PatientRemovedEvent, vehicleRemovedEvent: VehicleRemovedEvent, + transferPatientsInSpecificVehicleRequestEvent: + TransferPatientsInSpecificVehicleRequestEvent, + transferSpecificVehicleRequestEvent: TransferSpecificVehicleRequestEvent, + transferVehiclesRequestEvent: TransferVehiclesRequestEvent, + transferPatientsRequestEvent: TransferPatientsRequestEvent, + requestReceivedEvent: RequestReceivedEvent, + startTransferEvent: StartTransferEvent, + doTransferEvent: DoTransferEvent, }; export const simulationEventTypeOptions: Parameters = [ diff --git a/shared/src/simulation/events/index.ts b/shared/src/simulation/events/index.ts index 53848334f..a70406f25 100644 --- a/shared/src/simulation/events/index.ts +++ b/shared/src/simulation/events/index.ts @@ -16,3 +16,10 @@ export * from './material-removed'; export * from './personnel-removed'; export * from './patient-removed'; export * from './vehicle-removed'; +export * from './request-received'; +export * from './start-transfer'; +export * from './transfer-patients-in-specific-vehicle-request'; +export * from './transfer-patients-request'; +export * from './transfer-specific-vehicle-request'; +export * from './transfer-vehicles-request'; +export * from './do-transfer'; diff --git a/shared/src/simulation/events/request-received.ts b/shared/src/simulation/events/request-received.ts new file mode 100644 index 000000000..6dec7ce36 --- /dev/null +++ b/shared/src/simulation/events/request-received.ts @@ -0,0 +1,46 @@ +import { IsOptional, IsString, IsUUID } from 'class-validator'; +import { getCreate } from '../../models/utils'; +import { IsLiteralUnion, IsValue } from '../../utils/validators'; +import { ResourceDescription } from '../../models/utils/resource-description'; +import { IsResourceDescription } from '../../utils/validators/is-resource-description'; +import { + TransferDestination, + transferDestinationTypeAllowedValues, +} from '../utils/transfer-destination'; +import { UUID, uuidValidationOptions } from '../../utils'; +import type { SimulationEvent } from './simulation-event'; + +export class RequestReceivedEvent implements SimulationEvent { + @IsValue('requestReceivedEvent') + readonly type = 'requestReceivedEvent'; + + @IsResourceDescription() + readonly availableVehicles: ResourceDescription; + + @IsLiteralUnion(transferDestinationTypeAllowedValues) + readonly transferDestinationType: TransferDestination; + + @IsUUID(4, uuidValidationOptions) + readonly transferDestinationId: UUID; + + @IsString() + @IsOptional() + readonly key?: string; + + /** + * @deprecated Use {@link create} instead + */ + constructor( + availableVehicles: ResourceDescription, + transferDestinationType: TransferDestination, + transferDestinationId: UUID, + key?: string + ) { + this.availableVehicles = availableVehicles; + this.transferDestinationType = transferDestinationType; + this.transferDestinationId = transferDestinationId; + this.key = key; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/events/start-transfer.ts b/shared/src/simulation/events/start-transfer.ts new file mode 100644 index 000000000..66ca50f05 --- /dev/null +++ b/shared/src/simulation/events/start-transfer.ts @@ -0,0 +1,44 @@ +import { IsOptional, IsString, IsUUID } from 'class-validator'; +import { getCreate } from '../../models/utils'; +import { UUID, uuidValidationOptions } from '../../utils'; +import { IsLiteralUnion, IsValue } from '../../utils/validators'; +import { + TransferDestination, + transferDestinationTypeAllowedValues, +} from '../utils/transfer-destination'; +import type { SimulationEvent } from './simulation-event'; + +export class StartTransferEvent implements SimulationEvent { + @IsValue('startTransferEvent') + readonly type = 'startTransferEvent'; + + @IsUUID(4, uuidValidationOptions) + readonly vehicleId!: UUID; + + @IsLiteralUnion(transferDestinationTypeAllowedValues) + readonly transferDestinationType: TransferDestination; + + @IsUUID(4, uuidValidationOptions) + readonly transferDestinationId: UUID; + + @IsOptional() + @IsString() + readonly key?: string; + + /** + * @deprecated Use {@link create} instead + */ + constructor( + vehicleId: UUID, + transferDestinationType: TransferDestination, + transferDestinationId: UUID, + key?: string + ) { + this.vehicleId = vehicleId; + this.transferDestinationType = transferDestinationType; + this.transferDestinationId = transferDestinationId; + this.key = key; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/events/transfer-connection-missing.ts b/shared/src/simulation/events/transfer-connection-missing.ts index d923bb14f..b71d33eb4 100644 --- a/shared/src/simulation/events/transfer-connection-missing.ts +++ b/shared/src/simulation/events/transfer-connection-missing.ts @@ -1,4 +1,4 @@ -import { IsUUID } from 'class-validator'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; import { getCreate } from '../../models/utils'; import { UUID, uuidValidationOptions } from '../../utils'; import { IsValue } from '../../utils/validators'; @@ -14,12 +14,17 @@ export class TransferConnectionMissingEvent implements SimulationEvent { @IsUUID(4, uuidValidationOptions) public readonly transferPointId: UUID; + @IsOptional() + @IsString() + public readonly key?: string; + /** * @deprecated Use {@link create} instead */ - constructor(id: UUID, transferPointId: UUID) { + constructor(id: UUID, transferPointId: UUID, key?: string) { this.id = id; this.transferPointId = transferPointId; + this.key = key; } static readonly create = getCreate(this); diff --git a/shared/src/simulation/events/transfer-patients-in-specific-vehicle-request.ts b/shared/src/simulation/events/transfer-patients-in-specific-vehicle-request.ts new file mode 100644 index 000000000..6c935050a --- /dev/null +++ b/shared/src/simulation/events/transfer-patients-in-specific-vehicle-request.ts @@ -0,0 +1,45 @@ +import { IsUUID } from 'class-validator'; +import { getCreate } from '../../models/utils'; +import { UUID, UUIDSet, uuidValidationOptions } from '../../utils'; +import { IsLiteralUnion, IsUUIDSet, IsValue } from '../../utils/validators'; +import { + TransferDestination, + transferDestinationTypeAllowedValues, +} from '../utils/transfer-destination'; +import type { SimulationEvent } from './simulation-event'; + +export class TransferPatientsInSpecificVehicleRequestEvent + implements SimulationEvent +{ + @IsValue('transferPatientsInSpecificVehicleRequestEvent') + readonly type = 'transferPatientsInSpecificVehicleRequestEvent'; + + @IsUUIDSet() + readonly patientIds: UUIDSet; + + @IsUUID(4, uuidValidationOptions) + readonly vehicleId: UUID; + + @IsLiteralUnion(transferDestinationTypeAllowedValues) + readonly transferDestinationType: TransferDestination; + + @IsUUID(4, uuidValidationOptions) + readonly transferDestinationId: UUID; + + /** + * @deprecated Use {@link create} instead + */ + constructor( + patientIds: UUIDSet, + vehicleId: UUID, + transferDestinationType: TransferDestination, + transferDestinationId: UUID + ) { + this.patientIds = patientIds; + this.vehicleId = vehicleId; + this.transferDestinationType = transferDestinationType; + this.transferDestinationId = transferDestinationId; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/events/transfer-patients-request.ts b/shared/src/simulation/events/transfer-patients-request.ts new file mode 100644 index 000000000..2bc436bf3 --- /dev/null +++ b/shared/src/simulation/events/transfer-patients-request.ts @@ -0,0 +1,43 @@ +import { IsString, IsUUID } from 'class-validator'; +import { getCreate } from '../../models/utils'; +import { IsLiteralUnion, IsUUIDSet, IsValue } from '../../utils/validators'; +import { UUID, UUIDSet, uuidValidationOptions } from '../../utils'; +import { + TransferDestination, + transferDestinationTypeAllowedValues, +} from '../utils/transfer-destination'; +import type { SimulationEvent } from './simulation-event'; + +export class TransferPatientsRequestEvent implements SimulationEvent { + @IsValue('transferPatientsRequestEvent') + readonly type = 'transferPatientsRequestEvent'; + + @IsString() + readonly vehicleType: string; + + @IsUUIDSet() + readonly patientIds: UUIDSet; + + @IsLiteralUnion(transferDestinationTypeAllowedValues) + readonly transferDestinationType: TransferDestination; + + @IsUUID(4, uuidValidationOptions) + readonly transferDestinationId: UUID; + + /** + * @deprecated Use {@link create} instead + */ + constructor( + vehicleType: string, + patientIds: UUIDSet, + transferDestinationType: TransferDestination, + transferDestinationId: UUID + ) { + this.vehicleType = vehicleType; + this.patientIds = patientIds; + this.transferDestinationType = transferDestinationType; + this.transferDestinationId = transferDestinationId; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/events/transfer-specific-vehicle-request.ts b/shared/src/simulation/events/transfer-specific-vehicle-request.ts new file mode 100644 index 000000000..5571fa196 --- /dev/null +++ b/shared/src/simulation/events/transfer-specific-vehicle-request.ts @@ -0,0 +1,38 @@ +import { IsUUID } from 'class-validator'; +import { getCreate } from '../../models/utils'; +import { UUID, uuidValidationOptions } from '../../utils'; +import { IsLiteralUnion, IsValue } from '../../utils/validators'; +import { + TransferDestination, + transferDestinationTypeAllowedValues, +} from '../utils/transfer-destination'; +import type { SimulationEvent } from './simulation-event'; + +export class TransferSpecificVehicleRequestEvent implements SimulationEvent { + @IsValue('transferSpecificVehicleRequestEvent') + readonly type = 'transferSpecificVehicleRequestEvent'; + + @IsUUID(4, uuidValidationOptions) + readonly vehicleId: UUID; + + @IsLiteralUnion(transferDestinationTypeAllowedValues) + readonly transferDestinationType: TransferDestination; + + @IsUUID(4, uuidValidationOptions) + readonly transferDestinationId: UUID; + + /** + * @deprecated Use {@link create} instead + */ + constructor( + vehicleId: UUID, + transferDestinationType: TransferDestination, + transferDestinationId: UUID + ) { + this.vehicleId = vehicleId; + this.transferDestinationType = transferDestinationType; + this.transferDestinationId = transferDestinationId; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/events/transfer-vehicles-request.ts b/shared/src/simulation/events/transfer-vehicles-request.ts new file mode 100644 index 000000000..e355cfd6f --- /dev/null +++ b/shared/src/simulation/events/transfer-vehicles-request.ts @@ -0,0 +1,46 @@ +import { IsOptional, IsString, IsUUID } from 'class-validator'; +import { getCreate } from '../../models/utils'; +import { IsLiteralUnion, IsValue } from '../../utils/validators'; +import { ResourceDescription } from '../../models/utils/resource-description'; +import { IsResourceDescription } from '../../utils/validators/is-resource-description'; +import { UUID, uuidValidationOptions } from '../../utils'; +import { + TransferDestination, + transferDestinationTypeAllowedValues, +} from '../utils/transfer-destination'; +import type { SimulationEvent } from './simulation-event'; + +export class TransferVehiclesRequestEvent implements SimulationEvent { + @IsValue('transferVehiclesRequestEvent') + readonly type = 'transferVehiclesRequestEvent'; + + @IsResourceDescription() + readonly requestedVehicles: ResourceDescription; + + @IsLiteralUnion(transferDestinationTypeAllowedValues) + readonly transferDestinationType: TransferDestination; + + @IsUUID(4, uuidValidationOptions) + readonly transferDestinationId: UUID; + + @IsOptional() + @IsString() + readonly key?: string; + + /** + * @deprecated Use {@link create} instead + */ + constructor( + requestedVehicles: ResourceDescription, + transferDestinationType: TransferDestination, + transferDestinationId: UUID, + key?: string + ) { + this.requestedVehicles = requestedVehicles; + this.transferDestinationType = transferDestinationType; + this.transferDestinationId = transferDestinationId; + this.key = key; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/simulation/events/vehicle-transfer-successful.ts b/shared/src/simulation/events/vehicle-transfer-successful.ts index b7532ff3e..bd43dcbc4 100644 --- a/shared/src/simulation/events/vehicle-transfer-successful.ts +++ b/shared/src/simulation/events/vehicle-transfer-successful.ts @@ -9,9 +9,6 @@ export class VehicleTransferSuccessfulEvent implements SimulationEvent { @IsValue('vehicleTransferSuccessfulEvent') readonly type = 'vehicleTransferSuccessfulEvent'; - @IsUUID(4, uuidValidationOptions) - public readonly id: UUID; - @IsUUID(4, uuidValidationOptions) public readonly targetId: UUID; @@ -25,13 +22,7 @@ export class VehicleTransferSuccessfulEvent implements SimulationEvent { /** * @deprecated Use {@link create} instead */ - constructor( - id: UUID, - targetId: UUID, - key: string, - vehiclesSent: VehicleResource - ) { - this.id = id; + constructor(targetId: UUID, key: string, vehiclesSent: VehicleResource) { this.targetId = targetId; this.key = key; this.vehiclesSent = vehiclesSent; diff --git a/shared/src/simulation/events/vehicles-sent.ts b/shared/src/simulation/events/vehicles-sent.ts index 9c28f028c..91719142b 100644 --- a/shared/src/simulation/events/vehicles-sent.ts +++ b/shared/src/simulation/events/vehicles-sent.ts @@ -1,8 +1,8 @@ import { Type } from 'class-transformer'; -import { IsUUID, ValidateNested } from 'class-validator'; +import { IsString, ValidateNested } from 'class-validator'; import { getCreate } from '../../models/utils'; import { VehicleResource } from '../../models/utils/rescue-resource'; -import { UUID, uuidValidationOptions } from '../../utils'; +import { UUID } from '../../utils'; import { IsValue } from '../../utils/validators'; import type { SimulationEvent } from './simulation-event'; @@ -10,7 +10,8 @@ export class VehiclesSentEvent implements SimulationEvent { @IsValue('vehiclesSentEvent') readonly type = 'vehiclesSentEvent'; - @IsUUID(4, uuidValidationOptions) + // I want to use '' here because this id should not exist and is never used but if we insert `IsUUID()` this is forbidden + @IsString() public readonly id: UUID; @Type(() => VehicleResource) diff --git a/shared/src/simulation/utils/transfer-destination.ts b/shared/src/simulation/utils/transfer-destination.ts new file mode 100644 index 000000000..45e7eaa2d --- /dev/null +++ b/shared/src/simulation/utils/transfer-destination.ts @@ -0,0 +1,9 @@ +import type { AllowedValues } from '../../utils/validators'; + +export type TransferDestination = 'hospital' | 'transferPoint'; + +export const transferDestinationTypeAllowedValues: AllowedValues = + { + hospital: true, + transferPoint: true, + }; diff --git a/shared/src/state-migrations/29-remove-transfer-vehicles-activity-and-change-answer-request-behavior.ts b/shared/src/state-migrations/29-remove-transfer-vehicles-activity-and-change-answer-request-behavior.ts new file mode 100644 index 000000000..a0f2d32b9 --- /dev/null +++ b/shared/src/state-migrations/29-remove-transfer-vehicles-activity-and-change-answer-request-behavior.ts @@ -0,0 +1,79 @@ +import type { Migration } from './migration-functions'; + +export const removeTransferVehiclesActivityAndChangeAnswerRequestBehavior29: Migration = + { + action: (_intermediaryState, action) => { + if ( + (action as { type: string }).type === + '[SimulatedRegion] Add Behavior' + ) { + const typedAction = action as { + behaviorState: BehaviorStateStub; + }; + if ( + typedAction.behaviorState.type === 'answerRequestsBehavior' + ) { + typedAction.behaviorState.receivedEvents = []; + typedAction.behaviorState.requestsHandled = 0; + } + } else if ( + (action as { type: string }).type === + '[SimulatedRegion] Add simulated region' + ) { + const typedAction = action as { + simulatedRegion: SimulatedRegionStub; + }; + migrateSimulatedRegion(typedAction.simulatedRegion); + } + return true; + }, + state: (state) => { + const typedState = state as { + simulatedRegions: { + [key: string]: SimulatedRegionStub; + }; + }; + + Object.values(typedState.simulatedRegions).forEach( + (simulatedRegion) => { + migrateSimulatedRegion(simulatedRegion); + } + ); + }, + }; + +interface SimulatedRegionStub { + activities: { + [stateId: string]: { + type: string | 'transferVehiclesActivity'; + }; + }; + behaviors: BehaviorStateStub[]; +} + +type BehaviorStateStub = + | { + type: 'answerRequestsBehavior'; + receivedEvents: unknown[] | undefined; + requestsHandled: number | undefined; + } + | { + type: Exclude<'answerRequestsBehavior', unknown>; + }; + +function migrateSimulatedRegion(simulatedRegion: SimulatedRegionStub) { + Object.keys(simulatedRegion.activities).forEach((key) => { + if ( + simulatedRegion.activities[key]!.type === 'transferVehiclesActivity' + ) { + delete simulatedRegion.activities[key]; + } + }); + + simulatedRegion.behaviors.forEach((behavior) => { + if (behavior.type === 'answerRequestsBehavior') { + behavior.receivedEvents = []; + behavior.requestsHandled = 0; + } + }); +} diff --git a/shared/src/state-migrations/migration-functions.ts b/shared/src/state-migrations/migration-functions.ts index a987bce4b..1dfb9a6fb 100644 --- a/shared/src/state-migrations/migration-functions.ts +++ b/shared/src/state-migrations/migration-functions.ts @@ -17,6 +17,7 @@ import { addPatientStatusTags25 } from './25-add-patient-status-tags'; import { addSimulatedRegionBorderColor26 } from './26-add-border-color-to-simulated-region'; import { addOccupationToVehicles27 } from './27-add-occupation-to-vehicles'; import { activitiesToUnloadVehiclesBehavior28 } from './28-add-activities-to-unload-vehicles-behavior'; +import { removeTransferVehiclesActivityAndChangeAnswerRequestBehavior29 } from './29-remove-transfer-vehicles-activity-and-change-answer-request-behavior'; import { updateEocLog3 } from './3-update-eoc-log'; import { removeSetParticipantIdAction4 } from './4-remove-set-participant-id-action'; import { removeStatistics5 } from './5-remove-statistics'; @@ -79,4 +80,5 @@ export const migrations: { 26: addSimulatedRegionBorderColor26, 27: addOccupationToVehicles27, 28: activitiesToUnloadVehiclesBehavior28, + 29: removeTransferVehiclesActivityAndChangeAnswerRequestBehavior29, }; diff --git a/shared/src/state.ts b/shared/src/state.ts index 7d002dc4b..da63b3b65 100644 --- a/shared/src/state.ts +++ b/shared/src/state.ts @@ -160,5 +160,5 @@ export class ExerciseState { * * This number MUST be increased every time a change to any object (that is part of the state or the state itself) is made in a way that there may be states valid before that are no longer valid. */ - static readonly currentStateVersion = 28; + static readonly currentStateVersion = 29; } diff --git a/shared/src/store/action-reducers/hospital.ts b/shared/src/store/action-reducers/hospital.ts index 91abec4bd..69f19db43 100644 --- a/shared/src/store/action-reducers/hospital.ts +++ b/shared/src/store/action-reducers/hospital.ts @@ -6,7 +6,7 @@ import { Min, ValidateNested, } from 'class-validator'; -import { Hospital } from '../../models'; +import { Hospital } from '../../models/hospital'; import { HospitalPatient } from '../../models/hospital-patient'; import { cloneDeepMutable, UUID, uuidValidationOptions } from '../../utils'; import { IsValue } from '../../utils/validators'; diff --git a/shared/src/store/action-reducers/patient.ts b/shared/src/store/action-reducers/patient.ts index 960832c3b..590694c19 100644 --- a/shared/src/store/action-reducers/patient.ts +++ b/shared/src/store/action-reducers/patient.ts @@ -1,6 +1,6 @@ import { Type } from 'class-transformer'; import { IsString, IsUUID, MaxLength, ValidateNested } from 'class-validator'; -import { Patient } from '../../models'; +import { Patient } from '../../models/patient'; import { isOnMap, MapPosition, @@ -28,7 +28,7 @@ import { import { IsLiteralUnion, IsValue } from '../../utils/validators'; import type { Action, ActionReducer } from '../action-reducer'; import { ReducerError } from '../reducer-error'; -import { PatientRemovedEvent } from '../../simulation'; +import { PatientRemovedEvent } from '../../simulation/events'; import { sendSimulationEvent } from '../../simulation/events/utils'; import { updateTreatments } from './utils/calculate-treatments'; import { getElement } from './utils/get-element'; diff --git a/shared/src/store/action-reducers/simulation.ts b/shared/src/store/action-reducers/simulation.ts index 9ba17fcf4..cea6c035f 100644 --- a/shared/src/store/action-reducers/simulation.ts +++ b/shared/src/store/action-reducers/simulation.ts @@ -9,10 +9,13 @@ import { } from 'class-validator'; import { Type } from 'class-transformer'; import type { + ExerciseSimulationEvent, TreatPatientsBehaviorState, UnloadArrivingVehiclesBehaviorState, } from '../../simulation'; import { + TransferPatientsInSpecificVehicleRequestEvent, + TransferSpecificVehicleRequestEvent, updateBehaviorsRequestTarget, updateBehaviorsRequestInterval, ReportableInformation, @@ -28,14 +31,19 @@ import { uuidValidationOptions, cloneDeepMutable, uuidArrayValidationOptions, + UUIDSet, } from '../../utils'; -import { IsLiteralUnion, IsValue } from '../../utils/validators'; +import { IsLiteralUnion, IsUUIDSet, IsValue } from '../../utils/validators'; import type { Action, ActionReducer } from '../action-reducer'; import { ExpectedReducerError, ReducerError } from '../reducer-error'; import { requestTargetTypeOptions, ExerciseRequestTargetConfiguration, } from '../../models'; +import { + TransferDestination, + transferDestinationTypeAllowedValues, +} from '../../simulation/utils/transfer-destination'; import { getActivityById, getBehaviorById, getElement } from './utils'; export class UpdateTreatPatientsIntervalsAction implements Action { @@ -262,6 +270,74 @@ export class UpdatePromiseInvalidationIntervalAction implements Action { public readonly promiseInvalidationInterval!: number; } +export class UpdatePatientLoadTimeAction implements Action { + @IsValue('[TransferBehavior] Update Patient Load Time') + public readonly type = '[TransferBehavior] Update Patient Load Time'; + + @IsUUID(4, uuidValidationOptions) + public readonly simulatedRegionId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly behaviorId!: UUID; + + @IsInt() + @Min(0) + public readonly loadTimePerPatient!: number; +} + +export class UpdatePersonnelLoadTimeAction implements Action { + @IsValue('[TransferBehavior] Update Personnel Load Time') + public readonly type = '[TransferBehavior] Update Personnel Load Time'; + + @IsUUID(4, uuidValidationOptions) + public readonly simulatedRegionId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly behaviorId!: UUID; + + @IsInt() + @Min(0) + public readonly personnelLoadTime!: number; +} + +export class UpdateDelayBetweenSendsAction implements Action { + @IsValue('[TransferBehavior] Update Delay Between Sends') + public readonly type = '[TransferBehavior] Update Delay Between Sends'; + + @IsUUID(4, uuidValidationOptions) + public readonly simulatedRegionId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly behaviorId!: UUID; + + @IsInt() + @Min(0) + public readonly delayBetweenSends!: number; +} + +export class SendTransferRequestEventAction implements Action { + @IsValue('[TransferBehavior] Send Transfer Request Event') + public readonly type = '[TransferBehavior] Send Transfer Request Event'; + + @IsUUID(4, uuidValidationOptions) + public readonly simulatedRegionId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly behaviorId!: UUID; + + @IsUUID(4, uuidValidationOptions) + public readonly vehicleId!: UUID; + + @IsLiteralUnion(transferDestinationTypeAllowedValues) + public readonly destinationType!: TransferDestination; + + @IsUUID(4, uuidValidationOptions) + public readonly destinationId!: UUID; + + @IsUUIDSet() + public readonly patients!: UUIDSet; +} + export namespace SimulationActionReducers { export const updateTreatPatientsIntervals: ActionReducer = { @@ -677,4 +753,122 @@ export namespace SimulationActionReducers { }, rights: 'trainer', }; + + export const updatePatientLoadTime: ActionReducer = + { + action: UpdatePatientLoadTimeAction, + reducer( + draftState, + { simulatedRegionId, behaviorId, loadTimePerPatient } + ) { + const behaviorState = getBehaviorById( + draftState, + simulatedRegionId, + behaviorId, + 'transferBehavior' + ); + + behaviorState.loadTimePerPatient = loadTimePerPatient; + + return draftState; + }, + rights: 'trainer', + }; + + export const updatePersonnelLoadTime: ActionReducer = + { + action: UpdatePersonnelLoadTimeAction, + reducer( + draftState, + { simulatedRegionId, behaviorId, personnelLoadTime } + ) { + const behaviorState = getBehaviorById( + draftState, + simulatedRegionId, + behaviorId, + 'transferBehavior' + ); + + behaviorState.personnelLoadTime = personnelLoadTime; + + return draftState; + }, + rights: 'trainer', + }; + + export const updateDelayBetweenSends: ActionReducer = + { + action: UpdateDelayBetweenSendsAction, + reducer( + draftState, + { simulatedRegionId, behaviorId, delayBetweenSends } + ) { + const behaviorState = getBehaviorById( + draftState, + simulatedRegionId, + behaviorId, + 'transferBehavior' + ); + + behaviorState.delayBetweenSends = delayBetweenSends; + + // also update the value inside the activity if one is running already + + if (behaviorState.recurringActivityId) { + const reccuringActivity = getActivityById( + draftState, + simulatedRegionId, + behaviorState.recurringActivityId, + 'recurringEventActivity' + ); + reccuringActivity.recurrenceIntervalTime = + delayBetweenSends; + } + + return draftState; + }, + rights: 'trainer', + }; + + export const sendTransferRequestEvent: ActionReducer = + { + action: SendTransferRequestEventAction, + reducer( + draftState, + { + simulatedRegionId, + vehicleId, + destinationType, + destinationId, + patients, + } + ) { + const simulatedRegion = getElement( + draftState, + 'simulatedRegion', + simulatedRegionId + ); + + let event: ExerciseSimulationEvent; + if (Object.keys(cloneDeepMutable(patients)).length === 0) { + event = TransferSpecificVehicleRequestEvent.create( + vehicleId, + destinationType, + destinationId + ); + } else { + event = + TransferPatientsInSpecificVehicleRequestEvent.create( + patients, + vehicleId, + destinationType, + destinationId + ); + } + + sendSimulationEvent(simulatedRegion, event); + return draftState; + }, + rights: 'trainer', + }; } diff --git a/shared/src/store/action-reducers/vehicle.ts b/shared/src/store/action-reducers/vehicle.ts index 39602a0c7..c01b16778 100644 --- a/shared/src/store/action-reducers/vehicle.ts +++ b/shared/src/store/action-reducers/vehicle.ts @@ -1,6 +1,6 @@ import { Type } from 'class-transformer'; import { IsArray, IsString, IsUUID, ValidateNested } from 'class-validator'; -import { Material, Personnel, Vehicle } from '../../models'; +import { Material } from '../../models/material'; import { currentCoordinatesOf, currentSimulatedRegionIdOf, @@ -22,7 +22,7 @@ import { changePositionWithId, } from '../../models/utils/position/position-helpers-mutable'; import type { ExerciseState } from '../../state'; -import { imageSizeToPosition } from '../../state-helpers'; +import { imageSizeToPosition } from '../../state-helpers/image-size-to-position'; import type { Mutable } from '../../utils'; import { cloneDeepMutable, @@ -41,7 +41,9 @@ import { PersonnelAvailableEvent, PersonnelRemovedEvent, VehicleRemovedEvent, -} from '../../simulation'; +} from '../../simulation/events'; +import { Vehicle } from '../../models/vehicle'; +import { Personnel } from '../../models/personnel'; import { deletePatient } from './patient'; import { completelyLoadVehicle as completelyLoadVehicleHelper } from './utils/completely-load-vehicle'; import { getElement } from './utils/get-element'; diff --git a/test-scenarios b/test-scenarios index dd6572880..17e783104 160000 --- a/test-scenarios +++ b/test-scenarios @@ -1 +1 @@ -Subproject commit dd6572880ee480dfdfc5d0b119975d15be34e9d4 +Subproject commit 17e7831048c3c07ce50149d6853584596fab3d58