Skip to content

Commit

Permalink
feat: Simulate vehicle travel
Browse files Browse the repository at this point in the history
  • Loading branch information
jmccollum-woolpert committed Jun 10, 2024
1 parent 9d330d3 commit 2738c1b
Show file tree
Hide file tree
Showing 28 changed files with 770 additions and 41 deletions.
2 changes: 2 additions & 0 deletions application/frontend/src/app/core/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import * as ShipmentModelActions from './shipment-model.actions';
import * as ShipmentRouteActions from './shipment-route.actions';
import * as ShipmentsMetadataActions from './shipments-metadata.actions';
import * as StorageApiActions from './storage-api.actions';
import * as TravelSimulatorActions from '../actions/travel-simulator.actions';
import * as UIActions from './ui.actions';
import * as UndoRedoActions from './undo-redo.actions';
import * as UploadActions from './upload.actions';
Expand Down Expand Up @@ -71,6 +72,7 @@ export {
ShipmentModelActions,
ShipmentRouteActions,
StorageApiActions,
TravelSimulatorActions,
UIActions,
UndoRedoActions,
UploadActions,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
Copyright 2024 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { createAction, props } from '@ngrx/store';

export const setTime = createAction('[Travel Simulator] Set time', props<{ time: number }>());

export const setActive = createAction(
'[Travel Simulator] Set active',
props<{ active: boolean }>()
);
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ export class AppComponent {
'navigate_before',
'navigate_next',
'open_in_new',
'pause',
'pdf',
'play',
'pickup',
'route',
'satellite',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
(activeChange)="onToggleSelectMapItems($event)">
</app-select-map-items-button>
<app-zoom-home-button (zoomToHome)="onZoomToHome()" [disabled]="!bounds"></app-zoom-home-button>
<app-travel-simulator *ngIf="travelSimulatorVisible$ | async"></app-travel-simulator>
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { MockInfoWindowService, MockLayerService } from 'src/test/service-mocks'
import * as fromConfig from '../../selectors/config.selectors';
import * as fromMap from '../../selectors/map.selectors';
import * as fromUI from '../../selectors/ui.selectors';
import TravelSimulatorSelectors from '../../selectors/travel-simulator.selectors';
import {
DepotLayer,
MapService,
Expand Down Expand Up @@ -81,6 +82,12 @@ class MockPostSolveMapLegendComponent {
@Output() setLayerVisibility = new EventEmitter<{ layerId: MapLayerId; visible: boolean }>();
}

@Component({
selector: 'app-travel-simulator',
template: '',
})
class MockTravelSimulatorComponent {}

describe('MapComponent', () => {
let component: MapComponent;
let fixture: ComponentFixture<MapComponent>;
Expand All @@ -99,6 +106,7 @@ describe('MapComponent', () => {
MockMapWrapperComponent,
MockZoomHomeButtonComponent,
MockPostSolveMapLegendComponent,
MockTravelSimulatorComponent,
MapComponent,
],
providers: [
Expand All @@ -119,6 +127,7 @@ describe('MapComponent', () => {
{ selector: fromUI.selectPage, value: null },
{ selector: fromUI.selectHasMap, value: false },
{ selector: fromConfig.selectTimezoneOffset, value: 0 },
{ selector: TravelSimulatorSelectors.selectTravelSimulatorVisible, value: false },
],
}),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import * as fromPostSolve from '../../selectors/post-solve.selectors';
import * as fromPreSolve from '../../selectors/pre-solve.selectors';
import * as fromUI from '../../selectors/ui.selectors';
import { selectPage } from '../../selectors/ui.selectors';
import TravelSimulatorSelectors from '../../selectors/travel-simulator.selectors';
import {
MATERIAL_COLORS,
VehicleInfoWindowService,
Expand All @@ -71,6 +72,7 @@ export class MapComponent implements OnInit, OnDestroy {
selectionFilterActive$: Observable<boolean>;
timezoneOffset$: Observable<number>;
layers$: Observable<{ [id in MapLayerId]?: MapLayer }>;
travelSimulatorVisible$: Observable<boolean>;

get bounds(): google.maps.LatLngBounds {
return this.mapService.bounds;
Expand Down Expand Up @@ -100,6 +102,9 @@ export class MapComponent implements OnInit, OnDestroy {
this.mapSelectionToolsVisible$ = this.store.pipe(select(selectMapSelectionToolsVisible));
this.selectionFilterActive$ = this.store.pipe(select(selectSelectionFilterActive));
this.layers$ = this.store.pipe(select(fromMap.selectPostSolveMapLayers));
this.travelSimulatorVisible$ = this.store.pipe(
select(TravelSimulatorSelectors.selectTravelSimulatorVisible)
);

this.subscriptions.push(
this.store
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<div class="d-flex align-items-center">
<mat-slide-toggle [checked]="active$ | async" (change)="onToggleActive($event)" color="primary">
Travel simulator
</mat-slide-toggle>
<div *ngIf="active$ | async" class="d-flex align-items-center simulator-contents">
<mat-divider [vertical]="true" class="divider"></mat-divider>
<button
mat-mini-fab
color="primary"
class="play-button"
type="button"
[disabled]="!(active$ | async)"
(click)="animating ? onEndAnimate() : onBeginAnimate()">
<mat-icon [svgIcon]="animating ? 'pause' : 'play'"></mat-icon>
</button>
<mat-divider [vertical]="true" class="divider"></mat-divider>
<div class="d-flex align-items-center speed-container">
<div>Speed:</div>
<mat-radio-group
aria-label="Animation speed"
color="primary"
[(ngModel)]="animationSpeedMultiple">
<mat-radio-button [value]="0.5">0.5x</mat-radio-button>
<mat-radio-button [value]="1">1x</mat-radio-button>
<mat-radio-button [value]="3">3x</mat-radio-button>
<mat-radio-button [value]="5">5x</mat-radio-button>
</mat-radio-group>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@import '../../../../styles/variables.scss';

:host {
position: absolute;
bottom: 10px;
left: 10px;
padding: 10px;
}

.simulator-toggle {
font-size: 16px;
line-height: 24px;
}

.simulator-contents {
margin-left: 20px;
gap: 20px;
}

.divider {
height: 24px;
}

.play-button {
box-shadow: none;
height: 24px;
width: 24px;

.mat-button-wrapper {
position: relative;
top: -4px;
}
}

.speed-container {
gap: 10px;

mat-radio-group {
display: flex;
gap: 10px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
Copyright 2024 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { ComponentFixture, TestBed } from '@angular/core/testing';

import { TravelSimulatorComponent } from './travel-simulator.component';
import { provideMockStore } from '@ngrx/store/testing';
import ShipmentModelSelectors from '../../selectors/shipment-model.selectors';
import TravelSimulatorSelectors from '../../selectors/travel-simulator.selectors';

describe('TravelSimulatorComponent', () => {
let component: TravelSimulatorComponent;
let fixture: ComponentFixture<TravelSimulatorComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TravelSimulatorComponent],
providers: [
provideMockStore({
selectors: [
{ selector: ShipmentModelSelectors.selectGlobalStartTime, value: 0 },
{ selector: ShipmentModelSelectors.selectGlobalEndTime, value: 0 },
{ selector: TravelSimulatorSelectors.selectTime, value: 0 },
{ selector: TravelSimulatorSelectors.selectActive, value: false },
],
}),
],
}).compileComponents();

fixture = TestBed.createComponent(TravelSimulatorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright 2024 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
HostBinding,
OnDestroy,
OnInit,
} from '@angular/core';
import { Store, select } from '@ngrx/store';
import Long from 'long';
import { Observable, Subscription, interval } from 'rxjs';
import ShipmentModelSelectors from '../../selectors/shipment-model.selectors';
import { map } from 'rxjs/operators';
import { setActive, setTime } from '../../actions/travel-simulator.actions';
import TravelSimulatorSelectors from '../../selectors/travel-simulator.selectors';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { formatSecondsDate } from 'src/app/util/time-translation';

@Component({
selector: 'app-travel-simulator',
templateUrl: './travel-simulator.component.html',
styleUrls: ['./travel-simulator.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TravelSimulatorComponent implements OnInit, OnDestroy {
@HostBinding('class.app-map-panel') readonly mapPanel = true;

readonly animationRateMs: number = 33;
readonly animationStepSize: number = 2;

get animating(): boolean {
return this.animationTimer$?.closed === false;
}

active$: Observable<boolean>;
animationTimer$: Subscription;
animationSpeedMultiple = 1;
end$: Subscription;
end: number;
time$: Subscription;
time: number;

formatSecondsDate = formatSecondsDate;

constructor(private store: Store, private detectorRef: ChangeDetectorRef) {}

ngOnInit(): void {
this.end$ = this.store
.select(ShipmentModelSelectors.selectGlobalEndTime)
.pipe(map((value) => Long.fromValue(value).toNumber()))
.subscribe((end) => {
this.end = end;
this.detectorRef.markForCheck();
});

this.time$ = this.store.select(TravelSimulatorSelectors.selectTime).subscribe((time) => {
this.time = time;
this.detectorRef.markForCheck();
});

this.active$ = this.store.pipe(select(TravelSimulatorSelectors.selectActive));
}

ngOnDestroy(): void {
this.onEndAnimate();
this.time$.unsubscribe();
this.end$.unsubscribe();
}

onBeginAnimate(): void {
this.animationTimer$ = interval(this.animationRateMs).subscribe((_value) => {
const newTime = this.time + this.animationStepSize * this.animationSpeedMultiple;
if (newTime >= this.end) {
this.onEndAnimate();
return;
}
this.store.dispatch(setTime({ time: newTime }));
});
}

onEndAnimate(): void {
this.animationTimer$?.unsubscribe();
this.detectorRef.markForCheck();
}

onToggleActive(event: MatSlideToggleChange): void {
this.store.dispatch(setActive({ active: event.checked }));

if (!event.checked) {
this.onEndAnimate();
}
}
}
2 changes: 2 additions & 0 deletions application/frontend/src/app/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import { TransitionAttributesDialogComponent } from './components/transition-att
import { PrecedenceRulesDialogComponent } from './components/precedence-rules-dialog/precedence-rules-dialog.component';
import { ShipmentModelSettingsComponent } from './containers/shipment-model-settings/shipment-model-settings.component';
import { PostSolveMapLegendComponent } from './components/post-solve-map-legend/post-solve-map-legend.component';
import { TravelSimulatorComponent } from './containers/travel-simulator/travel-simulator.component';

export const COMPONENTS = [
BaseDocumentationDialogComponent,
Expand Down Expand Up @@ -155,6 +156,7 @@ export const CONTAINERS = [
ShipmentsControlBarComponent,
ShipmentsKpisComponent,
StorageApiSaveLoadDialogComponent,
TravelSimulatorComponent,
UploadDialogComponent,
ValidationResultDialogComponent,
VehicleInfoWindowComponent,
Expand Down
Loading

0 comments on commit 2738c1b

Please sign in to comment.