From 7e1537dd5efe0e132c614bd93febdcb7ac569283 Mon Sep 17 00:00:00 2001 From: Julian Schmidt Date: Thu, 16 Feb 2023 16:47:20 +0100 Subject: [PATCH 1/9] Refactor Viewport and SimulatedRegion position to be the center instead of a corner * Simplified GeometryHelper API by removing `Positions` * The ResizeRectangleInteraction now has a minimumSize and preventsflipping of the rectangle --- .../shared/core/drag-element.service.ts | 8 +- .../moveable-feature-manager.ts | 13 ++- .../simulated-region-feature-manager.ts | 23 +++-- .../viewport-feature-manager.ts | 10 +-- .../exercise-map/utility/geometry-helper.ts | 29 ++----- .../utility/point-geometry-helper.ts | 5 +- .../utility/polygon-geometry-helper.ts | 45 +++++----- .../utility/resize-rectangle-interaction.ts | 86 +++++++++++-------- .../utility/translate-interaction.ts | 7 +- shared/src/models/simulated-region.ts | 7 +- .../models/utils/position/position-helpers.ts | 32 +++---- shared/src/models/utils/size.ts | 6 +- shared/src/models/viewport.ts | 9 +- 13 files changed, 131 insertions(+), 149 deletions(-) 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 a813a6f22..59d66d70d 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 @@ -220,8 +220,8 @@ export class DragElementService { const width = height * Viewport.image.aspectRatio; const viewport = Viewport.create( { - x: position.x - width / 2, - y: position.y + height / 2, + x: position.x, + y: position.y, }, { height, @@ -282,8 +282,8 @@ export class DragElementService { const width = height * SimulatedRegion.image.aspectRatio; const simulatedRegion = SimulatedRegion.create( { - x: position.x - width / 2, - y: position.y + height / 2, + x: position.x, + y: position.y, }, { height, diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts index af8277d30..949f7ed45 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/moveable-feature-manager.ts @@ -1,4 +1,5 @@ -import type { MapBrowserEvent, Feature } from 'ol'; +import type { NgZone } from '@angular/core'; +import type { Feature, MapBrowserEvent } from 'ol'; import type Point from 'ol/geom/Point'; import type { TranslateEvent } from 'ol/interaction/Translate'; import type VectorLayer from 'ol/layer/Vector'; @@ -6,20 +7,18 @@ import type OlMap from 'ol/Map'; import type VectorSource from 'ol/source/Vector'; import type { Observable } from 'rxjs'; import { Subject } from 'rxjs'; -import type { NgZone } from '@angular/core'; // eslint-disable-next-line @typescript-eslint/no-shadow -import type { UUID, Element } from 'digital-fuesim-manv-shared'; +import type { Element, MapCoordinates, UUID } from 'digital-fuesim-manv-shared'; import type { FeatureManager } from '../utility/feature-manager'; -import { MovementAnimator } from '../utility/movement-animator'; import type { GeometryHelper, GeometryWithCoordinates, PositionableElement, - Positions, } from '../utility/geometry-helper'; +import { MovementAnimator } from '../utility/movement-animator'; +import type { OlMapInteractionsManager } from '../utility/ol-map-interactions-manager'; import type { OpenPopupOptions } from '../utility/popup-manager'; import { TranslateInteraction } from '../utility/translate-interaction'; -import type { OlMapInteractionsManager } from '../utility/ol-map-interactions-manager'; import { ElementManager } from './element-manager'; /** @@ -40,7 +39,7 @@ export abstract class MoveableFeatureManager< constructor( protected readonly olMap: OlMap, private readonly proposeMovementAction: ( - newPosition: Positions, + newPosition: MapCoordinates, element: ManagedElement ) => void, protected readonly geometryHelper: GeometryHelper< 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 08a3ddd40..856996482 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 @@ -60,11 +60,11 @@ export class SimulatedRegionFeatureManager ) { super( olMap, - (targetPositions, simulatedRegion) => { + (targetPosition, simulatedRegion) => { exerciseService.proposeAction({ type: '[SimulatedRegion] Move simulated region', simulatedRegionId: simulatedRegion.id, - targetPosition: targetPositions[0]![0]!, + targetPosition, }); }, new PolygonGeometryHelper() @@ -86,7 +86,7 @@ export class SimulatedRegionFeatureManager const feature = super.createFeature(element); ResizeRectangleInteraction.onResize( feature, - ({ topLeftCoordinate, scale }) => { + ({ centerCoordinate, scale }) => { const currentElement = this.getElementFromFeature( feature ) as SimulatedRegion; @@ -95,8 +95,8 @@ export class SimulatedRegionFeatureManager type: '[SimulatedRegion] Resize simulated region', simulatedRegionId: element.id, targetPosition: MapCoordinates.create( - topLeftCoordinate[0]!, - topLeftCoordinate[1]! + centerCoordinate[0]!, + centerCoordinate[1]! ), newSize: Size.create( currentElement.size.width * scale.x, @@ -142,19 +142,16 @@ export class SimulatedRegionFeatureManager return false; } if ( - ['vehicle', 'personnel', 'material', 'patient'].includes( - droppedElement.type - ) + droppedElement.type === 'vehicle' || + droppedElement.type === 'personnel' || + droppedElement.type === 'material' || + droppedElement.type === 'patient' ) { this.exerciseService.proposeAction( { type: '[SimulatedRegion] Add Element', simulatedRegionId: droppedOnSimulatedRegion.id, - elementToBeAddedType: droppedElement.type as - | 'material' - | 'patient' - | 'personnel' - | 'vehicle', + elementToBeAddedType: droppedElement.type, elementToBeAddedId: droppedElement.id, }, true 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 5e41c5af1..96156734c 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 @@ -63,11 +63,11 @@ export class ViewportFeatureManager ) { super( olMap, - (targetPositions, viewport) => { + (targetPosition, viewport) => { exerciseService.proposeAction({ type: '[Viewport] Move viewport', viewportId: viewport.id, - targetPosition: targetPositions[0]![0]!, + targetPosition, }); }, new PolygonGeometryHelper() @@ -87,7 +87,7 @@ export class ViewportFeatureManager const feature = super.createFeature(element); ResizeRectangleInteraction.onResize( feature, - ({ topLeftCoordinate, scale }) => { + ({ centerCoordinate, scale }) => { const currentElement = this.getElementFromFeature( feature ) as Viewport; @@ -96,8 +96,8 @@ export class ViewportFeatureManager type: '[Viewport] Resize viewport', viewportId: element.id, targetPosition: MapCoordinates.create( - topLeftCoordinate[0]!, - topLeftCoordinate[1]! + centerCoordinate[0]!, + centerCoordinate[1]! ), newSize: Size.create( currentElement.size.width * scale.x, 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 c73200b8a..f475d58e6 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 @@ -32,36 +32,25 @@ export type Coordinates = Exclude< null >; -type ArrayElement = ArrayType extends readonly (infer ElementType)[] - ? ElementType - : never; - -type SubstituteCoordinateForPoint = T extends Coordinate - ? MapCoordinates - : T extends Array> - ? SubstituteCoordinateForPoint>[] - : never; - -export type Positions = - SubstituteCoordinateForPoint>; - export interface CoordinatePair { startPosition: Coordinates; endPosition: Coordinates; } export interface GeometryHelper< - T extends GeometryWithCoordinates, + GeometryType extends GeometryWithCoordinates, Element extends PositionableElement = PositionableElement > { - create: (element: Element) => Feature; - getElementCoordinates: (element: Element) => Coordinates; - getFeatureCoordinates: (feature: Feature) => Coordinates; + create: (element: Element) => Feature; + getElementCoordinates: (element: Element) => Coordinates; + getFeatureCoordinates: ( + feature: Feature + ) => Coordinates; interpolateCoordinates: ( - positions: CoordinatePair, + positions: CoordinatePair, progress: number - ) => Coordinates; - getFeaturePosition: (feature: Feature) => Positions; + ) => Coordinates; + getFeaturePosition: (feature: Feature) => MapCoordinates; } export const interpolate = ( 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 a9c65dee8..9ba5d6acc 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,7 @@ import type { WithPosition } from 'digital-fuesim-manv-shared'; import { - MapCoordinates, currentCoordinatesOf, + MapCoordinates, } from 'digital-fuesim-manv-shared'; import { Feature } from 'ol'; import { Point } from 'ol/geom'; @@ -9,7 +9,6 @@ import type { CoordinatePair, Coordinates, GeometryHelper, - Positions, } from './geometry-helper'; import { interpolate } from './geometry-helper'; @@ -31,7 +30,7 @@ export class PointGeometryHelper implements GeometryHelper { ): Coordinates => interpolate(positions.startPosition, positions.endPosition, progress); - getFeaturePosition = (feature: Feature): Positions => + getFeaturePosition = (feature: Feature) => 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 29fc313d5..044fed407 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 @@ -3,12 +3,12 @@ import { MapCoordinates, } from 'digital-fuesim-manv-shared'; import { Feature } from 'ol'; +import { getCenter } from 'ol/extent'; import { Polygon } from 'ol/geom'; import type { CoordinatePair, Coordinates, GeometryHelper, - Positions, ResizableElement, } from './geometry-helper'; import { interpolate } from './geometry-helper'; @@ -21,24 +21,24 @@ export class PolygonGeometryHelper getElementCoordinates = ( element: ResizableElement - ): Coordinates => [ - [ - [currentCoordinatesOf(element).x, currentCoordinatesOf(element).y], + ): Coordinates => { + const center = currentCoordinatesOf(element); + const { width, height } = element.size; + return [ [ - currentCoordinatesOf(element).x + element.size.width, - currentCoordinatesOf(element).y, + // top left + [center.x - width / 2, center.y + height / 2], + // top right + [center.x + width / 2, center.y + height / 2], + // bottom right + [center.x + width / 2, center.y - height / 2], + // bottom left + [center.x - width / 2, center.y - height / 2], + // top left (close the rectangle) + [center.x - width / 2, center.y + height / 2], ], - [ - 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], - ], - ]; + ]; + }; getFeatureCoordinates = (feature: Feature): Coordinates => feature.getGeometry()!.getCoordinates(); @@ -57,10 +57,11 @@ export class PolygonGeometryHelper ) ); - getFeaturePosition = (feature: Feature): Positions => - this.getFeatureCoordinates(feature).map((coordinates) => - coordinates.map((coordinate) => - MapCoordinates.create(coordinate[0]!, coordinate[1]!) - ) + getFeaturePosition = (feature: Feature) => { + const centerCoordinates = getCenter(feature.getGeometry()!.getExtent()); + return MapCoordinates.create( + centerCoordinates[0]!, + centerCoordinates[1]! ); + }; } diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts index 38865567d..6c6831c24 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts @@ -2,12 +2,14 @@ import type { Feature, MapBrowserEvent } from 'ol'; import type { Coordinate } from 'ol/coordinate'; import { distance } from 'ol/coordinate'; import BaseEvent from 'ol/events/Event'; +import { getCenter } from 'ol/extent'; import type { Polygon } from 'ol/geom'; import PointerInteraction from 'ol/interaction/Pointer'; import type VectorSource from 'ol/source/Vector'; /** * Provides the ability to resize a rectangle by dragging any of its corners. + * It prevents the rectangle from flipping and from getting too small. */ export class ResizeRectangleInteraction extends PointerInteraction { /** @@ -21,7 +23,14 @@ export class ResizeRectangleInteraction extends PointerInteraction { */ private currentResizeValues?: CurrentResizeValues; - constructor(private readonly source: VectorSource) { + constructor( + private readonly source: VectorSource, + /** + * The minimum allowed distance between two corners of the rectangle. + */ + // TODO: Add a proper unit for this. Blocked by #374 + private readonly minimumSize = 10 + ) { super({ handleDownEvent: (event) => this._handleDownEvent(event), handleDragEvent: (event) => this._handleDragEvent(event), @@ -66,47 +75,54 @@ export class ResizeRectangleInteraction extends PointerInteraction { return false; } const mouseCoordinate = event.coordinate; - const newXScale = - (mouseCoordinate[0]! - this.currentResizeValues.originCorner[0]!) / - (this.currentResizeValues.draggedCorner[0]! - - this.currentResizeValues.originCorner[0]!); - const newYScale = - (this.currentResizeValues.originCorner[1]! - mouseCoordinate[1]!) / - (this.currentResizeValues.originCorner[1]! - - this.currentResizeValues.draggedCorner[1]!); - this.currentResizeValues.feature + const { draggedCorner, originCorner, currentScale, feature } = + this.currentResizeValues; + const newXScale = this.calculateNewScale( + draggedCorner[0]!, + originCorner[0]!, + mouseCoordinate[0]! + ); + const newYScale = this.calculateNewScale( + draggedCorner[1]!, + originCorner[1]!, + mouseCoordinate[1]! + ); + feature .getGeometry()! .scale( - newXScale / this.currentResizeValues.currentScale!.x, - newYScale / this.currentResizeValues.currentScale!.y, - this.currentResizeValues.originCorner + newXScale / currentScale.x, + newYScale / currentScale.y, + originCorner ); this.currentResizeValues.currentScale = { x: newXScale, y: newYScale }; return true; } + private calculateNewScale( + draggedCorner: number, + originCorner: number, + mouseCoordinate: number + ) { + const oldXLength = draggedCorner - originCorner; + const newXLength = mouseCoordinate - originCorner; + return ( + // We also want to prevent flipping the rectangle + (oldXLength < 0 + ? Math.min(newXLength, -this.minimumSize) + : Math.max(newXLength, this.minimumSize)) / oldXLength + ); + } + private _handleUpEvent(event: MapBrowserEvent): boolean { if (this.currentResizeValues === undefined) { return true; } - - const coordinates = this.currentResizeValues.feature - .getGeometry()! - .getCoordinates()![0]!; - const topLeftCoordinate = coordinates.reduce( - (smallestCoordinate, coordinate) => - coordinate[0]! <= smallestCoordinate[0]! || - coordinate[1]! >= smallestCoordinate[1]! - ? coordinate - : smallestCoordinate, - [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY] + const { currentScale, feature } = this.currentResizeValues; + const newCenterCoordinate = getCenter( + feature.getGeometry()!.getExtent() ); - this.currentResizeValues.feature.dispatchEvent( - new ResizeEvent( - this.currentResizeValues.currentScale, - this.currentResizeValues.originCorner, - topLeftCoordinate - ) + feature.dispatchEvent( + new ResizeEvent(currentScale, newCenterCoordinate) ); this.currentResizeValues = undefined; return false; @@ -127,6 +143,7 @@ interface CurrentResizeValues { draggedCorner: Coordinate; /** * The corner that doesn't move during the resize. + * It is always opposite to the dragged corner. */ originCorner: Coordinate; /** @@ -142,14 +159,7 @@ const resizeRectangleEventType = 'resizerectangle'; class ResizeEvent extends BaseEvent { constructor( public readonly scale: { x: number; y: number }, - /** - * The coordinate of the corner that didn't move during the resize. - */ - public readonly origin: Coordinate, - /** - * The new top left coordinate of the rectangle. - */ - public readonly topLeftCoordinate: Coordinate + public readonly centerCoordinate: Coordinate ) { super(resizeRectangleEventType); } diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts index 0b949eed7..6eae70b25 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts @@ -1,8 +1,9 @@ +import { MapCoordinates } from 'digital-fuesim-manv-shared'; import { isEqual } from 'lodash-es'; import type { Feature, MapBrowserEvent } from 'ol'; import type { Point } from 'ol/geom'; import { Translate } from 'ol/interaction'; -import type { GeometryWithCoordinates, Positions } from './geometry-helper'; +import type { GeometryWithCoordinates } from './geometry-helper'; /** * Translates (moves) a feature to a new position. @@ -48,8 +49,8 @@ export class TranslateInteraction extends Translate { */ public static onTranslateEnd( feature: Feature, - callback: (newCoordinates: Positions) => void, - getPosition: (feature: Feature) => Positions + callback: (newCoordinates: MapCoordinates) => void, + getPosition: (feature: Feature) => MapCoordinates ) { feature.addEventListener('translateend', (event) => { // The end coordinates in the event are the mouse coordinates and not the feature coordinates. diff --git a/shared/src/models/simulated-region.ts b/shared/src/models/simulated-region.ts index 8dba7803d..463e41a1e 100644 --- a/shared/src/models/simulated-region.ts +++ b/shared/src/models/simulated-region.ts @@ -28,7 +28,7 @@ export class SimulatedRegion { public readonly type = 'simulatedRegion'; /** - * top-left position + * The center coordinates of the simulatedRegion * * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. */ @@ -44,11 +44,10 @@ export class SimulatedRegion { public readonly name: string; /** - * @param position top-left position * @deprecated Use {@link create} instead */ - constructor(position: MapCoordinates, size: Size, name: string) { - this.position = MapPosition.create(position); + constructor(centerCoordinates: MapCoordinates, size: Size, name: string) { + this.position = MapPosition.create(centerCoordinates); this.size = size; this.name = name; } diff --git a/shared/src/models/utils/position/position-helpers.ts b/shared/src/models/utils/position/position-helpers.ts index 819a1b9e5..618b07979 100644 --- a/shared/src/models/utils/position/position-helpers.ts +++ b/shared/src/models/utils/position/position-helpers.ts @@ -134,29 +134,17 @@ export function simulatedRegionIdOfPosition(position: Position): UUID { } export function upperLeftCornerOf(element: WithExtent): MapCoordinates { - const corner = { ...currentCoordinatesOf(element) }; - - if (element.size.width < 0) { - corner.x += element.size.width; - } - - if (element.size.height < 0) { - corner.y -= element.size.height; - } - - return MapCoordinates.create(corner.x, corner.y); + const centerCoordinate = currentCoordinatesOf(element); + return MapCoordinates.create( + centerCoordinate.x - element.size.width / 2, + centerCoordinate.y + element.size.height / 2 + ); } export function lowerRightCornerOf(element: WithExtent): MapCoordinates { - const corner = { ...currentCoordinatesOf(element) }; - - if (element.size.width > 0) { - corner.x += element.size.width; - } - - if (element.size.height > 0) { - corner.y -= element.size.height; - } - - return MapCoordinates.create(corner.x, corner.y); + const centerCoordinate = currentCoordinatesOf(element); + return MapCoordinates.create( + centerCoordinate.x + element.size.width / 2, + centerCoordinate.y - element.size.height / 2 + ); } diff --git a/shared/src/models/utils/size.ts b/shared/src/models/utils/size.ts index 3c71fb87c..ca049a8b1 100644 --- a/shared/src/models/utils/size.ts +++ b/shared/src/models/utils/size.ts @@ -1,17 +1,17 @@ -import { IsNumber } from 'class-validator'; +import { IsPositive } from 'class-validator'; import { getCreate } from './get-create'; export class Size { /** * The width in meters. */ - @IsNumber() + @IsPositive() public readonly width: number; /** * The height in meters. */ - @IsNumber() + @IsPositive() public readonly height: number; /** diff --git a/shared/src/models/viewport.ts b/shared/src/models/viewport.ts index 400cebdbd..fd7a3798d 100644 --- a/shared/src/models/viewport.ts +++ b/shared/src/models/viewport.ts @@ -6,10 +6,10 @@ import { IsValue } from '../utils/validators'; import { getCreate, lowerRightCornerOf, + upperLeftCornerOf, MapPosition, Position, Size, - upperLeftCornerOf, } from './utils'; import type { ImageProperties, MapCoordinates } from './utils'; @@ -21,7 +21,7 @@ export class Viewport { public readonly type = 'viewport'; /** - * top-left position + * The center coordinates of the viewport * * @deprecated Do not access directly, use helper methods from models/utils/position/position-helpers(-mutable) instead. */ @@ -37,11 +37,10 @@ export class Viewport { public readonly name: string; /** - * @param position top-left position * @deprecated Use {@link create} instead */ - constructor(position: MapCoordinates, size: Size, name: string) { - this.position = MapPosition.create(position); + constructor(centerCoordinates: MapCoordinates, size: Size, name: string) { + this.position = MapPosition.create(centerCoordinates); this.size = size; this.name = name; } From cf3db983e10b164cdcae2615f06486f4235dda79 Mon Sep 17 00:00:00 2001 From: Julian Schmidt Date: Thu, 16 Feb 2023 17:01:35 +0100 Subject: [PATCH 2/9] Fix lint errors --- .../shared/exercise-map/utility/translate-interaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts index 6eae70b25..c8dcb9a64 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/translate-interaction.ts @@ -1,4 +1,4 @@ -import { MapCoordinates } from 'digital-fuesim-manv-shared'; +import type { MapCoordinates } from 'digital-fuesim-manv-shared'; import { isEqual } from 'lodash-es'; import type { Feature, MapBrowserEvent } from 'ol'; import type { Point } from 'ol/geom'; From 7c9e20f7ea69c479237ae641acfa75cea0c60e3e Mon Sep 17 00:00:00 2001 From: Julian Schmidt Date: Thu, 16 Feb 2023 23:24:52 +0100 Subject: [PATCH 3/9] Add migration --- ...rectangular-element-positions-to-center.ts | 39 +++++++++++++++++++ .../state-migrations/migration-functions.ts | 2 + shared/src/state.ts | 2 +- 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts diff --git a/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts b/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts new file mode 100644 index 000000000..d2a705eaf --- /dev/null +++ b/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts @@ -0,0 +1,39 @@ +import { ExerciseAction } from '../store'; +import type { Mutable } from '../utils'; +import type { Migration } from './migration-functions'; + +export const refactorRectangularElementPositionsToCenter21: Migration = { + actions: (_initialState, actions) => { + for (const action of actions as Mutable[]) { + switch (action?.type) { + case '[Viewport] Add viewport': + migrateRectangularElement(action.viewport); + break; + case '[Viewport] Move viewport': + case '[Viewport] Rename viewport': + // It would be too much work to migrate these actions, as we need the current viewport size to convert them + // This results in most replays having different viewport positions. + break; + } + } + }, + state: (state: any) => { + for (const viewport of Object.values(state.viewports)) { + migrateRectangularElement(viewport); + } + for (const simulatedRegion of Object.values(state.simulatedRegions)) { + migrateRectangularElement(simulatedRegion); + } + }, +}; + +function migrateRectangularElement(viewport: any) { + const { position, size } = viewport; + if (position.type === 'coordinates') { + // The position was previously the top-left corner of the rectangle if the width and height was positive. + position.x = position.x + size.width / 2; + position.y = position.y - size.height / 2; + } + size.width = Math.abs(size.width); + size.height = Math.abs(size.height); +} diff --git a/shared/src/state-migrations/migration-functions.ts b/shared/src/state-migrations/migration-functions.ts index 63a9e586c..202da8018 100644 --- a/shared/src/state-migrations/migration-functions.ts +++ b/shared/src/state-migrations/migration-functions.ts @@ -9,6 +9,7 @@ import { addTypeProperty17 } from './17-add-type-property'; import { replacePositionWithMetaPosition18 } from './18-replace-position-with-meta-position'; import { renameStartPointTypes19 } from './19-rename-start-point-types'; import { addSimulationProperties20 } from './20-add-simulation-properties'; +import { refactorRectangularElementPositionsToCenter21 } from './21-refactor-rectangular-element-positions-to-center'; import { updateEocLog3 } from './3-update-eoc-log'; import { removeSetParticipantIdAction4 } from './4-remove-set-participant-id-action'; import { removeStatistics5 } from './5-remove-statistics'; @@ -63,4 +64,5 @@ export const migrations: { 18: replacePositionWithMetaPosition18, 19: renameStartPointTypes19, 20: addSimulationProperties20, + 21: refactorRectangularElementPositionsToCenter21, }; diff --git a/shared/src/state.ts b/shared/src/state.ts index b54476fe7..061a5bcb0 100644 --- a/shared/src/state.ts +++ b/shared/src/state.ts @@ -154,5 +154,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 = 20; + static readonly currentStateVersion = 21; } From 2b373dc8cb8b10915cf2509b5abb81d8387885f4 Mon Sep 17 00:00:00 2001 From: Julian Schmidt Date: Thu, 16 Feb 2023 23:48:46 +0100 Subject: [PATCH 4/9] Improve the migration --- ...rectangular-element-positions-to-center.ts | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts b/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts index d2a705eaf..12a7a9148 100644 --- a/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts +++ b/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts @@ -1,19 +1,39 @@ -import { ExerciseAction } from '../store'; -import type { Mutable } from '../utils'; +import { cloneDeep } from 'lodash-es'; +import { UUID } from '../utils'; import type { Migration } from './migration-functions'; export const refactorRectangularElementPositionsToCenter21: Migration = { actions: (_initialState, actions) => { - for (const action of actions as Mutable[]) { + const oldViewportSizes: { + [viewportId: UUID]: { + width: number; + height: number; + }; + } = {}; + for (const action of actions as any) { switch (action?.type) { case '[Viewport] Add viewport': + if (action.viewport.position.type === 'coordinates') { + oldViewportSizes[action.viewport.id] = cloneDeep( + action.viewport.size + ); + } migrateRectangularElement(action.viewport); break; - case '[Viewport] Move viewport': - case '[Viewport] Rename viewport': - // It would be too much work to migrate these actions, as we need the current viewport size to convert them - // This results in most replays having different viewport positions. + case '[Viewport] Move viewport': { + const oldViewportSize = oldViewportSizes[action.viewportId]; + if (oldViewportSize) { + migratePosition(action.targetPosition, oldViewportSize); + } break; + } + case '[Viewport] Resize viewport': { + const oldViewportSize = cloneDeep(action.newSize); + oldViewportSizes[action.viewportId] = oldViewportSize; + migratePosition(action.targetPosition, oldViewportSize); + migrateSize(action.newSize); + break; + } } } }, @@ -27,13 +47,21 @@ export const refactorRectangularElementPositionsToCenter21: Migration = { }, }; -function migrateRectangularElement(viewport: any) { - const { position, size } = viewport; +function migrateRectangularElement(element: any) { + const { position, size } = element; if (position.type === 'coordinates') { - // The position was previously the top-left corner of the rectangle if the width and height was positive. - position.x = position.x + size.width / 2; - position.y = position.y - size.height / 2; + migratePosition(position.coordinates, size); } + migrateSize(size); +} + +function migratePosition(position: any, oldSize: any) { + // The position was previously the top-left corner of the rectangle if the width and height was positive. + position.x = position.x + oldSize.width / 2; + position.y = position.y - oldSize.height / 2; +} + +function migrateSize(size: any) { size.width = Math.abs(size.width); size.height = Math.abs(size.height); } From a98b373895535e99cc974738ddcde54f6402196a Mon Sep 17 00:00:00 2001 From: Julian Schmidt Date: Thu, 16 Feb 2023 23:54:34 +0100 Subject: [PATCH 5/9] Fix lint error --- .../21-refactor-rectangular-element-positions-to-center.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts b/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts index 12a7a9148..2105aec9d 100644 --- a/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts +++ b/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts @@ -1,5 +1,5 @@ import { cloneDeep } from 'lodash-es'; -import { UUID } from '../utils'; +import type { UUID } from '../utils'; import type { Migration } from './migration-functions'; export const refactorRectangularElementPositionsToCenter21: Migration = { From 1c5a392e806e6612baec457b7aa4c913be9c089c Mon Sep 17 00:00:00 2001 From: Julian Schmidt Date: Sat, 18 Mar 2023 15:45:41 +0100 Subject: [PATCH 6/9] Update migration function --- ...rectangular-element-positions-to-center.ts | 98 ++++++++++++------- .../state-migrations/migration-functions.ts | 2 +- 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts b/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts index 2105aec9d..67cb9fb32 100644 --- a/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts +++ b/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts @@ -1,41 +1,53 @@ import { cloneDeep } from 'lodash-es'; -import type { UUID } from '../utils'; import type { Migration } from './migration-functions'; export const refactorRectangularElementPositionsToCenter21: Migration = { - actions: (_initialState, actions) => { - const oldViewportSizes: { - [viewportId: UUID]: { - width: number; - height: number; - }; - } = {}; - for (const action of actions as any) { - switch (action?.type) { - case '[Viewport] Add viewport': - if (action.viewport.position.type === 'coordinates') { - oldViewportSizes[action.viewport.id] = cloneDeep( - action.viewport.size - ); - } - migrateRectangularElement(action.viewport); - break; - case '[Viewport] Move viewport': { - const oldViewportSize = oldViewportSizes[action.viewportId]; - if (oldViewportSize) { - migratePosition(action.targetPosition, oldViewportSize); - } - break; - } - case '[Viewport] Resize viewport': { - const oldViewportSize = cloneDeep(action.newSize); - oldViewportSizes[action.viewportId] = oldViewportSize; - migratePosition(action.targetPosition, oldViewportSize); - migrateSize(action.newSize); - break; - } + action: (intermediaryState: any, action: any) => { + switch (action.type) { + case '[Viewport] Add viewport': + migrateRectangularElement(action.viewport); + break; + case '[SimulatedRegion] Add simulated region': + migrateRectangularElement(action.simulatedRegion); + break; + case '[Viewport] Move viewport': { + const migratedViewport = cloneDeep( + intermediaryState.viewports[action.viewportId]! + ); + migratePositionWithMigratedSize( + action.targetPosition, + migratedViewport.size + ); + break; } + case '[SimulatedRegion] Move simulated region': { + const migratedSimulatedViewport = cloneDeep( + intermediaryState.simulatedRegions[ + action.simulatedRegionId + ]! + ); + migratePositionWithMigratedSize( + action.targetPosition, + migratedSimulatedViewport.size + ); + break; + } + case '[Viewport] Resize viewport': + migratePosition( + action.targetPosition, + cloneDeep(action.newSize) + ); + migrateSize(action.newSize); + break; + case '[SimulatedRegion] Resize simulated region': + migratePosition( + action.targetPosition, + cloneDeep(action.newSize) + ); + migrateSize(action.newSize); + break; } + return true; }, state: (state: any) => { for (const viewport of Object.values(state.viewports)) { @@ -55,10 +67,26 @@ function migrateRectangularElement(element: any) { migrateSize(size); } -function migratePosition(position: any, oldSize: any) { +/** + * Migrates the position of a rectangular element to the center of the element. + * @param oldSize the size of the element before this migration + */ +function migratePosition(coordinates: any, oldSize: any) { // The position was previously the top-left corner of the rectangle if the width and height was positive. - position.x = position.x + oldSize.width / 2; - position.y = position.y - oldSize.height / 2; + coordinates.x = coordinates.x + oldSize.width / 2; + coordinates.y = coordinates.y - oldSize.height / 2; +} + +/** + * Migrates the position of a rectangular element to the center of the element. + * + * This migration is not accurate for previously "flipped" viewports (height and/or width was negative). + * To catch this case, access to the size of the viewport before this migration would be required. + * + * @param migratedSize the size of the element after it had been migrated to the center + */ +function migratePositionWithMigratedSize(coordinates: any, migratedSize: any) { + migratePosition(coordinates, migratedSize); } function migrateSize(size: any) { diff --git a/shared/src/state-migrations/migration-functions.ts b/shared/src/state-migrations/migration-functions.ts index c5550a301..cc9ceae23 100644 --- a/shared/src/state-migrations/migration-functions.ts +++ b/shared/src/state-migrations/migration-functions.ts @@ -25,7 +25,7 @@ import { impossibleMigration } from './impossible-migration'; /** * Migrate a single action - * @param intermediaryState - The migrated exercise state just before the action is applied + * @param intermediaryState - The immutable migrated exercise state just before the action is applied * @param action - The action to migrate in place * @returns true if the migration was successful or false to indicate that the action should be deleted * @throws a {@link RestoreError} when a migration is not possible. From 2d64f36aa1ff3a636bba8c7af4f345a1a94303dd Mon Sep 17 00:00:00 2001 From: Julian Schmidt Date: Sat, 18 Mar 2023 15:46:33 +0100 Subject: [PATCH 7/9] Rename migration --- ...=> 25-refactor-rectangular-element-positions-to-center.ts} | 2 +- shared/src/state-migrations/migration-functions.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename shared/src/state-migrations/{21-refactor-rectangular-element-positions-to-center.ts => 25-refactor-rectangular-element-positions-to-center.ts} (98%) diff --git a/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts b/shared/src/state-migrations/25-refactor-rectangular-element-positions-to-center.ts similarity index 98% rename from shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts rename to shared/src/state-migrations/25-refactor-rectangular-element-positions-to-center.ts index 67cb9fb32..dd45dfc43 100644 --- a/shared/src/state-migrations/21-refactor-rectangular-element-positions-to-center.ts +++ b/shared/src/state-migrations/25-refactor-rectangular-element-positions-to-center.ts @@ -1,7 +1,7 @@ import { cloneDeep } from 'lodash-es'; import type { Migration } from './migration-functions'; -export const refactorRectangularElementPositionsToCenter21: Migration = { +export const refactorRectangularElementPositionsToCenter25: Migration = { action: (intermediaryState: any, action: any) => { switch (action.type) { case '[Viewport] Add viewport': diff --git a/shared/src/state-migrations/migration-functions.ts b/shared/src/state-migrations/migration-functions.ts index cc9ceae23..63e48e6b3 100644 --- a/shared/src/state-migrations/migration-functions.ts +++ b/shared/src/state-migrations/migration-functions.ts @@ -9,7 +9,7 @@ import { addTypeProperty17 } from './17-add-type-property'; import { replacePositionWithMetaPosition18 } from './18-replace-position-with-meta-position'; import { renameStartPointTypes19 } from './19-rename-start-point-types'; import { addSimulationProperties20 } from './20-add-simulation-properties'; -import { refactorRectangularElementPositionsToCenter21 } from './21-refactor-rectangular-element-positions-to-center'; +import { refactorRectangularElementPositionsToCenter25 } from './25-refactor-rectangular-element-positions-to-center'; import { fixTypoInRenameSimulatedRegion21 } from './21-fix-typo-in-rename-simulated-region'; import { removeIllegalVehicleMovementActions22 } from './22-remove-illegal-vehicle-movement-actions'; import { addTransferPointToSimulatedRegion23 } from './23-add-transfer-point-to-simulated-region'; @@ -72,5 +72,5 @@ export const migrations: { 22: removeIllegalVehicleMovementActions22, 23: addTransferPointToSimulatedRegion23, 24: addRadiograms24, - 25: refactorRectangularElementPositionsToCenter21, + 25: refactorRectangularElementPositionsToCenter25, }; From 7d51a52a019f272cc5028126f3f25ef58f229cfa Mon Sep 17 00:00:00 2001 From: Julian Schmidt Date: Sat, 18 Mar 2023 15:48:24 +0100 Subject: [PATCH 8/9] Rename variable --- .../utility/resize-rectangle-interaction.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts index 6c6831c24..5d19061e1 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/utility/resize-rectangle-interaction.ts @@ -103,13 +103,13 @@ export class ResizeRectangleInteraction extends PointerInteraction { originCorner: number, mouseCoordinate: number ) { - const oldXLength = draggedCorner - originCorner; - const newXLength = mouseCoordinate - originCorner; + const oldLength = draggedCorner - originCorner; + const newLength = mouseCoordinate - originCorner; return ( // We also want to prevent flipping the rectangle - (oldXLength < 0 - ? Math.min(newXLength, -this.minimumSize) - : Math.max(newXLength, this.minimumSize)) / oldXLength + (oldLength < 0 + ? Math.min(newLength, -this.minimumSize) + : Math.max(newLength, this.minimumSize)) / oldLength ); } From e0c3aaa265515af5509a5af288c95434babcb15b Mon Sep 17 00:00:00 2001 From: Julian Schmidt Date: Sat, 18 Mar 2023 15:57:23 +0100 Subject: [PATCH 9/9] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e48e2d0c..19506aa4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project does **not** adhere to [Semantic Versioning](https://semver.org ## [Unreleased] +### Changed + +- Viewports and simulated regions now have a minimal size to be resized to. Already placed viewports and simulated regions below this size are not affected. +- Viewports and simulated regions can now not be flipped horizontally or vertically during resizing. Due to not completely accurate migrations, it could be that the positions of such regions in imported exercises are now slightly off. + ### Added - There are now radiograms, which can be used by the simulation to send messages to the trainees