在Angular2+项目中使用googlemaps
前期方案调研(部分截图)
国外项目需要引入地图中画线和测距离业务,对比了国外几大地图服务商,客户决定使用流行的google maps。另外googlemaps需要付费使用
一、Googlemaps是什么?
Google是 最早的地图服务提供商之一,有自己的地图检索网站和app,同时开放了Dynamic Maps JS, GeoLocation API 等商务服务。
二、使用步骤
1.开发前准备
- 在google官方网站上申请API Key
- 在管理页面 开启Maps JavaScript API. 如果需要其他服务,如测距等需要另外操作
- 以下是一些参考网址
API Key:
https://developers.google.com/maps/documentation/geocoding/get-api-key
Billing Form:
https://console.cloud.google.com/freetrial/signup/tos
Quick calculator:
https://mapsplatformtransition.withgoogle.com/calculator
Pricing List:
https://cloud.google.com/maps-platform/pricing/sheet/
2.引入库
package.json(示例):
"@types/googlemaps": "^3.38.1",
3.部分参考代码
import { Injectable, Inject, Optional, NgZone, OnDestroy, Component } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { MAP_CONFIG_TOKEN } from './config';
import { isMapsApiLoaded } from './util';
@Component({
template: ''
})
export abstract class XXApiLoader implements OnDestroy {
api$: ReplaySubject<any> = new ReplaySubject(1);
abstract load();
protected constructor(@Inject(Object)protected config) {
this.config = this.config || {apiUrl: 'https://maps.google.com/maps/api/js'};
}
ngOnDestroy() {
this.api$.complete();
}
}
@Injectable()
export class XXAsyncCallbackApiLoader extends XXApiLoader {
constructor(protected zone: NgZone, @Optional() @Inject(MAP_CONFIG_TOKEN) config, private service: XXService) {
super(config);
}
load() {
if (typeof window === 'undefined') {
return;
}
if (isMapsApiLoaded()) {
// @ts-ignore
this.api$.next(google.maps);
} else if (!document.querySelector('#map-api')) {
(<any>window)['mapRef'] = (<any>window)['mapRef'] || [];
// @ts-ignore
(<any>window)['mapRef'].push({ zone: this.zone, componentFn: () => this.api$.next(google.maps)});
this.addGoogleMapsApi();
}
}
private addGoogleMapsApi() {
(<any>window)['initMap'] = (<any>window)['initMap'] || function() {
(<any>window)['mapRef'].forEach(mapRef => {
mapRef.zone.run(function() { mapRef.componentFn(); });
});
(<any>window)['mapRef'].splice(0, (<any>window)['mapRef'].length);
};
const script = document.createElement( 'script' );
script.id = 'map-api';
// script.src = "https://maps.google.com/maps/api/js?callback=initMap";
let apiUrl = this.service.getGoogleMapAPIKey() ? this.service.getGoogleMapAPIKey() : this.config.apiUrl ;
apiUrl += apiUrl.indexOf('?') !== -1 ? '&' : '?';
script.src = apiUrl + 'callback=initMap';
document.querySelector('body').appendChild(script);
}
}
@Injectable()
export class XXAsyncApiLoader extends XXApiLoader {
constructor(@Optional() @Inject(NG_MAP_CONFIG_TOKEN) config, private iddService: FenceIDDService ) {
super(config);
}
load() {
if (typeof window === 'undefined') {
return;
}
if (isMapsApiLoaded()) {
// @ts-ignore
this.api$.next(google.maps);
} else if (!document.querySelector('#ngui-map-api')) {
const script = document.createElement('script');
script.id = 'ngui-map-api';
script.async = true;
// @ts-ignore
script.onload = () => this.api$.next(google.maps);
script.src = this.service.getGoogleMapAPIKey() ? this.service.getGoogleMapAPIKey() : this.config.apiUrl;
document.querySelector('body').appendChild(script);
}
}
}
import {Injectable, SimpleChanges, NgZone, OnDestroy, AfterViewInit} from '@angular/core';
import { OptionBuilder } from './option-builder';
import { GeoCoder } from './geo-coder';
/**
* collection of map instance-related properties and methods
*/
@Injectable()
export class XXMap implements OnDestroy, AfterViewInit {
constructor(
private geoCoder: GeoCoder,
private optionBuilder: OptionBuilder,
private zone: NgZone,
private logger: LoggerService
) {}
setObjectEvents(definedEvents: string[], thisObj: any, prefix: string) {
definedEvents.forEach(definedEvent => {
const eventName = this.getEventName(definedEvent),
zone = this.zone;
zone.runOutsideAngular(() => {
thisObj[prefix].addListener(eventName, function(event) {
const param: any = event ? event : {};
param.target = this;
zone.run(() => thisObj[definedEvent].emit(param));
});
});
});
}
clearObjectEvents(definedEvents: string[], thisObj: any, prefix: string) {
definedEvents.forEach(definedEvent => {
const eventName = this.getEventName(definedEvent);
this.zone.runOutsideAngular(() => {
if (thisObj[prefix]) {
// @ts-ignore
google.maps.event.clearListeners(thisObj[prefix], eventName);
}
});
});
if (thisObj[prefix]) {
if (thisObj[prefix].setMap) {
thisObj[prefix].setMap(null);
}
delete thisObj[prefix].nguiMapComponent;
delete thisObj[prefix];
}
}
updateGoogleObject = (object: any, changes: SimpleChanges) => {
let val: any, currentValue: any, setMethodName: string;
if (object) {
for (const key in changes) {
setMethodName = `set${key.replace(/^[a-z]/, x => x.toUpperCase()) }`;
currentValue = changes[key].currentValue;
if (['position', 'center'].indexOf(key) !== -1 && typeof currentValue === 'string') {
// To preserve setMethod name in Observable callback, wrap it as a function, then execute
// tslint:disable-next-line: no-shadowed-variable
((setMethodName) => {
this.geoCoder.geocode({address: currentValue}).subscribe(results => {
if (typeof object[setMethodName] === 'function') {
object[setMethodName](results[0].geometry.location);
} else {
this.logger.error(
'Not all options are dynamically updatable according to Googles Maps API V3 documentation.\n' +
'Please check Google Maps API documentation, and use "setOptions" instead.'
);
}
});
})(setMethodName);
} else {
val = this.optionBuilder.googlize(currentValue);
if (typeof object[setMethodName] === 'function') {
object[setMethodName](val);
} else {
this.logger.warn(
'Not all options are dynamically updatable according to Googles Maps API V3 documentation.\n' +
'Please check Google Maps API documentation, and use "setOptions" instead.'
);
}
}
}
}
}
private getEventName(definedEvent) {
return definedEvent
.replace(/([A-Z])/g, ($1) => `_${$1.toLowerCase()}`) // positionChanged -> position_changed
.replace(/^map_/, ''); // map_click -> click to avoid DOM conflicts
}
ngAfterViewInit(): void {
this.logger.onInit(XXMap);
}
ngOnDestroy(): void {
this.logger.onDestroy(XXMap);
}
}
import {
Component,
ElementRef,
ViewEncapsulation,
EventEmitter,
SimpleChanges,
Output,
NgZone,
AfterViewInit, AfterViewChecked, OnChanges, OnDestroy
} from '@angular/core';
import { OptionBuilder } from '../services/option-builder';
import { NavigatorGeolocation } from '../services/navigator-geolocation';
import { GeoCoder } from '../services/geo-coder';
import { XXMap } from '../services/xx-map';
import { XXMapApiLoader } from '../services/api-loader';
import { InfoWindow } from './info-window';
import { Subject } from 'rxjs';
import { debounceTime, tap, first } from 'rxjs/operators';
import { toCamelCase } from '../services/util';
const INPUTS = [
'backgroundColor', 'center', 'disableDefaultUI', 'disableDoubleClickZoom', 'draggable', 'draggableCursor',
'draggingCursor', 'heading', 'keyboardShortcuts', 'mapMaker', 'mapTypeControl', 'mapTypeId', 'maxZoom', 'minZoom',
'noClear', 'overviewMapControl', 'panControl', 'panControlOptions', 'rotateControl', 'scaleControl', 'scrollwheel',
'streetView', 'styles', 'tilt', 'zoom', 'streetViewControl', 'zoomControl', 'zoomControlOptions', 'mapTypeControlOptions',
'overviewMapControlOptions', 'rotateControlOptions', 'scaleControlOptions', 'streetViewControlOptions', 'fullscreenControl', 'fullscreenControlOptions',
'options',
// ngui-map-specific inputs
'geoFallbackCenter'
];
const OUTPUTS = [
'bounds_changed', 'center_changed', 'click', 'dblclick', 'drag', 'dragend', 'dragstart', 'heading_changed', 'idle',
'maptypeid_changed', 'mousemove', 'mouseout', 'mouseover', 'projection_changed', 'resize', 'rightclick',
'tilesloaded', 'tile_changed', 'zoom_changed',
// to avoid DOM event conflicts
'mapClick', 'mapMouseover', 'mapMouseout', 'mapMousemove', 'mapDrag', 'mapDragend', 'mapDragstart'
];
@Component({
selector: 'ngui-map',
providers: [XXMap, OptionBuilder, GeoCoder, NavigatorGeolocation],
styles: [`
xx-map {display: block; height: 400px;}
.google-map {width: 100%; height: 100%}
`],
inputs: INPUTS,
outputs: OUTPUTS,
encapsulation: ViewEncapsulation.None,
template: `
<div class="google-map"></div>
<ng-content></ng-content>
`,
})
export class XXMapComponent implements OnChanges, OnDestroy, AfterViewInit, AfterViewChecked {
@Output() public mapReady$: EventEmitter<any> = new EventEmitter();
public el: HTMLElement;
public map: google.maps.Map;
public mapOptions: google.maps.MapOptions = {};
public inputChanges$ = new Subject();
// map objects by group
public infoWindows: { [id: string]: InfoWindow } = { };
// map has been fully initialized
public mapIdledOnce: boolean = false;
private initializeMapAfterDisplayed = false;
private apiLoaderSub;
constructor(
public optionBuilder: OptionBuilder,
public elementRef: ElementRef,
public geolocation: NavigatorGeolocation,
public geoCoder: GeoCoder,
public xxMap: XXMap,
public apiLoader: XXMapApiLoader,
public zone: NgZone,
) {
apiLoader.load();
// all outputs needs to be initialized,
// http://stackoverflow.com/questions/37765519/angular2-directive-cannot-read-property-subscribe-of-undefined-with-outputs
OUTPUTS.forEach(output => this[output] = new EventEmitter());
}
ngAfterViewInit() {
this.apiLoaderSub = this.apiLoader.api$
.pipe(first())
.subscribe(() => this.initializeMap());
}
ngAfterViewChecked() {
if (this.initializeMapAfterDisplayed && this.el && this.el.offsetWidth > 0) {
this.initializeMap();
}
}
ngOnChanges(changes: SimpleChanges) {
this.inputChanges$.next(changes);
}
initializeMap(): void {
this.el = this.elementRef.nativeElement.querySelector('.google-map');
if (this.el && this.el.offsetWidth === 0) {
this.initializeMapAfterDisplayed = true;
return;
}
this.initializeMapAfterDisplayed = false;
this.mapOptions = this.optionBuilder.googlizeAllInputs(INPUTS, this);
console.log('map mapOptions', this.mapOptions);
this.mapOptions.zoom = this.mapOptions.zoom || 15;
typeof this.mapOptions.center === 'string' && (delete this.mapOptions.center);
this.zone.runOutsideAngular(() => {
this.map = new google.maps.Map(this.el, this.mapOptions);
this.map['mapObjectName'] = 'XXMapComponent';
if (!this.mapOptions.center) { // if center is not given as lat/lng
this.setCenter();
}
// set google events listeners and emits to this outputs listeners
this.XXMap.setObjectEvents(OUTPUTS, this, 'map');
this.map.addListener('idle', () => {
if (!this.mapIdledOnce) {
this.mapIdledOnce = true;
setTimeout(() => {
this.mapReady$.emit(this.map);
});
}
});
// update map when input changes
this.inputChanges$.pipe(
debounceTime(1000),
tap((changes: SimpleChanges) => this.XXMap.updateGoogleObject(this.map, changes)),
).subscribe();
if (typeof window !== 'undefined' && (<any>window)['mapRef']) {
// expose map object for test and debugging on (<any>window)
(<any>window)['mapRef'].map = this.map;
}
});
}
setCenter(): void {
if (!this['center']) { // center is not from user. Thus, we set the current location
this.geolocation.getCurrentPosition().subscribe(
position => {
console.log('setting map center from current location');
let latLng = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
this.map.setCenter(latLng);
},
error => {
console.error('map: Error finding the current position');
this.map.setCenter(this.mapOptions['geoFallbackCenter'] || new google.maps.LatLng(0, 0));
}
);
}
else if (typeof this['center'] === 'string') {
this.geoCoder.geocode({address: this['center']}).subscribe(
results => {
console.log('setting map center from address', this['center']);
this.map.setCenter(results[0].geometry.location);
},
error => {
this.map.setCenter(this.mapOptions['geoFallbackCenter'] || new google.maps.LatLng(0, 0));
});
}
}
openInfoWindow(id: string, anchor: google.maps.MVCObject) {
this.infoWindows[id].open(anchor);
}
closeInfoWindow(id: string) {
// if infoWindow for id exists, close the infoWindow
if (this.infoWindows[id])
this.infoWindows[id].close();
}
ngOnDestroy() {
this.inputChanges$.complete();
if (this.el && !this.initializeMapAfterDisplayed) {
this.nguiMap.clearObjectEvents(OUTPUTS, this, 'map');
}
if (this.apiLoaderSub) {
this.apiLoaderSub.unsubscribe();
}
}
// map.markers, map.circles, map.heatmapLayers.. etc
addToMapObjectGroup(mapObjectName: string, mapObject: any) {
let groupName = toCamelCase(mapObjectName.toLowerCase()) + 's'; // e.g. markers
this.map[groupName] = this.map[groupName] || [];
this.map[groupName].push(mapObject);
}
removeFromMapObjectGroup(mapObjectName: string, mapObject: any) {
let groupName = toCamelCase(mapObjectName.toLowerCase()) + 's'; // e.g. markers
if (this.map && this.map[groupName]) {
let index = this.map[groupName].indexOf(mapObject);
console.log('index', mapObject, index);
(index > -1) && this.map[groupName].splice(index, 1);
}
}
}
总结
例如:以上就是今天要讲的内容,本文仅仅简单介绍了Angular2+ 上加入Googlemaps相关的业务功能。