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

Feature/43 add map #44

Merged
merged 49 commits into from
Jan 30, 2022
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
d555181
Add openLayers + ability to render and move patients
Dassderdie Jan 11, 2022
a887905
Add "Move patient" action
Dassderdie Jan 11, 2022
005ebce
Add handle-changes helper function
Dassderdie Jan 11, 2022
c2ff6b6
Add moving patients functioniality to the frontend
Dassderdie Jan 11, 2022
d1a2fdc
Check for existence of patient in move patient reducer
Dassderdie Jan 12, 2022
5c7c4f9
Prevent open layers control buttons from blocking the map
Dassderdie Jan 12, 2022
8e56c58
Animate the movement of patients
Dassderdie Jan 12, 2022
04db1ab
Improve code structure
Dassderdie Jan 12, 2022
3b92efc
Add max zoom level
Dassderdie Jan 12, 2022
53c3394
Make patient dimensions more realistic
Dassderdie Jan 12, 2022
529c19c
Refactor movementAnimations to own class
Dassderdie Jan 12, 2022
0107e21
Increase dimensions of the map
Dassderdie Jan 12, 2022
44242b0
Merge remote-tracking branch 'origin/dev' into feature/43-add-map
Dassderdie Jan 14, 2022
5154eff
Cache the tiles when zooming in and out
Dassderdie Jan 14, 2022
4ae1ec9
Prevent clipping during animations/zoom
Dassderdie Jan 14, 2022
0e4b090
Fix element sometimes not being rendered when moving into viewport
Dassderdie Jan 16, 2022
c60bea5
Merge remote-tracking branch 'origin/dev' into feature/43-add-map
Dassderdie Jan 17, 2022
a4e41d4
Add temporary utilites for better testing
Dassderdie Jan 17, 2022
9807b88
Use custom images for patients instead of Circles
Dassderdie Jan 17, 2022
95cd379
Move patientStyle to patientRenderer
Dassderdie Jan 17, 2022
0181df1
Refactor ElementRenderer code
Dassderdie Jan 18, 2022
bfe966a
Improve ImageStyleHelper
Dassderdie Jan 19, 2022
d10b93d
Improve code style of exercise map
Dassderdie Jan 19, 2022
b563045
Refactor renderers
Dassderdie Jan 20, 2022
c3b35de
More refactoring
Dassderdie Jan 21, 2022
23b173d
Fix performance problem with large numbers of svg features
Dassderdie Jan 21, 2022
30dfea8
Remove incorrect documentation
Dassderdie Jan 21, 2022
6b1e49e
Add vehicles, personell and material
Dassderdie Jan 21, 2022
403fe8e
Tweak settings for demonstration
Dassderdie Jan 24, 2022
20c2af4
Merge remote-tracking branch 'origin/dev' into feature/43-add-map
Dassderdie Jan 24, 2022
e33fda7
Fix incorrect type usage
Dassderdie Jan 24, 2022
43080dd
More refactoring
Dassderdie Jan 24, 2022
9ea425b
Smaller code improvements
Dassderdie Jan 24, 2022
70c6d8b
Move creation buttons from app-component to own component
Dassderdie Jan 24, 2022
c7ec342
Fix some comments
Dassderdie Jan 24, 2022
32f31f4
Multiple smaller improvements
Dassderdie Jan 24, 2022
fe50165
Fix cypress
Dassderdie Jan 24, 2022
3e6387d
Merge remote-tracking branch 'origin/dev' into feature/43-add-map
Dassderdie Jan 24, 2022
147f102
Address most of the feedback
Dassderdie Jan 29, 2022
de9c555
Add ImmutableJson type (as a placeholder)
Dassderdie Jan 29, 2022
52d4bd9
Use Position type instead of literal
Dassderdie Jan 29, 2022
37741d5
Merge remote-tracking branch 'origin/dev' into feature/43-add-map
Dassderdie Jan 29, 2022
14d35cd
Improve comments
Dassderdie Jan 29, 2022
e5bfe40
Fix lint error
Dassderdie Jan 29, 2022
ec5bbc0
Address mistakes made during merge
Dassderdie Jan 30, 2022
6dafb03
Remove angular-jest to solve ci error
Dassderdie Jan 30, 2022
8dbfe09
Remove --force-exit
Dassderdie Jan 30, 2022
9d31a70
Merge pull request #148 from hpi-sam/experimental/fix-frontend-tests
Dassderdie Jan 30, 2022
7bc30d6
Apply last feedback
Dassderdie Jan 30, 2022
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
2 changes: 1 addition & 1 deletion frontend/cypress/integration/landing-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ describe('The landing page', () => {

it('is possible to make visual regression tests (for e.g. a canvas)', () => {
cy.visit('/');
cy.get('.container').compareSnapshot('landing-page', 0.1);
cy.get('#app-container').compareSnapshot('landing-page', 0.1);
});
});
480 changes: 464 additions & 16 deletions frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"bootstrap": "^5.1.3",
"digital-fuesim-manv-shared": "file:../shared",
"lodash-es": "4.17.21",
"ol": "^6.11.0",
"rxjs": "~7.5.1",
"socket.io-client": "^4.4.1",
"tslib": "^2.3.1",
Expand Down
14 changes: 2 additions & 12 deletions frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="container">
<div class="container-fluid" id="app-container">
<br />
<div
*ngIf="
Expand Down Expand Up @@ -51,16 +51,6 @@
</div>

<ng-template #joinedTemplate>
<div>
<app-patients-list></app-patients-list>

<button
(click)="addPatient()"
class="btn btn-primary"
id="addPatient"
>
Add Patient
</button>
</div>
<app-trainer-map-editor></app-trainer-map-editor>
</ng-template>
</div>
23 changes: 0 additions & 23 deletions frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Component } from '@angular/core';
import { Patient, uuid } from 'digital-fuesim-manv-shared';
import { ApiService } from './core/api.service';

@Component({
Expand All @@ -16,28 +15,6 @@ export class AppComponent {

constructor(public readonly apiService: ApiService) {}

// Action
public async addPatient(
patient: Patient = new Patient(
{ hair: 'brown', eyeColor: 'blue', name: 'John Doe', age: 42 },
'green',
'green',
Date.now().toString()
)
) {
patient.vehicleId = uuid();
const response = await this.apiService.proposeAction(
{
type: '[Patient] Add patient',
patient,
},
true
);
if (!response.success) {
console.error(response.message);
}
}

public joinExercise() {
this.apiService.joinExercise(
this.exerciseId,
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import type { AppState } from './state/app.state';
import { PatientsListComponent } from './components/patients-list/patients-list.component';
import { appReducers } from './state/app.reducer';
import { ExerciseMapModule } from './shared/exercise-map/exercise-map.module';
import { TrainerMapEditorComponent } from './shared/trainer-map-editor/trainer-map-editor.component';

@NgModule({
declarations: [AppComponent, PatientsListComponent],
declarations: [AppComponent, TrainerMapEditorComponent],
imports: [
CommonModule,
BrowserModule,
HttpClientModule,
FormsModule,
AppRoutingModule,
StoreModule.forRoot<AppState>(appReducers),
ExerciseMapModule,
],
providers: [],
bootstrap: [AppComponent],
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div #openLayersContainer style="height: calc(100vh - 100px)"></div>
Dassderdie marked this conversation as resolved.
Show resolved Hide resolved
215 changes: 215 additions & 0 deletions frontend/src/app/shared/exercise-map/exercise-map.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import type { AfterViewInit, OnDestroy, Type } from '@angular/core';
import {
ElementRef,
ViewChild,
Component,
ChangeDetectionStrategy,
NgZone,
} from '@angular/core';
import OlMap from 'ol/Map';
import TileLayer from 'ol/layer/Tile';
import XYZ from 'ol/source/XYZ';
import { Store } from '@ngrx/store';
import type { AppState } from 'src/app/state/app.state';
import type { Observable } from 'rxjs';
import { pairwise, debounceTime, startWith, Subject, takeUntil } from 'rxjs';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { Translate, defaults as defaultInteractions } from 'ol/interaction';
import { View } from 'ol';
import { ApiService } from 'src/app/core/api.service';
import type { Position, UUID } from 'digital-fuesim-manv-shared';
import type Point from 'ol/geom/Point';
import { getSelectWithPosition } from 'src/app/state/exercise/exercise.selectors';
import type { WithPosition } from '../utility/types/with-position';
import { PatientFeatureManager } from './feature-managers/patient-feature-manager';
import { handleChanges } from './utility/handle-changes';
import type { FeatureManager } from './feature-managers/feature-manager';
import { VehicleFeatureManager } from './feature-managers/vehicle-feature-manager';
import { PersonellFeatureManager } from './feature-managers/personell-feature-manager';
import { MaterialFeatureManager } from './feature-managers/material-feature-manager';
import type { CommonFeatureManager } from './feature-managers/common-feature-manager';
import { TranslateHelper } from './utility/translate-helper';

@Component({
selector: 'app-exercise-map',
templateUrl: './exercise-map.component.html',
styleUrls: ['./exercise-map.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExerciseMapComponent implements AfterViewInit, OnDestroy {
@ViewChild('openLayersContainer')
openLayersContainer!: ElementRef<HTMLDivElement>;

private readonly destroy$ = new Subject<void>();
private olMap?: OlMap;

constructor(
private readonly store: Store<AppState>,
private readonly ngZone: NgZone,
private readonly apiService: ApiService
) {}

ngAfterViewInit(): void {
// run outside angular zone for better performance
this.ngZone.runOutsideAngular(() => {
this.setupMap();
});
}

private setupMap() {
// Layers
const satelliteLayer = this.createTileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
20
);
const patientLayer = this.createElementLayer();
const vehicleLayer = this.createElementLayer();
const personellLayer = this.createElementLayer();
const materialLayer = this.createElementLayer();
ClFeSc marked this conversation as resolved.
Show resolved Hide resolved

// Interactions
const translateInteraction = new Translate({
layers: [patientLayer, vehicleLayer, personellLayer, materialLayer],
});

this.olMap = new OlMap({
interactions: defaultInteractions().extend([translateInteraction]),
target: this.openLayersContainer.nativeElement,
layers: [
satelliteLayer,
vehicleLayer,
patientLayer,
personellLayer,
materialLayer,
],
view: new View({
center: [1461850, 6871673],
ClFeSc marked this conversation as resolved.
Show resolved Hide resolved
zoom: 20,
maxZoom: 23,
}),
});
// Cursors
this.olMap.on('pointermove', (event) => {
this.setCursorStyle(
this.olMap!.hasFeatureAtPixel(event.pixel) ? 'pointer' : ''
);
});
// TODO:
ClFeSc marked this conversation as resolved.
Show resolved Hide resolved
// translateInteraction.on('translatestart', () => {
// this.setCursorStyle('grabbing');
// });
// translateInteraction.on('translateend', () => {
// this.setCursorStyle('');
// });
TranslateHelper.registerTranslateEvents(translateInteraction);

// FeatureManagers
this.createAndRegisterFeatureManager(
PatientFeatureManager,
patientLayer,
this.store.select(getSelectWithPosition('patients'))
);
this.createAndRegisterFeatureManager(
VehicleFeatureManager,
vehicleLayer,
this.store.select(getSelectWithPosition('vehicles'))
);
this.createAndRegisterFeatureManager(
PersonellFeatureManager,
personellLayer,
this.store.select(getSelectWithPosition('personell'))
);
this.createAndRegisterFeatureManager(
MaterialFeatureManager,
materialLayer,
this.store.select(getSelectWithPosition('materials'))
);
}

// If the signature of the FeatureManager classes change, the initialisation should be done individually
private createAndRegisterFeatureManager<
Element extends Readonly<{ id: UUID; position: Position }>
>(
// `Type` is an utility type from angular, that returns the type of the constructor function
featureManagerClass: Type<CommonFeatureManager<Element>>,
ClFeSc marked this conversation as resolved.
Show resolved Hide resolved
layer: VectorLayer<VectorSource<Point>>,
elementDictionary$: Observable<{ [id: UUID]: Element }>
) {
const featureManager = new featureManagerClass(
this.olMap!,
layer,
this.apiService
);
this.registerFeatureManager(featureManager, elementDictionary$);
}

private registerFeatureManager<Element extends WithPosition<object>>(
featureManager: FeatureManager<Element, any, any>,
elementDictionary$: Observable<{ [id: UUID]: Element }>
) {
elementDictionary$
.pipe(
// TODO: this is workaround for not emitting synchronously
// currently, the setState of the optimistic update and the actions that are reapplied each bring the state to synchronously emit
debounceTime(0),
startWith({}),
pairwise(),
takeUntil(this.destroy$)
)
.subscribe(([oldElementDictionary, newElementDictionary]) => {
// run outside angular zone for better performance
this.ngZone.runOutsideAngular(() => {
handleChanges(
oldElementDictionary,
newElementDictionary,
(element) => featureManager.onElementCreated(element),
(element) => featureManager.onElementDeleted(element),
(oldElement, newElement) =>
featureManager.onElementChanged(
oldElement,
newElement
)
);
});
});
}

private setCursorStyle(cursorStyle: string) {
this.olMap!.getTargetElement().style.cursor = cursorStyle;
}

/**
* @param renderBuffer The size of the largest symbol, line width or label on the highest zoom level.
*/
private createElementLayer(renderBuffer = 250) {
return new VectorLayer({
// TODO: these two settings prevent clipping during animation/interaction but cause a performance hit -> disable if needed
updateWhileAnimating: true,
updateWhileInteracting: true,
renderBuffer,
source: new VectorSource<Point>(),
});
}

/**
* @param url the url to the server that serves the tiles. Must include `{x}`, `{y}` or `{-y}` and `{z}`placeholders.
* @param maxZoom The maximum `{z}` value the tile server accepts
*/
private createTileLayer(url: string, maxZoom: number) {
return new TileLayer({
source: new XYZ({
url,
maxZoom,
// We want to keep the tiles cached if we are zooming in and out fast
cacheSize: 1000,
}),
});
}

ngOnDestroy(): void {
this.destroy$.next();
this.olMap?.dispose();
this.olMap?.setTarget(undefined);
}
}
10 changes: 10 additions & 0 deletions frontend/src/app/shared/exercise-map/exercise-map.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExerciseMapComponent } from './exercise-map.component';

@NgModule({
declarations: [ExerciseMapComponent],
imports: [CommonModule],
exports: [ExerciseMapComponent],
})
export class ExerciseMapModule {}
Loading