-
Notifications
You must be signed in to change notification settings - Fork 15
Implement global measures #1345
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
Open
Jogius
wants to merge
48
commits into
dev-next
Choose a base branch
from
feature/measures
base: dev-next
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+4,097
−202
Open
Changes from 26 commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
2ced797
feat: add measure datamodel
Jogius 40ca1b9
feat: add measure selectors and bar
Jogius 19fcfdc
feat: add measure execution
Jogius de6efe3
feat: redesign measure toolbar
Jogius 5639f55
feat: add alarm and eoc log measure properties
Jogius 4cccd5d
feat: add measures to trainer view
Jogius 0c3d24e
feat: add measure template creation form and modal
Jogius 02a53a4
feat: add form validation with zod
Jogius 1a36e66
feat: drag and drop reordering measure templates
Jogius e0e91e5
fix: apply style and linting
Jogius 921d90d
fix: allow drag scrolling with normal modal
Jogius 12ab0f0
feat: add scroll buttons to measures
Jogius d76c715
fix: bump state version and change property deps
Jogius b2de849
feat: add edit measure template modal
Jogius 7651e15
feat: alarm group and transferpoint display
Jogius 3f79fa5
feat: add drawing properties
Jogius 1ca40dc
feat: add measure categories
Jogius 78b2000
update: improve hint text UI
Jogius bfd6b33
fix: measure execution
Jogius 13f9c7f
feat: allow replacement of measures
Jogius 6095e73
feat: add abort/complete buttons
Jogius 89d4cef
feat: add delay abort
Jogius 4c575de
fix: use signal from for alarm modal
Jogius 885a4da
fix: use signal form for measure template
Jogius 2fde377
chore: reformatting with new prettier
Jogius b85214f
fix: address review comments
Jogius 8bbcc0d
fix: remove templates and minor fixes
Jogius 0948144
fix: minor review changes
Jogius c23446d
Fix some design aspects of global measures
hansegucker 9fd232f
fix: improve measure template form
Jogius 231e830
fix: bump state version
Jogius 56bb7fa
feat: remove dnd, add arrow buttons
Jogius 35d8b9e
Merge branch 'dev-next' into feature/measures
fekoch c13db1a
fix: bump state version to 52
Jogius bf43859
fix: honor exercise pause in delay
Jogius f17ecff
fix: add type to measure related types
Jogius 456dc28
feat: add measures to log
Jogius e727b30
feat: make drawing strokes wider
Jogius c4b48cd
Merge branch 'dev-next' into feature/measures
Jogius 6f678a0
fix: change log text
Jogius 72db18e
fix: address some review comments
Jogius 3698d91
feat: migrate actions to zod
Jogius bedbd62
chore: rename drawing type to polygon
Jogius 65ca0bc
chore: clean up measures data model
Jogius 86cc780
Merge branch 'dev-next' into feature/measures
Jogius 82c0b68
chore: code improvements
Jogius ed22bbe
Merge branch 'dev-next' into feature/measures
Jogius cfc8296
fix: measures in fullscreen and animation
Jogius File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { Injectable } from '@angular/core'; | ||
| import type { MapCoordinates, DrawingType } from 'fuesim-digital-shared'; | ||
| import { Observable, Subject } from 'rxjs'; | ||
|
|
||
| export interface DrawRequest { | ||
| drawingType: DrawingType; | ||
| strokeColor: string; | ||
| fillColor?: string; | ||
| endEvent: Observable<boolean | null>; | ||
| } | ||
|
|
||
| export interface DrawingResult { | ||
| points: MapCoordinates[]; | ||
| } | ||
|
|
||
| @Injectable({ | ||
| providedIn: 'root', | ||
| }) | ||
| export class DrawingInteractionService { | ||
| private readonly drawRequest = new Subject<DrawRequest>(); | ||
| private pendingResolve: ((result: DrawingResult | null) => void) | null = | ||
| null; | ||
|
|
||
| public get onDrawRequest$(): Observable<DrawRequest> { | ||
| return this.drawRequest.asObservable(); | ||
| } | ||
|
|
||
| public async requestDrawing( | ||
| request: DrawRequest | ||
| ): Promise<DrawingResult | null> { | ||
| return new Promise<DrawingResult | null>((resolve) => { | ||
| if (this.pendingResolve !== null) this.pendingResolve(null); | ||
| this.pendingResolve = resolve; | ||
| this.drawRequest.next(request); | ||
| }); | ||
| } | ||
|
|
||
| public completeDrawing(result: DrawingResult | null): void { | ||
| if (this.pendingResolve) { | ||
| this.pendingResolve(result); | ||
| this.pendingResolve = null; | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,257 @@ | ||
| import { inject, Injectable, signal } from '@angular/core'; | ||
| import { Store } from '@ngrx/store'; | ||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||
| import { | ||
| MeasureProperty, | ||
| MeasurePropertyInstance, | ||
| MeasureTemplate, | ||
| newDrawing, | ||
| uuid, | ||
| } from 'fuesim-digital-shared'; | ||
| import { filter, firstValueFrom, map, Subject, timer, race } from 'rxjs'; | ||
|
|
||
| import { AppState } from '../state/app.state'; | ||
| import { selectLastClientName } from '../state/application/selectors/application.selectors'; | ||
| import { | ||
| openAlarmModal, | ||
| openEocLogModal, | ||
| } from '../pages/exercises/exercise/shared/measure-modals/open-measure-modals'; | ||
| import { selectStateSnapshot } from '../state/get-state-snapshot'; | ||
| import { | ||
| createSelectAlarmGroup, | ||
| selectCurrentTime, | ||
| } from '../state/application/selectors/exercise.selectors'; | ||
| import { getVehicleParameters } from '../shared/functions/vehicle-parameters'; | ||
| import { ConfirmationModalService } from './confirmation-modal/confirmation-modal.service'; | ||
| import { ExerciseService } from './exercise.service'; | ||
| import { DrawingInteractionService } from './drawing-interaction.service'; | ||
| import { MessageService } from './messages/message.service'; | ||
|
|
||
| @Injectable({ | ||
| providedIn: 'root', | ||
| }) | ||
| export class MeasureService { | ||
| private readonly store = inject<Store<AppState>>(Store); | ||
| private readonly exerciseService = inject(ExerciseService); | ||
| private readonly messageService = inject(MessageService); | ||
| private readonly ngbModalService = inject(NgbModal); | ||
| private readonly confirmationModalService = inject( | ||
| ConfirmationModalService | ||
| ); | ||
| private readonly drawingInteractionService = inject( | ||
| DrawingInteractionService | ||
| ); | ||
|
|
||
| private readonly clientName = this.store.selectSignal(selectLastClientName); | ||
|
|
||
| public readonly activeMeasure = signal<MeasureTemplate | null>(null); | ||
| public readonly activeProperty = signal<MeasureProperty | null>(null); | ||
| public endEvent?: Subject<boolean | null>; | ||
|
|
||
| /** | ||
| * Handles the execution of a single property for a measure. | ||
| * @param template The template this property is being executed for. | ||
| * @param property The property being executed. | ||
| * @returns - false if user canceled the measure | ||
| * - true if property was executed successfully but returns no instance | ||
| * - MeasurePropertyInstance if property executed successfully and returns an instance | ||
| */ | ||
| private async handle( | ||
| template: MeasureTemplate, | ||
| property: MeasureProperty | ||
| ): Promise<MeasurePropertyInstance | boolean> { | ||
| this.activeProperty.set(property); | ||
| switch (property.type) { | ||
| case 'delay': | ||
| this.endEvent = new Subject<boolean | null>(); | ||
| return firstValueFrom( | ||
| race( | ||
| this.endEvent.pipe(filter((e) => e !== null)), | ||
| timer(property.delay * 1000).pipe(map(() => true)) | ||
| ) | ||
| ); | ||
| case 'response': | ||
| await this.confirmationModalService.confirm({ | ||
| title: template.name, | ||
| description: property.response, | ||
| confirmationString: '', | ||
| }); | ||
| return true; | ||
| case 'manualConfirm': { | ||
| const res = await this.confirmationModalService.confirm({ | ||
| title: template.name, | ||
| description: property.prompt, | ||
| confirmationString: property.confirmationString, | ||
| }); | ||
| return res ?? false; | ||
| } | ||
| case 'alarm': { | ||
| const modalRef = openAlarmModal( | ||
| this.ngbModalService, | ||
| property.alarmGroups, | ||
| property.targetTransferPointIds | ||
| ); | ||
| try { | ||
| const result = await modalRef.result; | ||
| return { | ||
| type: 'alarmInstance', | ||
| alarmGroup: result.alarmGroup, | ||
| targetTransferPointId: result.targetTransferPointId, | ||
| vehicleParameters: getVehicleParameters( | ||
| this.store, | ||
| selectStateSnapshot( | ||
| createSelectAlarmGroup(result.alarmGroup), | ||
| this.store | ||
| ) | ||
| ), | ||
| }; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| case 'eocLog': { | ||
| let message: string; | ||
| if (property.confirm) { | ||
| const modalRef = openEocLogModal( | ||
| this.ngbModalService, | ||
| property.message, | ||
| property.editable | ||
| ); | ||
| try { | ||
| const result = await modalRef.result; | ||
| message = result.message; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } else { | ||
| // The zod schema enforces that the message is editable | ||
| // when it is empty, and that it has to be confirmed when | ||
| // it is editable. Thus, message must be defined here. | ||
| message = property.message!; | ||
| } | ||
|
|
||
| return { | ||
| type: 'eocLogInstance', | ||
| message, | ||
| }; | ||
| } | ||
| case 'drawFreehand': { | ||
| this.endEvent = new Subject<boolean | null>(); | ||
| const result = | ||
| await this.drawingInteractionService.requestDrawing({ | ||
| drawingType: 'freehand', | ||
| strokeColor: property.strokeColor, | ||
| fillColor: property.fillColor, | ||
| endEvent: this.endEvent, | ||
| }); | ||
| if (!result) return false; | ||
| const drawing = newDrawing( | ||
| 'freehand', | ||
| result.points, | ||
| property.strokeColor, | ||
| property.fillColor | ||
| ); | ||
| this.exerciseService.proposeAction({ | ||
| type: '[Drawing] Add drawing', | ||
| drawing, | ||
| }); | ||
| return { | ||
| type: 'drawingInstance', | ||
| id: drawing.id, | ||
| }; | ||
| } | ||
| case 'drawLine': { | ||
| this.endEvent = new Subject<boolean | null>(); | ||
| const result = | ||
| await this.drawingInteractionService.requestDrawing({ | ||
| drawingType: 'line', | ||
| strokeColor: property.strokeColor, | ||
| endEvent: this.endEvent, | ||
| }); | ||
| if (!result) return false; | ||
| const drawing = newDrawing( | ||
| 'line', | ||
| result.points, | ||
| property.strokeColor, | ||
| undefined | ||
| ); | ||
| this.exerciseService.proposeAction({ | ||
| type: '[Drawing] Add drawing', | ||
| drawing, | ||
| }); | ||
| return { | ||
| type: 'drawingInstance', | ||
| id: drawing.id, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private resetActive() { | ||
| this.activeMeasure.set(null); | ||
| this.activeProperty.set(null); | ||
| } | ||
|
|
||
| public async executeMeasure(template: MeasureTemplate) { | ||
| this.activeMeasure.set(template); | ||
|
|
||
| const instances = []; | ||
| for (const property of template.properties) { | ||
| // eslint-disable-next-line no-await-in-loop | ||
| const result = await this.handle(template, property); | ||
|
|
||
| if (result === false) { | ||
| this.resetActive(); | ||
| this.abort(instances); | ||
| return; | ||
| } else if (result === true) continue; | ||
|
|
||
| instances.push(result); | ||
| } | ||
|
|
||
| this.resetActive(); | ||
| this.confirm(template, instances); | ||
| } | ||
|
|
||
| private confirm( | ||
| template: MeasureTemplate, | ||
| instances: MeasurePropertyInstance[] | ||
| ) { | ||
| this.exerciseService | ||
| .proposeAction({ | ||
| type: '[Measure] Add Measure', | ||
| measure: { | ||
| clientName: this.clientName() ?? 'Unknown', | ||
| id: uuid(), | ||
| instances, | ||
| templateId: template.id, | ||
| timestamp: selectStateSnapshot( | ||
| selectCurrentTime, | ||
| this.store | ||
| ), | ||
| }, | ||
| }) | ||
| .catch((e) => { | ||
| this.messageService.postError({ | ||
| title: 'Fehler beim Erstellen der Maßnahme', | ||
| error: e.message, | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| private abort(instances: MeasurePropertyInstance[]) { | ||
| for (const instance of instances) { | ||
| switch (instance.type) { | ||
| case 'drawingInstance': { | ||
| this.exerciseService.proposeAction({ | ||
| type: '[Drawing] Remove drawing', | ||
| drawingId: instance.id, | ||
| }); | ||
| break; | ||
| } | ||
| default: | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
...app/pages/exercises/exercise/shared/editor-panel/measure-card/measure-card.component.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <div class="card text-center me-1 mb-1" [attr.data-cy]="dataCy()"> | ||
| <div class="card-body-wrapper"> | ||
| <div class="card-body px-1 py-1"> | ||
| <span class="card-text" placement="bottom"> | ||
| {{ name() }} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| <div style="top: 0; right: 0" class="position-absolute"> | ||
| @if (enableEditButton()) { | ||
| <button | ||
| (mousedown)="elementEdit.emit(); $event.stopPropagation()" | ||
| class="btn btn-outline-warning btn-sm" | ||
| > | ||
| <i class="bi bi-pencil-fill"></i> | ||
| </button> | ||
| } | ||
| @if (enableDeleteButton()) { | ||
| <button | ||
| (mousedown)="elementDelete.emit(); $event.stopPropagation()" | ||
| class="btn btn-outline-danger btn-sm" | ||
| > | ||
| <i class="bi bi-trash"></i> | ||
| </button> | ||
| } | ||
| </div> | ||
| </div> | ||
18 changes: 18 additions & 0 deletions
18
...app/pages/exercises/exercise/shared/editor-panel/measure-card/measure-card.component.scss
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| .card-body-wrapper { | ||
| // Match the height of map-editor-card (70px image + ~30px title) | ||
| height: 105px; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| } | ||
|
|
||
| .card-text { | ||
| display: -webkit-box; | ||
| line-clamp: 4; | ||
| -webkit-line-clamp: 4; | ||
| -webkit-box-orient: vertical; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| word-break: break-word; | ||
| font-weight: 500; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.