diff --git a/backend/src/exercise/patient-ticking.ts b/backend/src/exercise/patient-ticking.ts index 1f6039936..38f130b0d 100644 --- a/backend/src/exercise/patient-ticking.ts +++ b/backend/src/exercise/patient-ticking.ts @@ -5,6 +5,7 @@ import type { PersonnelType, } from 'digital-fuesim-manv-shared'; import { + isOnMap, getElement, healthPointsDefaults, isAlive, @@ -29,7 +30,7 @@ export function patientTick( return ( Object.values(state.patients) // Only look at patients that are alive and have a position, i.e. are not in a vehicle - .filter((patient) => isAlive(patient.health) && patient.position) + .filter((patient) => isAlive(patient.health) && isOnMap(patient)) .map((patient) => { // update the time a patient is being treated, to check for pretriage later const treatmentTime = Patient.isTreatedByPersonnel(patient) diff --git a/frontend/src/app/pages/exercises/exercise/shared/core/drag-element.service.ts b/frontend/src/app/pages/exercises/exercise/shared/core/drag-element.service.ts index 143f58bed..2da6f3c52 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/core/drag-element.service.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/core/drag-element.service.ts @@ -175,10 +175,7 @@ export class DragElementService { this.exerciseService.proposeAction( { type: '[Patient] Add patient', - patient: { - ...patient, - position, - }, + patient, }, true ); diff --git a/frontend/src/app/pages/exercises/exercise/shared/core/statistics/statistics.service.ts b/frontend/src/app/pages/exercises/exercise/shared/core/statistics/statistics.service.ts index 530c82ddb..dc54695d2 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/core/statistics/statistics.service.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/core/statistics/statistics.service.ts @@ -1,17 +1,21 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; +import { + isNotInVehicle, + currentCoordinatesOf, + isOnMap, + loopTroughTime, + uuid, + Viewport, + isNotInTransfer, +} from 'digital-fuesim-manv-shared'; import type { + Personnel, Client, ExerciseState, Patient, Vehicle, } from 'digital-fuesim-manv-shared'; -import { - loopTroughTime, - Personnel, - uuid, - Viewport, -} from 'digital-fuesim-manv-shared'; import { countBy } from 'lodash-es'; import { ReplaySubject } from 'rxjs'; import { ApiService } from 'src/app/core/api.service'; @@ -108,18 +112,27 @@ export class StatisticsService { ), Object.values(draftState.patients).filter( (patient) => - patient.position && - Viewport.isInViewport(viewport, patient.position) + isOnMap(patient) && + Viewport.isInViewport( + viewport, + currentCoordinatesOf(patient) + ) ), Object.values(draftState.vehicles).filter( (vehicle) => - vehicle.position && - Viewport.isInViewport(viewport, vehicle.position) + isOnMap(vehicle) && + Viewport.isInViewport( + viewport, + currentCoordinatesOf(vehicle) + ) ), Object.values(draftState.personnel).filter( (personnel) => - personnel.position && - Viewport.isInViewport(viewport, personnel.position) + isOnMap(personnel) && + Viewport.isInViewport( + viewport, + currentCoordinatesOf(personnel) + ) ) ), ]) @@ -148,8 +161,8 @@ export class StatisticsService { personnel: countBy( personnel.filter( (_personnel) => - !Personnel.isInVehicle(_personnel) && - _personnel.transfer === undefined + isNotInVehicle(_personnel) && + isNotInTransfer(_personnel) ), (_personnel) => _personnel.personnelType ), diff --git a/frontend/src/app/pages/exercises/exercise/shared/emergency-operations-center/send-alarm-group-interface/send-alarm-group-interface.component.ts b/frontend/src/app/pages/exercises/exercise/shared/emergency-operations-center/send-alarm-group-interface/send-alarm-group-interface.component.ts index 06755aa93..0b70f0a33 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/emergency-operations-center/send-alarm-group-interface/send-alarm-group-interface.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/emergency-operations-center/send-alarm-group-interface/send-alarm-group-interface.component.ts @@ -3,7 +3,7 @@ import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import type { AlarmGroup, UUID } from 'digital-fuesim-manv-shared'; import { - Position, + MapCoordinates, AlarmGroupStartPoint, createVehicleParameters, TransferPoint, @@ -97,10 +97,13 @@ export class SendAlarmGroupInterfaceComponent implements OnDestroy { this.store ), // TODO: This position is not correct but needs to be provided. - // Here one should use a MetaPosition with the Transfer. + // Here one should use a Position with the Transfer. // But this is part of later Refactoring. - // Also it is irrelevant, because the correctMetaPosition is set immediately after this is called. - Position.create(0, 0) + // We need the Transfer to be created before the Vehicle is created, + // else we need to provide a Position that is immediately overwritten by the Add to Transfer Action. + // This is done here + // Good Thing is, it is irrelevant, because the correctPosition is set immediately after this is called. + MapCoordinates.create(0, 0) ); return [ diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/material-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/material-feature-manager.ts index a93e7e716..d4561aeb0 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/material-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/material-feature-manager.ts @@ -6,7 +6,6 @@ import type VectorLayer from 'ol/layer/Vector'; import type OlMap from 'ol/Map'; import type VectorSource from 'ol/source/Vector'; import type { ExerciseService } from 'src/app/core/exercise.service'; -import type { WithPosition } from '../../utility/types/with-position'; import { MaterialPopupComponent } from '../shared/material-popup/material-popup.component'; import { PointGeometryHelper } from '../utility/point-geometry-helper'; import { ImagePopupHelper } from '../utility/popup-helper'; @@ -14,9 +13,7 @@ import { ImageStyleHelper } from '../utility/style-helper/image-style-helper'; import { NameStyleHelper } from '../utility/style-helper/name-style-helper'; import { MoveableFeatureManager } from './moveable-feature-manager'; -export class MaterialFeatureManager extends MoveableFeatureManager< - WithPosition -> { +export class MaterialFeatureManager extends MoveableFeatureManager { private readonly imageStyleHelper = new ImageStyleHelper( (feature) => (this.getElementFromFeature(feature) as Material).image ); diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts index f1595228a..71067000a 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts @@ -11,7 +11,6 @@ import type { ExerciseService } from 'src/app/core/exercise.service'; import type { AppState } from 'src/app/state/app.state'; import { selectConfiguration } from 'src/app/state/application/selectors/exercise.selectors'; import { selectStateSnapshot } from 'src/app/state/get-state-snapshot'; -import type { WithPosition } from '../../utility/types/with-position'; import { PatientPopupComponent } from '../shared/patient-popup/patient-popup.component'; import { PointGeometryHelper } from '../utility/point-geometry-helper'; import { ImagePopupHelper } from '../utility/popup-helper'; @@ -19,9 +18,7 @@ import { CircleStyleHelper } from '../utility/style-helper/circle-style-helper'; import { ImageStyleHelper } from '../utility/style-helper/image-style-helper'; import { MoveableFeatureManager } from './moveable-feature-manager'; -export class PatientFeatureManager extends MoveableFeatureManager< - WithPosition -> { +export class PatientFeatureManager extends MoveableFeatureManager { private readonly popupHelper = new ImagePopupHelper(this.olMap, this.layer); private readonly imageStyleHelper = new ImageStyleHelper((feature) => { diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/personnel-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/personnel-feature-manager.ts index e9c25dfb3..54dcd2e18 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/personnel-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/personnel-feature-manager.ts @@ -6,7 +6,6 @@ import type VectorLayer from 'ol/layer/Vector'; import type OlMap from 'ol/Map'; import type VectorSource from 'ol/source/Vector'; import type { ExerciseService } from 'src/app/core/exercise.service'; -import type { WithPosition } from '../../utility/types/with-position'; import { PersonnelPopupComponent } from '../shared/personnel-popup/personnel-popup.component'; import { PointGeometryHelper } from '../utility/point-geometry-helper'; import { ImagePopupHelper } from '../utility/popup-helper'; @@ -14,9 +13,7 @@ import { ImageStyleHelper } from '../utility/style-helper/image-style-helper'; import { NameStyleHelper } from '../utility/style-helper/name-style-helper'; import { MoveableFeatureManager } from './moveable-feature-manager'; -export class PersonnelFeatureManager extends MoveableFeatureManager< - WithPosition -> { +export class PersonnelFeatureManager extends MoveableFeatureManager { private readonly imageStyleHelper = new ImageStyleHelper( (feature) => (this.getElementFromFeature(feature) as Personnel).image ); diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts index bfb8af563..b9c3dd49c 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/simulated-region-feature-manager.ts @@ -1,6 +1,6 @@ import type { Store } from '@ngrx/store'; import type { UUID, SimulatedRegion } from 'digital-fuesim-manv-shared'; -import { Position, Size } from 'digital-fuesim-manv-shared'; +import { MapCoordinates, Size } from 'digital-fuesim-manv-shared'; import type { Feature, MapBrowserEvent } from 'ol'; import type { Polygon } from 'ol/geom'; import type VectorLayer from 'ol/layer/Vector'; @@ -67,7 +67,7 @@ export class SimulatedRegionFeatureManager { type: '[SimulatedRegion] Resize simulated region', simulatedRegionId: element.id, - targetPosition: Position.create( + targetPosition: MapCoordinates.create( topLeftCoordinate[0]!, topLeftCoordinate[1]! ), diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/vehicle-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/vehicle-feature-manager.ts index 314d0a1a7..04cd680a2 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/vehicle-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/vehicle-feature-manager.ts @@ -7,7 +7,6 @@ import type VectorLayer from 'ol/layer/Vector'; import type OlMap from 'ol/Map'; import type VectorSource from 'ol/source/Vector'; import type { ExerciseService } from 'src/app/core/exercise.service'; -import type { WithPosition } from '../../utility/types/with-position'; import { VehiclePopupComponent } from '../shared/vehicle-popup/vehicle-popup.component'; import { PointGeometryHelper } from '../utility/point-geometry-helper'; import { ImagePopupHelper } from '../utility/popup-helper'; @@ -15,9 +14,7 @@ import { ImageStyleHelper } from '../utility/style-helper/image-style-helper'; import { NameStyleHelper } from '../utility/style-helper/name-style-helper'; import { MoveableFeatureManager } from './moveable-feature-manager'; -export class VehicleFeatureManager extends MoveableFeatureManager< - WithPosition -> { +export class VehicleFeatureManager extends MoveableFeatureManager { private readonly imageStyleHelper = new ImageStyleHelper( (feature) => (this.getElementFromFeature(feature) as Vehicle).image ); diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts index ab1050d9f..15807372f 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/viewport-feature-manager.ts @@ -1,6 +1,6 @@ import type { Store } from '@ngrx/store'; import type { UUID } from 'digital-fuesim-manv-shared'; -import { Position, Size, Viewport } from 'digital-fuesim-manv-shared'; +import { MapCoordinates, Size, Viewport } from 'digital-fuesim-manv-shared'; import type { Feature, MapBrowserEvent } from 'ol'; import type { Coordinate } from 'ol/coordinate'; import type { Polygon } from 'ol/geom'; @@ -75,7 +75,7 @@ export class ViewportFeatureManager { type: '[Viewport] Resize viewport', viewportId: element.id, - targetPosition: Position.create( + targetPosition: MapCoordinates.create( topLeftCoordinate[0]!, topLeftCoordinate[1]! ), diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/shared/vehicle-popup/vehicle-popup.component.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/shared/vehicle-popup/vehicle-popup.component.ts index fc13ff075..fb887ace0 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/shared/vehicle-popup/vehicle-popup.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/shared/vehicle-popup/vehicle-popup.component.ts @@ -2,7 +2,7 @@ import type { OnInit } from '@angular/core'; import { Component, EventEmitter, Output } from '@angular/core'; import { Store } from '@ngrx/store'; import type { UUID, Vehicle } from 'digital-fuesim-manv-shared'; -import { Material, Patient, Personnel } from 'digital-fuesim-manv-shared'; +import { isInVehicle } from 'digital-fuesim-manv-shared'; import type { Observable } from 'rxjs'; import { combineLatest, map, switchMap } from 'rxjs'; import { ExerciseService } from 'src/app/core/exercise.service'; @@ -45,23 +45,21 @@ export class VehiclePopupComponent implements PopupComponent, OnInit { ).map((materialId) => this.store .select(createSelectMaterial(materialId)) - .pipe(map((material) => Material.isInVehicle(material))) + .pipe(map((material) => isInVehicle(material))) ); const personnelAreInVehicle$ = Object.keys( _vehicle.personnelIds ).map((personnelId) => this.store .select(createSelectPersonnel(personnelId)) - .pipe( - map((personnel) => Personnel.isInVehicle(personnel)) - ) + .pipe(map((personnel) => isInVehicle(personnel))) ); const patientsAreInVehicle$ = Object.keys( _vehicle.patientIds ).map((patientId) => this.store .select(createSelectPatient(patientId)) - .pipe(map((patient) => Patient.isInVehicle(patient))) + .pipe(map((patient) => isInVehicle(patient))) ); return combineLatest([ ...materialsAreInVehicle$, @@ -70,7 +68,7 @@ export class VehiclePopupComponent implements PopupComponent, OnInit { ]); }), map((areInVehicle) => - areInVehicle.every((isInVehicle) => !isInVehicle) + areInVehicle.every((isInAVehicle) => !isInAVehicle) ) ); } diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/geometry-helper.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/geometry-helper.ts index 539119712..c73200b8a 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/geometry-helper.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/geometry-helper.ts @@ -1,4 +1,9 @@ -import type { Position, Size, UUID } from 'digital-fuesim-manv-shared'; +import type { + MapCoordinates, + Position, + Size, + UUID, +} from 'digital-fuesim-manv-shared'; import type { Feature } from 'ol'; import type { Coordinate } from 'ol/coordinate'; import type { Geometry } from 'ol/geom'; @@ -32,7 +37,7 @@ type ArrayElement = ArrayType extends readonly (infer ElementType)[] : never; type SubstituteCoordinateForPoint = T extends Coordinate - ? Position + ? MapCoordinates : T extends Array> ? SubstituteCoordinateForPoint>[] : never; diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/ol-map-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/ol-map-manager.ts index 624d24df9..722590345 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/ol-map-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/ol-map-manager.ts @@ -5,6 +5,7 @@ import type { MergeIntersection, UUID, } from 'digital-fuesim-manv-shared'; +import { currentCoordinatesOf } from 'digital-fuesim-manv-shared'; import type { Feature } from 'ol'; import { Overlay, View } from 'ol'; import type { Polygon } from 'ol/geom'; @@ -356,10 +357,10 @@ export class OlMapManager { const center = view.getCenter()!; const previousZoom = view.getZoom()!; const targetExtent = [ - viewport.position.x, - viewport.position.y - viewport.size.height, - viewport.position.x + viewport.size.width, - viewport.position.y, + currentCoordinatesOf(viewport).x, + currentCoordinatesOf(viewport).y - viewport.size.height, + currentCoordinatesOf(viewport).x + viewport.size.width, + currentCoordinatesOf(viewport).y, ]; view.fit(targetExtent); const matchingZoom = view.getZoom()!; @@ -517,20 +518,22 @@ export class OlMapManager { return; } const minX = Math.min( - ...viewports.map((viewport) => viewport.position.x) + ...viewports.map((viewport) => currentCoordinatesOf(viewport).x) ); const minY = Math.min( ...viewports.map( - (viewport) => viewport.position.y - viewport.size.height + (viewport) => + currentCoordinatesOf(viewport).y - viewport.size.height ) ); const maxX = Math.max( ...viewports.map( - (viewport) => viewport.position.x + viewport.size.width + (viewport) => + currentCoordinatesOf(viewport).x + viewport.size.width ) ); const maxY = Math.max( - ...viewports.map((viewport) => viewport.position.y) + ...viewports.map((viewport) => currentCoordinatesOf(viewport).y) ); const padding = 25; view.fit([minX, minY, maxX, maxY], { diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/point-geometry-helper.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/point-geometry-helper.ts index 8280d565b..a9c65dee8 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/point-geometry-helper.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/point-geometry-helper.ts @@ -1,7 +1,10 @@ -import { Position } from 'digital-fuesim-manv-shared'; +import type { WithPosition } from 'digital-fuesim-manv-shared'; +import { + MapCoordinates, + currentCoordinatesOf, +} from 'digital-fuesim-manv-shared'; import { Feature } from 'ol'; import { Point } from 'ol/geom'; -import type { WithPosition } from '../../utility/types/with-position'; import type { CoordinatePair, Coordinates, @@ -11,12 +14,13 @@ import type { import { interpolate } from './geometry-helper'; export class PointGeometryHelper implements GeometryHelper { - create = (element: WithPosition): Feature => + create = (element: WithPosition): Feature => new Feature(new Point(this.getElementCoordinates(element))); - getElementCoordinates = ( - element: WithPosition - ): Coordinates => [element.position.x, element.position.y]; + getElementCoordinates = (element: WithPosition): Coordinates => [ + currentCoordinatesOf(element).x, + currentCoordinatesOf(element).y, + ]; getFeatureCoordinates = (feature: Feature): Coordinates => feature.getGeometry()!.getCoordinates(); @@ -28,7 +32,7 @@ export class PointGeometryHelper implements GeometryHelper { interpolate(positions.startPosition, positions.endPosition, progress); getFeaturePosition = (feature: Feature): Positions => - Position.create( + MapCoordinates.create( this.getFeatureCoordinates(feature)[0]!, this.getFeatureCoordinates(feature)[1]! ); diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/polygon-geometry-helper.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/polygon-geometry-helper.ts index 12ef3428c..29fc313d5 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/polygon-geometry-helper.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/polygon-geometry-helper.ts @@ -1,4 +1,7 @@ -import { Position } from 'digital-fuesim-manv-shared'; +import { + currentCoordinatesOf, + MapCoordinates, +} from 'digital-fuesim-manv-shared'; import { Feature } from 'ol'; import { Polygon } from 'ol/geom'; import type { @@ -20,14 +23,20 @@ export class PolygonGeometryHelper element: ResizableElement ): Coordinates => [ [ - [element.position.x, element.position.y], - [element.position.x + element.size.width, element.position.y], + [currentCoordinatesOf(element).x, currentCoordinatesOf(element).y], [ - element.position.x + element.size.width, - element.position.y - element.size.height, + currentCoordinatesOf(element).x + element.size.width, + currentCoordinatesOf(element).y, ], - [element.position.x, element.position.y - element.size.height], - [element.position.x, element.position.y], + [ + currentCoordinatesOf(element).x + element.size.width, + currentCoordinatesOf(element).y - element.size.height, + ], + [ + currentCoordinatesOf(element).x, + currentCoordinatesOf(element).y - element.size.height, + ], + [currentCoordinatesOf(element).x, currentCoordinatesOf(element).y], ], ]; @@ -51,7 +60,7 @@ export class PolygonGeometryHelper getFeaturePosition = (feature: Feature): Positions => this.getFeatureCoordinates(feature).map((coordinates) => coordinates.map((coordinate) => - Position.create(coordinate[0]!, coordinate[1]!) + MapCoordinates.create(coordinate[0]!, coordinate[1]!) ) ); } diff --git a/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.html b/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.html index 4d592c856..0138c6716 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.html +++ b/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.html @@ -1,3 +1,4 @@ + @@ -37,21 +38,21 @@ @@ -71,21 +72,21 @@ diff --git a/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.ts b/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.ts index 35fdd6a1a..da2e76397 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/transfer-overview/transfer-overview-table/transfer-overview-table.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; +import { currentTransferOf } from 'digital-fuesim-manv-shared'; import type { AppState } from 'src/app/state/app.state'; import { selectExerciseStatus, @@ -20,6 +21,8 @@ export class TransferOverviewTableComponent { selectPersonnelInTransfer ); + public currentTransferOf = currentTransferOf; + public readonly exerciseStatus$ = this.store.select(selectExerciseStatus); constructor(private readonly store: Store) {} diff --git a/frontend/src/app/pages/exercises/exercise/shared/utility/types/with-position.ts b/frontend/src/app/pages/exercises/exercise/shared/utility/types/with-position.ts deleted file mode 100644 index 64c7c42dd..000000000 --- a/frontend/src/app/pages/exercises/exercise/shared/utility/types/with-position.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Position } from 'digital-fuesim-manv-shared'; - -export type WithPosition = T & { - position: Position; -}; diff --git a/frontend/src/app/shared/types/catering-line.ts b/frontend/src/app/shared/types/catering-line.ts index 673762727..8326a5261 100644 --- a/frontend/src/app/shared/types/catering-line.ts +++ b/frontend/src/app/shared/types/catering-line.ts @@ -1,8 +1,8 @@ -import type { UUID, Position } from 'digital-fuesim-manv-shared'; +import type { UUID, MapCoordinates } from 'digital-fuesim-manv-shared'; export interface CateringLine { readonly id: `${UUID}:${UUID}`; - readonly catererPosition: Position; - readonly patientPosition: Position; + readonly catererPosition: MapCoordinates; + readonly patientPosition: MapCoordinates; } diff --git a/frontend/src/app/shared/types/transfer-line.ts b/frontend/src/app/shared/types/transfer-line.ts index 3c615d376..2769f4d7e 100644 --- a/frontend/src/app/shared/types/transfer-line.ts +++ b/frontend/src/app/shared/types/transfer-line.ts @@ -1,9 +1,9 @@ -import type { Position, UUID } from 'digital-fuesim-manv-shared'; +import type { MapCoordinates, UUID } from 'digital-fuesim-manv-shared'; export interface TransferLine { readonly id: `${UUID}:${UUID}`; - readonly startPosition: Position; - readonly endPosition: Position; + readonly startPosition: MapCoordinates; + readonly endPosition: MapCoordinates; readonly duration: number; } diff --git a/frontend/src/app/state/application/selectors/exercise.selectors.ts b/frontend/src/app/state/application/selectors/exercise.selectors.ts index 0fb94a5a2..42e6ab4a8 100644 --- a/frontend/src/app/state/application/selectors/exercise.selectors.ts +++ b/frontend/src/app/state/application/selectors/exercise.selectors.ts @@ -2,10 +2,10 @@ import { createSelector } from '@ngrx/store'; import type { ExerciseState, Personnel, - Transfer, UUID, Vehicle, } from 'digital-fuesim-manv-shared'; +import { isInTransfer, currentCoordinatesOf } from 'digital-fuesim-manv-shared'; import type { TransferLine } from 'src/app/shared/types/transfer-line'; import type { AppState } from '../../app.state'; @@ -119,8 +119,10 @@ export const selectTransferLines = createSelector( Object.entries(transferPoint.reachableTransferPoints).map( ([connectedId, { duration }]) => ({ id: `${transferPoint.id}:${connectedId}` as const, - startPosition: transferPoint.position, - endPosition: transferPoints[connectedId]!.position, + startPosition: currentCoordinatesOf(transferPoint), + endPosition: currentCoordinatesOf( + transferPoints[connectedId]! + ), duration, }) ) @@ -159,15 +161,15 @@ export function createSelectReachableHospitals(transferPointId: UUID) { export const selectVehiclesInTransfer = createSelector( selectVehicles, (vehicles) => - Object.values(vehicles).filter( - (vehicle) => vehicle.transfer !== undefined - ) as (Vehicle & { transfer: Transfer })[] + Object.values(vehicles).filter((vehicle) => + isInTransfer(vehicle) + ) as Vehicle[] ); export const selectPersonnelInTransfer = createSelector( selectPersonnel, (personnel) => - Object.values(personnel).filter( - (_personnel) => _personnel.transfer !== undefined - ) as (Personnel & { transfer: Transfer })[] + Object.values(personnel).filter((_personnel) => + isInTransfer(_personnel) + ) as Personnel[] ); diff --git a/frontend/src/app/state/application/selectors/shared.selectors.ts b/frontend/src/app/state/application/selectors/shared.selectors.ts index 706764625..fbd0c8421 100644 --- a/frontend/src/app/state/application/selectors/shared.selectors.ts +++ b/frontend/src/app/state/application/selectors/shared.selectors.ts @@ -4,15 +4,18 @@ import type { Material, Patient, Personnel, - Position, SimulatedRegion, TransferPoint, UUID, Vehicle, + WithPosition, +} from 'digital-fuesim-manv-shared'; +import { + currentCoordinatesOf, + isOnMap, + Viewport, } from 'digital-fuesim-manv-shared'; -import { Viewport } from 'digital-fuesim-manv-shared'; import { pickBy } from 'lodash-es'; -import type { WithPosition } from 'src/app/pages/exercises/exercise/shared/utility/types/with-position'; import type { CateringLine } from 'src/app/shared/types/catering-line'; import type { AppState } from '../../app.state'; import { @@ -63,20 +66,16 @@ export const selectRestrictedViewport = createSelector( * @returns a selector that returns a UUIDMap of all elements that have a position and are in the viewport restriction */ function selectVisibleElementsFactory< - Element extends { readonly position?: Position }, + Element extends WithPosition, Elements extends { readonly [key: UUID]: Element } = { readonly [key: UUID]: Element; - }, - ElementsWithPosition extends { - [Id in keyof Elements]: WithPosition; - } = { [Id in keyof Elements]: WithPosition } + } >( selectElements: (state: AppState) => Elements, - isInViewport: ( - element: WithPosition, - viewport: Viewport - ) => boolean = (element, viewport) => - Viewport.isInViewport(viewport, element.position) + isInViewport: (element: Element, viewport: Viewport) => boolean = ( + element, + viewport + ) => Viewport.isInViewport(viewport, currentCoordinatesOf(element)) ) { return createSelector( selectRestrictedViewport, @@ -86,14 +85,11 @@ function selectVisibleElementsFactory< elements, (element) => // Is placed on the map - element.position && + isOnMap(element) && // No viewport restriction (!restrictedViewport || - isInViewport( - element as WithPosition, - restrictedViewport - )) - ) as ElementsWithPosition + isInViewport(element, restrictedViewport)) + ) ); } @@ -129,7 +125,7 @@ export const selectVisibleCateringLines = createSelector( (viewport, materials, personnel, patients) => // Mostly, there are fewer untreated patients than materials and personnel that are not treating Object.values(patients) - .filter((patient) => patient.position !== undefined) + .filter((patient) => isOnMap(patient)) .flatMap((patient) => [ ...Object.keys(patient.assignedPersonnelIds).map( @@ -140,9 +136,9 @@ export const selectVisibleCateringLines = createSelector( ), ].map((caterer) => ({ id: `${caterer.id}:${patient.id}` as const, - patientPosition: patient.position!, + patientPosition: currentCoordinatesOf(patient), // If the catering element is treating a patient, it must have a position - catererPosition: caterer.position!, + catererPosition: currentCoordinatesOf(caterer), })) ) // To improve performance, all Lines where both ends are not in the viewport diff --git a/shared/src/data/dummy-objects/patient.ts b/shared/src/data/dummy-objects/patient.ts index 7fd5fc3c6..dfeb9b58b 100644 --- a/shared/src/data/dummy-objects/patient.ts +++ b/shared/src/data/dummy-objects/patient.ts @@ -1,6 +1,6 @@ import { FunctionParameters, Patient, PatientHealthState } from '../../models'; -import { MapCoordinates } from '../../models/utils/map-coordinates'; -import { MapPosition } from '../../models/utils/map-position'; +import { MapCoordinates } from '../../models/utils/position/map-coordinates'; +import { MapPosition } from '../../models/utils/position/map-position'; import { PatientStatusCode } from '../../models/utils/patient-status-code'; import { defaultPatientCategories } from '../default-state/patient-templates'; diff --git a/shared/src/models/map-image.ts b/shared/src/models/map-image.ts index ad15022a7..9d8f158a5 100644 --- a/shared/src/models/map-image.ts +++ b/shared/src/models/map-image.ts @@ -2,7 +2,9 @@ import { Type } from 'class-transformer'; import { IsBoolean, IsInt, IsUUID, ValidateNested } from 'class-validator'; import { uuid, UUID, uuidValidationOptions } from '../utils'; import { IsValue } from '../utils/validators'; -import { Position, getCreate, ImageProperties } from './utils'; +import { IsPosition } from '../utils/validators/is-position'; +import type { MapCoordinates } from './utils'; +import { MapPosition, getCreate, ImageProperties, Position } from './utils'; export class MapImage { @IsUUID(4, uuidValidationOptions) @@ -11,8 +13,11 @@ export class MapImage { @IsValue('mapImage' as const) public readonly type = 'mapImage'; + /** + * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. + */ @ValidateNested() - @Type(() => Position) + @IsPosition() public readonly position: Position; @ValidateNested() @@ -38,12 +43,12 @@ export class MapImage { * @deprecated Use {@link create} instead */ constructor( - topLeft: Position, + topLeft: MapCoordinates, image: ImageProperties, isLocked: boolean, zIndex: number ) { - this.position = topLeft; + this.position = MapPosition.create(topLeft); this.image = image; this.isLocked = isLocked; this.zIndex = zIndex; diff --git a/shared/src/models/material.ts b/shared/src/models/material.ts index 4fea13d4d..0df36780c 100644 --- a/shared/src/models/material.ts +++ b/shared/src/models/material.ts @@ -6,15 +6,14 @@ import { IsNumber, Min, Max, - IsOptional, } from 'class-validator'; import { maxTreatmentRange } from '../state-helpers/max-treatment-range'; import { uuidValidationOptions, UUID, uuid, UUIDSet } from '../utils'; import { IsUUIDSet, IsValue } from '../utils/validators'; -import { IsMetaPosition } from '../utils/validators/is-metaposition'; +import { IsPosition } from '../utils/validators/is-position'; import type { MaterialTemplate } from './material-template'; -import { CanCaterFor, Position, ImageProperties, getCreate } from './utils'; -import { MetaPosition } from './utils/meta-position'; +import { CanCaterFor, ImageProperties, getCreate } from './utils'; +import { Position } from './utils/position/position'; export class Material { @IsUUID(4, uuidValidationOptions) @@ -56,18 +55,12 @@ export class Material { @Max(maxTreatmentRange) public readonly treatmentRange: number; - @IsMetaPosition() - @ValidateNested() - public readonly metaPosition: MetaPosition; - /** - * @deprecated use {@link metaPosition} - * if undefined, is in vehicle with {@link this.vehicleId} + * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. */ + @IsPosition() @ValidateNested() - @Type(() => Position) - @IsOptional() - public readonly position?: Position; + public readonly position: Position; @ValidateNested() @Type(() => ImageProperties) @@ -84,18 +77,16 @@ export class Material { canCaterFor: CanCaterFor, treatmentRange: number, overrideTreatmentRange: number, - metaPosition: MetaPosition, - position?: Position + position: Position ) { this.vehicleId = vehicleId; this.vehicleName = vehicleName; this.assignedPatientIds = assignedPatientIds; - this.position = position; this.image = image; this.canCaterFor = canCaterFor; this.treatmentRange = treatmentRange; this.overrideTreatmentRange = overrideTreatmentRange; - this.metaPosition = metaPosition; + this.position = position; } static readonly create = getCreate(this); @@ -104,7 +95,7 @@ export class Material { materialTemplate: MaterialTemplate, vehicleId: UUID, vehicleName: string, - metaPosition: MetaPosition + position: Position ): Material { return this.create( vehicleId, @@ -114,12 +105,7 @@ export class Material { materialTemplate.canCaterFor, materialTemplate.treatmentRange, materialTemplate.overrideTreatmentRange, - metaPosition, - undefined + position ); } - - static isInVehicle(material: Material): boolean { - return material.metaPosition.type === 'vehicle'; - } } diff --git a/shared/src/models/patient-template.ts b/shared/src/models/patient-template.ts index 82a4905c4..0c3140dbd 100644 --- a/shared/src/models/patient-template.ts +++ b/shared/src/models/patient-template.ts @@ -16,7 +16,7 @@ import { Patient } from './patient'; import type { FunctionParameters } from './patient-health-state'; import { PretriageInformation } from './utils/pretriage-information'; import { PatientHealthState } from './patient-health-state'; -import type { MetaPosition } from './utils/meta-position'; +import type { Position } from './utils/position/position'; export class PatientTemplate { @IsUUID(4, uuidValidationOptions) @@ -72,7 +72,7 @@ export class PatientTemplate { public static generatePatient( template: PatientTemplate, patientStatusCode: PatientStatusCode, - metaPosition: MetaPosition + position: Position ): Patient { // Randomize function parameters const healthStates = Object.fromEntries( @@ -113,7 +113,7 @@ export class PatientTemplate { template.image, template.health, '', - metaPosition + position ); } } diff --git a/shared/src/models/patient.ts b/shared/src/models/patient.ts index 6c5cecee4..a8f015149 100644 --- a/shared/src/models/patient.ts +++ b/shared/src/models/patient.ts @@ -2,7 +2,6 @@ import { Type } from 'class-transformer'; import { IsUUID, ValidateNested, - IsOptional, IsNumber, Max, Min, @@ -18,7 +17,7 @@ import { IsUUIDSet, IsValue, } from '../utils/validators'; -import { IsMetaPosition } from '../utils/validators/is-metaposition'; +import { IsPosition } from '../utils/validators/is-position'; import { PatientHealthState } from './patient-health-state'; import { BiometricInformation, @@ -26,12 +25,11 @@ import { PatientStatus, patientStatusAllowedValues, ImageProperties, - Position, healthPointsDefaults, HealthPoints, getCreate, } from './utils'; -import { MetaPosition } from './utils/meta-position'; +import { Position } from './utils/position/position'; import { PersonalInformation } from './utils/personal-information'; import { PretriageInformation } from './utils/pretriage-information'; @@ -72,26 +70,12 @@ export class Patient { @Type(() => ImageProperties) public readonly image: ImageProperties; - @IsMetaPosition() - @ValidateNested() - public readonly metaPosition: MetaPosition; - /** - * @deprecated use {@link metaPosition} - * Exclusive-or to {@link vehicleId} + * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. */ + @IsPosition() @ValidateNested() - @Type(() => Position) - @IsOptional() - public readonly position?: Position; - - /** - * @deprecated use {@link metaPosition} - * Exclusive-or to {@link position} - */ - @IsUUID(4, uuidValidationOptions) - @IsOptional() - public readonly vehicleId?: UUID; + public readonly position: Position; /** * The time the patient already is in the current state @@ -172,7 +156,7 @@ export class Patient { image: ImageProperties, health: HealthPoints, remarks: string, - metaPosition: MetaPosition + position: Position ) { this.personalInformation = personalInformation; this.biometricInformation = biometricInformation; @@ -185,7 +169,7 @@ export class Patient { this.image = image; this.health = health; this.remarks = remarks; - this.metaPosition = metaPosition; + this.position = position; } static readonly create = getCreate(this); @@ -207,10 +191,6 @@ export class Patient { return patient.treatmentTime >= this.pretriageTimeThreshold; } - static isInVehicle(patient: Patient): boolean { - return patient.metaPosition.type === 'vehicle'; - } - static isTreatedByPersonnel(patient: Patient) { return !isEmpty(patient.assignedPersonnelIds); } diff --git a/shared/src/models/personnel.ts b/shared/src/models/personnel.ts index 16ebc287c..7e5d55bd7 100644 --- a/shared/src/models/personnel.ts +++ b/shared/src/models/personnel.ts @@ -6,22 +6,19 @@ import { IsNumber, Min, Max, - IsOptional, } from 'class-validator'; import { maxTreatmentRange } from '../state-helpers/max-treatment-range'; import { uuidValidationOptions, UUID, uuid, UUIDSet } from '../utils'; import { IsLiteralUnion, IsUUIDSet, IsValue } from '../utils/validators'; -import { IsMetaPosition } from '../utils/validators/is-metaposition'; +import { IsPosition } from '../utils/validators/is-position'; import type { PersonnelTemplate } from './personnel-template'; import { PersonnelType, CanCaterFor, ImageProperties, - Position, - Transfer, getCreate, } from './utils'; -import { MetaPosition } from './utils/meta-position'; +import { Position } from './utils/position/position'; import { personnelTypeAllowedValues } from './utils/personnel-type'; export class Personnel { @@ -71,27 +68,12 @@ export class Personnel { @Type(() => ImageProperties) public readonly image: ImageProperties; - @IsMetaPosition() - @ValidateNested() - public readonly metaPosition: MetaPosition; - - /** - * @deprecated use {@link metaPosition} - * If undefined, the personnel is either in the vehicle with {@link this.vehicleId} or in transfer. - */ - @ValidateNested() - @Type(() => Position) - @IsOptional() - public readonly position?: Position; - /** - * * @deprecated use {@link metaPosition} - * If undefined, the personnel is either in the vehicle with {@link this.vehicleId} or has a {@link position}. + * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. */ + @IsPosition() @ValidateNested() - @Type(() => Transfer) - @IsOptional() - public readonly transfer?: Transfer; + public readonly position: Position; /** * @deprecated Use {@link create} instead @@ -105,19 +87,17 @@ export class Personnel { canCaterFor: CanCaterFor, treatmentRange: number, overrideTreatmentRange: number, - metaPosition: MetaPosition, - position?: Position + position: Position ) { this.vehicleId = vehicleId; this.vehicleName = vehicleName; this.personnelType = personnelType; this.assignedPatientIds = assignedPatientIds; - this.position = position; this.image = image; this.canCaterFor = canCaterFor; this.treatmentRange = treatmentRange; this.overrideTreatmentRange = overrideTreatmentRange; - this.metaPosition = metaPosition; + this.position = position; } static readonly create = getCreate(this); @@ -126,7 +106,7 @@ export class Personnel { personnelTemplate: PersonnelTemplate, vehicleId: UUID, vehicleName: string, - metaPosition: MetaPosition + position: Position ): Personnel { return this.create( vehicleId, @@ -137,12 +117,7 @@ export class Personnel { personnelTemplate.canCaterFor, personnelTemplate.treatmentRange, personnelTemplate.overrideTreatmentRange, - metaPosition, - undefined + position ); } - - static isInVehicle(personnel: Personnel): boolean { - return personnel.metaPosition.type === 'vehicle'; - } } diff --git a/shared/src/models/simulated-region.ts b/shared/src/models/simulated-region.ts index 8d4f47cc4..1e170c7f5 100644 --- a/shared/src/models/simulated-region.ts +++ b/shared/src/models/simulated-region.ts @@ -1,9 +1,18 @@ import { Type } from 'class-transformer'; import { IsString, IsUUID, ValidateNested } from 'class-validator'; import { UUID, uuid, uuidValidationOptions } from '../utils'; +import { IsPosition } from '../utils/validators/is-position'; import { IsValue } from '../utils/validators'; -import { getCreate, Position, Size } from './utils'; -import type { ImageProperties } from './utils'; +import { + getCreate, + isInSimulatedRegion, + MapPosition, + Position, + currentSimulatedRegionIdOf, + Size, +} from './utils'; +import type { ImageProperties, MapCoordinates } from './utils'; +import type { WithPosition } from './utils/position/with-position'; export class SimulatedRegion { @IsUUID(4, uuidValidationOptions) @@ -14,9 +23,11 @@ export class SimulatedRegion { /** * top-left position + * + * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. */ @ValidateNested() - @Type(() => Position) + @IsPosition() public readonly position: Position; @ValidateNested() @@ -30,8 +41,8 @@ export class SimulatedRegion { * @param position top-left position * @deprecated Use {@link create} instead */ - constructor(position: Position, size: Size, name: string) { - this.position = position; + constructor(position: MapCoordinates, size: Size, name: string) { + this.position = MapPosition.create(position); this.size = size; this.name = name; } @@ -46,11 +57,11 @@ export class SimulatedRegion { static isInSimulatedRegion( region: SimulatedRegion, - position: Position + withPosition: WithPosition ): boolean { - // This class was copied from viewport.ts - // We will have to implement this logic differently - // later, for now, this is a stub method - return false; + return ( + isInSimulatedRegion(withPosition) && + currentSimulatedRegionIdOf(withPosition) === region.id + ); } } diff --git a/shared/src/models/transfer-point.ts b/shared/src/models/transfer-point.ts index 5aa895ca5..3691a5cac 100644 --- a/shared/src/models/transfer-point.ts +++ b/shared/src/models/transfer-point.ts @@ -1,4 +1,3 @@ -import { Type } from 'class-transformer'; import { IsString, IsUUID, ValidateNested } from 'class-validator'; import { UUID, uuid, UUIDSet, uuidValidationOptions } from '../utils'; import { @@ -6,8 +5,9 @@ import { IsUUIDSet, IsValue, } from '../utils/validators'; -import type { ImageProperties } from './utils'; -import { getCreate, Position } from './utils'; +import { IsPosition } from '../utils/validators/is-position'; +import type { ImageProperties, MapCoordinates } from './utils'; +import { MapPosition, Position, getCreate } from './utils'; export class TransferPoint { @IsUUID(4, uuidValidationOptions) @@ -16,8 +16,11 @@ export class TransferPoint { @IsValue('transferPoint' as const) public readonly type = 'transferPoint'; + /** + * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. + */ @ValidateNested() - @Type(() => Position) + @IsPosition() public readonly position: Position; @IsReachableTransferPoints() @@ -36,13 +39,13 @@ export class TransferPoint { * @deprecated Use {@link create} instead */ constructor( - position: Position, + position: MapCoordinates, reachableTransferPoints: ReachableTransferPoints, reachableHospitals: UUIDSet, internalName: string, externalName: string ) { - this.position = position; + this.position = MapPosition.create(position); this.reachableTransferPoints = reachableTransferPoints; this.reachableHospitals = reachableHospitals; this.internalName = internalName; diff --git a/shared/src/models/utils/get-create.ts b/shared/src/models/utils/get-create.ts index 5cccc3042..4dab6f921 100644 --- a/shared/src/models/utils/get-create.ts +++ b/shared/src/models/utils/get-create.ts @@ -9,7 +9,7 @@ import type { Constructor } from '../../utils'; * * @example * ```typescript - * export class Position { + * export class Coordinates { * @IsNumber() * public a: number; * /** diff --git a/shared/src/models/utils/index.ts b/shared/src/models/utils/index.ts index 3f5f846fd..7199bde54 100644 --- a/shared/src/models/utils/index.ts +++ b/shared/src/models/utils/index.ts @@ -1,10 +1,11 @@ -export { Position } from './position'; -export { MetaPosition } from './meta-position'; -export { MapPosition } from './map-position'; -export { VehiclePosition } from './vehicle-position'; -export { TransferPosition } from './transfer-position'; -export { SimulatedRegionPosition } from './simulated-region-position'; -export { MapCoordinates } from './map-coordinates'; +export { Position } from './position/position'; +export { WithPosition } from './position/with-position'; +export { MapPosition } from './position/map-position'; +export { VehiclePosition } from './position/vehicle-position'; +export { TransferPosition } from './position/transfer-position'; +export { SimulatedRegionPosition } from './position/simulated-region-position'; +export { MapCoordinates } from './position/map-coordinates'; +export * from './position/position-helpers'; export * from './patient-status'; export { Size } from './size'; export { Role } from './role'; diff --git a/shared/src/models/utils/map-position.ts b/shared/src/models/utils/map-position.ts deleted file mode 100644 index 726e0525f..000000000 --- a/shared/src/models/utils/map-position.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; -import { IsValue } from '../../utils/validators'; -import { getCreate } from './get-create'; -import { MapCoordinates } from './map-coordinates'; - -export class MapPosition { - @IsValue('coordinates') - public readonly type = 'coordinates'; - - @Type(() => MapCoordinates) - @ValidateNested() - public readonly position: MapCoordinates; - - /** - * @deprecated Use {@link create} instead - */ - constructor(position: MapCoordinates) { - this.position = position; - } - - static readonly create = getCreate(this); -} diff --git a/shared/src/models/utils/position.ts b/shared/src/models/utils/position.ts deleted file mode 100644 index f61d5a5f9..000000000 --- a/shared/src/models/utils/position.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { IsNumber } from 'class-validator'; -import { getCreate } from './get-create'; - -export class Position { - @IsNumber() - public readonly x: number; - - @IsNumber() - public readonly y: number; - - /** - * @deprecated Use {@link create} instead - */ - constructor(x: number, y: number) { - this.x = x; - this.y = y; - } - - static readonly create = getCreate(this); -} diff --git a/shared/src/models/utils/map-coordinates.ts b/shared/src/models/utils/position/map-coordinates.ts similarity index 89% rename from shared/src/models/utils/map-coordinates.ts rename to shared/src/models/utils/position/map-coordinates.ts index cab5696bd..75963fef0 100644 --- a/shared/src/models/utils/map-coordinates.ts +++ b/shared/src/models/utils/position/map-coordinates.ts @@ -1,5 +1,5 @@ import { IsNumber } from 'class-validator'; -import { getCreate } from './get-create'; +import { getCreate } from '../get-create'; export class MapCoordinates { @IsNumber() diff --git a/shared/src/models/utils/position/map-position.ts b/shared/src/models/utils/position/map-position.ts new file mode 100644 index 000000000..9dd5a121f --- /dev/null +++ b/shared/src/models/utils/position/map-position.ts @@ -0,0 +1,32 @@ +import { Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { IsValue } from '../../../utils/validators'; +import { getCreate } from '../get-create'; +import { MapCoordinates } from './map-coordinates'; +// import needed to display @link Links in Comments +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { isOnMap, isNotOnMap, currentCoordinatesOf } from './position-helpers'; + +export class MapPosition { + /** + * @deprecated Use {@link isOnMap } or {@link isNotOnMap} instead + */ + @IsValue('coordinates') + public readonly type = 'coordinates'; + + /** + * @deprecated Use {@link currentCoordinatesOf} instead + */ + @Type(() => MapCoordinates) + @ValidateNested() + public readonly coordinates: MapCoordinates; + + /** + * @deprecated Use {@link create} instead + */ + constructor(position: MapCoordinates) { + this.coordinates = position; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/models/utils/position/position-helpers-mutable.ts b/shared/src/models/utils/position/position-helpers-mutable.ts new file mode 100644 index 000000000..862fa8f22 --- /dev/null +++ b/shared/src/models/utils/position/position-helpers-mutable.ts @@ -0,0 +1,97 @@ +import type { ExerciseState } from '../../../state'; +import { getElement } from '../../../store/action-reducers/utils'; +import { updateTreatments } from '../../../store/action-reducers/utils/calculate-treatments'; +import type { SpatialElementType } from '../../../store/action-reducers/utils/spatial-elements'; +import { + removeElementPosition, + updateElementPosition, +} from '../../../store/action-reducers/utils/spatial-elements'; +import type { Mutable, UUID } from '../../../utils'; +import { cloneDeepMutable } from '../../../utils'; +import type { Position } from './position'; +import { + coordinatesOfPosition, + isPositionNotOnMap, + isPositionOnMap, + isNotOnMap, + isOnMap, +} from './position-helpers'; + +type MutablePosition = Mutable; + +interface WithMutablePosition { + position: MutablePosition; + type: MovableType; +} +interface WithMutablePositionAndId extends WithMutablePosition { + id: UUID; +} +type MovableType = + | 'alarmGroup' + | 'client' + | 'hospital' + | 'mapImage' + | 'material' + | 'patient' + | 'personnel' + | 'simulatedRegion' + | 'transferPoint' + | 'vehicle' + | 'viewport'; + +export function changePositionWithId( + of: UUID, + to: Position, + type: MovableType, + inState: Mutable +) { + changePosition(getElement(inState, type, of) as any, to, inState); +} + +export function changePosition( + of: WithMutablePosition, + to: Position, + inState: Mutable +) { + if ( + of.type === 'patient' || + of.type === 'personnel' || + of.type === 'material' + ) { + updateSpatialElementTree( + of as WithMutablePositionAndId, + to, + of.type, + inState + ); + of.position = cloneDeepMutable(to); + updateTreatments(inState, of as any); + return; + } + of.position = cloneDeepMutable(to); +} + +function updateSpatialElementTree( + element: WithMutablePositionAndId, + to: Position, + type: SpatialElementType, + state: Mutable +) { + if (isOnMap(element) && isPositionOnMap(to)) { + updateElementPosition( + state, + type, + element.id, + coordinatesOfPosition(to) + ); + } else if (isOnMap(element) && isPositionNotOnMap(to)) { + removeElementPosition(state, type, element.id); + } else if (isNotOnMap(element) && isPositionOnMap(to)) { + updateElementPosition( + state, + type, + element.id, + coordinatesOfPosition(to) + ); + } +} diff --git a/shared/src/models/utils/position/position-helpers.ts b/shared/src/models/utils/position/position-helpers.ts new file mode 100644 index 000000000..a87984a24 --- /dev/null +++ b/shared/src/models/utils/position/position-helpers.ts @@ -0,0 +1,133 @@ +import type { UUID } from '../../../utils'; +import type { Transfer } from '../transfer'; +import type { MapCoordinates } from './map-coordinates'; +import type { MapPosition } from './map-position'; +import type { Position } from './position'; +import type { SimulatedRegionPosition } from './simulated-region-position'; +import type { TransferPosition } from './transfer-position'; +import type { VehiclePosition } from './vehicle-position'; +import type { WithPosition } from './with-position'; + +export function isOnMap(withPosition: WithPosition): boolean { + return isPositionOnMap(withPosition.position); +} +export function isInVehicle(withPosition: WithPosition): boolean { + return isPositionInVehicle(withPosition.position); +} +export function isInTransfer(withPosition: WithPosition): boolean { + return isPositionInTransfer(withPosition.position); +} +export function isInSimulatedRegion(withPosition: WithPosition): boolean { + return isPositionInSimulatedRegion(withPosition.position); +} +export function isNotOnMap(withPosition: WithPosition): boolean { + return !isOnMap(withPosition); +} +export function isNotInVehicle(withPosition: WithPosition): boolean { + return !isInVehicle(withPosition); +} +export function isNotInTransfer(withPosition: WithPosition): boolean { + return !isInTransfer(withPosition); +} +export function isNotInSimulatedRegion(withPosition: WithPosition): boolean { + return !isInSimulatedRegion(withPosition); +} + +export function currentCoordinatesOf( + withPosition: WithPosition +): MapCoordinates { + if (isOnMap(withPosition)) { + return coordinatesOfPosition(withPosition.position); + } + throw new TypeError( + `Expected position of object to be on Map. Was of type ${withPosition.position.type}.` + ); +} + +export function currentVehicleIdOf(withPosition: WithPosition): UUID { + if (isInVehicle(withPosition)) { + return vehicleIdOfPosition(withPosition.position); + } + throw new TypeError( + `Expected position of object to be in vehicle. Was of type ${withPosition.position.type}.` + ); +} + +export function currentTransferOf(withPosition: WithPosition): Transfer { + if (isInTransfer(withPosition)) { + return transferOfPosition(withPosition.position); + } + throw new TypeError( + `Expected position of object to be in transfer. Was of type ${withPosition.position.type}.` + ); +} + +export function currentSimulatedRegionIdOf(withPosition: WithPosition): UUID { + if (isInSimulatedRegion(withPosition)) { + return simulatedRegionIdOfPosition(withPosition.position); + } + throw new TypeError( + `Expected position of object to be in simulatedRegion. Was of type ${withPosition.position.type}.` + ); +} + +export function isPositionOnMap(position: Position): boolean { + return position.type === 'coordinates'; +} +export function isPositionInVehicle(position: Position): boolean { + return position.type === 'vehicle'; +} +export function isPositionInTransfer(position: Position): boolean { + return position.type === 'transfer'; +} +export function isPositionInSimulatedRegion(position: Position): boolean { + return position.type === 'simulatedRegion'; +} +export function isPositionNotOnMap(position: Position): boolean { + return !isPositionOnMap(position); +} +export function isPositionNotInVehicle(position: Position): boolean { + return !isPositionInVehicle(position); +} +export function isPositionNotInTransfer(position: Position): boolean { + return !isPositionInTransfer(position); +} +export function isPositionNotInSimulatedRegion(position: Position): boolean { + return !isPositionInSimulatedRegion(position); +} + +export function coordinatesOfPosition(position: Position): MapCoordinates { + if (isPositionOnMap(position)) { + return (position as MapPosition).coordinates; + } + throw new TypeError( + `Expected position to be on Map. Was of type ${position.type}.` + ); +} + +export function vehicleIdOfPosition(position: Position): UUID { + if (isPositionInVehicle(position)) { + return (position as VehiclePosition).vehicleId; + } + throw new TypeError( + `Expected position to be in vehicle. Was of type ${position.type}.` + ); +} + +export function transferOfPosition(position: Position): Transfer { + if (isPositionInTransfer(position)) { + return (position as TransferPosition).transfer; + } + throw new TypeError( + `Expected position to be in transfer. Was of type ${position.type}.` + ); +} + +export function simulatedRegionIdOfPosition(position: Position): UUID { + if (isPositionInSimulatedRegion(position)) { + return (position as SimulatedRegionPosition).simulatedRegionId; + } + throw new TypeError( + `Expected position to be in simulatedRegion. Was of type ${position.type}.` + ); +} diff --git a/shared/src/models/utils/meta-position.ts b/shared/src/models/utils/position/position.ts similarity index 92% rename from shared/src/models/utils/meta-position.ts rename to shared/src/models/utils/position/position.ts index 888ee823b..055c0f545 100644 --- a/shared/src/models/utils/meta-position.ts +++ b/shared/src/models/utils/position/position.ts @@ -3,7 +3,7 @@ import type { SimulatedRegionPosition } from './simulated-region-position'; import type { TransferPosition } from './transfer-position'; import type { VehiclePosition } from './vehicle-position'; -export type MetaPosition = +export type Position = | MapPosition | SimulatedRegionPosition | TransferPosition diff --git a/shared/src/models/utils/position/simulated-region-position.ts b/shared/src/models/utils/position/simulated-region-position.ts new file mode 100644 index 000000000..fc1dc9fc6 --- /dev/null +++ b/shared/src/models/utils/position/simulated-region-position.ts @@ -0,0 +1,38 @@ +import { IsUUID } from 'class-validator'; +import { UUID } from '../../../utils'; +import { IsValue } from '../../../utils/validators'; +import { getCreate } from '../get-create'; +import { + // import needed to display @link Links in Comments + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isInSimulatedRegion, + // import needed to display @link Links in Comments + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isNotInSimulatedRegion, + // import needed to display @link Links in Comments + // eslint-disable-next-line @typescript-eslint/no-unused-vars + currentSimulatedRegionIdOf, +} from './position-helpers'; + +export class SimulatedRegionPosition { + /** + * @deprecated Use {@link isInSimulatedRegion } or {@link isNotInSimulatedRegion} instead + */ + @IsValue('simulatedRegion') + public readonly type = 'simulatedRegion'; + + /** + * @deprecated Use {@link currentSimulatedRegionIdOf } instead + */ + @IsUUID() + public readonly simulatedRegionId: UUID; + + /** + * @deprecated Use {@link create} instead + */ + constructor(simulatedRegionId: UUID) { + this.simulatedRegionId = simulatedRegionId; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/models/utils/position/transfer-position.ts b/shared/src/models/utils/position/transfer-position.ts new file mode 100644 index 000000000..f2f8b9672 --- /dev/null +++ b/shared/src/models/utils/position/transfer-position.ts @@ -0,0 +1,40 @@ +import { Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { IsValue } from '../../../utils/validators'; +import { getCreate } from '../get-create'; +import { Transfer } from '../transfer'; +import { + // import needed to display @link Links in Comments + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isInTransfer, + // import needed to display @link Links in Comments + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isNotInTransfer, + // import needed to display @link Links in Comments + // eslint-disable-next-line @typescript-eslint/no-unused-vars + currentTransferOf, +} from './position-helpers'; + +export class TransferPosition { + /** + * @deprecated Use {@link isInTransfer } or {@link isNotInTransfer} instead + */ + @IsValue('transfer') + public readonly type = 'transfer'; + + /** + * @deprecated Use {@link currentTransferOf } instead + */ + @Type(() => Transfer) + @ValidateNested() + public readonly transfer: Transfer; + + /** + * @deprecated Use {@link create} instead + */ + constructor(transfer: Transfer) { + this.transfer = transfer; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/models/utils/position/vehicle-position.ts b/shared/src/models/utils/position/vehicle-position.ts new file mode 100644 index 000000000..4f91a3e0b --- /dev/null +++ b/shared/src/models/utils/position/vehicle-position.ts @@ -0,0 +1,38 @@ +import { IsUUID } from 'class-validator'; +import { UUID } from '../../../utils'; +import { IsValue } from '../../../utils/validators'; +import { getCreate } from '../get-create'; +import { + // import needed to display @link Links in Comments + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isInVehicle, + // import needed to display @link Links in Comments + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isNotInVehicle, + // import needed to display @link Links in Comments + // eslint-disable-next-line @typescript-eslint/no-unused-vars + currentVehicleIdOf, +} from './position-helpers'; + +export class VehiclePosition { + /** + * @deprecated Use {@link isInVehicle } or {@link isNotInVehicle} instead + */ + @IsValue('vehicle') + public readonly type = 'vehicle'; + + /** + * @deprecated Use {@link currentVehicleIdOf } instead + */ + @IsUUID() + public readonly vehicleId: UUID; + + /** + * @deprecated Use {@link create} instead + */ + constructor(vehicleId: UUID) { + this.vehicleId = vehicleId; + } + + static readonly create = getCreate(this); +} diff --git a/shared/src/models/utils/position/with-position.ts b/shared/src/models/utils/position/with-position.ts new file mode 100644 index 000000000..9fca34434 --- /dev/null +++ b/shared/src/models/utils/position/with-position.ts @@ -0,0 +1,5 @@ +import type { Position } from './position'; + +export interface WithPosition { + readonly position: Position; +} diff --git a/shared/src/models/utils/simulated-region-position.ts b/shared/src/models/utils/simulated-region-position.ts deleted file mode 100644 index d797cdc66..000000000 --- a/shared/src/models/utils/simulated-region-position.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { UUID } from '../../utils'; -import { IsValue } from '../../utils/validators'; -import { getCreate } from './get-create'; - -export class SimulatedRegionPosition { - @IsValue('simulatedRegion') - public readonly type = 'simulatedRegion'; - - public readonly simulatedRegionId: UUID; - - /** - * @deprecated Use {@link create} instead - */ - constructor(simulatedRegionId: UUID) { - this.simulatedRegionId = simulatedRegionId; - } - - static readonly create = getCreate(this); -} diff --git a/shared/src/models/utils/spatial-tree.ts b/shared/src/models/utils/spatial-tree.ts index b617941aa..33ea781dc 100644 --- a/shared/src/models/utils/spatial-tree.ts +++ b/shared/src/models/utils/spatial-tree.ts @@ -9,7 +9,7 @@ import RBush from 'rbush'; import knn from 'rbush-knn'; import type { Mutable, UUID } from '../../utils'; import { ImmutableJsonObject } from '../../utils'; -import type { Position, Size } from '.'; +import type { MapCoordinates, Size } from '.'; import { getCreate } from '.'; /** @@ -64,7 +64,7 @@ export class SpatialTree { public static addElement( spatialTree: Mutable, elementId: UUID, - position: Position + position: MapCoordinates ) { const pointRBush = this.getPointRBush(spatialTree); pointRBush.insert({ @@ -77,7 +77,7 @@ export class SpatialTree { public static removeElement( spatialTree: Mutable, elementId: UUID, - position: Mutable | Position + position: MapCoordinates | Mutable ) { const pointRBush = this.getPointRBush(spatialTree); pointRBush.remove( @@ -93,8 +93,8 @@ export class SpatialTree { public static moveElement( spatialTree: Mutable, elementId: UUID, - startPosition: Mutable | Position, - targetPosition: Position + startPosition: MapCoordinates | Mutable, + targetPosition: MapCoordinates ) { // TODO: use the move function from RBush, when available: https://github.com/mourner/rbush/issues/28 this.removeElement(spatialTree, elementId, startPosition); @@ -109,7 +109,7 @@ export class SpatialTree { */ public static findAllElementsInCircle( spatialTree: Mutable, - circlePosition: Position, + circlePosition: MapCoordinates, radius: number ): UUID[] { // knn does not work great with `0`|`undefined` as it interprets either as `infinity` @@ -136,7 +136,7 @@ export class SpatialTree { */ public static findAllElementsInRectangle( spatialTree: Mutable, - topLeftPosition: Position, + topLeftPosition: MapCoordinates, size: Size ) { return this.getPointRBush(spatialTree).search({ @@ -154,12 +154,12 @@ export class SpatialTree { * @param id of the element */ interface PointRBushElement { - position: Position; + position: MapCoordinates; id: UUID; } /** - * An RBush that works with our {@link Position} format (elements being points) + * An RBush that works with our {@link MapCoordinates} format (elements being points) * @see https://github.com/mourner/rbush#data-format */ class PointRBush extends RBush { diff --git a/shared/src/models/utils/transfer-position.ts b/shared/src/models/utils/transfer-position.ts deleted file mode 100644 index 26e9b1a43..000000000 --- a/shared/src/models/utils/transfer-position.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Type } from 'class-transformer'; -import { ValidateNested } from 'class-validator'; -import { IsValue } from '../../utils/validators'; -import { getCreate } from './get-create'; -import { Transfer } from './transfer'; - -export class TransferPosition { - @IsValue('transfer') - public readonly type = 'transfer'; - - @Type(() => Transfer) - @ValidateNested() - public readonly transfer: Transfer; - - /** - * @deprecated Use {@link create} instead - */ - constructor(transfer: Transfer) { - this.transfer = transfer; - } - - static readonly create = getCreate(this); -} diff --git a/shared/src/models/utils/vehicle-position.ts b/shared/src/models/utils/vehicle-position.ts deleted file mode 100644 index 80970ae93..000000000 --- a/shared/src/models/utils/vehicle-position.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IsUUID } from 'class-validator'; -import { UUID } from '../../utils'; -import { IsValue } from '../../utils/validators'; -import { getCreate } from './get-create'; - -export class VehiclePosition { - @IsValue('vehicle') - public readonly type = 'vehicle'; - - @IsUUID() - public readonly vehicleId: UUID; - - /** - * @deprecated Use {@link create} instead - */ - constructor(vehicleId: UUID) { - this.vehicleId = vehicleId; - } - - static readonly create = getCreate(this); -} diff --git a/shared/src/models/vehicle.ts b/shared/src/models/vehicle.ts index e053b021a..cdc6fba41 100644 --- a/shared/src/models/vehicle.ts +++ b/shared/src/models/vehicle.ts @@ -1,17 +1,11 @@ import { Type } from 'class-transformer'; -import { - IsNumber, - IsOptional, - IsString, - IsUUID, - ValidateNested, -} from 'class-validator'; +import { IsNumber, IsString, IsUUID, ValidateNested } from 'class-validator'; import { uuid, uuidValidationOptions, UUID, UUIDSet } from '../utils'; import { IsUUIDSet, IsValue } from '../utils/validators'; -import { IsMetaPosition } from '../utils/validators/is-metaposition'; -import { getCreate, Position, Transfer } from './utils'; +import { IsPosition } from '../utils/validators/is-position'; +import { getCreate } from './utils'; import { ImageProperties } from './utils/image-properties'; -import { MetaPosition } from './utils/meta-position'; +import { Position } from './utils/position/position'; export class Vehicle { @IsUUID(4, uuidValidationOptions) @@ -32,32 +26,17 @@ export class Vehicle { @IsNumber() public readonly patientCapacity: number; - @IsMetaPosition() - @ValidateNested() - public readonly metaPosition: MetaPosition; - /** - * @deprecated use {@link metaPosition} - * Exclusive-or to {@link transfer} + * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. */ + @IsPosition() @ValidateNested() - @Type(() => Position) - @IsOptional() - public readonly position?: Position; + public readonly position: Position; @ValidateNested() @Type(() => ImageProperties) public readonly image: ImageProperties; - /** - * @deprecated use {@link metaPosition} - * Exclusive-or to {@link position} - */ - @ValidateNested() - @Type(() => Transfer) - @IsOptional() - public readonly transfer?: Transfer; - @IsUUIDSet() public readonly personnelIds: UUIDSet = {}; @@ -73,14 +52,14 @@ export class Vehicle { materialIds: UUIDSet, patientCapacity: number, image: ImageProperties, - metaPosition: MetaPosition + position: Position ) { this.vehicleType = vehicleType; this.name = name; this.materialIds = materialIds; this.patientCapacity = patientCapacity; this.image = image; - this.metaPosition = metaPosition; + this.position = position; } static readonly create = getCreate(this); diff --git a/shared/src/models/viewport.ts b/shared/src/models/viewport.ts index 4a147e472..081635bc4 100644 --- a/shared/src/models/viewport.ts +++ b/shared/src/models/viewport.ts @@ -1,9 +1,16 @@ import { Type } from 'class-transformer'; import { IsString, IsUUID, ValidateNested } from 'class-validator'; import { UUID, uuid, uuidValidationOptions } from '../utils'; +import { IsPosition } from '../utils/validators/is-position'; import { IsValue } from '../utils/validators'; -import { getCreate, Position, Size } from './utils'; -import type { ImageProperties } from './utils'; +import { + currentCoordinatesOf, + getCreate, + MapPosition, + Position, + Size, +} from './utils'; +import type { ImageProperties, MapCoordinates } from './utils'; export class Viewport { @IsUUID(4, uuidValidationOptions) @@ -14,9 +21,11 @@ export class Viewport { /** * top-left position + * + * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. */ @ValidateNested() - @Type(() => Position) + @IsPosition() public readonly position: Position; @ValidateNested() @@ -30,8 +39,8 @@ export class Viewport { * @param position top-left position * @deprecated Use {@link create} instead */ - constructor(position: Position, size: Size, name: string) { - this.position = position; + constructor(position: MapCoordinates, size: Size, name: string) { + this.position = MapPosition.create(position); this.size = size; this.name = name; } @@ -44,12 +53,14 @@ export class Viewport { aspectRatio: 1600 / 900, }; - static isInViewport(viewport: Viewport, position: Position): boolean { + static isInViewport(viewport: Viewport, position: MapCoordinates): boolean { return ( - viewport.position.x <= position.x && - position.x <= viewport.position.x + viewport.size.width && - viewport.position.y - viewport.size.height <= position.y && - position.y <= viewport.position.y + currentCoordinatesOf(viewport).x <= position.x && + position.x <= + currentCoordinatesOf(viewport).x + viewport.size.width && + currentCoordinatesOf(viewport).y - viewport.size.height <= + position.y && + position.y <= currentCoordinatesOf(viewport).y ); } } diff --git a/shared/src/state-helpers/create-vehicle-parameters.ts b/shared/src/state-helpers/create-vehicle-parameters.ts index bacbb4ef4..661973a9a 100644 --- a/shared/src/state-helpers/create-vehicle-parameters.ts +++ b/shared/src/state-helpers/create-vehicle-parameters.ts @@ -2,10 +2,10 @@ import type { Vehicle, VehicleTemplate } from '../models'; import { Material, Personnel } from '../models'; import type { MaterialTemplate } from '../models/material-template'; import type { PersonnelTemplate } from '../models/personnel-template'; -import type { PersonnelType, Position } from '../models/utils'; -import { MapPosition } from '../models/utils/map-position'; +import type { PersonnelType, MapCoordinates } from '../models/utils'; +import { MapPosition } from '../models/utils/position/map-position'; import type { MaterialType } from '../models/utils/material-type'; -import { VehiclePosition } from '../models/utils/vehicle-position'; +import { VehiclePosition } from '../models/utils/position/vehicle-position'; import { uuid } from '../utils'; import { arrayToUUIDSet } from '../utils/array-to-uuid-set'; @@ -22,7 +22,7 @@ export function createVehicleParameters( personnelTemplates: { [Key in PersonnelType]: PersonnelTemplate; }, - vehiclePosition: Position + vehiclePosition: MapCoordinates ): { materials: Material[]; personnel: Personnel[]; @@ -56,8 +56,7 @@ export function createVehicleParameters( image: vehicleTemplate.image, patientIds: {}, personnelIds: arrayToUUIDSet(personnel.map((p) => p.id)), - position: vehiclePosition, - metaPosition: MapPosition.create(vehiclePosition), + position: MapPosition.create(vehiclePosition), }; return { materials, diff --git a/shared/src/state-migrations/16-add-meta-position.ts b/shared/src/state-migrations/16-add-meta-position.ts index bca88a103..344228c2d 100644 --- a/shared/src/state-migrations/16-add-meta-position.ts +++ b/shared/src/state-migrations/16-add-meta-position.ts @@ -40,6 +40,17 @@ export const addMetaPosition16: Migration = { transfer?: any; metaPosition?: any; }; + materials: { + position?: { x: number; y: number }; + vehicleId?: UUID; + metaPosition?: any; + }[]; + personnel: { + position?: { x: number; y: number }; + transfer?: any; + vehicleId?: UUID; + metaPosition?: any; + }[]; }; if (typedAction.vehicle.position) { typedAction.vehicle.metaPosition = { @@ -54,6 +65,52 @@ export const addMetaPosition16: Migration = { type: 'transfer', transfer: typedAction.vehicle.transfer, }; + } else { + typedAction.vehicle.metaPosition = { + type: 'coordinates', + position: { + x: 0, + y: 0, + }, + }; + } + for (const personnel of typedAction.personnel) { + if (personnel.position) { + personnel.metaPosition = { + type: 'coordinates', + position: { + x: personnel.position.x, + y: personnel.position.y, + }, + }; + } else if (personnel.transfer) { + personnel.metaPosition = { + type: 'transfer', + transfer: personnel.transfer, + }; + } else if (personnel.vehicleId) { + personnel.metaPosition = { + type: 'vehicle', + vehicleId: personnel.vehicleId, + }; + } + } + + for (const material of typedAction.materials) { + if (material.position) { + material.metaPosition = { + type: 'coordinates', + position: { + x: material.position.x, + y: material.position.y, + }, + }; + } else if (material.vehicleId) { + material.metaPosition = { + type: 'vehicle', + vehicleId: material.vehicleId, + }; + } } } }); diff --git a/shared/src/state-migrations/18-replace-position-with-meta-position.ts b/shared/src/state-migrations/18-replace-position-with-meta-position.ts new file mode 100644 index 000000000..df30f0ea4 --- /dev/null +++ b/shared/src/state-migrations/18-replace-position-with-meta-position.ts @@ -0,0 +1,386 @@ +import type { UUID } from '../utils'; +import type { Migration } from './migration-functions'; + +export const replacePositionWithMetaPosition18: Migration = { + actions: (_initialState, actions) => { + actions.forEach((action) => { + if ( + (action as { type: string } | null)?.type === + '[Patient] Add patient' + ) { + const typedAction = action as { + patient: { + metaPosition?: + | any + | { type: 'coordinates'; position: any } + | { type: any }; + position: any; + }; + }; + if (typedAction.patient.metaPosition?.type === 'coordinates') { + typedAction.patient.metaPosition.coordinates = + typedAction.patient.metaPosition.position; + delete typedAction.patient.metaPosition.position; + } + typedAction.patient.position = typedAction.patient.metaPosition; + delete typedAction.patient.metaPosition; + } + if ( + (action as { type: string } | null)?.type === + '[Vehicle] Add vehicle' + ) { + const typedAction = action as { + vehicle: { + metaPosition?: + | any + | { type: 'coordinates'; position: any } + | { type: any }; + position: any; + }; + materials: { + metaPosition?: + | any + | { type: 'coordinates'; position: any } + | { type: any }; + position: any; + }[]; + personnel: { + transfer?: any; + metaPosition?: + | any + | { type: 'coordinates'; position: any } + | { type: any }; + position: any; + }[]; + }; + for (const material of typedAction.materials) { + if (material.metaPosition?.type === 'coordinates') { + material.metaPosition.coordinates = + material.metaPosition.position; + delete material.metaPosition.position; + } + material.position = material.metaPosition; + delete material.metaPosition; + } + + for (const personnel of typedAction.personnel) { + delete personnel.transfer; + if (personnel.metaPosition?.type === 'coordinates') { + personnel.metaPosition.coordinates = + personnel.metaPosition.position; + delete personnel.metaPosition.position; + } + personnel.position = personnel.metaPosition; + delete personnel.metaPosition; + } + + if (typedAction.vehicle.metaPosition?.type === 'coordinates') { + typedAction.vehicle.metaPosition.coordinates = + typedAction.vehicle.metaPosition.position; + delete typedAction.vehicle.metaPosition.position; + } + typedAction.vehicle.position = typedAction.vehicle.metaPosition; + delete typedAction.vehicle.metaPosition; + } + if ( + (action as { type: string } | null)?.type === + '[Viewport] Add viewport' + ) { + const typedAction = action as { + viewport: { + position: + | { + type: 'coordinates'; + coordinates: { x: number; y: number }; + } + | { x: number; y: number }; + }; + }; + typedAction.viewport.position = { + type: 'coordinates', + coordinates: { + x: ( + typedAction.viewport.position as { + x: number; + y: number; + } + ).x, + y: ( + typedAction.viewport.position as { + x: number; + y: number; + } + ).y, + }, + }; + } + if ( + (action as { type: string } | null)?.type === + '[SimulatedRegion] Add simulated region' + ) { + const typedAction = action as { + simulatedRegion: { + position: + | { + type: 'coordinates'; + coordinates: { x: number; y: number }; + } + | { x: number; y: number }; + }; + }; + typedAction.simulatedRegion.position = { + type: 'coordinates', + coordinates: { + x: ( + typedAction.simulatedRegion.position as { + x: number; + y: number; + } + ).x, + y: ( + typedAction.simulatedRegion.position as { + x: number; + y: number; + } + ).y, + }, + }; + } + if ( + (action as { type: string } | null)?.type === + '[MapImage] Add MapImage' + ) { + const typedAction = action as { + mapImage: { + position: + | { + type: 'coordinates'; + coordinates: { x: number; y: number }; + } + | { x: number; y: number }; + }; + }; + typedAction.mapImage.position = { + type: 'coordinates', + coordinates: { + x: ( + typedAction.mapImage.position as { + x: number; + y: number; + } + ).x, + y: ( + typedAction.mapImage.position as { + x: number; + y: number; + } + ).y, + }, + }; + } + if ( + (action as { type: string } | null)?.type === + '[TransferPoint] Add TransferPoint' + ) { + const typedAction = action as { + transferPoint: { + position: + | { + type: 'coordinates'; + coordinates: { x: number; y: number }; + } + | { x: number; y: number }; + }; + }; + typedAction.transferPoint.position = { + type: 'coordinates', + coordinates: { + x: ( + typedAction.transferPoint.position as { + x: number; + y: number; + } + ).x, + y: ( + typedAction.transferPoint.position as { + x: number; + y: number; + } + ).y, + }, + }; + } + }); + }, + state: (state) => { + const typedState = state as { + patients: { + [patientId: UUID]: { + vehicleId?: any; + transfer?: any; + metaPosition?: + | any + | { type: 'coordinates'; position: any } + | { type: any }; + position: any; + }; + }; + materials: { + [materialId: UUID]: { + metaPosition?: + | any + | { type: 'coordinates'; position: any } + | { type: any }; + position: any; + }; + }; + vehicles: { + [vehicleId: UUID]: { + transfer?: any; + metaPosition?: + | any + | { type: 'coordinates'; position: any } + | { type: any }; + position: any; + }; + }; + personnel: { + [personnelId: UUID]: { + transfer?: any; + metaPosition?: + | any + | { type: 'coordinates'; position: any } + | { type: any }; + position: any; + }; + }; + viewports: { + [viewportId: UUID]: { + position: + | { + type: 'coordinates'; + coordinates: { x: number; y: number }; + } + | { x: number; y: number }; + }; + }; + simulatedRegions: { + [simulatedRegionId: UUID]: { + position: + | { + type: 'coordinates'; + coordinates: { x: number; y: number }; + } + | { x: number; y: number }; + }; + }; + mapImages: { + [simulatedRegionId: UUID]: { + position: + | { + type: 'coordinates'; + coordinates: { x: number; y: number }; + } + | { x: number; y: number }; + }; + }; + transferPoints: { + [simulatedRegionId: UUID]: { + position: + | { + type: 'coordinates'; + coordinates: { x: number; y: number }; + } + | { x: number; y: number }; + }; + }; + }; + + Object.values(typedState.patients).forEach((patient) => { + delete patient.transfer; + delete patient.vehicleId; + if (patient.metaPosition?.type === 'coordinates') { + patient.metaPosition.coordinates = + patient.metaPosition.position; + delete patient.metaPosition.position; + } + patient.position = patient.metaPosition; + delete patient.metaPosition; + }); + + Object.values(typedState.materials).forEach((material) => { + if (material.metaPosition?.type === 'coordinates') { + material.metaPosition.coordinates = + material.metaPosition.position; + delete material.metaPosition.position; + } + material.position = material.metaPosition; + delete material.metaPosition; + }); + + Object.values(typedState.vehicles).forEach((vehicle) => { + delete vehicle.transfer; + + if (vehicle.metaPosition?.type === 'coordinates') { + vehicle.metaPosition.coordinates = + vehicle.metaPosition.position; + delete vehicle.metaPosition.position; + } + vehicle.position = vehicle.metaPosition; + delete vehicle.metaPosition; + }); + + Object.values(typedState.personnel).forEach((personnel) => { + delete personnel.transfer; + if (personnel.metaPosition?.type === 'coordinates') { + personnel.metaPosition.coordinates = + personnel.metaPosition.position; + delete personnel.metaPosition.position; + } + personnel.position = personnel.metaPosition; + delete personnel.metaPosition; + }); + Object.values(typedState.viewports).forEach((viewport) => { + viewport.position = { + type: 'coordinates', + coordinates: { + x: (viewport.position as { x: number; y: number }).x, + y: (viewport.position as { x: number; y: number }).y, + }, + }; + }); + Object.values(typedState.simulatedRegions).forEach( + (simulatedRegion) => { + simulatedRegion.position = { + type: 'coordinates', + coordinates: { + x: ( + simulatedRegion.position as { x: number; y: number } + ).x, + y: ( + simulatedRegion.position as { x: number; y: number } + ).y, + }, + }; + } + ); + Object.values(typedState.mapImages).forEach((mapImage) => { + mapImage.position = { + type: 'coordinates', + coordinates: { + x: (mapImage.position as { x: number; y: number }).x, + y: (mapImage.position as { x: number; y: number }).y, + }, + }; + }); + Object.values(typedState.transferPoints).forEach((transferPoint) => { + transferPoint.position = { + type: 'coordinates', + coordinates: { + x: (transferPoint.position as { x: number; y: number }).x, + y: (transferPoint.position as { x: number; y: number }).y, + }, + }; + }); + }, +}; diff --git a/shared/src/state-migrations/migration-functions.ts b/shared/src/state-migrations/migration-functions.ts index 965436bb5..780cdf86d 100644 --- a/shared/src/state-migrations/migration-functions.ts +++ b/shared/src/state-migrations/migration-functions.ts @@ -6,6 +6,7 @@ import { addPersonnelAndMaterialToState14 } from './14-add-personnel-and-materia import { addSimulatedRegions15 } from './15-add-simulated-regions'; import { addMetaPosition16 } from './16-add-meta-position'; import { addTypeProperty17 } from './17-add-type-property'; +import { replacePositionWithMetaPosition18 } from './18-replace-position-with-meta-position'; import { updateEocLog3 } from './3-update-eoc-log'; import { removeSetParticipantIdAction4 } from './4-remove-set-participant-id-action'; import { removeStatistics5 } from './5-remove-statistics'; @@ -57,4 +58,5 @@ export const migrations: { 15: addSimulatedRegions15, 16: addMetaPosition16, 17: addTypeProperty17, + 18: replacePositionWithMetaPosition18, }; diff --git a/shared/src/state.ts b/shared/src/state.ts index de96e2b71..646d02c7b 100644 --- a/shared/src/state.ts +++ b/shared/src/state.ts @@ -148,5 +148,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 = 17; + static readonly currentStateVersion = 18; } diff --git a/shared/src/store/action-reducers/exercise.ts b/shared/src/store/action-reducers/exercise.ts index 1fd29a514..c20d48425 100644 --- a/shared/src/store/action-reducers/exercise.ts +++ b/shared/src/store/action-reducers/exercise.ts @@ -8,9 +8,16 @@ import { } from 'class-validator'; import type { Personnel, Vehicle } from '../../models'; import { Patient } from '../../models'; -import { getStatus } from '../../models/utils'; +import { + getStatus, + isNotInTransfer, + currentTransferOf, + TransferPosition, +} from '../../models/utils'; +import { changePosition } from '../../models/utils/position/position-helpers-mutable'; import type { ExerciseState } from '../../state'; import type { Mutable } from '../../utils'; +import { cloneDeepMutable } from '../../utils'; import type { ElementTypePluralMap } from '../../utils/element-type-plural-map'; import { elementTypePluralMap } from '../../utils/element-type-plural-map'; import { IsValue } from '../../utils/validators'; @@ -145,15 +152,21 @@ function refreshTransfer( ): void { const elements = draftState[elementTypePluralMap[type]]; Object.values(elements).forEach((element: Mutable) => { - if (!element.transfer) { + if (isNotInTransfer(element)) { return; } - if (element.transfer.isPaused) { - element.transfer.endTimeStamp += tickInterval; + if (currentTransferOf(element).isPaused) { + const newTransfer = cloneDeepMutable(currentTransferOf(element)); + newTransfer.endTimeStamp += tickInterval; + changePosition( + element, + TransferPosition.create(newTransfer), + draftState + ); return; } // Not transferred yet - if (element.transfer.endTimeStamp > draftState.currentTime) { + if (currentTransferOf(element).endTimeStamp > draftState.currentTime) { return; } letElementArrive(draftState, type, element.id); diff --git a/shared/src/store/action-reducers/map-images.ts b/shared/src/store/action-reducers/map-images.ts index 7384033ff..d564fbaad 100644 --- a/shared/src/store/action-reducers/map-images.ts +++ b/shared/src/store/action-reducers/map-images.ts @@ -9,7 +9,8 @@ import { ValidateNested, } from 'class-validator'; import { MapImage } from '../../models'; -import { Position } from '../../models/utils'; +import { MapPosition, MapCoordinates } from '../../models/utils'; +import { changePosition } from '../../models/utils/position/position-helpers-mutable'; import type { ExerciseState } from '../../state'; import type { Mutable } from '../../utils'; import { @@ -39,8 +40,8 @@ export class MoveMapImageAction implements Action { public readonly mapImageId!: UUID; @ValidateNested() - @Type(() => Position) - public readonly targetPosition!: Position; + @Type(() => MapCoordinates) + public readonly targetPosition!: MapCoordinates; } export class ScaleMapImageAction implements Action { @@ -134,7 +135,11 @@ export namespace MapImagesActionReducers { action: MoveMapImageAction, reducer: (draftState, { mapImageId, targetPosition }) => { const mapImage = getElement(draftState, 'mapImage', mapImageId); - mapImage.position = cloneDeepMutable(targetPosition); + changePosition( + mapImage, + MapPosition.create(targetPosition), + draftState + ); return draftState; }, rights: 'trainer', diff --git a/shared/src/store/action-reducers/material.ts b/shared/src/store/action-reducers/material.ts index 4d1847dc4..681107209 100644 --- a/shared/src/store/action-reducers/material.ts +++ b/shared/src/store/action-reducers/material.ts @@ -1,10 +1,10 @@ import { Type } from 'class-transformer'; import { IsUUID, ValidateNested } from 'class-validator'; -import { Position } from '../../models/utils'; +import { MapPosition, MapCoordinates } from '../../models/utils'; +import { changePositionWithId } from '../../models/utils/position/position-helpers-mutable'; import { UUID, uuidValidationOptions } from '../../utils'; import { IsValue } from '../../utils/validators'; import type { Action, ActionReducer } from '../action-reducer'; -import { updateElementPosition } from './utils/spatial-elements'; export class MoveMaterialAction implements Action { @IsValue('[Material] Move material' as const) @@ -14,19 +14,19 @@ export class MoveMaterialAction implements Action { public readonly materialId!: UUID; @ValidateNested() - @Type(() => Position) - public readonly targetPosition!: Position; + @Type(() => MapCoordinates) + public readonly targetPosition!: MapCoordinates; } export namespace MaterialActionReducers { export const moveMaterial: ActionReducer = { action: MoveMaterialAction, reducer: (draftState, { materialId, targetPosition }) => { - updateElementPosition( - draftState, - 'material', + changePositionWithId( materialId, - targetPosition + MapPosition.create(targetPosition), + 'material', + draftState ); return draftState; }, diff --git a/shared/src/store/action-reducers/patient.ts b/shared/src/store/action-reducers/patient.ts index b3953393a..e8df777d5 100644 --- a/shared/src/store/action-reducers/patient.ts +++ b/shared/src/store/action-reducers/patient.ts @@ -2,10 +2,16 @@ import { Type } from 'class-transformer'; import { IsString, IsUUID, MaxLength, ValidateNested } from 'class-validator'; import { Patient } from '../../models'; import { + isOnMap, + MapPosition, PatientStatus, patientStatusAllowedValues, - Position, + MapCoordinates, } from '../../models/utils'; +import { + changePosition, + changePositionWithId, +} from '../../models/utils/position/position-helpers-mutable'; import type { ExerciseState } from '../../state'; import type { Mutable } from '../../utils'; import { @@ -19,11 +25,7 @@ import type { Action, ActionReducer } from '../action-reducer'; import { ReducerError } from '../reducer-error'; import { updateTreatments } from './utils/calculate-treatments'; import { getElement } from './utils/get-element'; -import { - addElementPosition, - removeElementPosition, - updateElementPosition, -} from './utils/spatial-elements'; +import { removeElementPosition } from './utils/spatial-elements'; export function deletePatient( draftState: Mutable, @@ -49,8 +51,8 @@ export class MovePatientAction implements Action { public readonly patientId!: UUID; @ValidateNested() - @Type(() => Position) - public readonly targetPosition!: Position; + @Type(() => MapCoordinates) + public readonly targetPosition!: MapCoordinates; } export class RemovePatientAction implements Action { @@ -120,7 +122,7 @@ export namespace PatientActionReducers { } const mutablePatient = cloneDeepMutable(patient); draftState.patients[mutablePatient.id] = mutablePatient; - addElementPosition(draftState, 'patient', mutablePatient.id); + changePosition(mutablePatient, patient.position, draftState); return draftState; }, rights: 'trainer', @@ -129,11 +131,11 @@ export namespace PatientActionReducers { export const movePatient: ActionReducer = { action: MovePatientAction, reducer: (draftState, { patientId, targetPosition }) => { - updateElementPosition( - draftState, - 'patient', + changePositionWithId( patientId, - targetPosition + MapPosition.create(targetPosition), + 'patient', + draftState ); return draftState; }, @@ -155,7 +157,7 @@ export namespace PatientActionReducers { const patient = getElement(draftState, 'patient', patientId); patient.pretriageStatus = patientStatus; - if (patient.metaPosition.type === 'coordinates') { + if (isOnMap(patient)) { updateTreatments(draftState, patient); } diff --git a/shared/src/store/action-reducers/personnel.ts b/shared/src/store/action-reducers/personnel.ts index 843c122ed..0078da337 100644 --- a/shared/src/store/action-reducers/personnel.ts +++ b/shared/src/store/action-reducers/personnel.ts @@ -1,10 +1,10 @@ import { Type } from 'class-transformer'; import { IsUUID, ValidateNested } from 'class-validator'; -import { Position } from '../../models/utils'; +import { MapPosition, MapCoordinates } from '../../models/utils'; +import { changePositionWithId } from '../../models/utils/position/position-helpers-mutable'; import { UUID, uuidValidationOptions } from '../../utils'; import { IsValue } from '../../utils/validators'; import type { Action, ActionReducer } from '../action-reducer'; -import { updateElementPosition } from './utils/spatial-elements'; export class MovePersonnelAction implements Action { @IsValue('[Personnel] Move personnel' as const) @@ -14,19 +14,19 @@ export class MovePersonnelAction implements Action { public readonly personnelId!: UUID; @ValidateNested() - @Type(() => Position) - public readonly targetPosition!: Position; + @Type(() => MapCoordinates) + public readonly targetPosition!: MapCoordinates; } export namespace PersonnelActionReducers { export const movePersonnel: ActionReducer = { action: MovePersonnelAction, reducer: (draftState, { personnelId, targetPosition }) => { - updateElementPosition( - draftState, - 'personnel', + changePositionWithId( personnelId, - targetPosition + MapPosition.create(targetPosition), + 'personnel', + draftState ); return draftState; }, diff --git a/shared/src/store/action-reducers/simulated-region.ts b/shared/src/store/action-reducers/simulated-region.ts index 4de91f2ec..b36f437f6 100644 --- a/shared/src/store/action-reducers/simulated-region.ts +++ b/shared/src/store/action-reducers/simulated-region.ts @@ -1,7 +1,11 @@ import { Type } from 'class-transformer'; import { IsString, IsUUID, ValidateNested } from 'class-validator'; import { SimulatedRegion } from '../../models'; -import { Position, Size } from '../../models/utils'; +import { MapCoordinates, MapPosition, Size } from '../../models/utils'; +import { + changePosition, + changePositionWithId, +} from '../../models/utils/position/position-helpers-mutable'; import { cloneDeepMutable, UUID, uuidValidationOptions } from '../../utils'; import { IsValue } from '../../utils/validators'; import type { Action, ActionReducer } from '../action-reducer'; @@ -28,8 +32,8 @@ export class MoveSimulatedRegionAction implements Action { @IsUUID(4, uuidValidationOptions) public readonly simulatedRegionId!: UUID; @ValidateNested() - @Type(() => Position) - public readonly targetPosition!: Position; + @Type(() => MapCoordinates) + public readonly targetPosition!: MapCoordinates; } export class ResizeSimulatedRegionAction implements Action { @@ -38,8 +42,8 @@ export class ResizeSimulatedRegionAction implements Action { @IsUUID(4, uuidValidationOptions) public readonly simulatedRegionId!: UUID; @ValidateNested() - @Type(() => Position) - public readonly targetPosition!: Position; + @Type(() => MapCoordinates) + public readonly targetPosition!: MapCoordinates; @ValidateNested() @Type(() => Size) public readonly newSize!: Size; @@ -82,12 +86,12 @@ export namespace SimulatedRegionActionReducers { { action: MoveSimulatedRegionAction, reducer: (draftState, { simulatedRegionId, targetPosition }) => { - const simulatedRegion = getElement( - draftState, + changePositionWithId( + simulatedRegionId, + MapPosition.create(targetPosition), 'simulatedRegion', - simulatedRegionId + draftState ); - simulatedRegion.position = cloneDeepMutable(targetPosition); return draftState; }, rights: 'trainer', @@ -105,7 +109,11 @@ export namespace SimulatedRegionActionReducers { 'simulatedRegion', simulatedRegionId ); - simulatedRegion.position = cloneDeepMutable(targetPosition); + changePosition( + simulatedRegion, + MapPosition.create(targetPosition), + draftState + ); simulatedRegion.size = cloneDeepMutable(newSize); return draftState; }, diff --git a/shared/src/store/action-reducers/transfer-point.ts b/shared/src/store/action-reducers/transfer-point.ts index f72d916c9..2e2abf06d 100644 --- a/shared/src/store/action-reducers/transfer-point.ts +++ b/shared/src/store/action-reducers/transfer-point.ts @@ -7,7 +7,14 @@ import { ValidateNested, } from 'class-validator'; import { TransferPoint } from '../../models'; -import { Position } from '../../models/utils'; +import { + currentCoordinatesOf, + isInTransfer, + MapCoordinates, + MapPosition, + currentTransferOf, +} from '../../models/utils'; +import { changePositionWithId } from '../../models/utils/position/position-helpers-mutable'; import { cloneDeepMutable, UUID, uuidValidationOptions } from '../../utils'; import { IsValue } from '../../utils/validators'; import type { Action, ActionReducer } from '../action-reducer'; @@ -34,8 +41,8 @@ export class MoveTransferPointAction implements Action { public readonly transferPointId!: UUID; @ValidateNested() - @Type(() => Position) - public readonly targetPosition!: Position; + @Type(() => MapCoordinates) + public readonly targetPosition!: MapCoordinates; } export class RenameTransferPointAction implements Action { @@ -122,12 +129,12 @@ export namespace TransferPointActionReducers { export const moveTransferPoint: ActionReducer = { action: MoveTransferPointAction, reducer: (draftState, { transferPointId, targetPosition }) => { - const transferPoint = getElement( - draftState, + changePositionWithId( + transferPointId, + MapPosition.create(targetPosition), 'transferPoint', - transferPointId + draftState ); - transferPoint.position = cloneDeepMutable(targetPosition); return draftState; }, rights: 'trainer', @@ -184,8 +191,8 @@ export namespace TransferPointActionReducers { const _duration = duration ?? estimateDuration( - transferPoint1.position, - transferPoint2.position + currentCoordinatesOf(transferPoint1), + currentCoordinatesOf(transferPoint2) ); transferPoint1.reachableTransferPoints[transferPointId2] = { duration: _duration, @@ -240,8 +247,9 @@ export namespace TransferPointActionReducers { vehicleId ); if ( - vehicle.transfer?.targetTransferPointId === - transferPointId + isInTransfer(vehicle) && + currentTransferOf(vehicle).targetTransferPointId === + transferPointId ) { letElementArrive(draftState, vehicle.type, vehicleId); } @@ -253,8 +261,9 @@ export namespace TransferPointActionReducers { personnelId ); if ( - personnel.transfer?.targetTransferPointId === - transferPointId + isInTransfer(personnel) && + currentTransferOf(personnel).targetTransferPointId === + transferPointId ) { letElementArrive( draftState, @@ -326,7 +335,10 @@ export namespace TransferPointActionReducers { * @returns an estimated duration in ms to drive between the the two given positions * The resulting value is a multiple of 0.1 minutes. */ -function estimateDuration(startPosition: Position, targetPosition: Position) { +function estimateDuration( + startPosition: MapCoordinates, + targetPosition: MapCoordinates +) { // TODO: tweak these values more // How long in ms it takes to start + stop moving const overheadSummand = 10 * 1000; diff --git a/shared/src/store/action-reducers/transfer.ts b/shared/src/store/action-reducers/transfer.ts index eb0b2f5f1..d21a3304f 100644 --- a/shared/src/store/action-reducers/transfer.ts +++ b/shared/src/store/action-reducers/transfer.ts @@ -1,8 +1,18 @@ import { Type } from 'class-transformer'; import { IsInt, IsOptional, IsUUID, ValidateNested } from 'class-validator'; import { TransferPoint } from '../../models'; -import type { Position } from '../../models/utils'; -import { StartPoint, startPointTypeOptions } from '../../models/utils'; +import type { MapCoordinates } from '../../models/utils'; +import { + isInTransfer, + isNotInTransfer, + currentTransferOf, + TransferPosition, + currentCoordinatesOf, + MapPosition, + StartPoint, + startPointTypeOptions, +} from '../../models/utils'; +import { changePosition } from '../../models/utils/position/position-helpers-mutable'; import type { ExerciseState } from '../../state'; import { imageSizeToPosition } from '../../state-helpers'; import type { Mutable } from '../../utils'; @@ -12,10 +22,6 @@ import { IsLiteralUnion, IsValue } from '../../utils/validators'; import type { Action, ActionReducer } from '../action-reducer'; import { ReducerError } from '../reducer-error'; import { getElement } from './utils'; -import { - removeElementPosition, - updateElementPosition, -} from './utils/spatial-elements'; export type TransferableElementType = 'personnel' | 'vehicle'; const transferableElementTypeAllowedValues: AllowedValues = @@ -33,31 +39,22 @@ export function letElementArrive( ) { const element = getElement(draftState, elementType, elementId); // check that element is in transfer, this should be always the case where this function is used - if (!element.transfer) { + if (isNotInTransfer(element)) { throw getNotInTransferError(element.id); } const targetTransferPoint = getElement( draftState, 'transferPoint', - element.transfer.targetTransferPointId + currentTransferOf(element).targetTransferPointId ); - const newPosition: Mutable = { - x: targetTransferPoint.position.x, + const newPosition: Mutable = { + x: currentCoordinatesOf(targetTransferPoint).x, y: - targetTransferPoint.position.y + + currentCoordinatesOf(targetTransferPoint).y + // Position it in the upper half of the transferPoint imageSizeToPosition(TransferPoint.image.height / 3), }; - if (elementType === 'personnel') { - updateElementPosition(draftState, 'personnel', element.id, newPosition); - } else { - element.position = newPosition; - element.metaPosition = { - type: 'coordinates', - position: newPosition, - }; - } - delete element.transfer; + changePosition(element, MapPosition.create(newPosition), draftState); } export class AddToTransferAction implements Action { @@ -137,7 +134,8 @@ export namespace TransferActionReducers { // check if transferPoint exists getElement(draftState, 'transferPoint', targetTransferPointId); const element = getElement(draftState, elementType, elementId); - if (element.transfer) { + + if (isInTransfer(element)) { throw new ReducerError( `Element with id ${element.id} is already in transfer` ); @@ -165,29 +163,17 @@ export namespace TransferActionReducers { duration = startPoint.duration; } - // Remove the position of the element - if (elementType === 'personnel') { - removeElementPosition(draftState, 'personnel', element.id); - } else { - element.position = undefined; - } // Set the element to transfer - element.transfer = { - startPoint: cloneDeepMutable(startPoint), - targetTransferPointId, - endTimeStamp: draftState.currentTime + duration, - isPaused: false, - }; - - element.metaPosition = { - type: 'transfer', - transfer: { + changePosition( + element, + TransferPosition.create({ startPoint: cloneDeepMutable(startPoint), targetTransferPointId, endTimeStamp: draftState.currentTime + duration, isPaused: false, - }, - }; + }), + draftState + ); return draftState; }, @@ -201,21 +187,28 @@ export namespace TransferActionReducers { { elementType, elementId, targetTransferPointId, timeToAdd } ) => { const element = getElement(draftState, elementType, elementId); - if (!element.transfer) { + if (isNotInTransfer(element)) { throw getNotInTransferError(element.id); } + const newTransfer = cloneDeepMutable(currentTransferOf(element)); if (targetTransferPointId) { // check if transferPoint exists + getElement(draftState, 'transferPoint', targetTransferPointId); - element.transfer.targetTransferPointId = targetTransferPointId; + newTransfer.targetTransferPointId = targetTransferPointId; } if (timeToAdd) { // The endTimeStamp shouldn't be less then the current time - element.transfer.endTimeStamp = Math.max( + newTransfer.endTimeStamp = Math.max( draftState.currentTime, - element.transfer.endTimeStamp + timeToAdd + newTransfer.endTimeStamp + timeToAdd ); } + changePosition( + element, + TransferPosition.create(newTransfer), + draftState + ); return draftState; }, rights: 'trainer', @@ -230,7 +223,7 @@ export namespace TransferActionReducers { // check if transferPoint exists getElement(draftState, 'transferPoint', targetTransferPointId); const element = getElement(draftState, elementType, elementId); - if (!element.transfer) { + if (isNotInTransfer(element)) { throw getNotInTransferError(element.id); } letElementArrive(draftState, elementType, elementId); @@ -244,10 +237,18 @@ export namespace TransferActionReducers { action: TogglePauseTransferAction, reducer: (draftState, { elementType, elementId }) => { const element = getElement(draftState, elementType, elementId); - if (!element.transfer) { + if (isNotInTransfer(element)) { throw getNotInTransferError(element.id); } - element.transfer.isPaused = !element.transfer.isPaused; + const newTransfer = cloneDeepMutable( + currentTransferOf(element) + ); + newTransfer.isPaused = !newTransfer.isPaused; + changePosition( + element, + TransferPosition.create(newTransfer), + draftState + ); return draftState; }, rights: 'trainer', diff --git a/shared/src/store/action-reducers/utils/calculate-distance.ts b/shared/src/store/action-reducers/utils/calculate-distance.ts index 66fd06b9b..986f53d8a 100644 --- a/shared/src/store/action-reducers/utils/calculate-distance.ts +++ b/shared/src/store/action-reducers/utils/calculate-distance.ts @@ -1,8 +1,8 @@ -import type { Position } from '../../../models/utils'; +import type { MapCoordinates } from '../../../models/utils'; /** * @returns the distance between the two positions in meters. */ -export function calculateDistance(a: Position, b: Position) { +export function calculateDistance(a: MapCoordinates, b: MapCoordinates) { return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2); } diff --git a/shared/src/store/action-reducers/utils/calculate-treatments.spec.ts b/shared/src/store/action-reducers/utils/calculate-treatments.spec.ts index ca0fde55a..d48852cd7 100644 --- a/shared/src/store/action-reducers/utils/calculate-treatments.spec.ts +++ b/shared/src/store/action-reducers/utils/calculate-treatments.spec.ts @@ -4,11 +4,16 @@ import { defaultMaterialTemplates } from '../../../data/default-state/material-t import { defaultPersonnelTemplates } from '../../../data/default-state/personnel-templates'; import type { Patient } from '../../../models'; import { Material, Personnel } from '../../../models'; -import type { MetaPosition, PatientStatus } from '../../../models/utils'; -import { CanCaterFor, Position } from '../../../models/utils'; -import { MapPosition } from '../../../models/utils/map-position'; +import type { Position, PatientStatus } from '../../../models/utils'; +import { + currentCoordinatesOf, + isPositionOnMap, + CanCaterFor, + MapCoordinates, +} from '../../../models/utils'; +import { MapPosition } from '../../../models/utils/position/map-position'; import { SpatialTree } from '../../../models/utils/spatial-tree'; -import { VehiclePosition } from '../../../models/utils/vehicle-position'; +import { VehiclePosition } from '../../../models/utils/position/vehicle-position'; import { ExerciseState } from '../../../state'; import type { Mutable, UUID } from '../../../utils'; import { cloneDeepMutable, uuid } from '../../../utils'; @@ -71,37 +76,33 @@ function addPatient( state: Mutable, pretriageStatus: PatientStatus, realStatus: PatientStatus, - position?: Position + position?: MapCoordinates ): Mutable { const patient = cloneDeepMutable(generateDummyPatient()); patient.pretriageStatus = pretriageStatus; patient.realStatus = realStatus; if (position) { - patient.position = cloneDeepMutable(position); - patient.metaPosition = { + patient.position = { type: 'coordinates', - position: cloneDeepMutable(position), + coordinates: cloneDeepMutable(position), }; SpatialTree.addElement( state.spatialTrees.patients, patient.id, - patient.position + position ); } state.patients[patient.id] = patient; return patient; } -function addPersonnel( - state: Mutable, - metaPosition: MetaPosition -) { +function addPersonnel(state: Mutable, position: Position) { const personnel = cloneDeepMutable( Personnel.generatePersonnel( defaultPersonnelTemplates.notSan, uuid(), 'RTW 3/83/1', - metaPosition + position ) ); personnel.canCaterFor = { @@ -110,28 +111,24 @@ function addPersonnel( green: 0, logicalOperator: 'and', }; - if (metaPosition.type === 'coordinates') { - personnel.position = cloneDeepMutable(metaPosition.position); + if (isPositionOnMap(position)) { SpatialTree.addElement( state.spatialTrees.personnel, personnel.id, - personnel.position + currentCoordinatesOf(personnel) ); } state.personnel[personnel.id] = personnel; return personnel; } -function addMaterial( - state: Mutable, - metaPosition: MetaPosition -) { +function addMaterial(state: Mutable, position: Position) { const material = cloneDeepMutable( Material.generateMaterial( defaultMaterialTemplates.standard, uuid(), 'RTW 3/83/1', - metaPosition + position ) ); material.canCaterFor = { @@ -140,12 +137,11 @@ function addMaterial( green: 0, logicalOperator: 'and', }; - if (metaPosition.type === 'coordinates') { - material.position = cloneDeepMutable(metaPosition.position); + if (isPositionOnMap(position)) { SpatialTree.addElement( state.spatialTrees.materials, material.id, - material.position + currentCoordinatesOf(material) ); } state.materials[material.id] = material; @@ -222,7 +218,12 @@ describe('calculate treatment', () => { (state) => { (['green', 'yellow', 'red'] as PatientStatus[]).forEach( (color) => { - addPatient(state, color, color, Position.create(0, 0)); + addPatient( + state, + color, + color, + MapCoordinates.create(0, 0) + ); } ); } @@ -233,7 +234,12 @@ describe('calculate treatment', () => { it('does nothing when there are only dead patients', () => { const { beforeState, newState } = setupStateAndApplyTreatments( (state) => { - addPatient(state, 'black', 'black', Position.create(0, 0)); + addPatient( + state, + 'black', + 'black', + MapCoordinates.create(0, 0) + ); } ); expect(newState).toStrictEqual(beforeState); @@ -242,7 +248,12 @@ describe('calculate treatment', () => { it('does nothing when all personnel is in a vehicle', () => { const { beforeState, newState } = setupStateAndApplyTreatments( (state) => { - addPatient(state, 'green', 'green', Position.create(0, 0)); + addPatient( + state, + 'green', + 'green', + MapCoordinates.create(0, 0) + ); addPersonnel(state, VehiclePosition.create('')); } ); @@ -252,7 +263,12 @@ describe('calculate treatment', () => { it('does nothing when all material is in a vehicle', () => { const { beforeState, newState } = setupStateAndApplyTreatments( (state) => { - addPatient(state, 'green', 'green', Position.create(0, 0)); + addPatient( + state, + 'green', + 'green', + MapCoordinates.create(0, 0) + ); addMaterial(state, VehiclePosition.create('')); } ); @@ -271,13 +287,13 @@ describe('calculate treatment', () => { state, 'green', 'green', - Position.create(0, 0) + MapCoordinates.create(0, 0) ).id; ids.redPatient = addPatient( state, 'red', 'red', - Position.create(2, 2) + MapCoordinates.create(2, 2) ).id; ids.material = addMaterial( state, @@ -306,13 +322,13 @@ describe('calculate treatment', () => { state, 'green', 'green', - Position.create(-3, -3) + MapCoordinates.create(-3, -3) ).id; ids.redPatient = addPatient( state, 'red', 'red', - Position.create(3, 3) + MapCoordinates.create(3, 3) ).id; ids.material = addMaterial( state, @@ -320,7 +336,6 @@ describe('calculate treatment', () => { ).id; } ); - console.log(ids); assertCatering(beforeState, newState, [ { catererId: ids.material, @@ -342,13 +357,13 @@ describe('calculate treatment', () => { state, 'green', 'green', - Position.create(-10, -10) + MapCoordinates.create(-10, -10) ).id; ids.redPatient = addPatient( state, 'red', 'red', - Position.create(20, 20) + MapCoordinates.create(20, 20) ).id; ids.material = addMaterial( state, @@ -371,13 +386,13 @@ describe('calculate treatment', () => { state, 'green', 'green', - Position.create(-1, -1) + MapCoordinates.create(-1, -1) ).id; ids.redPatient = addPatient( state, 'red', 'red', - Position.create(2, 2) + MapCoordinates.create(2, 2) ).id; const material = addMaterial( state, diff --git a/shared/src/store/action-reducers/utils/calculate-treatments.ts b/shared/src/store/action-reducers/utils/calculate-treatments.ts index 090729406..514c07cd1 100644 --- a/shared/src/store/action-reducers/utils/calculate-treatments.ts +++ b/shared/src/store/action-reducers/utils/calculate-treatments.ts @@ -1,7 +1,8 @@ import { groupBy } from 'lodash-es'; import type { Material, Personnel } from '../../../models'; import { Patient } from '../../../models'; -import type { PatientStatus, Position } from '../../../models/utils'; +import type { MapCoordinates, PatientStatus } from '../../../models/utils'; +import { currentCoordinatesOf, isNotOnMap } from '../../../models/utils'; import { SpatialTree } from '../../../models/utils/spatial-tree'; import type { ExerciseState } from '../../../state'; import { maxTreatmentRange } from '../../../state-helpers/max-treatment-range'; @@ -99,7 +100,7 @@ function tryToCaterFor( */ function updateCateringAroundPatient( state: Mutable, - position: Position, + position: MapCoordinates, elementType: 'material' | 'personnel', elementIdsToBeSkipped: Set ) { @@ -169,7 +170,7 @@ export function updateTreatments( // Currently, the treatment pattern algorithm is stable. This means that completely done from scratch, // the result would semantically be the same. This could be changed later. - if (element.position === undefined) { + if (isNotOnMap(element)) { // The element is no longer in a position (get it?!) to be treated or treat a patient removeTreatmentsOfElement(state, element); return; @@ -196,13 +197,13 @@ export function updateTreatments( updateCateringAroundPatient( state, - element.position, + currentCoordinatesOf(element), 'personnel', alreadyUpdatedElementIds ); updateCateringAroundPatient( state, - element.position, + currentCoordinatesOf(element), 'material', alreadyUpdatedElementIds ); @@ -225,7 +226,7 @@ function updateCatering( cateringElement.canCaterFor.yellow === 0 && cateringElement.canCaterFor.green === 0) || // The element is no longer in a position to treat a patient - cateringElement.position === undefined + isNotOnMap(cateringElement) ) { return; } @@ -243,7 +244,7 @@ function updateCatering( if (cateringElement.overrideTreatmentRange > 0) { const patientIdsInOverrideRange = SpatialTree.findAllElementsInCircle( state.spatialTrees.patients, - cateringElement.position, + currentCoordinatesOf(cateringElement), cateringElement.overrideTreatmentRange ); // In the overrideTreatmentRange (the override circle) only the distance to the patient is important - his/her injuries are ignored @@ -272,7 +273,7 @@ function updateCatering( const patientsInTreatmentRange: Mutable[] = SpatialTree.findAllElementsInCircle( state.spatialTrees.patients, - cateringElement.position, + currentCoordinatesOf(cateringElement), cateringElement.treatmentRange ) // Filter out every patient in the overrideTreatmentRange diff --git a/shared/src/store/action-reducers/utils/spatial-elements.ts b/shared/src/store/action-reducers/utils/spatial-elements.ts index 43fa7a604..04f03bec4 100644 --- a/shared/src/store/action-reducers/utils/spatial-elements.ts +++ b/shared/src/store/action-reducers/utils/spatial-elements.ts @@ -1,11 +1,15 @@ -import type { Position } from '../../../models/utils'; +import type { MapCoordinates } from '../../../models/utils'; +import { + isOnMap, + isNotOnMap, + currentCoordinatesOf, +} from '../../../models/utils'; import { SpatialTree } from '../../../models/utils/spatial-tree'; import type { ExerciseState } from '../../../state'; import type { Mutable, UUID } from '../../../utils'; import { cloneDeepMutable } from '../../../utils'; import type { ElementTypePluralMap } from '../../../utils/element-type-plural-map'; import { elementTypePluralMap } from '../../../utils/element-type-plural-map'; -import { updateTreatments } from './calculate-treatments'; import { getElement } from './get-element'; /** @@ -13,7 +17,7 @@ import { getElement } from './get-element'; * The position of the element must be changed via one of the function in this file. * In addition, the respective functions must be called when an element gets added or removed. */ -type SpatialElementType = 'material' | 'patient' | 'personnel'; +export type SpatialElementType = 'material' | 'patient' | 'personnel'; type SpatialTypePluralMap = Pick; export type SpatialElementPlural = SpatialTypePluralMap[SpatialElementType]; @@ -27,15 +31,14 @@ export function addElementPosition( elementId: UUID ) { const element = getElement(state, elementType, elementId); - if (element.position === undefined) { + if (isNotOnMap(element)) { return; } SpatialTree.addElement( state.spatialTrees[elementTypePluralMap[elementType]], element.id, - element.position + currentCoordinatesOf(element) ); - updateTreatments(state, element); } /** @@ -45,11 +48,11 @@ export function updateElementPosition( state: Mutable, elementType: SpatialElementType, elementId: UUID, - targetPosition: Position + targetPosition: MapCoordinates ) { const element = getElement(state, elementType, elementId); - const startPosition = element.position; - if (startPosition !== undefined) { + if (isOnMap(element)) { + const startPosition = cloneDeepMutable(currentCoordinatesOf(element)); SpatialTree.moveElement( state.spatialTrees[elementTypePluralMap[elementType]], element.id, @@ -63,12 +66,6 @@ export function updateElementPosition( targetPosition ); } - element.position = cloneDeepMutable(targetPosition); - element.metaPosition = { - type: 'coordinates', - position: cloneDeepMutable(targetPosition), - }; - updateTreatments(state, element); } /** @@ -81,14 +78,12 @@ export function removeElementPosition( elementId: UUID ) { const element = getElement(state, elementType, elementId); - if (element.position === undefined) { + if (isNotOnMap(element)) { return; } SpatialTree.removeElement( state.spatialTrees[elementTypePluralMap[elementType]], element.id, - element.position + cloneDeepMutable(currentCoordinatesOf(element)) ); - element.position = undefined; - updateTreatments(state, element); } diff --git a/shared/src/store/action-reducers/vehicle.ts b/shared/src/store/action-reducers/vehicle.ts index 227586a35..6716c9326 100644 --- a/shared/src/store/action-reducers/vehicle.ts +++ b/shared/src/store/action-reducers/vehicle.ts @@ -1,7 +1,20 @@ import { Type } from 'class-transformer'; import { IsArray, IsString, IsUUID, ValidateNested } from 'class-validator'; import { Material, Personnel, Vehicle } from '../../models'; -import { Position } from '../../models/utils'; +import { + currentCoordinatesOf, + isInTransfer, + isInVehicle, + isNotInTransfer, + isNotOnMap, + MapCoordinates, + MapPosition, + VehiclePosition, +} from '../../models/utils'; +import { + changePosition, + changePositionWithId, +} from '../../models/utils/position/position-helpers-mutable'; import type { ExerciseState } from '../../state'; import { imageSizeToPosition } from '../../state-helpers'; import type { Mutable } from '../../utils'; @@ -16,11 +29,7 @@ import type { Action, ActionReducer } from '../action-reducer'; import { ReducerError } from '../reducer-error'; import { deletePatient } from './patient'; import { getElement } from './utils/get-element'; -import { - addElementPosition, - removeElementPosition, - updateElementPosition, -} from './utils/spatial-elements'; +import { removeElementPosition } from './utils/spatial-elements'; export function deleteVehicle( draftState: Mutable, @@ -79,8 +88,8 @@ export class MoveVehicleAction implements Action { public readonly vehicleId!: UUID; @ValidateNested() - @Type(() => Position) - public readonly targetPosition!: Position; + @Type(() => MapCoordinates) + public readonly targetPosition!: MapCoordinates; } export class RemoveVehicleAction implements Action { @@ -150,20 +159,20 @@ export namespace VehicleActionReducers { } draftState.vehicles[vehicle.id] = cloneDeepMutable(vehicle); for (const material of cloneDeepMutable(materials)) { - material.metaPosition = { - type: 'vehicle', - vehicleId: vehicle.id, - }; + changePosition( + material, + VehiclePosition.create(vehicle.id), + draftState + ); draftState.materials[material.id] = material; - addElementPosition(draftState, 'material', material.id); } for (const person of cloneDeepMutable(personnel)) { - person.metaPosition = { - type: 'vehicle', - vehicleId: vehicle.id, - }; + changePosition( + person, + VehiclePosition.create(vehicle.id), + draftState + ); draftState.personnel[person.id] = person; - addElementPosition(draftState, 'personnel', person.id); } return draftState; }, @@ -173,12 +182,12 @@ export namespace VehicleActionReducers { export const moveVehicle: ActionReducer = { action: MoveVehicleAction, reducer: (draftState, { vehicleId, targetPosition }) => { - const vehicle = getElement(draftState, 'vehicle', vehicleId); - vehicle.position = cloneDeepMutable(targetPosition); - vehicle.metaPosition = { - type: 'coordinates', - position: cloneDeepMutable(targetPosition), - }; + changePositionWithId( + vehicleId, + MapPosition.create(targetPosition), + 'vehicle', + draftState + ); return draftState; }, rights: 'participant', @@ -213,13 +222,12 @@ export namespace VehicleActionReducers { action: UnloadVehicleAction, reducer: (draftState, { vehicleId }) => { const vehicle = getElement(draftState, 'vehicle', vehicleId); - const unloadMetaPosition = vehicle.metaPosition; - if (unloadMetaPosition.type !== 'coordinates') { + if (isNotOnMap(vehicle)) { throw new ReducerError( `Vehicle with id ${vehicleId} is currently not on the map` ); } - const unloadPosition = unloadMetaPosition.position; + const unloadPosition = currentCoordinatesOf(vehicle); const materialIds = Object.keys(vehicle.materialIds); const personnelIds = Object.keys(vehicle.personnelIds); const patientIds = Object.keys(vehicle.patientIds); @@ -239,10 +247,14 @@ export namespace VehicleActionReducers { for (const patientId of patientIds) { x += space; - updateElementPosition(draftState, 'patient', patientId, { - x, - y: unloadPosition.y, - }); + changePositionWithId( + patientId, + MapPosition.create( + MapCoordinates.create(x, unloadPosition.y) + ), + 'patient', + draftState + ); delete vehicle.patientIds[patientId]; } @@ -253,15 +265,14 @@ export namespace VehicleActionReducers { 'personnel', personnelId ); - if (Personnel.isInVehicle(personnel)) { - updateElementPosition( - draftState, - 'personnel', + if (isInVehicle(personnel)) { + changePositionWithId( personnelId, - { - x, - y: unloadPosition.y, - } + MapPosition.create( + MapCoordinates.create(x, unloadPosition.y) + ), + 'personnel', + draftState ); } } @@ -269,11 +280,14 @@ export namespace VehicleActionReducers { for (const materialId of materialIds) { x += space; const material = getElement(draftState, 'material', materialId); - if (Material.isInVehicle(material)) { - updateElementPosition(draftState, 'material', materialId, { - x, - y: unloadPosition.y, - }); + if (isInVehicle(material)) { + changePosition( + material, + MapPosition.create( + MapCoordinates.create(x, unloadPosition.y) + ), + draftState + ); } } @@ -301,11 +315,11 @@ export namespace VehicleActionReducers { `Material with id ${material.id} is not assignable to the vehicle with id ${vehicle.id}` ); } - material.metaPosition = { - type: 'vehicle', - vehicleId, - }; - removeElementPosition(draftState, 'material', material.id); + changePosition( + material, + VehiclePosition.create(vehicleId), + draftState + ); break; } case 'personnel': { @@ -314,7 +328,7 @@ export namespace VehicleActionReducers { 'personnel', elementToBeLoadedId ); - if (personnel.transfer !== undefined) { + if (isInTransfer(personnel)) { throw new ReducerError( `Personnel with id ${elementToBeLoadedId} is currently in transfer` ); @@ -324,14 +338,10 @@ export namespace VehicleActionReducers { `Personnel with id ${personnel.id} is not assignable to the vehicle with id ${vehicle.id}` ); } - personnel.metaPosition = { - type: 'vehicle', - vehicleId, - }; - removeElementPosition( - draftState, - 'personnel', - personnel.id + changePosition( + personnel, + VehiclePosition.create(vehicleId), + draftState ); break; } @@ -350,27 +360,17 @@ export namespace VehicleActionReducers { ); } vehicle.patientIds[elementToBeLoadedId] = true; - - patient.metaPosition = { - type: 'vehicle', - vehicleId, - }; - removeElementPosition(draftState, patient.type, patient.id); - + changePosition( + patient, + VehiclePosition.create(vehicleId), + draftState + ); // Load in all materials Object.keys(vehicle.materialIds).forEach((materialId) => { - getElement( - draftState, - 'material', - materialId - ).metaPosition = { - type: 'vehicle', - vehicleId, - }; - removeElementPosition( - draftState, - 'material', - materialId + changePosition( + getElement(draftState, 'material', materialId), + VehiclePosition.create(vehicleId), + draftState ); }); @@ -379,22 +379,23 @@ export namespace VehicleActionReducers { .filter( // Skip personnel currently in transfer (personnelId) => - getElement(draftState, 'personnel', personnelId) - .transfer === undefined + isNotInTransfer( + getElement( + draftState, + 'personnel', + personnelId + ) + ) ) .forEach((personnelId) => { - getElement( - draftState, - 'personnel', - personnelId - ).metaPosition = { - type: 'vehicle', - vehicleId, - }; - removeElementPosition( - draftState, - 'personnel', - personnelId + changePosition( + getElement( + draftState, + 'personnel', + personnelId + ), + VehiclePosition.create(vehicleId), + draftState ); }); } diff --git a/shared/src/store/action-reducers/viewport.ts b/shared/src/store/action-reducers/viewport.ts index f81ee56a3..3719ab8a1 100644 --- a/shared/src/store/action-reducers/viewport.ts +++ b/shared/src/store/action-reducers/viewport.ts @@ -1,7 +1,11 @@ import { Type } from 'class-transformer'; import { IsString, IsUUID, ValidateNested } from 'class-validator'; import { Viewport } from '../../models'; -import { Position, Size } from '../../models/utils'; +import { MapCoordinates, MapPosition, Size } from '../../models/utils'; +import { + changePosition, + changePositionWithId, +} from '../../models/utils/position/position-helpers-mutable'; import { cloneDeepMutable, UUID, uuidValidationOptions } from '../../utils'; import { IsValue } from '../../utils/validators'; import type { Action, ActionReducer } from '../action-reducer'; @@ -28,8 +32,8 @@ export class MoveViewportAction implements Action { @IsUUID(4, uuidValidationOptions) public readonly viewportId!: UUID; @ValidateNested() - @Type(() => Position) - public readonly targetPosition!: Position; + @Type(() => MapCoordinates) + public readonly targetPosition!: MapCoordinates; } export class ResizeViewportAction implements Action { @@ -38,8 +42,8 @@ export class ResizeViewportAction implements Action { @IsUUID(4, uuidValidationOptions) public readonly viewportId!: UUID; @ValidateNested() - @Type(() => Position) - public readonly targetPosition!: Position; + @Type(() => MapCoordinates) + public readonly targetPosition!: MapCoordinates; @ValidateNested() @Type(() => Size) public readonly newSize!: Size; @@ -79,8 +83,12 @@ export namespace ViewportActionReducers { export const moveViewport: ActionReducer = { action: MoveViewportAction, reducer: (draftState, { viewportId, targetPosition }) => { - const viewport = getElement(draftState, 'viewport', viewportId); - viewport.position = cloneDeepMutable(targetPosition); + changePositionWithId( + viewportId, + MapPosition.create(targetPosition), + 'viewport', + draftState + ); return draftState; }, rights: 'trainer', @@ -90,7 +98,11 @@ export namespace ViewportActionReducers { action: ResizeViewportAction, reducer: (draftState, { viewportId, targetPosition, newSize }) => { const viewport = getElement(draftState, 'viewport', viewportId); - viewport.position = cloneDeepMutable(targetPosition); + changePosition( + viewport, + MapPosition.create(targetPosition), + draftState + ); viewport.size = cloneDeepMutable(newSize); return draftState; }, diff --git a/shared/src/store/reduce-exercise-state.spec.ts b/shared/src/store/reduce-exercise-state.spec.ts index ac72e6cdd..317638bcc 100644 --- a/shared/src/store/reduce-exercise-state.spec.ts +++ b/shared/src/store/reduce-exercise-state.spec.ts @@ -1,5 +1,6 @@ import type { Viewport } from '../models'; import type { ExerciseStatus } from '../models/utils'; +import { MapCoordinates, MapPosition } from '../models/utils'; import { ExerciseState } from '../state'; import type { UUID } from '../utils'; import { uuid } from '../utils'; @@ -15,7 +16,7 @@ describe('exerciseReducer', () => { type: 'viewport', name: 'Test', size: { width: 100, height: 100 }, - position: { x: 0, y: 0 }, + position: MapPosition.create(MapCoordinates.create(0, 0)), } as const; } diff --git a/shared/src/store/validate-exercise-action.spec.ts b/shared/src/store/validate-exercise-action.spec.ts index da9aab590..f5486b287 100644 --- a/shared/src/store/validate-exercise-action.spec.ts +++ b/shared/src/store/validate-exercise-action.spec.ts @@ -1,5 +1,5 @@ -import type { Position } from '../models/utils'; import { Viewport } from '../models'; +import type { MapCoordinates } from '../models/utils'; import type { ExerciseAction } from './action-reducers'; import { validateExerciseAction } from '.'; @@ -78,9 +78,12 @@ describe('validateExerciseAction', () => { width: 1, }, position: { - // this is of type string instead of number - x: '0' as unknown as number, - y: 0, + type: 'coordinates', + coordinates: { + // this is of type string instead of number + x: '0' as unknown as number, + y: 0, + }, }, }, }) @@ -120,10 +123,13 @@ describe('validateExerciseAction', () => { width: 1, }, position: { - x: 0, - y: 0, - z: 0, - } as unknown as Position, + type: 'coordinates', + coordinates: { + x: 0, + y: 0, + z: 0, + } as unknown as MapCoordinates, + }, }, }) ).not.toEqual([]); diff --git a/shared/src/utils/validators/is-metaposition.ts b/shared/src/utils/validators/is-position.ts similarity index 55% rename from shared/src/utils/validators/is-metaposition.ts rename to shared/src/utils/validators/is-position.ts index e9999b011..cf72d7f66 100644 --- a/shared/src/utils/validators/is-metaposition.ts +++ b/shared/src/utils/validators/is-position.ts @@ -1,28 +1,28 @@ import { Type } from 'class-transformer'; -import { MapPosition } from '../../models/utils/map-position'; -import type { MetaPosition } from '../../models/utils/meta-position'; -import { SimulatedRegionPosition } from '../../models/utils/simulated-region-position'; -import { TransferPosition } from '../../models/utils/transfer-position'; -import { VehiclePosition } from '../../models/utils/vehicle-position'; +import { MapPosition } from '../../models/utils/position/map-position'; +import type { Position } from '../../models/utils/position/position'; +import { SimulatedRegionPosition } from '../../models/utils/position/simulated-region-position'; +import { TransferPosition } from '../../models/utils/position/transfer-position'; +import { VehiclePosition } from '../../models/utils/position/vehicle-position'; import { IsLiteralUnion } from './is-literal-union'; -class MetaPositionBase { - @IsLiteralUnion({ +class PositionBase { + @IsLiteralUnion({ coordinates: true, simulatedRegion: true, transfer: true, vehicle: true, }) - public type: MetaPosition['type']; + public type: Position['type']; - constructor(type: MetaPosition['type']) { + constructor(type: Position['type']) { this.type = type; } } // eslint-disable-next-line @typescript-eslint/naming-convention -export const IsMetaPosition = () => - Type(() => MetaPositionBase, { +export const IsPosition = () => + Type(() => PositionBase, { keepDiscriminatorProperty: true, discriminator: { property: 'type',