Skip to content

Commit fc59354

Browse files
Add terra draw (#358)
1 parent 37c13d6 commit fc59354

7 files changed

Lines changed: 274 additions & 52 deletions

File tree

application/frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
"protobufjs": "^7.4.0",
6161
"rxjs": "~7.8.2",
6262
"split.js": "^1.6.2",
63+
"terra-draw": "^1.31.1",
64+
"terra-draw-google-maps-adapter": "^1.6.0",
6365
"tslib": "^2.8.1",
6466
"zone.js": "~0.14.10"
6567
},

application/frontend/src/app/core/containers/map-wrapper/map-wrapper.component.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
Component,
2020
ElementRef,
2121
EventEmitter,
22+
HostBinding,
2223
Input,
2324
OnDestroy,
2425
OnInit,
@@ -28,6 +29,11 @@ import { Observable, Subscription } from 'rxjs';
2829
import { tap } from 'rxjs/operators';
2930
import { MapService } from '../../services';
3031

32+
export interface MapInitializeEvent {
33+
map: google.maps.Map;
34+
element: HTMLElement;
35+
}
36+
3137
@Component({
3238
selector: 'app-map-wrapper',
3339
templateUrl: './map-wrapper.component.html',
@@ -36,7 +42,8 @@ import { MapService } from '../../services';
3642
})
3743
export class MapWrapperComponent implements OnInit, OnDestroy {
3844
@Input() options: google.maps.MapOptions;
39-
@Output() mapInitialize = new EventEmitter<google.maps.Map>();
45+
@Output() mapInitialize = new EventEmitter<MapInitializeEvent>();
46+
@HostBinding('attr.id') readonly mapId = 'google-map-container';
4047
private readonly subscriptions: Subscription[] = [];
4148
private previousMapWidth: number;
4249

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

5966
// Reset the map bounds when it is toggled on.
6067
// bounds_changed fires off whenever the viewable bounds of the map change, even from user interaction,

application/frontend/src/app/core/containers/map/map.component.ts

Lines changed: 28 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { combineLatest, Observable, Subscription } from 'rxjs';
2626
import { distinctUntilChanged, take } from 'rxjs/operators';
2727
import { selectMapSelectionToolsVisible, State } from 'src/app/reducers';
2828
import { ActiveFilter, FilterOption } from 'src/app/shared/models';
29-
import { boundsToTurfPolygon, mapsPolygonToTurfPolygon } from 'src/app/util';
3029
import {
3130
MapActions,
3231
PreSolveShipmentActions,
@@ -47,10 +46,10 @@ import * as fromUI from '../../selectors/ui.selectors';
4746
import { selectPage } from '../../selectors/ui.selectors';
4847
import TravelSimulatorSelectors from '../../selectors/travel-simulator.selectors';
4948
import {
50-
MATERIAL_COLORS,
5149
VehicleInfoWindowService,
5250
VisitRequestInfoWindowService,
5351
MultiselectInfoWindowService,
52+
TerraDrawService,
5453
} from '../../services';
5554
import { DepotLayer } from '../../services/depot-layer.service';
5655
import { MapService } from '../../services/map.service';
@@ -60,6 +59,7 @@ import { PreSolveVehicleLayer } from '../../services/pre-solve-vehicle-layer.ser
6059
import { PreSolveVisitRequestLayer } from '../../services/pre-solve-visit-request-layer.service';
6160
import { RouteLayer } from '../../services/route-layer.service';
6261
import { MapLayer, MapLayerId } from '../../models/map';
62+
import { MapInitializeEvent } from '../map-wrapper/map-wrapper.component';
6363

6464
@Component({
6565
selector: 'app-map',
@@ -80,8 +80,8 @@ export class MapComponent implements OnInit, OnDestroy {
8080
}
8181

8282
private readonly subscriptions: Subscription[] = [];
83-
private drawingManager: google.maps.drawing.DrawingManager;
8483
private map: google.maps.Map;
84+
private mapElement: HTMLElement;
8585
private page: Page;
8686

8787
constructor(
@@ -96,7 +96,8 @@ export class MapComponent implements OnInit, OnDestroy {
9696
private preSolveVisitRequestLayer: PreSolveVisitRequestLayer,
9797
public vehicleInfoWindowService: VehicleInfoWindowService,
9898
public visitRequestInfoWindowService: VisitRequestInfoWindowService,
99-
public multiselectInfoWindowService: MultiselectInfoWindowService
99+
public multiselectInfoWindowService: MultiselectInfoWindowService,
100+
private terraDrawService: TerraDrawService
100101
) {}
101102

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

161162
ngOnDestroy(): void {
162163
this.subscriptions.splice(0).forEach((subscription) => subscription.unsubscribe());
164+
this.terraDrawService.destroy();
163165
}
164166

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

197199
onToggleSelectMapItems(mode: SelectionMode): void {
198-
if (mode === SelectionMode.Off) {
199-
this.drawingManager.setMap(null);
200-
} else {
201-
this.drawingManager.setMap(this.map);
202-
this.drawingManager.setDrawingMode(
203-
mode === SelectionMode.Bbox
204-
? google.maps.drawing.OverlayType.RECTANGLE
205-
: google.maps.drawing.OverlayType.POLYGON
206-
);
207-
}
200+
this.terraDrawService.setMode(mode);
208201
}
209202

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

218-
onMapInitialize(map: google.maps.Map): void {
219-
this.map = map;
220-
this.drawingManager = this.createDrawingTools();
211+
onMapInitialize(event: MapInitializeEvent): void {
212+
this.map = event.map;
213+
this.mapElement = event.element;
214+
this.initializeTerraDraw();
221215
}
222216

223-
createDrawingTools(): google.maps.drawing.DrawingManager {
224-
const drawingManager = new google.maps.drawing.DrawingManager({
225-
drawingControl: false,
226-
drawingMode: google.maps.drawing.OverlayType.RECTANGLE,
227-
rectangleOptions: {
228-
fillOpacity: 0.0,
229-
strokeColor: MATERIAL_COLORS.Red.hex,
230-
},
231-
polygonOptions: {
232-
fillOpacity: 0.0,
233-
strokeColor: MATERIAL_COLORS.Red.hex,
234-
},
235-
});
217+
private initializeTerraDraw(): void {
218+
if (!this.mapElement) {
219+
console.error('Map element not found for TerraDraw initialization');
220+
return;
221+
}
236222

237-
google.maps.event.addListener(drawingManager, 'overlaycomplete', (event) => {
238-
let polygon;
239-
if (event.type === 'rectangle') {
240-
const bounds = event.overlay.getBounds();
241-
polygon = boundsToTurfPolygon(bounds);
242-
} else {
243-
polygon = mapsPolygonToTurfPolygon(event.overlay);
244-
}
245-
event.overlay.setMap(null);
223+
this.terraDrawService.initialize(this.map, this.mapElement);
246224

247-
if (this.page === Page.Shipments) {
248-
this.store.dispatch(MapActions.selectPreSolveShipmentMapItems({ polygon }));
249-
} else if (this.page === Page.Vehicles) {
250-
this.store.dispatch(MapActions.selectPreSolveVehicleMapItems({ polygon }));
251-
} else if (this.page === Page.RoutesChart) {
252-
this.store.dispatch(MapActions.selectPostSolveMapItems({ polygon }));
253-
}
254-
});
255-
return drawingManager;
225+
this.subscriptions.push(
226+
this.terraDrawService.onFeatureComplete$.subscribe((polygon) => {
227+
if (this.page === Page.Shipments) {
228+
this.store.dispatch(MapActions.selectPreSolveShipmentMapItems({ polygon }));
229+
} else if (this.page === Page.Vehicles) {
230+
this.store.dispatch(MapActions.selectPreSolveVehicleMapItems({ polygon }));
231+
} else if (this.page === Page.RoutesChart) {
232+
this.store.dispatch(MapActions.selectPostSolveMapItems({ polygon }));
233+
}
234+
})
235+
);
256236
}
257237

258238
isPreSolve(): boolean {

application/frontend/src/app/core/services/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export * from './pre-solve-visit-request-layer.service';
3636
export * from './routes-chart.service';
3737
export * from './route-layer.service';
3838
export * from './storage-api.service';
39+
export * from './terra-draw.service';
3940
export * from './upload.service';
4041
export * from './validation.service';
4142
export * from './vehicle-info-window.service';
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { TestBed } from '@angular/core/testing';
18+
import { TerraDrawService } from './terra-draw.service';
19+
20+
describe('TerraDrawService', () => {
21+
let service: TerraDrawService;
22+
23+
beforeEach(() => {
24+
TestBed.configureTestingModule({});
25+
service = TestBed.inject(TerraDrawService);
26+
});
27+
28+
it('should be created', () => {
29+
expect(service).toBeTruthy();
30+
});
31+
32+
it('should not be ready initially', () => {
33+
expect(service.isReady).toBeFalse();
34+
});
35+
36+
it('should handle setMode gracefully when not initialized', () => {
37+
// Should not throw when called before initialization
38+
expect(() => service.setMode(1)).not.toThrow(); // SelectionMode.Bbox
39+
expect(() => service.setMode(2)).not.toThrow(); // SelectionMode.Polygon
40+
expect(() => service.setMode(0)).not.toThrow(); // SelectionMode.Off
41+
});
42+
43+
it('should handle destroy gracefully when not initialized', () => {
44+
expect(() => service.destroy()).not.toThrow();
45+
});
46+
});

0 commit comments

Comments
 (0)