diff --git a/application/frontend/src/app/core/actions/routes-chart.actions.ts b/application/frontend/src/app/core/actions/routes-chart.actions.ts index 1da4b964..5b13bb44 100644 --- a/application/frontend/src/app/core/actions/routes-chart.actions.ts +++ b/application/frontend/src/app/core/actions/routes-chart.actions.ts @@ -56,6 +56,11 @@ export const removeFilter = createAction( props<{ filter: ActiveFilter }>() ); +export const setFilters = createAction( + '[RoutesChart] Set Filters', + props<{ filters: ActiveFilter[] }>() +); + export const selectRange = createAction( '[RoutesChart] Select Range', props<{ rangeIndex: number }>() diff --git a/application/frontend/src/app/core/reducers/routes-chart.reducer.ts b/application/frontend/src/app/core/reducers/routes-chart.reducer.ts index 47e805e9..860c660b 100644 --- a/application/frontend/src/app/core/reducers/routes-chart.reducer.ts +++ b/application/frontend/src/app/core/reducers/routes-chart.reducer.ts @@ -113,6 +113,11 @@ export const reducer = createReducer( filters: state.filters.filter((f) => !(f.id === filter.id && f.params === filter.params)), pageIndex: 0, })), + on(RoutesChartActions.setFilters, (state, { filters }) => ({ + ...state, + filters, + pageIndex: 0, + })), on( RoutesChartActions.anchorRangeOffset, RoutesChartActions.nextRangeOffset, diff --git a/application/frontend/src/app/core/selectors/visit.selectors.ts b/application/frontend/src/app/core/selectors/visit.selectors.ts index f3b6e35c..1f185d9e 100644 --- a/application/frontend/src/app/core/selectors/visit.selectors.ts +++ b/application/frontend/src/app/core/selectors/visit.selectors.ts @@ -39,6 +39,11 @@ const selectVisitRequests = createSelector( (ids: number[] | string[], visitRequests) => ids.map((id) => visitRequests[id]) ); +const selectVisitRequestsByIds = (ids: number[]) => + createSelector(fromVisitRequest.selectEntities, (visitRequests) => + ids.map((id) => visitRequests[id]) + ); + const selectVisitForEdit = (id: number, shipmentRouteId: number) => createSelector( selectById(id), @@ -85,6 +90,7 @@ export const VisitSelectors = { selectDeliveryByShipmentId, selectPickupByShipmentId, selectChangedVisitsFromIds, + selectVisitRequestsByIds, }; export default VisitSelectors; diff --git a/application/frontend/src/app/core/services/map.service.ts b/application/frontend/src/app/core/services/map.service.ts index a73fa427..31b0b08f 100644 --- a/application/frontend/src/app/core/services/map.service.ts +++ b/application/frontend/src/app/core/services/map.service.ts @@ -105,11 +105,13 @@ export class MapService { ); } - setBounds(bounds: google.maps.LatLngBounds): void { + setBounds(bounds: google.maps.LatLngBounds, save = true): void { if (bounds) { this.map?.fitBounds(bounds); } - this.bounds = bounds; + if (save) { + this.bounds = bounds; + } } zoomToHome(): void { diff --git a/application/frontend/src/app/routes-chart/components/base-routes-row/base-routes-row.component.html b/application/frontend/src/app/routes-chart/components/base-routes-row/base-routes-row.component.html index 5e0d6abf..801fb21c 100644 --- a/application/frontend/src/app/routes-chart/components/base-routes-row/base-routes-row.component.html +++ b/application/frontend/src/app/routes-chart/components/base-routes-row/base-routes-row.component.html @@ -66,6 +66,6 @@ [pendingNewPois]="pendingNewPois" [pendingOldVisitIds]="pendingOldVisitIds" [color]="color" - (pointOfInterestClick)="pointOfInterestClick.emit($event)"> + (clickVisitIds)="clickVisitIds.emit($event)"> diff --git a/application/frontend/src/app/routes-chart/components/base-routes-row/base-routes-row.component.ts b/application/frontend/src/app/routes-chart/components/base-routes-row/base-routes-row.component.ts index a45eb73b..5c1c5b6d 100644 --- a/application/frontend/src/app/routes-chart/components/base-routes-row/base-routes-row.component.ts +++ b/application/frontend/src/app/routes-chart/components/base-routes-row/base-routes-row.component.ts @@ -59,9 +59,9 @@ export class BaseRoutesRowComponent implements OnInit { @Input() changedVisits: ChangedVisits; @Input() color = '#1a73e8'; @Output() selectedChange = new EventEmitter(); - @Output() pointOfInterestClick = new EventEmitter(); @Output() editVehicle = new EventEmitter(); @Output() viewMetadata = new EventEmitter(); + @Output() clickVisitIds = new EventEmitter(); @HostBinding('class') className = 'item item-container'; allowExperimentalFeatures: boolean; diff --git a/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.html b/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.html index 4034fc28..208bb28f 100644 --- a/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.html +++ b/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.html @@ -15,7 +15,7 @@ [relaxationTimes]="relaxationTimes$ | async" [range]="range$ | async" [color]="color$ | async" - (pointOfInterestClick)="onPointOfInterestClick($event)" (editVehicle)="onEditVehicle($event)" - (viewMetadata)="onViewMetadata($event)"> + (viewMetadata)="onViewMetadata($event)" + (clickVisitIds)="onClickVisitIds($event)"> diff --git a/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.spec.ts b/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.spec.ts index 22c01074..b9d53c03 100644 --- a/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.spec.ts +++ b/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.spec.ts @@ -23,12 +23,10 @@ import * as Long from 'long'; import { PointOfInterest, PointOfInterestClick, - PointOfInterestStartDrag, ShipmentRoute, Timeline, ChangedVisits, Vehicle, - PointOfInterestTimelineOverlapBegin, } from 'src/app/core/models'; import * as fromConfig from 'src/app/core/selectors/config.selectors'; import * as fromPointOfInterest from 'src/app/core/selectors/point-of-interest.selectors'; @@ -39,10 +37,11 @@ import * as fromTimeline from 'src/app/core/selectors/timeline.selectors'; import * as fromVehicle from 'src/app/core/selectors/vehicle.selectors'; import VisitSelectors from 'src/app/core/selectors/visit.selectors'; import { SharedModule } from 'src/app/shared/shared.module'; -import { ValidationService } from '../../../core/services'; +import { MapService, ValidationService } from '../../../core/services'; import { RoutesRowComponent } from './routes-row.component'; import * as fromDispatcher from 'src/app/core/selectors/dispatcher.selectors'; import ShipmentModelSelectors from '../../../core/selectors/shipment-model.selectors'; +import { MockMapService } from 'src/test/service-mocks'; @Component({ selector: 'app-base-routes-row', @@ -80,6 +79,7 @@ describe('RoutesRowComponent', () => { imports: [RouterTestingModule, SharedModule], declarations: [MockBaseRoutesRowComponent, RoutesRowComponent], providers: [ + { provide: MapService, useClass: MockMapService }, { provide: ValidationService, useValue: jasmine.createSpyObj('validationService', ['getErrorEntityIds']), @@ -141,6 +141,13 @@ describe('RoutesRowComponent', () => { ) ); + spyOn(VisitSelectors, 'selectVisitRequestsByIds').and.returnValue( + createSelector( + () => null, + (_state) => [] + ) + ); + fixture.detectChanges(); }); diff --git a/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.ts b/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.ts index df63d941..5b17535b 100644 --- a/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.ts +++ b/application/frontend/src/app/routes-chart/containers/routes-row/routes-row.component.ts @@ -33,7 +33,6 @@ import { ShipmentRoute, Timeline, Vehicle, - PointOfInterestClick, IConstraintRelaxation, ChangedVisits, } from 'src/app/core/models'; @@ -46,12 +45,13 @@ import * as fromVehicle from 'src/app/core/selectors/vehicle.selectors'; import * as fromRoot from 'src/app/reducers'; import RequestSettingsSelectors from 'src/app/core/selectors/request-settings.selectors'; import VisitSelectors from 'src/app/core/selectors/visit.selectors'; -import { ValidationService } from 'src/app/core/services'; +import { MapService, ValidationService } from 'src/app/core/services'; import { PostSolveMetricsActions } from 'src/app/core/actions'; import { Router } from '@angular/router'; import { Page } from 'src/app/core/models'; import { durationSeconds } from 'src/app/util'; import * as fromDispatcher from 'src/app/core/selectors/dispatcher.selectors'; +import { NumberFilterParams } from 'src/app/shared/models'; @Component({ selector: 'app-routes-row', @@ -81,7 +81,8 @@ export class RoutesRowComponent implements OnChanges, OnInit, OnDestroy { constructor( private router: Router, private store: Store, - private validationService: ValidationService + private validationService: ValidationService, + private mapService: MapService ) {} ngOnChanges(changes: SimpleChanges): void { @@ -208,13 +209,6 @@ export class RoutesRowComponent implements OnChanges, OnInit, OnDestroy { this.store.dispatch(action({ routeId: this.route.id })); } - onPointOfInterestClick(pointOfInterestClick: PointOfInterestClick): void { - if (pointOfInterestClick.visitId < 1) { - return; - } - this.store.dispatch(RoutesChartActions.editVisit({ visitId: pointOfInterestClick.visitId })); - } - onEditVehicle(vehicleId: number): void { this.store.dispatch(PreSolveVehicleActions.editVehicle({ vehicleId })); } @@ -223,4 +217,37 @@ export class RoutesRowComponent implements OnChanges, OnInit, OnDestroy { this.store.dispatch(PostSolveMetricsActions.showMetadataForRoute({ id })); this.router.navigateByUrl('/' + Page.RoutesMetadata, { skipLocationChange: true }); } + + onClickVisitIds(ids: number[]): void { + this.store.dispatch( + RoutesChartActions.setFilters({ + filters: [ + { + id: 'routeId', + label: `Route ID = ${this.route.id}`, + params: { + operation: '=', + value: this.route.id, + } as NumberFilterParams, + }, + ], + }) + ); + + this.store + .pipe(select(VisitSelectors.selectVisitRequestsByIds(ids))) + .subscribe((visitRequests) => { + const bounds = new google.maps.LatLngBounds(); + visitRequests.forEach((vr) => + bounds.extend({ + lat: vr.arrivalWaypoint.location.latLng.latitude, + lng: vr.arrivalWaypoint.location.latLng.longitude, + }) + ); + this.mapService.setBounds(bounds, false); + if (this.mapService.map.getZoom() > 16) { + this.mapService.map.setZoom(16); + } + }); + } } diff --git a/application/frontend/src/app/shared/components/points-of-interest/points-of-interest.component.ts b/application/frontend/src/app/shared/components/points-of-interest/points-of-interest.component.ts index 426e3084..b27308d4 100644 --- a/application/frontend/src/app/shared/components/points-of-interest/points-of-interest.component.ts +++ b/application/frontend/src/app/shared/components/points-of-interest/points-of-interest.component.ts @@ -28,12 +28,7 @@ import { ViewChild, } from '@angular/core'; import * as Long from 'long'; -import { - PointOfInterest, - PointOfInterestCategory, - PointOfInterestClick, - ChangedVisits, -} from 'src/app/core/models'; +import { PointOfInterest, PointOfInterestCategory, ChangedVisits } from 'src/app/core/models'; import { defaultTimeFormat, formatSecondsDate, timeToPixel } from 'src/app/util'; import { Cluster, PointsOfInterestImageAttribute, pointsOfInterestImages } from '../../models'; @@ -68,7 +63,7 @@ export class PointsOfInterestComponent implements OnChanges { @Input() routeId: number; @Input() changedVisits: ChangedVisits; @Input() color: string; - @Output() pointOfInterestClick = new EventEmitter(); + @Output() clickVisitIds = new EventEmitter(); get imageAttributeLookup(): { [key: string]: PointsOfInterestImageAttribute } { return PointsOfInterestComponent.imageAttributeLookup; @@ -120,11 +115,20 @@ export class PointsOfInterestComponent implements OnChanges { // Not the primary button return; } - this.pointOfInterestClick.emit({ - category: point[1], - visitId: point[0], - relativeTo: event.target as Element, - }); + + const visitIds = [point[0]]; + const cluster = this.clusters.find( + (cluster) => cluster.start <= point[2] && cluster.end >= point[2] + ); + if (cluster) { + this.points.forEach((p) => { + if (cluster.start <= p[2] && cluster.end >= p[2]) { + visitIds.push(p[0]); + } + }); + } + + this.clickVisitIds.emit(visitIds); event.preventDefault(); event.stopPropagation(); } diff --git a/application/frontend/src/config.json b/application/frontend/src/config.json index 248ac239..e380634a 100644 --- a/application/frontend/src/config.json +++ b/application/frontend/src/config.json @@ -56,6 +56,7 @@ }, "options": { "zoom": 12, + "maxZoom": 18, "center": { "lat": 48.8566, "lng": 2.3522