Skip to content
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
2 changes: 2 additions & 0 deletions application/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
"protobufjs": "^7.4.0",
"rxjs": "~7.8.2",
"split.js": "^1.6.2",
"terra-draw": "^1.31.1",
"terra-draw-google-maps-adapter": "^1.6.0",
"tslib": "^2.8.1",
"zone.js": "~0.14.10"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
Component,
ElementRef,
EventEmitter,
HostBinding,
Input,
OnDestroy,
OnInit,
Expand All @@ -28,6 +29,11 @@ import { Observable, Subscription } from 'rxjs';
import { tap } from 'rxjs/operators';
import { MapService } from '../../services';

export interface MapInitializeEvent {
map: google.maps.Map;
element: HTMLElement;
}

@Component({
selector: 'app-map-wrapper',
templateUrl: './map-wrapper.component.html',
Expand All @@ -36,7 +42,8 @@ import { MapService } from '../../services';
})
export class MapWrapperComponent implements OnInit, OnDestroy {
@Input() options: google.maps.MapOptions;
@Output() mapInitialize = new EventEmitter<google.maps.Map>();
@Output() mapInitialize = new EventEmitter<MapInitializeEvent>();
@HostBinding('attr.id') readonly mapId = 'google-map-container';
private readonly subscriptions: Subscription[] = [];
private previousMapWidth: number;

Expand All @@ -54,7 +61,7 @@ export class MapWrapperComponent implements OnInit, OnDestroy {
private initializeMap(options: google.maps.MapOptions): Observable<google.maps.Map> {
return this.mapService.initMap(this.el.nativeElement, options).pipe(
tap((map) => {
this.mapInitialize.emit(map);
this.mapInitialize.emit({ map, element: this.el.nativeElement });

// Reset the map bounds when it is toggled on.
// bounds_changed fires off whenever the viewable bounds of the map change, even from user interaction,
Expand Down
76 changes: 28 additions & 48 deletions application/frontend/src/app/core/containers/map/map.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import { combineLatest, Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, take } from 'rxjs/operators';
import { selectMapSelectionToolsVisible, State } from 'src/app/reducers';
import { ActiveFilter, FilterOption } from 'src/app/shared/models';
import { boundsToTurfPolygon, mapsPolygonToTurfPolygon } from 'src/app/util';
import {
MapActions,
PreSolveShipmentActions,
Expand All @@ -47,10 +46,10 @@ import * as fromUI from '../../selectors/ui.selectors';
import { selectPage } from '../../selectors/ui.selectors';
import TravelSimulatorSelectors from '../../selectors/travel-simulator.selectors';
import {
MATERIAL_COLORS,
VehicleInfoWindowService,
VisitRequestInfoWindowService,
MultiselectInfoWindowService,
TerraDrawService,
} from '../../services';
import { DepotLayer } from '../../services/depot-layer.service';
import { MapService } from '../../services/map.service';
Expand All @@ -60,6 +59,7 @@ import { PreSolveVehicleLayer } from '../../services/pre-solve-vehicle-layer.ser
import { PreSolveVisitRequestLayer } from '../../services/pre-solve-visit-request-layer.service';
import { RouteLayer } from '../../services/route-layer.service';
import { MapLayer, MapLayerId } from '../../models/map';
import { MapInitializeEvent } from '../map-wrapper/map-wrapper.component';

@Component({
selector: 'app-map',
Expand All @@ -80,8 +80,8 @@ export class MapComponent implements OnInit, OnDestroy {
}

private readonly subscriptions: Subscription[] = [];
private drawingManager: google.maps.drawing.DrawingManager;
private map: google.maps.Map;
private mapElement: HTMLElement;
private page: Page;

constructor(
Expand All @@ -96,7 +96,8 @@ export class MapComponent implements OnInit, OnDestroy {
private preSolveVisitRequestLayer: PreSolveVisitRequestLayer,
public vehicleInfoWindowService: VehicleInfoWindowService,
public visitRequestInfoWindowService: VisitRequestInfoWindowService,
public multiselectInfoWindowService: MultiselectInfoWindowService
public multiselectInfoWindowService: MultiselectInfoWindowService,
private terraDrawService: TerraDrawService
) {}

ngOnInit(): void {
Expand Down Expand Up @@ -160,6 +161,7 @@ export class MapComponent implements OnInit, OnDestroy {

ngOnDestroy(): void {
this.subscriptions.splice(0).forEach((subscription) => subscription.unsubscribe());
this.terraDrawService.destroy();
}

private createFilter(option: FilterOption): ActiveFilter {
Expand Down Expand Up @@ -195,16 +197,7 @@ export class MapComponent implements OnInit, OnDestroy {
}

onToggleSelectMapItems(mode: SelectionMode): void {
if (mode === SelectionMode.Off) {
this.drawingManager.setMap(null);
} else {
this.drawingManager.setMap(this.map);
this.drawingManager.setDrawingMode(
mode === SelectionMode.Bbox
? google.maps.drawing.OverlayType.RECTANGLE
: google.maps.drawing.OverlayType.POLYGON
);
}
this.terraDrawService.setMode(mode);
}

onTypeChange(satellite: boolean): void {
Expand All @@ -215,44 +208,31 @@ export class MapComponent implements OnInit, OnDestroy {
this.mapService.zoomToHome();
}

onMapInitialize(map: google.maps.Map): void {
this.map = map;
this.drawingManager = this.createDrawingTools();
onMapInitialize(event: MapInitializeEvent): void {
this.map = event.map;
this.mapElement = event.element;
this.initializeTerraDraw();
}

createDrawingTools(): google.maps.drawing.DrawingManager {
const drawingManager = new google.maps.drawing.DrawingManager({
drawingControl: false,
drawingMode: google.maps.drawing.OverlayType.RECTANGLE,
rectangleOptions: {
fillOpacity: 0.0,
strokeColor: MATERIAL_COLORS.Red.hex,
},
polygonOptions: {
fillOpacity: 0.0,
strokeColor: MATERIAL_COLORS.Red.hex,
},
});
private initializeTerraDraw(): void {
if (!this.mapElement) {
console.error('Map element not found for TerraDraw initialization');
return;
}

google.maps.event.addListener(drawingManager, 'overlaycomplete', (event) => {
let polygon;
if (event.type === 'rectangle') {
const bounds = event.overlay.getBounds();
polygon = boundsToTurfPolygon(bounds);
} else {
polygon = mapsPolygonToTurfPolygon(event.overlay);
}
event.overlay.setMap(null);
this.terraDrawService.initialize(this.map, this.mapElement);

if (this.page === Page.Shipments) {
this.store.dispatch(MapActions.selectPreSolveShipmentMapItems({ polygon }));
} else if (this.page === Page.Vehicles) {
this.store.dispatch(MapActions.selectPreSolveVehicleMapItems({ polygon }));
} else if (this.page === Page.RoutesChart) {
this.store.dispatch(MapActions.selectPostSolveMapItems({ polygon }));
}
});
return drawingManager;
this.subscriptions.push(
this.terraDrawService.onFeatureComplete$.subscribe((polygon) => {
if (this.page === Page.Shipments) {
this.store.dispatch(MapActions.selectPreSolveShipmentMapItems({ polygon }));
} else if (this.page === Page.Vehicles) {
this.store.dispatch(MapActions.selectPreSolveVehicleMapItems({ polygon }));
} else if (this.page === Page.RoutesChart) {
this.store.dispatch(MapActions.selectPostSolveMapItems({ polygon }));
}
})
);
}

isPreSolve(): boolean {
Expand Down
1 change: 1 addition & 0 deletions application/frontend/src/app/core/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export * from './pre-solve-visit-request-layer.service';
export * from './routes-chart.service';
export * from './route-layer.service';
export * from './storage-api.service';
export * from './terra-draw.service';
export * from './upload.service';
export * from './validation.service';
export * from './vehicle-info-window.service';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
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 { TestBed } from '@angular/core/testing';
import { TerraDrawService } from './terra-draw.service';

describe('TerraDrawService', () => {
let service: TerraDrawService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(TerraDrawService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('should not be ready initially', () => {
expect(service.isReady).toBeFalse();
});

it('should handle setMode gracefully when not initialized', () => {
// Should not throw when called before initialization
expect(() => service.setMode(1)).not.toThrow(); // SelectionMode.Bbox
expect(() => service.setMode(2)).not.toThrow(); // SelectionMode.Polygon
expect(() => service.setMode(0)).not.toThrow(); // SelectionMode.Off
});

it('should handle destroy gracefully when not initialized', () => {
expect(() => service.destroy()).not.toThrow();
});
});
Loading
Loading