Skip to content
This repository was archived by the owner on Jun 23, 2025. It is now read-only.

Commit 4d3103c

Browse files
authored
feat(core): support auto fitBounds
This adds support for auto fitBounds features. Right now, only markers and custom components are supported. We'll add support for other core components later
1 parent 89b6e5c commit 4d3103c

File tree

13 files changed

+453
-23
lines changed

13 files changed

+453
-23
lines changed

.prettierrc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"singleQuote": true
3+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
+++
2+
date = "2018-09-22T09:31:00-01:00"
3+
draft = false
4+
title = "Enable auto fit bounds"
5+
6+
+++
7+
8+
Angular Google Maps (AGM) has an auto fit bounds feature, that adds all containing components to the bounds of the map.
9+
To enable it, set the `fitBounds` input of `agm-map` to `true` and add the `agmFitBounds` input/directive to `true` for all components
10+
you want to include in the bounds of the map.
11+
12+
```html
13+
<agm-map [fitBounds]="true">
14+
<agm-marker [agmFitBounds]="true"></agm-marker>
15+
16+
<!-- not included -->
17+
<agm-marker [agmFitBounds]="false"></agm-marker>
18+
</agm-map>
19+
```
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
+++
2+
date = "2018-09-22T09:31:00-01:00"
3+
draft = false
4+
title = "Support auto fit bounds for custom components"
5+
6+
+++
7+
8+
Angular Google Maps (AGM) has an auto fit bounds feature, that adds all containing components to the bounds of the map:
9+
10+
```html
11+
<agm-map [fitBounds]="true">
12+
<agm-marker [agmFitBounds]="true"></agm-marker>
13+
</agm-map>
14+
```
15+
16+
Let`s say we have a custom component, that extends the features of AGM:
17+
18+
19+
```html
20+
<agm-map [fitBounds]="true">
21+
<my-custom-component></my-custom-component>
22+
</agm-map>
23+
```
24+
25+
To add support the auto fit bounds feature for `<my-custom-component>`, we have to implement the `FitBoundsAccessor`:
26+
27+
```typescript
28+
import { FitBoundsAccessor, FitBoundsDetails } from '@agm/core';
29+
import { forwardRef, Component } from '@angular/core';
30+
31+
@Component({
32+
selector: 'my-custom-component',
33+
template: '',
34+
providers: [
35+
{provide: FitBoundsAccessor, useExisting: forwardRef(() => MyCustomComponent)}
36+
],
37+
})
38+
export class MyCustomComponent implements FitBoundsAccessor {
39+
**
40+
* This is a method you need to implement with your custom logic.
41+
*/
42+
getFitBoundsDetails$(): Observable<FitBoundsDetails> {
43+
return ...;
44+
}
45+
}
46+
```
47+
48+
The last step is to change your template. Add the `agmFitBounds` input/directive and set the value to true:
49+
50+
```html
51+
<agm-map [fitBounds]="true">
52+
<my-custom-component [agmFitBounds]="true"></my-custom-component>
53+
</agm-map>
54+
```

packages/core/core.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {LazyMapsAPILoader} from './services/maps-api-loader/lazy-maps-api-loader
1313
import {LAZY_MAPS_API_CONFIG, LazyMapsAPILoaderConfigLiteral} from './services/maps-api-loader/lazy-maps-api-loader';
1414
import {MapsAPILoader} from './services/maps-api-loader/maps-api-loader';
1515
import {BROWSER_GLOBALS_PROVIDERS} from './utils/browser-globals';
16+
import {AgmFitBounds} from '@agm/core/directives/fit-bounds';
1617

1718
/**
1819
* @internal
@@ -21,7 +22,7 @@ export function coreDirectives() {
2122
return [
2223
AgmMap, AgmMarker, AgmInfoWindow, AgmCircle, AgmRectangle,
2324
AgmPolygon, AgmPolyline, AgmPolylinePoint, AgmKmlLayer,
24-
AgmDataLayer
25+
AgmDataLayer, AgmFitBounds
2526
];
2627
}
2728

packages/core/directives.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export {AgmMarker} from './directives/marker';
88
export {AgmPolygon} from './directives/polygon';
99
export {AgmPolyline} from './directives/polyline';
1010
export {AgmPolylinePoint} from './directives/polyline-point';
11+
export {AgmFitBounds} from './directives/fit-bounds';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Directive, OnInit, Self, OnDestroy, Input, OnChanges, SimpleChanges } from '@angular/core';
2+
import { FitBoundsService, FitBoundsAccessor, FitBoundsDetails } from '../services/fit-bounds';
3+
import { Subscription, Subject } from 'rxjs';
4+
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
5+
import { LatLng, LatLngLiteral } from '@agm/core';
6+
7+
/**
8+
* Adds the given directive to the auto fit bounds feature when the value is true.
9+
* To make it work with you custom AGM component, you also have to implement the {@link FitBoundsAccessor} abstract class.
10+
* @example
11+
* <agm-marker [agmFitBounds]="true"></agm-marker>
12+
*/
13+
@Directive({
14+
selector: '[agmFitBounds]'
15+
})
16+
export class AgmFitBounds implements OnInit, OnDestroy, OnChanges {
17+
/**
18+
* If the value is true, the element gets added to the bounds of the map.
19+
* Default: true.
20+
*/
21+
@Input() agmFitBounds: boolean = true;
22+
23+
private _destroyed$: Subject<void> = new Subject<void>();
24+
private _latestFitBoundsDetails: FitBoundsDetails | null = null;
25+
26+
constructor(
27+
@Self() private readonly _fitBoundsAccessor: FitBoundsAccessor,
28+
private readonly _fitBoundsService: FitBoundsService
29+
) {}
30+
31+
/**
32+
* @internal
33+
*/
34+
ngOnChanges(changes: SimpleChanges) {
35+
this._updateBounds();
36+
}
37+
38+
/**
39+
* @internal
40+
*/
41+
ngOnInit() {
42+
this._fitBoundsAccessor
43+
.getFitBoundsDetails$()
44+
.pipe(
45+
distinctUntilChanged(
46+
(x: FitBoundsDetails, y: FitBoundsDetails) =>
47+
x.latLng.lat === y.latLng.lng
48+
),
49+
takeUntil(this._destroyed$)
50+
)
51+
.subscribe(details => this._updateBounds(details));
52+
}
53+
54+
private _updateBounds(newFitBoundsDetails?: FitBoundsDetails) {
55+
if (newFitBoundsDetails) {
56+
this._latestFitBoundsDetails = newFitBoundsDetails;
57+
}
58+
if (!this._latestFitBoundsDetails) {
59+
return;
60+
}
61+
if (this.agmFitBounds) {
62+
this._fitBoundsService.addToBounds(this._latestFitBoundsDetails.latLng);
63+
} else {
64+
this._fitBoundsService.removeFromBounds(this._latestFitBoundsDetails.latLng);
65+
}
66+
}
67+
68+
/**
69+
* @internal
70+
*/
71+
ngOnDestroy() {
72+
this._destroyed$.next();
73+
this._destroyed$.complete();
74+
if (this._latestFitBoundsDetails !== null) {
75+
this._fitBoundsService.removeFromBounds(this._latestFitBoundsDetails.latLng);
76+
}
77+
}
78+
}

packages/core/directives/map.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import {PolygonManager} from '../services/managers/polygon-manager';
1515
import {PolylineManager} from '../services/managers/polyline-manager';
1616
import {KmlLayerManager} from './../services/managers/kml-layer-manager';
1717
import {DataLayerManager} from './../services/managers/data-layer-manager';
18+
import {FitBoundsService} from '../services/fit-bounds';
19+
20+
declare var google: any;
1821

1922
/**
2023
* AgmMap renders a Google Map.
@@ -43,7 +46,8 @@ import {DataLayerManager} from './../services/managers/data-layer-manager';
4346
selector: 'agm-map',
4447
providers: [
4548
GoogleMapsAPIWrapper, MarkerManager, InfoWindowManager, CircleManager, RectangleManager,
46-
PolylineManager, PolygonManager, KmlLayerManager, DataLayerManager
49+
PolylineManager, PolygonManager, KmlLayerManager, DataLayerManager, DataLayerManager,
50+
FitBoundsService
4751
],
4852
host: {
4953
// todo: deprecated - we will remove it with the next version
@@ -180,8 +184,9 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {
180184

181185
/**
182186
* Sets the viewport to contain the given bounds.
187+
* If this option to `true`, the bounds get automatically computed from all elements that use the {@link AgmFitBounds} directive.
183188
*/
184-
@Input() fitBounds: LatLngBoundsLiteral|LatLngBounds = null;
189+
@Input() fitBounds: LatLngBoundsLiteral|LatLngBounds|boolean = false;
185190

186191
/**
187192
* The initial enabled/disabled state of the Scale control. This is disabled by default.
@@ -267,6 +272,7 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {
267272
];
268273

269274
private _observableSubscriptions: Subscription[] = [];
275+
private _fitBoundsSubscription: Subscription;
270276

271277
/**
272278
* This event emitter gets emitted when the user clicks on the map (but not when they click on a
@@ -317,7 +323,7 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {
317323
*/
318324
@Output() mapReady: EventEmitter<any> = new EventEmitter<any>();
319325

320-
constructor(private _elem: ElementRef, private _mapsWrapper: GoogleMapsAPIWrapper) {}
326+
constructor(private _elem: ElementRef, private _mapsWrapper: GoogleMapsAPIWrapper, protected _fitBoundsService: FitBoundsService) {}
321327

322328
/** @internal */
323329
ngOnInit() {
@@ -378,6 +384,9 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {
378384

379385
// remove all listeners from the map instance
380386
this._mapsWrapper.clearInstanceListeners();
387+
if (this._fitBoundsSubscription) {
388+
this._fitBoundsSubscription.unsubscribe();
389+
}
381390
}
382391

383392
/* @internal */
@@ -417,13 +426,13 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {
417426

418427
private _updatePosition(changes: SimpleChanges) {
419428
if (changes['latitude'] == null && changes['longitude'] == null &&
420-
changes['fitBounds'] == null) {
429+
!changes['fitBounds']) {
421430
// no position update needed
422431
return;
423432
}
424433

425434
// we prefer fitBounds in changes
426-
if (changes['fitBounds'] && this.fitBounds != null) {
435+
if ('fitBounds' in changes) {
427436
this._fitBounds();
428437
return;
429438
}
@@ -447,11 +456,42 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {
447456
}
448457

449458
private _fitBounds() {
459+
switch (this.fitBounds) {
460+
case true:
461+
this._subscribeToFitBoundsUpdates();
462+
break;
463+
case false:
464+
if (this._fitBoundsSubscription) {
465+
this._fitBoundsSubscription.unsubscribe();
466+
}
467+
break;
468+
default:
469+
this._updateBounds(this.fitBounds);
470+
}
471+
}
472+
473+
private _subscribeToFitBoundsUpdates() {
474+
this._fitBoundsSubscription = this._fitBoundsService.getBounds$().subscribe(b => this._updateBounds(b));
475+
}
476+
477+
protected _updateBounds(bounds: LatLngBounds|LatLngBoundsLiteral) {
478+
if (this._isLatLngBoundsLiteral(bounds)) {
479+
const newBounds = <LatLngBounds>google.maps.LatLngBounds();
480+
newBounds.union(bounds);
481+
bounds = newBounds;
482+
}
483+
if (bounds.isEmpty()) {
484+
return;
485+
}
450486
if (this.usePanning) {
451-
this._mapsWrapper.panToBounds(this.fitBounds);
487+
this._mapsWrapper.panToBounds(bounds);
452488
return;
453489
}
454-
this._mapsWrapper.fitBounds(this.fitBounds);
490+
this._mapsWrapper.fitBounds(bounds);
491+
}
492+
493+
private _isLatLngBoundsLiteral(bounds: LatLngBounds|LatLngBoundsLiteral): bounds is LatLngBoundsLiteral {
494+
return (<any>bounds).extend === undefined;
455495
}
456496

457497
private _handleMapCenterChange() {

packages/core/directives/marker.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
import {Directive, EventEmitter, OnChanges, OnDestroy, SimpleChange,
2-
AfterContentInit, ContentChildren, QueryList, Input, Output
3-
} from '@angular/core';
4-
import {Subscription} from 'rxjs';
5-
6-
import {MouseEvent} from '../map-types';
1+
import { AfterContentInit, ContentChildren, Directive, EventEmitter, Input, OnChanges, OnDestroy, Output, QueryList, SimpleChange, forwardRef } from '@angular/core';
2+
import { Observable, ReplaySubject, Subscription } from 'rxjs';
3+
import { tap } from 'rxjs/operators';
4+
import { MarkerLabel, MouseEvent } from '../map-types';
5+
import { FitBoundsAccessor, FitBoundsDetails } from '../services/fit-bounds';
76
import * as mapTypes from '../services/google-maps-types';
8-
import {MarkerManager} from '../services/managers/marker-manager';
9-
10-
import {AgmInfoWindow} from './info-window';
11-
import {MarkerLabel} from '../map-types';
7+
import { MarkerManager } from '../services/managers/marker-manager';
8+
import { AgmInfoWindow } from './info-window';
129

1310
let markerId = 0;
1411

@@ -37,13 +34,16 @@ let markerId = 0;
3734
*/
3835
@Directive({
3936
selector: 'agm-marker',
37+
providers: [
38+
{provide: FitBoundsAccessor, useExisting: forwardRef(() => AgmMarker)}
39+
],
4040
inputs: [
4141
'latitude', 'longitude', 'title', 'label', 'draggable: markerDraggable', 'iconUrl',
4242
'openInfoWindow', 'opacity', 'visible', 'zIndex', 'animation'
4343
],
4444
outputs: ['markerClick', 'dragEnd', 'mouseOver', 'mouseOut']
4545
})
46-
export class AgmMarker implements OnDestroy, OnChanges, AfterContentInit {
46+
export class AgmMarker implements OnDestroy, OnChanges, AfterContentInit, FitBoundsAccessor {
4747
/**
4848
* The latitude position of the marker.
4949
*/
@@ -144,6 +144,8 @@ export class AgmMarker implements OnDestroy, OnChanges, AfterContentInit {
144144
private _id: string;
145145
private _observableSubscriptions: Subscription[] = [];
146146

147+
protected readonly _fitBoundsDetails$: ReplaySubject<FitBoundsDetails> = new ReplaySubject<FitBoundsDetails>(1);
148+
147149
constructor(private _markerManager: MarkerManager) { this._id = (markerId++).toString(); }
148150

149151
/* @internal */
@@ -174,12 +176,14 @@ export class AgmMarker implements OnDestroy, OnChanges, AfterContentInit {
174176
}
175177
if (!this._markerAddedToManger) {
176178
this._markerManager.addMarker(this);
179+
this._updateFitBoundsDetails();
177180
this._markerAddedToManger = true;
178181
this._addEventListeners();
179182
return;
180183
}
181184
if (changes['latitude'] || changes['longitude']) {
182185
this._markerManager.updateMarkerPosition(this);
186+
this._updateFitBoundsDetails();
183187
}
184188
if (changes['title']) {
185189
this._markerManager.updateTitle(this);
@@ -210,6 +214,17 @@ export class AgmMarker implements OnDestroy, OnChanges, AfterContentInit {
210214
}
211215
}
212216

217+
/**
218+
* @internal
219+
*/
220+
getFitBoundsDetails$(): Observable<FitBoundsDetails> {
221+
return this._fitBoundsDetails$.asObservable();
222+
}
223+
224+
protected _updateFitBoundsDetails() {
225+
this._fitBoundsDetails$.next({latLng: {lat: this.latitude, lng: this.longitude}});
226+
}
227+
213228
private _addEventListeners() {
214229
const cs = this._markerManager.createEventObservable('click', this).subscribe(() => {
215230
if (this.openInfoWindow) {

packages/core/services.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export {DataLayerManager} from './services/managers/data-layer-manager';
1010
export {GoogleMapsScriptProtocol, LAZY_MAPS_API_CONFIG, LazyMapsAPILoader, LazyMapsAPILoaderConfigLiteral} from './services/maps-api-loader/lazy-maps-api-loader';
1111
export {MapsAPILoader} from './services/maps-api-loader/maps-api-loader';
1212
export {NoOpMapsAPILoader} from './services/maps-api-loader/noop-maps-api-loader';
13+
export {FitBoundsAccessor, FitBoundsDetails} from './services/fit-bounds';

0 commit comments

Comments
 (0)