Skip to content

Commit

Permalink
feat(core): auto fitBounds
Browse files Browse the repository at this point in the history
  • Loading branch information
sebholstein committed Jun 3, 2018
1 parent fc042ae commit 6770c9f
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 20 deletions.
3 changes: 3 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"singleQuote": true
}
3 changes: 2 additions & 1 deletion packages/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {LazyMapsAPILoader} from './services/maps-api-loader/lazy-maps-api-loader
import {LAZY_MAPS_API_CONFIG, LazyMapsAPILoaderConfigLiteral} from './services/maps-api-loader/lazy-maps-api-loader';
import {MapsAPILoader} from './services/maps-api-loader/maps-api-loader';
import {BROWSER_GLOBALS_PROVIDERS} from './utils/browser-globals';
import {AgmFitBounds} from '@agm/core/directives/fit-bounds';

/**
* @internal
Expand All @@ -20,7 +21,7 @@ export function coreDirectives() {
return [
AgmMap, AgmMarker, AgmInfoWindow, AgmCircle,
AgmPolygon, AgmPolyline, AgmPolylinePoint, AgmKmlLayer,
AgmDataLayer
AgmDataLayer, AgmFitBounds
];
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export {AgmMarker} from './directives/marker';
export {AgmPolygon} from './directives/polygon';
export {AgmPolyline} from './directives/polyline';
export {AgmPolylinePoint} from './directives/polyline-point';
export {AgmFitBounds} from './directives/fit-bounds';
75 changes: 75 additions & 0 deletions packages/core/directives/fit-bounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Directive, OnInit, Self, OnDestroy, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FitBoundsService, FitBoundsAccessor, FitBoundsDetails } from '../services/fit-bounds';
import { Subscription, Subject } from 'rxjs';
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { LatLng, LatLngLiteral } from '@agm/core';

/**
* TODO: docs
*/
@Directive({
selector: '[agmFitBounds]'
})
export class AgmFitBounds implements OnInit, OnDestroy, OnChanges {
/**
* If the value is true, the element gets added to the bounds of the map.
* Default: true.
*/
@Input() agmFitBounds: boolean = true;

private _destroyed$: Subject<void> = new Subject<void>();
private _latestFitBoundsDetails: FitBoundsDetails | null = null;

constructor(
@Self() private readonly _fitBoundsAccessor: FitBoundsAccessor,
private readonly _fitBoundsService: FitBoundsService
) {}

/**
* @internal
*/
ngOnChanges(changes: SimpleChanges) {
this._updateBounds();
}

/**
* @internal
*/
ngOnInit() {
this._fitBoundsAccessor
.getFitBoundsDetails$()
.pipe(
distinctUntilChanged(
(x: FitBoundsDetails, y: FitBoundsDetails) =>
x.latLng.lat === y.latLng.lng
),
takeUntil(this._destroyed$)
)
.subscribe(details => this._updateBounds(details));
}

private _updateBounds(newFitBoundsDetails?: FitBoundsDetails) {
if (newFitBoundsDetails) {
this._latestFitBoundsDetails = newFitBoundsDetails;
}
if (!this._latestFitBoundsDetails) {
return;
}
if (this.agmFitBounds) {
this._fitBoundsService.addToBounds(this._latestFitBoundsDetails.latLng);
} else {
this._fitBoundsService.removeFromBounds(this._latestFitBoundsDetails.latLng);
}
}

/**
* @internal
*/
ngOnDestroy() {
this._destroyed$.next();
this._destroyed$.complete();
if (this._latestFitBoundsDetails !== null) {
this._fitBoundsService.removeFromBounds(this._latestFitBoundsDetails.latLng);
}
}
}
53 changes: 46 additions & 7 deletions packages/core/directives/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {PolygonManager} from '../services/managers/polygon-manager';
import {PolylineManager} from '../services/managers/polyline-manager';
import {KmlLayerManager} from './../services/managers/kml-layer-manager';
import {DataLayerManager} from './../services/managers/data-layer-manager';
import {FitBoundsService} from '../services/fit-bounds';

declare var google: any;

/**
* AgmMap renders a Google Map.
Expand Down Expand Up @@ -42,7 +45,7 @@ import {DataLayerManager} from './../services/managers/data-layer-manager';
selector: 'agm-map',
providers: [
GoogleMapsAPIWrapper, MarkerManager, InfoWindowManager, CircleManager, PolylineManager,
PolygonManager, KmlLayerManager, DataLayerManager
PolygonManager, KmlLayerManager, DataLayerManager, FitBoundsService
],
host: {
// todo: deprecated - we will remove it with the next version
Expand Down Expand Up @@ -179,8 +182,9 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {

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

/**
* The initial enabled/disabled state of the Scale control. This is disabled by default.
Expand Down Expand Up @@ -266,6 +270,7 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {
];

private _observableSubscriptions: Subscription[] = [];
private _fitBoundsSubscription: Subscription;

/**
* This event emitter gets emitted when the user clicks on the map (but not when they click on a
Expand Down Expand Up @@ -316,7 +321,7 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {
*/
@Output() mapReady: EventEmitter<any> = new EventEmitter<any>();

constructor(private _elem: ElementRef, private _mapsWrapper: GoogleMapsAPIWrapper) {}
constructor(private _elem: ElementRef, private _mapsWrapper: GoogleMapsAPIWrapper, protected _fitBoundsService: FitBoundsService) {}

/** @internal */
ngOnInit() {
Expand Down Expand Up @@ -377,6 +382,9 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {

// remove all listeners from the map instance
this._mapsWrapper.clearInstanceListeners();
if (this._fitBoundsSubscription) {
this._fitBoundsSubscription.unsubscribe();
}
}

/* @internal */
Expand Down Expand Up @@ -416,13 +424,13 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {

private _updatePosition(changes: SimpleChanges) {
if (changes['latitude'] == null && changes['longitude'] == null &&
changes['fitBounds'] == null) {
!changes['fitBounds']) {
// no position update needed
return;
}

// we prefer fitBounds in changes
if (changes['fitBounds'] && this.fitBounds != null) {
if ('fitBounds' in changes) {
this._fitBounds();
return;
}
Expand All @@ -446,11 +454,42 @@ export class AgmMap implements OnChanges, OnInit, OnDestroy {
}

private _fitBounds() {
switch (this.fitBounds) {
case true:
this._subscribeToFitBoundsUpdates();
break;
case false:
if (this._fitBoundsSubscription) {
this._fitBoundsSubscription.unsubscribe();
}
break;
default:
this._updateBounds(this.fitBounds);
}
}

private _subscribeToFitBoundsUpdates() {
this._fitBoundsSubscription = this._fitBoundsService.bounds$.subscribe(b => this._updateBounds(b));
}

protected _updateBounds(bounds: LatLngBounds|LatLngBoundsLiteral) {
if (this._isLatLngBoundsLiteral(bounds)) {
const newBounds = <LatLngBounds>google.maps.LatLngBounds();
newBounds.union(bounds);
bounds = newBounds;
}
if (bounds.isEmpty()) {
return;
}
if (this.usePanning) {
this._mapsWrapper.panToBounds(this.fitBounds);
this._mapsWrapper.panToBounds(bounds);
return;
}
this._mapsWrapper.fitBounds(this.fitBounds);
this._mapsWrapper.fitBounds(bounds);
}

private _isLatLngBoundsLiteral(bounds: LatLngBounds|LatLngBoundsLiteral): bounds is LatLngBoundsLiteral {
return (<any>bounds).extend === undefined;
}

private _handleMapCenterChange() {
Expand Down
36 changes: 26 additions & 10 deletions packages/core/directives/marker.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import {Directive, EventEmitter, OnChanges, OnDestroy, SimpleChange,
AfterContentInit, ContentChildren, QueryList, Input, Output
} from '@angular/core';
import {Subscription} from 'rxjs';

import {MouseEvent} from '../map-types';
import { AfterContentInit, ContentChildren, Directive, EventEmitter, Input, OnChanges, OnDestroy, Output, QueryList, SimpleChange, forwardRef } from '@angular/core';
import { Observable, ReplaySubject, Subscription } from 'rxjs';
import { tap } from 'rxjs/operators';
import { MarkerLabel, MouseEvent } from '../map-types';
import { FitBoundsAccessor, FitBoundsDetails } from '../services/fit-bounds';
import * as mapTypes from '../services/google-maps-types';
import {MarkerManager} from '../services/managers/marker-manager';
import { MarkerManager } from '../services/managers/marker-manager';
import { AgmInfoWindow } from './info-window';

import {AgmInfoWindow} from './info-window';
import {MarkerLabel} from '../map-types';

let markerId = 0;

Expand Down Expand Up @@ -37,13 +35,16 @@ let markerId = 0;
*/
@Directive({
selector: 'agm-marker',
providers: [
{provide: FitBoundsAccessor, useExisting: forwardRef(() => AgmMarker)}
],
inputs: [
'latitude', 'longitude', 'title', 'label', 'draggable: markerDraggable', 'iconUrl',
'openInfoWindow', 'opacity', 'visible', 'zIndex', 'animation'
],
outputs: ['markerClick', 'dragEnd', 'mouseOver', 'mouseOut']
})
export class AgmMarker implements OnDestroy, OnChanges, AfterContentInit {
export class AgmMarker implements OnDestroy, OnChanges, AfterContentInit, FitBoundsAccessor {
/**
* The latitude position of the marker.
*/
Expand Down Expand Up @@ -139,6 +140,8 @@ export class AgmMarker implements OnDestroy, OnChanges, AfterContentInit {
private _id: string;
private _observableSubscriptions: Subscription[] = [];

protected readonly _fitBoundsDetails$: ReplaySubject<FitBoundsDetails> = new ReplaySubject<FitBoundsDetails>(1);

constructor(private _markerManager: MarkerManager) { this._id = (markerId++).toString(); }

/* @internal */
Expand All @@ -163,12 +166,14 @@ export class AgmMarker implements OnDestroy, OnChanges, AfterContentInit {
}
if (!this._markerAddedToManger) {
this._markerManager.addMarker(this);
this._updateFitBoundsDetails();
this._markerAddedToManger = true;
this._addEventListeners();
return;
}
if (changes['latitude'] || changes['longitude']) {
this._markerManager.updateMarkerPosition(this);
this._updateFitBoundsDetails();
}
if (changes['title']) {
this._markerManager.updateTitle(this);
Expand Down Expand Up @@ -199,6 +204,17 @@ export class AgmMarker implements OnDestroy, OnChanges, AfterContentInit {
}
}

/**
* @internal
*/
getFitBoundsDetails$(): Observable<FitBoundsDetails> {
return this._fitBoundsDetails$.asObservable().pipe(tap(() => console.log('subscribe')));
}

protected _updateFitBoundsDetails() {
this._fitBoundsDetails$.next({latLng: {lat: this.latitude, lng: this.longitude}});
}

private _addEventListeners() {
const cs = this._markerManager.createEventObservable('click', this).subscribe(() => {
if (this.openInfoWindow) {
Expand Down
60 changes: 60 additions & 0 deletions packages/core/services/fit-bounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, from } from 'rxjs';
import { flatMap, map, skipWhile } from 'rxjs/operators';
import { LatLng, LatLngBounds, LatLngLiteral } from './google-maps-types';
import { MapsAPILoader } from './maps-api-loader/maps-api-loader';

declare var google: any;

export interface FitBoundsDetails {
latLng: LatLng|LatLngLiteral;
}

export abstract class FitBoundsAccessor {
abstract getFitBoundsDetails$(): Observable<FitBoundsDetails>;
}

@Injectable()
export class FitBoundsService {
readonly bounds$: Observable<LatLngBounds>;
protected readonly _boundsChangeDebounceTime$: BehaviorSubject<number> = new BehaviorSubject<number>(200);
protected readonly _includeInBounds$: BehaviorSubject<Map<string, LatLng | LatLngLiteral>> = new BehaviorSubject<Map<string, LatLng | LatLngLiteral>>(new Map<string, LatLng | LatLngLiteral>());
protected _emitPaused: boolean = false;

constructor(loader: MapsAPILoader) {
this.bounds$ = from(loader.load()).pipe(
flatMap(() => this._includeInBounds$),
skipWhile(() => this._emitPaused),
// debounce(() => this._boundsChangeDebounceTime$),
map((includeInBounds: Map<string, LatLng | LatLngLiteral>) => {
const bounds = new google.maps.LatLngBounds() as LatLngBounds;
includeInBounds.forEach(b => bounds.extend(b));
return bounds;
})
);
}

addToBounds(latLng: LatLng | LatLngLiteral) {
const id = this._createIdentifier(latLng);
if (this._includeInBounds$.value.has(id)) {
return;
}
const map = this._includeInBounds$.value;
map.set(id, latLng);
this._includeInBounds$.next(map);
}

removeFromBounds(latLng: LatLng|LatLngLiteral) {
const map = this._includeInBounds$.value;
map.delete(this._createIdentifier(latLng));
this._includeInBounds$.next(map);
}

changeFitBoundsDebounceTime(timeMs: number) {
this._boundsChangeDebounceTime$.next(timeMs);
}

protected _createIdentifier(latLng: LatLng|LatLngLiteral): string {
return `${latLng.lat}+${latLng.lng}`;
}
}
3 changes: 2 additions & 1 deletion packages/core/services/google-maps-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface LatLng {
constructor(lat: number, lng: number): void;
lat(): number;
lng(): number;
toString(): string;
}

export interface Marker extends MVCObject {
Expand Down Expand Up @@ -97,7 +98,7 @@ export interface CircleOptions {
export interface LatLngBounds {
contains(latLng: LatLng): boolean;
equals(other: LatLngBounds|LatLngBoundsLiteral): boolean;
extend(point: LatLng): void;
extend(point: LatLng|LatLngLiteral): void;
getCenter(): LatLng;
getNorthEast(): LatLng;
getSouthWest(): LatLng;
Expand Down
2 changes: 1 addition & 1 deletion tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"no-attribute-parameter-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"no-forward-ref": true,
"no-forward-ref": false,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"pipe-naming": [true, "camelCase", "agm"],
Expand Down

0 comments on commit 6770c9f

Please sign in to comment.