Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simulated regions that can be placed on the map #601

Merged
merged 7 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PatientTemplate,
TransferPoint,
Viewport,
SimulatedRegion,
} from 'digital-fuesim-manv-shared';
import type OlMap from 'ol/Map';
import { ExerciseService } from 'src/app/core/exercise.service';
Expand Down Expand Up @@ -228,6 +229,29 @@ export class DragElementService {
true
);
break;
case 'simulatedRegion': {
// This ratio has been determined by trial and error
const height = SimulatedRegion.image.height / 23.5;
benn02 marked this conversation as resolved.
Show resolved Hide resolved
const width = height * SimulatedRegion.image.aspectRatio;
this.exerciseService.proposeAction(
{
type: '[SimulatedRegion] Add simulated region',
simulatedRegion: SimulatedRegion.create(
{
x: position.x - width / 2,
y: position.y + height / 2,
},
{
height,
width,
},
'Einsatzabschnitt ???'
),
},
true
);
break;
}
default:
break;
}
Expand Down Expand Up @@ -260,6 +284,12 @@ type TransferTemplate =
type: 'patient';
template: PatientCategory;
}
| {
type: 'simulatedRegion';
template: {
image: ImageProperties;
};
}
| {
type: 'transferPoint';
template: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ViewportPopupComponent } from './shared/viewport-popup/viewport-popup.c
import { PersonnelPopupComponent } from './shared/personnel-popup/personnel-popup.component';
import { MaterialPopupComponent } from './shared/material-popup/material-popup.component';
import { CaterCapacityComponent } from './shared/cater-capacity/cater-capacity.component';
import { SimulatedRegionPopupComponent } from './shared/simulated-region-popup/simulated-region-popup.component';

@NgModule({
declarations: [
Expand All @@ -27,6 +28,7 @@ import { CaterCapacityComponent } from './shared/cater-capacity/cater-capacity.c
PersonnelPopupComponent,
MaterialPopupComponent,
CaterCapacityComponent,
SimulatedRegionPopupComponent,
],
imports: [
CommonModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ export class DeleteFeatureManager implements FeatureManager<Feature<Point>> {
});
return true;
}
if (exerciseState.simulatedRegions[id]) {
this.exerciseService.proposeAction({
type: '[SimulatedRegion] Remove simulated region',
simulatedRegionId: id,
});
return true;
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import type { Store } from '@ngrx/store';
import type { UUID } from 'digital-fuesim-manv-shared';
import { Size, SimulatedRegion } from 'digital-fuesim-manv-shared';
import type { Feature, MapBrowserEvent } from 'ol';
import type { Coordinate } from 'ol/coordinate';
import type LineString from 'ol/geom/LineString';
import type VectorLayer from 'ol/layer/Vector';
import type OlMap from 'ol/Map';
import type VectorSource from 'ol/source/Vector';
import Stroke from 'ol/style/Stroke';
import Style from 'ol/style/Style';
import type { ExerciseService } from 'src/app/core/exercise.service';
import type { AppState } from 'src/app/state/app.state';
import { selectCurrentRole } from 'src/app/state/application/selectors/shared.selectors';
import { selectStateSnapshot } from 'src/app/state/get-state-snapshot';
import { SimulatedRegionPopupComponent } from '../shared/simulated-region-popup/simulated-region-popup.component';
import { calculatePopupPositioning } from '../utility/calculate-popup-positioning';
import type { FeatureManager } from '../utility/feature-manager';
import { ModifyHelper } from '../utility/modify-helper';
import {
createLineString,
ElementFeatureManager,
getCoordinateArray,
} from './element-feature-manager';

export function isInSimulatedRegion(
coordinate: Coordinate,
simulatedRegion: SimulatedRegion
): boolean {
return SimulatedRegion.isInSimulatedRegion(simulatedRegion, {
x: coordinate[0]!,
y: coordinate[1]!,
});
}

export class SimulatedRegionFeatureManager
extends ElementFeatureManager<SimulatedRegion, LineString>
implements FeatureManager<Feature<LineString>>
{
readonly type = 'simulatedRegions';

override unsupportedChangeProperties = new Set(['id'] as const);

constructor(
olMap: OlMap,
layer: VectorLayer<VectorSource<LineString>>,
private readonly exerciseService: ExerciseService,
private readonly store: Store<AppState>
) {
super(
olMap,
layer,
(targetPositions, simulatedRegion) => {
exerciseService.proposeAction({
type: '[SimulatedRegion] Move simulated region',
simulatedRegionId: simulatedRegion.id,
targetPosition: targetPositions[0]!,
});
},
createLineString
);
this.layer.setStyle(this.style);
}
private readonly modifyHelper = new ModifyHelper();

private readonly style = new Style({
geometry(thisFeature) {
const modifyGeometry = thisFeature.get('modifyGeometry');
return modifyGeometry
? modifyGeometry.geometry
: thisFeature.getGeometry();
},
stroke: new Stroke({
color: '#cccc00',
width: 2,
}),
});

override createFeature(element: SimulatedRegion): Feature<LineString> {
const feature = super.createFeature(element);
this.modifyHelper.onModifyEnd(feature, (newPositions) => {
// Skip when not all coordinates are properly set.
if (
!newPositions.every(
(position) =>
Number.isFinite(position.x) &&
Number.isFinite(position.y)
)
) {
const simulatedRegion =
this.getElementFromFeature(feature)!.value;
this.recreateFeature(simulatedRegion);
return;
}
const lineString = newPositions;

// We expect the simulatedRegion LineString to have 4 points.
const topLeft = lineString[0]!;
const bottomRight = lineString[2]!;
this.exerciseService.proposeAction({
type: '[SimulatedRegion] Resize simulated region',
simulatedRegionId: element.id,
targetPosition: topLeft,
newSize: Size.create(
bottomRight.x - topLeft.x,
topLeft.y - bottomRight.y
),
});
});
return feature;
}

override changeFeature(
oldElement: SimulatedRegion,
newElement: SimulatedRegion,
changedProperties: ReadonlySet<keyof SimulatedRegion>,
elementFeature: Feature<LineString>
): void {
if (
changedProperties.has('position') ||
changedProperties.has('size')
) {
const newFeature = this.getFeatureFromElement(newElement);
if (!newFeature) {
throw new TypeError('newFeature undefined');
}
this.movementAnimator.animateFeatureMovement(
elementFeature,
getCoordinateArray(newElement)
);
}
// If the style has updated, we need to redraw the feature
elementFeature.changed();
}

public override onFeatureClicked(
event: MapBrowserEvent<any>,
feature: Feature<any>
): void {
super.onFeatureClicked(event, feature);
if (selectStateSnapshot(selectCurrentRole, this.store) !== 'trainer') {
return;
}
const zoom = this.olMap.getView().getZoom()!;
const margin = 10 / zoom;

this.togglePopup$.next({
component: SimulatedRegionPopupComponent,
context: {
simulatedRegionId: feature.getId() as UUID,
},
// We want the popup to be centered on the mouse position
...calculatePopupPositioning(
event.coordinate,
{
height: margin,
width: margin,
},
this.olMap.getView().getCenter()!
),
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<ng-container *ngIf="simulatedRegion$ | async as simulatedRegion">
<h5 class="popover-header">
Simulierter Bereich {{ simulatedRegion.name }}
<button
(click)="closePopup.emit()"
type="button"
class="btn-close float-end"
></button>
</h5>
<div class="popover-body" style="width: 350px; height: 300px">
<ng-container *ngIf="(currentRole$ | async) === 'trainer'">
<div class="form-group pb-3">
<label class="form-label">Name</label>
<input
#nameInput="ngModel"
[ngModel]="simulatedRegion.name"
(appSaveOnTyping)="renameSimulatedRegion($event)"
required
type="text"
class="form-control"
/>
<app-display-validation
[ngModelInput]="nameInput"
></app-display-validation>
</div>
</ng-container>
</div>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { OnInit } from '@angular/core';
import { Component, EventEmitter, Output } from '@angular/core';
import { Store } from '@ngrx/store';
import type { UUID, SimulatedRegion } from 'digital-fuesim-manv-shared';
import type { Observable } from 'rxjs';
import { ExerciseService } from 'src/app/core/exercise.service';
import type { AppState } from 'src/app/state/app.state';
import { createSelectSimulatedRegion } from 'src/app/state/application/selectors/exercise.selectors';
import { selectCurrentRole } from 'src/app/state/application/selectors/shared.selectors';

@Component({
selector: 'app-simulated-region-popup',
templateUrl: './simulated-region-popup.component.html',
styleUrls: ['./simulated-region-popup.component.scss'],
})
export class SimulatedRegionPopupComponent implements OnInit {
// These properties are only set after OnInit
public simulatedRegionId!: UUID;

@Output() readonly closePopup = new EventEmitter<void>();

public simulatedRegion$?: Observable<SimulatedRegion>;
public readonly currentRole$ = this.store.select(selectCurrentRole);

constructor(
private readonly store: Store<AppState>,
private readonly exerciseService: ExerciseService
) {}

ngOnInit() {
this.simulatedRegion$ = this.store.select(
createSelectSimulatedRegion(this.simulatedRegionId)
);
}

public renameSimulatedRegion(newName: string) {
this.exerciseService.proposeAction({
type: '[SimulatedRegion] Rename simulatedRegion',
simulatedRegionId: this.simulatedRegionId,
newName,
});
}
}
Loading