ngmap
Version:
The Simplest AngularJS Google Maps V3 Directive
1,517 lines (1,342 loc) • 111 kB
JavaScript
(function(root, factory) {
if (typeof exports === "object") {
module.exports = factory(require('angular'));
} else if (typeof define === "function" && define.amd) {
define(['angular'], factory);
} else{
factory(root.angular);
}
}(this, function(angular) {
/**
* AngularJS Google Maps Ver. 1.18.4
*
* The MIT License (MIT)
*
* Copyright (c) 2014, 2015, 1016 Allen Kim
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
angular.module('ngMap', []);
/**
* @ngdoc controller
* @name MapController
*/
(function() {
'use strict';
var Attr2MapOptions;
var __MapController = function(
$scope, $element, $attrs, $parse, $interpolate, _Attr2MapOptions_, NgMap, NgMapPool, escapeRegExp
) {
Attr2MapOptions = _Attr2MapOptions_;
var vm = this;
var exprStartSymbol = $interpolate.startSymbol();
var exprEndSymbol = $interpolate.endSymbol();
vm.mapOptions; /** @memberof __MapController */
vm.mapEvents; /** @memberof __MapController */
vm.eventListeners; /** @memberof __MapController */
/**
* Add an object to the collection of group
* @memberof __MapController
* @function addObject
* @param groupName the name of collection that object belongs to
* @param obj an object to add into a collection, i.e. marker, shape
*/
vm.addObject = function(groupName, obj) {
if (vm.map) {
vm.map[groupName] = vm.map[groupName] || {};
var len = Object.keys(vm.map[groupName]).length;
vm.map[groupName][obj.id || len] = obj;
if (vm.map instanceof google.maps.Map) {
//infoWindow.setMap works like infoWindow.open
if (groupName != "infoWindows" && obj.setMap) {
obj.setMap && obj.setMap(vm.map);
}
if (obj.centered && obj.position) {
vm.map.setCenter(obj.position);
}
(groupName == 'markers') && vm.objectChanged('markers');
(groupName == 'customMarkers') && vm.objectChanged('customMarkers');
}
}
};
/**
* Delete an object from the collection and remove from map
* @memberof __MapController
* @function deleteObject
* @param {Array} objs the collection of objects. i.e., map.markers
* @param {Object} obj the object to be removed. i.e., marker
*/
vm.deleteObject = function(groupName, obj) {
/* delete from group */
if (obj.map) {
var objs = obj.map[groupName];
for (var name in objs) {
if (objs[name] === obj) {
void 0;
google.maps.event.clearInstanceListeners(obj);
delete objs[name];
}
}
/* delete from map */
obj.map && obj.setMap && obj.setMap(null);
(groupName == 'markers') && vm.objectChanged('markers');
(groupName == 'customMarkers') && vm.objectChanged('customMarkers');
}
};
/**
* @memberof __MapController
* @function observeAttrSetObj
* @param {Hash} orgAttrs attributes before its initialization
* @param {Hash} attrs attributes after its initialization
* @param {Object} obj map object that an action is to be done
* @description watch changes of attribute values and
* do appropriate action based on attribute name
*/
vm.observeAttrSetObj = function(orgAttrs, attrs, obj) {
if (attrs.noWatcher) {
return false;
}
var attrsToObserve = Attr2MapOptions.getAttrsToObserve(orgAttrs);
for (var i=0; i<attrsToObserve.length; i++) {
var attrName = attrsToObserve[i];
attrs.$observe(attrName, NgMap.observeAndSet(attrName, obj));
}
};
/**
* @memberof __MapController
* @function zoomToIncludeMarkers
*/
vm.zoomToIncludeMarkers = function() {
// Only fit to bounds if we have any markers
// object.keys is supported in all major browsers (IE9+)
if ((vm.map.markers != null && Object.keys(vm.map.markers).length > 0) || (vm.map.customMarkers != null && Object.keys(vm.map.customMarkers).length > 0)) {
var bounds = new google.maps.LatLngBounds();
for (var k1 in vm.map.markers) {
bounds.extend(vm.map.markers[k1].getPosition());
}
for (var k2 in vm.map.customMarkers) {
bounds.extend(vm.map.customMarkers[k2].getPosition());
}
if (vm.mapOptions.maximumZoom) {
vm.enableMaximumZoomCheck = true; //enable zoom check after resizing for markers
}
vm.map.fitBounds(bounds);
}
};
/**
* @memberof __MapController
* @function objectChanged
* @param {String} group name of group e.g., markers
*/
vm.objectChanged = function(group) {
if ( vm.map &&
(group == 'markers' || group == 'customMarkers') &&
vm.map.zoomToIncludeMarkers == 'auto'
) {
vm.zoomToIncludeMarkers();
}
};
/**
* @memberof __MapController
* @function initializeMap
* @description
* . initialize Google map on <div> tag
* . set map options, events, and observers
* . reset zoom to include all (custom)markers
*/
vm.initializeMap = function() {
var mapOptions = vm.mapOptions,
mapEvents = vm.mapEvents;
var lazyInitMap = vm.map; //prepared for lazy init
vm.map = NgMapPool.getMapInstance($element[0]);
NgMap.setStyle($element[0]);
// set objects for lazyInit
if (lazyInitMap) {
/**
* rebuild mapOptions for lazyInit
* because attributes values might have been changed
*/
var filtered = Attr2MapOptions.filter($attrs);
var options = Attr2MapOptions.getOptions(filtered);
var controlOptions = Attr2MapOptions.getControlOptions(filtered);
mapOptions = angular.extend(options, controlOptions);
void 0;
for (var group in lazyInitMap) {
var groupMembers = lazyInitMap[group]; //e.g. markers
if (typeof groupMembers == 'object') {
for (var id in groupMembers) {
vm.addObject(group, groupMembers[id]);
}
}
}
vm.map.showInfoWindow = vm.showInfoWindow;
vm.map.hideInfoWindow = vm.hideInfoWindow;
}
// set options
mapOptions.zoom = (mapOptions.zoom && !isNaN(mapOptions.zoom)) ? +mapOptions.zoom : 15;
var center = mapOptions.center;
var exprRegExp = new RegExp(escapeRegExp(exprStartSymbol) + '.*' + escapeRegExp(exprEndSymbol));
if (!mapOptions.center ||
((typeof center === 'string') && center.match(exprRegExp))
) {
mapOptions.center = new google.maps.LatLng(0, 0);
} else if( (typeof center === 'string') && center.match(/^[0-9.-]*,[0-9.-]*$/) ){
var lat = parseFloat(center.split(',')[0]);
var lng = parseFloat(center.split(',')[1]);
mapOptions.center = new google.maps.LatLng(lat, lng);
} else if (!(center instanceof google.maps.LatLng)) {
var geoCenter = mapOptions.center;
delete mapOptions.center;
NgMap.getGeoLocation(geoCenter, mapOptions.geoLocationOptions).
then(function (latlng) {
vm.map.setCenter(latlng);
var geoCallback = mapOptions.geoCallback;
geoCallback && $parse(geoCallback)($scope);
}, function () {
if (mapOptions.geoFallbackCenter) {
vm.map.setCenter(mapOptions.geoFallbackCenter);
}
});
}
vm.map.setOptions(mapOptions);
// set events
for (var eventName in mapEvents) {
var event = mapEvents[eventName];
var listener = google.maps.event.addListener(vm.map, eventName, event);
vm.eventListeners[eventName] = listener;
}
// set observers
vm.observeAttrSetObj(orgAttrs, $attrs, vm.map);
vm.singleInfoWindow = mapOptions.singleInfoWindow;
google.maps.event.trigger(vm.map, 'resize');
google.maps.event.addListenerOnce(vm.map, "idle", function () {
NgMap.addMap(vm);
if (mapOptions.zoomToIncludeMarkers) {
vm.zoomToIncludeMarkers();
}
//TODO: it's for backward compatibiliy. will be removed
$scope.map = vm.map;
$scope.$emit('mapInitialized', vm.map);
//callback
if ($attrs.mapInitialized) {
$parse($attrs.mapInitialized)($scope, {map: vm.map});
}
});
//add maximum zoom listeners if zoom-to-include-markers and and maximum-zoom are valid attributes
if (mapOptions.zoomToIncludeMarkers && mapOptions.maximumZoom) {
google.maps.event.addListener(vm.map, 'zoom_changed', function() {
if (vm.enableMaximumZoomCheck == true) {
vm.enableMaximumZoomCheck = false;
google.maps.event.addListenerOnce(vm.map, 'bounds_changed', function() {
vm.map.setZoom(Math.min(mapOptions.maximumZoom, vm.map.getZoom()));
});
}
});
}
};
$scope.google = google; //used by $scope.eval to avoid eval()
/**
* get map options and events
*/
var orgAttrs = Attr2MapOptions.orgAttributes($element);
var filtered = Attr2MapOptions.filter($attrs);
var options = Attr2MapOptions.getOptions(filtered, {scope: $scope});
var controlOptions = Attr2MapOptions.getControlOptions(filtered);
var mapOptions = angular.extend(options, controlOptions);
var mapEvents = Attr2MapOptions.getEvents($scope, filtered);
void 0;
Object.keys(mapEvents).length && void 0;
vm.mapOptions = mapOptions;
vm.mapEvents = mapEvents;
vm.eventListeners = {};
if (options.lazyInit) { // allows controlled initialization
// parse angular expression for dynamic ids
if (!!$attrs.id &&
// starts with, at position 0
$attrs.id.indexOf(exprStartSymbol, 0) === 0 &&
// ends with
$attrs.id.indexOf(exprEndSymbol, $attrs.id.length - exprEndSymbol.length) !== -1) {
var idExpression = $attrs.id.slice(2,-2);
var mapId = $parse(idExpression)($scope);
} else {
var mapId = $attrs.id;
}
vm.map = {id: mapId}; //set empty, not real, map
NgMap.addMap(vm);
} else {
vm.initializeMap();
}
//Trigger Resize
if(options.triggerResize) {
google.maps.event.trigger(vm.map, 'resize');
}
$element.bind('$destroy', function() {
NgMapPool.returnMapInstance(vm.map);
NgMap.deleteMap(vm);
});
}; // __MapController
__MapController.$inject = [
'$scope', '$element', '$attrs', '$parse', '$interpolate', 'Attr2MapOptions', 'NgMap', 'NgMapPool', 'escapeRegexpFilter'
];
angular.module('ngMap').controller('__MapController', __MapController);
})();
/**
* @ngdoc directive
* @name bicycling-layer
* @param Attr2Options {service}
* convert html attribute to Google map api options
* @description
* Requires: map directive
* Restrict To: Element
*
* @example
*
* <map zoom="13" center="34.04924594193164, -118.24104309082031">
* <bicycling-layer></bicycling-layer>
* </map>
*/
(function() {
'use strict';
var parser;
var linkFunc = function(scope, element, attrs, mapController) {
mapController = mapController[0]||mapController[1];
var orgAttrs = parser.orgAttributes(element);
var filtered = parser.filter(attrs);
var options = parser.getOptions(filtered, {scope: scope});
var events = parser.getEvents(scope, filtered);
void 0;
var layer = getLayer(options, events);
mapController.addObject('bicyclingLayers', layer);
mapController.observeAttrSetObj(orgAttrs, attrs, layer); //observers
element.bind('$destroy', function() {
mapController.deleteObject('bicyclingLayers', layer);
});
};
var getLayer = function(options, events) {
var layer = new google.maps.BicyclingLayer(options);
for (var eventName in events) {
google.maps.event.addListener(layer, eventName, events[eventName]);
}
return layer;
};
var bicyclingLayer= function(Attr2MapOptions) {
parser = Attr2MapOptions;
return {
restrict: 'E',
require: ['?^map','?^ngMap'],
link: linkFunc
};
};
bicyclingLayer.$inject = ['Attr2MapOptions'];
angular.module('ngMap').directive('bicyclingLayer', bicyclingLayer);
})();
/**
* @ngdoc directive
* @name custom-control
* @param Attr2Options {service} convert html attribute to Google map api options
* @param $compile {service} AngularJS $compile service
* @description
* Build custom control and set to the map with position
*
* Requires: map directive
*
* Restrict To: Element
*
* @attr {String} position position of this control
* i.e. TOP_RIGHT
* @attr {Number} index index of the control
* @example
*
* Example:
* <map center="41.850033,-87.6500523" zoom="3">
* <custom-control id="home" position="TOP_LEFT" index="1">
* <div style="background-color: white;">
* <b>Home</b>
* </div>
* </custom-control>
* </map>
*
*/
(function() {
'use strict';
var parser, NgMap;
var linkFunc = function(scope, element, attrs, mapController, $transclude) {
mapController = mapController[0]||mapController[1];
var filtered = parser.filter(attrs);
var options = parser.getOptions(filtered, {scope: scope});
var events = parser.getEvents(scope, filtered);
var innerScope = scope.$new();
/**
* build a custom control element
*/
var customControlEl = element[0].parentElement.removeChild(element[0]);
var content = $transclude( innerScope, function( clone ) {
element.empty();
element.append( clone );
element.on( '$destroy', function() {
innerScope.$destroy();
});
});
/**
* set events
*/
for (var eventName in events) {
google.maps.event.addDomListener(customControlEl, eventName, events[eventName]);
}
mapController.addObject('customControls', customControlEl);
var position = options.position;
mapController.map.controls[google.maps.ControlPosition[position]].push(customControlEl);
element.bind('$destroy', function() {
mapController.deleteObject('customControls', customControlEl);
});
};
var customControl = function(Attr2MapOptions, _NgMap_) {
parser = Attr2MapOptions, NgMap = _NgMap_;
return {
restrict: 'E',
require: ['?^map','?^ngMap'],
link: linkFunc,
transclude: true
}; // return
};
customControl.$inject = ['Attr2MapOptions', 'NgMap'];
angular.module('ngMap').directive('customControl', customControl);
})();
/**
* @ngdoc directive
* @memberof ngmap
* @name custom-marker
* @param Attr2Options {service} convert html attribute to Google map api options
* @param $timeout {service} AngularJS $timeout
* @description
* Marker with html
* Requires: map directive
* Restrict To: Element
*
* @attr {String} position required, position on map
* @attr {Number} z-index optional
* @attr {Boolean} visible optional
* @example
*
* Example:
* <map center="41.850033,-87.6500523" zoom="3">
* <custom-marker position="41.850033,-87.6500523">
* <div>
* <b>Home</b>
* </div>
* </custom-marker>
* </map>
*
*/
/* global document */
(function() {
'use strict';
var parser, $timeout, $compile, NgMap;
var supportedTransform = (function getSupportedTransform() {
var prefixes = 'transform WebkitTransform MozTransform OTransform msTransform'.split(' ');
var div = document.createElement('div');
for(var i = 0; i < prefixes.length; i++) {
if(div && div.style[prefixes[i]] !== undefined) {
return prefixes[i];
}
}
return false;
})();
var CustomMarker = function(options) {
options = options || {};
this.el = document.createElement('div');
this.el.style.display = 'block';
this.el.style.visibility = "hidden";
this.visible = true;
for (var key in options) { /* jshint ignore:line */
this[key] = options[key];
}
};
var setCustomMarker = function() {
CustomMarker.prototype = new google.maps.OverlayView();
CustomMarker.prototype.setContent = function(html, scope) {
this.el.innerHTML = html;
this.el.style.position = 'absolute';
this.el.style.top = 0;
this.el.style.left = 0;
if (scope) {
$compile(angular.element(this.el).contents())(scope);
}
};
CustomMarker.prototype.getDraggable = function() {
return this.draggable;
};
CustomMarker.prototype.setDraggable = function(draggable) {
this.draggable = draggable;
};
CustomMarker.prototype.getPosition = function() {
return this.position;
};
CustomMarker.prototype.setPosition = function(position) {
position && (this.position = position); /* jshint ignore:line */
var _this = this;
if (this.getProjection() && typeof this.position.lng == 'function') {
void 0;
var setPosition = function() {
if (!_this.getProjection()) { return; }
var posPixel = _this.getProjection().fromLatLngToDivPixel(_this.position);
var x = Math.round(posPixel.x - (_this.el.offsetWidth/2));
var y = Math.round(posPixel.y - _this.el.offsetHeight - 10); // 10px for anchor
if (supportedTransform) {
_this.el.style[supportedTransform] = "translate(" + x + "px, " + y + "px)";
} else {
_this.el.style.left = x + "px";
_this.el.style.top = y + "px";
}
_this.el.style.visibility = "visible";
};
if (_this.el.offsetWidth && _this.el.offsetHeight) {
setPosition();
} else {
//delayed left/top calculation when width/height are not set instantly
$timeout(setPosition, 300);
}
}
};
CustomMarker.prototype.setZIndex = function(zIndex) {
if (zIndex === undefined) return;
(this.zIndex !== zIndex) && (this.zIndex = zIndex); /* jshint ignore:line */
(this.el.style.zIndex !== this.zIndex) && (this.el.style.zIndex = this.zIndex);
};
CustomMarker.prototype.getVisible = function() {
return this.visible;
};
CustomMarker.prototype.setVisible = function(visible) {
if (this.el.style.display === 'none' && visible)
{
this.el.style.display = 'block';
} else if (this.el.style.display !== 'none' && !visible) {
this.el.style.display = 'none';
}
this.visible = visible;
};
CustomMarker.prototype.addClass = function(className) {
var classNames = this.el.className.trim().split(' ');
(classNames.indexOf(className) == -1) && classNames.push(className); /* jshint ignore:line */
this.el.className = classNames.join(' ');
};
CustomMarker.prototype.removeClass = function(className) {
var classNames = this.el.className.split(' ');
var index = classNames.indexOf(className);
(index > -1) && classNames.splice(index, 1); /* jshint ignore:line */
this.el.className = classNames.join(' ');
};
CustomMarker.prototype.onAdd = function() {
this.getPanes().overlayMouseTarget.appendChild(this.el);
};
CustomMarker.prototype.draw = function() {
this.setPosition();
this.setZIndex(this.zIndex);
this.setVisible(this.visible);
};
CustomMarker.prototype.onRemove = function() {
this.el.parentNode.removeChild(this.el);
//this.el = null;
};
};
var linkFunc = function(orgHtml, varsToWatch) {
//console.log('orgHtml', orgHtml, 'varsToWatch', varsToWatch);
return function(scope, element, attrs, mapController) {
mapController = mapController[0]||mapController[1];
var orgAttrs = parser.orgAttributes(element);
var filtered = parser.filter(attrs);
var options = parser.getOptions(filtered, {scope: scope});
var events = parser.getEvents(scope, filtered);
/**
* build a custom marker element
*/
element[0].style.display = 'none';
void 0;
var customMarker = new CustomMarker(options);
// Do we really need a timeout with $scope.$apply() here?
setTimeout(function() { //apply contents, class, and location after it is compiled
scope.$watch('[' + varsToWatch.join(',') + ']', function(newVal, oldVal) {
customMarker.setContent(orgHtml, scope);
}, true);
customMarker.setContent(element[0].innerHTML, scope);
var classNames =
(element[0].firstElementChild) && (element[0].firstElementChild.className || '');
customMarker.class && (classNames += " " + customMarker.class);
customMarker.addClass('custom-marker');
classNames && customMarker.addClass(classNames);
void 0;
if (!(options.position instanceof google.maps.LatLng)) {
NgMap.getGeoLocation(options.position).then(
function(latlng) {
customMarker.setPosition(latlng);
}
);
}
});
void 0;
for (var eventName in events) { /* jshint ignore:line */
google.maps.event.addDomListener(
customMarker.el, eventName, events[eventName]);
}
mapController.addObject('customMarkers', customMarker);
//set observers
mapController.observeAttrSetObj(orgAttrs, attrs, customMarker);
element.bind('$destroy', function() {
//Is it required to remove event listeners when DOM is removed?
mapController.deleteObject('customMarkers', customMarker);
});
}; // linkFunc
};
var customMarkerDirective = function(
_$timeout_, _$compile_, $interpolate, Attr2MapOptions, _NgMap_, escapeRegExp
) {
parser = Attr2MapOptions;
$timeout = _$timeout_;
$compile = _$compile_;
NgMap = _NgMap_;
var exprStartSymbol = $interpolate.startSymbol();
var exprEndSymbol = $interpolate.endSymbol();
var exprRegExp = new RegExp(escapeRegExp(exprStartSymbol) + '([^' + exprEndSymbol.substring(0, 1) + ']+)' + escapeRegExp(exprEndSymbol), 'g');
return {
restrict: 'E',
require: ['?^map','?^ngMap'],
compile: function(element) {
void 0;
setCustomMarker();
element[0].style.display ='none';
var orgHtml = element.html();
var matches = orgHtml.match(exprRegExp);
var varsToWatch = [];
//filter out that contains '::', 'this.'
(matches || []).forEach(function(match) {
var toWatch = match.replace(exprStartSymbol,'').replace(exprEndSymbol,'');
if (match.indexOf('::') == -1 &&
match.indexOf('this.') == -1 &&
varsToWatch.indexOf(toWatch) == -1) {
varsToWatch.push(match.replace(exprStartSymbol,'').replace(exprEndSymbol,''));
}
});
return linkFunc(orgHtml, varsToWatch);
}
}; // return
};// function
customMarkerDirective.$inject =
['$timeout', '$compile', '$interpolate', 'Attr2MapOptions', 'NgMap', 'escapeRegexpFilter'];
angular.module('ngMap').directive('customMarker', customMarkerDirective);
})();
/**
* @ngdoc directive
* @name directions
* @description
* Enable directions on map.
* e.g., origin, destination, draggable, waypoints, etc
*
* Requires: map directive
*
* Restrict To: Element
*
* @attr {String} DirectionsRendererOptions
* [Any DirectionsRendererOptions](https://developers.google.com/maps/documentation/javascript/reference#DirectionsRendererOptions)
* @attr {String} DirectionsRequestOptions
* [Any DirectionsRequest options](https://developers.google.com/maps/documentation/javascript/reference#DirectionsRequest)
* @example
* <map zoom="14" center="37.7699298, -122.4469157">
* <directions
* draggable="true"
* panel="directions-panel"
* travel-mode="{{travelMode}}"
* waypoints="[{location:'kingston', stopover:true}]"
* origin="{{origin}}"
* destination="{{destination}}">
* </directions>
* </map>
*/
/* global document */
(function() {
'use strict';
var NgMap, $timeout, NavigatorGeolocation;
var requestTimeout, routeRequest;
// Delay for each route render to accumulate all requests into a single one
// This is required for simultaneous origin\waypoints\destination change
// 20ms should be enough to merge all request data
var routeRenderDelay = 20;
var getDirectionsRenderer = function(options, events) {
if (options.panel) {
options.panel = document.getElementById(options.panel) ||
document.querySelector(options.panel);
}
var renderer = new google.maps.DirectionsRenderer(options);
for (var eventName in events) {
google.maps.event.addListener(renderer, eventName, events[eventName]);
}
return renderer;
};
var updateRoute = function(renderer, options) {
var directionsService = new google.maps.DirectionsService();
/* filter out valid keys only for DirectionsRequest object*/
var request = options;
request.travelMode = request.travelMode || 'DRIVING';
var validKeys = [
'origin', 'destination', 'travelMode', 'transitOptions', 'unitSystem',
'durationInTraffic', 'waypoints', 'optimizeWaypoints',
'provideRouteAlternatives', 'avoidHighways', 'avoidTolls', 'region'
];
if (request) {
for(var key in request) {
if (request.hasOwnProperty(key)) {
(validKeys.indexOf(key) === -1) && (delete request[key]);
}
}
}
if(request.waypoints) {
// Check for acceptable values
if(!Array.isArray(request.waypoints)) {
delete request.waypoints;
}
}
var showDirections = function(request) {
if (requestTimeout && request) {
if (!routeRequest) {
routeRequest = request;
} else {
for (var attr in request) {
if (request.hasOwnProperty(attr)) {
routeRequest[attr] = request[attr];
}
}
}
} else {
requestTimeout = $timeout(function() {
if (!routeRequest) {
routeRequest = request;
}
directionsService.route(routeRequest, function(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
renderer.setDirections(response);
// Unset request for the next call
routeRequest = undefined;
}
});
$timeout.cancel(requestTimeout);
// Unset expired timeout for the next call
requestTimeout = undefined;
}, routeRenderDelay);
}
};
if (request && request.origin && request.destination) {
if (request.origin == 'current-location') {
NavigatorGeolocation.getCurrentPosition().then(function(ll) {
request.origin = new google.maps.LatLng(ll.coords.latitude, ll.coords.longitude);
showDirections(request);
});
} else if (request.destination == 'current-location') {
NavigatorGeolocation.getCurrentPosition().then(function(ll) {
request.destination = new google.maps.LatLng(ll.coords.latitude, ll.coords.longitude);
showDirections(request);
});
} else {
showDirections(request);
}
}
};
var directions = function(
Attr2MapOptions, _$timeout_, _NavigatorGeolocation_, _NgMap_) {
var parser = Attr2MapOptions;
NgMap = _NgMap_;
$timeout = _$timeout_;
NavigatorGeolocation = _NavigatorGeolocation_;
var linkFunc = function(scope, element, attrs, mapController) {
mapController = mapController[0]||mapController[1];
var orgAttrs = parser.orgAttributes(element);
var filtered = parser.filter(attrs);
var options = parser.getOptions(filtered, {scope: scope});
var events = parser.getEvents(scope, filtered);
var attrsToObserve = parser.getAttrsToObserve(orgAttrs);
var attrsToObserve = [];
if (!filtered.noWatcher) {
attrsToObserve = parser.getAttrsToObserve(orgAttrs);
}
var renderer = getDirectionsRenderer(options, events);
mapController.addObject('directionsRenderers', renderer);
attrsToObserve.forEach(function(attrName) {
(function(attrName) {
attrs.$observe(attrName, function(val) {
if (attrName == 'panel') {
$timeout(function(){
var panel =
document.getElementById(val) || document.querySelector(val);
void 0;
panel && renderer.setPanel(panel);
});
} else if (options[attrName] !== val) { //apply only if changed
var optionValue = parser.toOptionValue(val, {key: attrName});
void 0;
options[attrName] = optionValue;
updateRoute(renderer, options);
}
});
})(attrName);
});
NgMap.getMap().then(function() {
updateRoute(renderer, options);
});
element.bind('$destroy', function() {
mapController.deleteObject('directionsRenderers', renderer);
});
};
return {
restrict: 'E',
require: ['?^map','?^ngMap'],
link: linkFunc
};
}; // var directions
directions.$inject =
['Attr2MapOptions', '$timeout', 'NavigatorGeolocation', 'NgMap'];
angular.module('ngMap').directive('directions', directions);
})();
/**
* @ngdoc directive
* @name drawing-manager
* @param Attr2Options {service} convert html attribute to Google map api options
* @description
* Requires: map directive
* Restrict To: Element
*
* @example
* Example:
*
* <map zoom="13" center="37.774546, -122.433523" map-type-id="SATELLITE">
* <drawing-manager
* on-overlaycomplete="onMapOverlayCompleted()"
* position="ControlPosition.TOP_CENTER"
* drawingModes="POLYGON,CIRCLE"
* drawingControl="true"
* circleOptions="fillColor: '#FFFF00';fillOpacity: 1;strokeWeight: 5;clickable: false;zIndex: 1;editable: true;" >
* </drawing-manager>
* </map>
*
* TODO: Add remove button.
* currently, for our solution, we have the shapes/markers in our own
* controller, and we use some css classes to change the shape button
* to a remove button (<div>X</div>) and have the remove operation in our own controller.
*/
(function() {
'use strict';
angular.module('ngMap').directive('drawingManager', [
'Attr2MapOptions', function(Attr2MapOptions) {
var parser = Attr2MapOptions;
return {
restrict: 'E',
require: ['?^map','?^ngMap'],
link: function(scope, element, attrs, mapController) {
mapController = mapController[0]||mapController[1];
var filtered = parser.filter(attrs);
var options = parser.getOptions(filtered, {scope: scope});
var controlOptions = parser.getControlOptions(filtered);
var events = parser.getEvents(scope, filtered);
/**
* set options
*/
var drawingManager = new google.maps.drawing.DrawingManager({
drawingMode: options.drawingmode,
drawingControl: options.drawingcontrol,
drawingControlOptions: controlOptions.drawingControlOptions,
circleOptions:options.circleoptions,
markerOptions:options.markeroptions,
polygonOptions:options.polygonoptions,
polylineOptions:options.polylineoptions,
rectangleOptions:options.rectangleoptions
});
//Observers
attrs.$observe('drawingControlOptions', function (newValue) {
drawingManager.drawingControlOptions = parser.getControlOptions({drawingControlOptions: newValue}).drawingControlOptions;
drawingManager.setDrawingMode(null);
drawingManager.setMap(mapController.map);
});
/**
* set events
*/
for (var eventName in events) {
google.maps.event.addListener(drawingManager, eventName, events[eventName]);
}
mapController.addObject('mapDrawingManager', drawingManager);
element.bind('$destroy', function() {
mapController.deleteObject('mapDrawingManager', drawingManager);
});
}
}; // return
}]);
})();
/**
* @ngdoc directive
* @name dynamic-maps-engine-layer
* @description
* Requires: map directive
* Restrict To: Element
*
* @example
* Example:
* <map zoom="14" center="[59.322506, 18.010025]">
* <dynamic-maps-engine-layer
* layer-id="06673056454046135537-08896501997766553811">
* </dynamic-maps-engine-layer>
* </map>
*/
(function() {
'use strict';
angular.module('ngMap').directive('dynamicMapsEngineLayer', [
'Attr2MapOptions', function(Attr2MapOptions) {
var parser = Attr2MapOptions;
var getDynamicMapsEngineLayer = function(options, events) {
var layer = new google.maps.visualization.DynamicMapsEngineLayer(options);
for (var eventName in events) {
google.maps.event.addListener(layer, eventName, events[eventName]);
}
return layer;
};
return {
restrict: 'E',
require: ['?^map','?^ngMap'],
link: function(scope, element, attrs, mapController) {
mapController = mapController[0]||mapController[1];
var filtered = parser.filter(attrs);
var options = parser.getOptions(filtered, {scope: scope});
var events = parser.getEvents(scope, filtered, events);
var layer = getDynamicMapsEngineLayer(options, events);
mapController.addObject('mapsEngineLayers', layer);
}
}; // return
}]);
})();
/**
* @ngdoc directive
* @name fusion-tables-layer
* @description
* Requires: map directive
* Restrict To: Element
*
* @example
* Example:
* <map zoom="11" center="41.850033, -87.6500523">
* <fusion-tables-layer query="{
* select: 'Geocodable address',
* from: '1mZ53Z70NsChnBMm-qEYmSDOvLXgrreLTkQUvvg'}">
* </fusion-tables-layer>
* </map>
*/
(function() {
'use strict';
angular.module('ngMap').directive('fusionTablesLayer', [
'Attr2MapOptions', function(Attr2MapOptions) {
var parser = Attr2MapOptions;
var getLayer = function(options, events) {
var layer = new google.maps.FusionTablesLayer(options);
for (var eventName in events) {
google.maps.event.addListener(layer, eventName, events[eventName]);
}
return layer;
};
return {
restrict: 'E',
require: ['?^map','?^ngMap'],
link: function(scope, element, attrs, mapController) {
mapController = mapController[0]||mapController[1];
var filtered = parser.filter(attrs);
var options = parser.getOptions(filtered, {scope: scope});
var events = parser.getEvents(scope, filtered, events);
void 0;
var layer = getLayer(options, events);
mapController.addObject('fusionTablesLayers', layer);
element.bind('$destroy', function() {
mapController.deleteObject('fusionTablesLayers', layer);
});
}
}; // return
}]);
})();
/**
* @ngdoc directive
* @name heatmap-layer
* @param Attr2Options {service} convert html attribute to Google map api options
* @description
* Requires: map directive
* Restrict To: Element
*
* @example
*
* <map zoom="11" center="[41.875696,-87.624207]">
* <heatmap-layer data="taxiData"></heatmap-layer>
* </map>
*/
(function() {
'use strict';
angular.module('ngMap').directive('heatmapLayer', [
'Attr2MapOptions', '$window', function(Attr2MapOptions, $window) {
var parser = Attr2MapOptions;
return {
restrict: 'E',
require: ['?^map','?^ngMap'],
link: function(scope, element, attrs, mapController) {
mapController = mapController[0]||mapController[1];
var filtered = parser.filter(attrs);
/**
* set options
*/
var options = parser.getOptions(filtered, {scope: scope});
options.data = $window[attrs.data] || parseScope(attrs.data, scope);
if (options.data instanceof Array) {
options.data = new google.maps.MVCArray(options.data);
} else {
throw "invalid heatmap data";
}
var layer = new google.maps.visualization.HeatmapLayer(options);
/**
* set events
*/
var events = parser.getEvents(scope, filtered);
void 0;
mapController.addObject('heatmapLayers', layer);
//helper get nexted path
function parseScope( path, obj ) {
return path.split('.').reduce( function( prev, curr ) {
return prev[curr];
}, obj || this );
}
}
}; // return
}]);
})();
/**
* @ngdoc directive
* @name info-window
* @param Attr2MapOptions {service}
* convert html attribute to Google map api options
* @param $compile {service} $compile service
* @description
* Defines infoWindow and provides compile method
*
* Requires: map directive
*
* Restrict To: Element
*
* NOTE: this directive should **NOT** be used with `ng-repeat`
* because InfoWindow itself is a template, and a template must be
* reused by each marker, thus, should not be redefined repeatedly
* by `ng-repeat`.
*
* @attr {Boolean} visible
* Indicates to show it when map is initialized
* @attr {Boolean} visible-on-marker
* Indicates to show it on a marker when map is initialized
* @attr {Expression} geo-callback
* if position is an address, the expression is will be performed
* when geo-lookup is successful. e.g., geo-callback="showDetail()"
* @attr {String} <InfoWindowOption> Any InfoWindow options,
* https://developers.google.com/maps/documentation/javascript/reference?csw=1#InfoWindowOptions
* @attr {String} <InfoWindowEvent> Any InfoWindow events,
* https://developers.google.com/maps/documentation/javascript/reference
* @example
* Usage:
* <map MAP_ATTRIBUTES>
* <info-window id="foo" ANY_OPTIONS ANY_EVENTS"></info-window>
* </map>
*
* Example:
* <map center="41.850033,-87.6500523" zoom="3">
* <info-window id="1" position="41.850033,-87.6500523" >
* <div ng-non-bindable>
* Chicago, IL<br/>
* LatLng: {{chicago.lat()}}, {{chicago.lng()}}, <br/>
* World Coordinate: {{worldCoordinate.x}}, {{worldCoordinate.y}}, <br/>
* Pixel Coordinate: {{pixelCoordinate.x}}, {{pixelCoordinate.y}}, <br/>
* Tile Coordinate: {{tileCoordinate.x}}, {{tileCoordinate.y}} at Zoom Level {{map.getZoom()}}
* </div>
* </info-window>
* </map>
*/
/* global google */
(function() {
'use strict';
var infoWindow = function(Attr2MapOptions, $compile, $q, $templateRequest, $timeout, $parse, NgMap) {
var parser = Attr2MapOptions;
var getInfoWindow = function(options, events, element) {
var infoWindow;
/**
* set options
*/
if (options.position && !(options.position instanceof google.maps.LatLng)) {
delete options.position;
}
infoWindow = new google.maps.InfoWindow(options);
/**
* set events
*/
for (var eventName in events) {
if (eventName) {
google.maps.event.addListener(infoWindow, eventName, events[eventName]);
}
}
/**
* set template and template-related functions
* it must have a container element with ng-non-bindable
*/
var templatePromise = $q(function(resolve) {
if (angular.isString(element)) {
$templateRequest(element).then(function (requestedTemplate) {
resolve(angular.element(requestedTemplate).wrap('<div>').parent());
}, function(message) {
throw "info-window template request failed: " + message;
});
}
else {
resolve(element);
}
}).then(function(resolvedTemplate) {
var template = resolvedTemplate.html().trim();
if (angular.element(template).length != 1) {
throw "info-window working as a template must have a container";
}
infoWindow.__template = template.replace(/\s?ng-non-bindable[='"]+/,"");
});
infoWindow.__open = function(map, scope, anchor) {
templatePromise.then(function() {
$timeout(function() {
anchor && (scope.anchor = anchor);
var el = $compile(infoWindow.__template)(scope);
infoWindow.setContent(el[0]);
scope.$apply();
if (anchor && anchor.getPosition) {
infoWindow.open(map, anchor);
} else if (anchor && anchor instanceof google.maps.LatLng) {
infoWindow.open(map);
infoWindow.setPosition(anchor);
} else {
infoWindow.open(map);
}
$timeout(function() { // to avoid racing condition
var infoWindowContainerEl = infoWindow.content.parentElement.parentElement.parentElement;
infoWindowContainerEl.className = "ng-map-info-window";
});
});
});
};
return infoWindow;
};
var linkFunc = function(scope, element, attrs, mapController) {
mapController = mapController[0]||mapController[1];
element.css('display','none');
var orgAttrs = parser.orgAttributes(element);
var filtered = parser.filter(attrs);
var options = parser.getOptions(filtered, {scope: scope});
var events = parser.getEvents(scope, filtered);
var infoWindow = getInfoWindow(options, events, options.template || element);
var address;
if (options.position && !(options.position instanceof google.maps.LatLng)) {
address = options.position;
}
if (address) {
NgMap.getGeoLocation(address).then(function(latlng) {
infoWindow.setPosition(latlng);
infoWindow.__open(mapController.map, scope, latlng);
var geoCallback = attrs.geoCallback;
geoCallback && $parse(geoCallback)(scope);
});
}
mapController.addObject('infoWindows', infoWindow);
mapController.observeAttrSetObj(orgAttrs, attrs, infoWindow);
mapController.showInfoWindow =
mapController.map.showInfoWindow = mapController.showInfoWindow ||
function(p1, p2, p3) { //event, id, marker
var id = typeof p1 == 'string' ? p1 : p2;
var marker = typeof p1 == 'string' ? p2 : p3;
if (typeof marker == 'string') {
//Check if markers if defined to avoid odd 'undefined' errors
if (
typeof mapController.map.markers != "undefined"
&& typeof mapController.map.markers[marker] != "undefined") {
marker = mapController.map.markers[marker];
} else if (
//additionally check if that marker is a custom marker
typeof mapController.map.customMarkers !== "undefined"
&& typeof mapController.map.customMarkers[marker] !== "undefined") {
marker = mapController.map.customMarkers[marker];
} else {
//Better error output if marker with that id is not defined
throw new Error("Cant open info window for id " + marker + ". Marker or CustomMarker is not defined")
}
}
var infoWindow = mapController.map.infoWindows[id];
var anchor = marker ? marker : (this.getPosition ? this : null);
infoWindow.__open(mapController.map, scope, anchor);
if(mapController.singleInfoWindow) {
if(mapController.lastInfoWindow) {
scope.hideInfoWindow(mapController.lastInfoWindow);
}
mapController.lastInfoWindow = id;
}
};
mapController.hideInfoWindow =
mapController.map.hideInfoWindow = mapController.hideInfoWindow ||
function(p1, p2) {
var id = typeof p1 == 'string' ? p1 : p2;
var infoWindow = mapController.map.infoWindows[id];
infoWindow.close();
};
//TODO DEPRECATED
scope.showInfoWindow = mapController.map.showInfoWindow;
scope.hideInfoWindow = mapController.map.hideInfoWindow;
var map = infoWindow.mapId ? {id:infoWindow.mapId} : 0;
NgMap.getMap(map).then(function(map) {
infoWindow.visible && infoWindow.__open(map, scope);
if (infoWindow.visibleOnMarker) {
var markerId = infoWindow.visibleOnMarker;
infoWindow.__open(map, scope, map.markers[markerId]);
}
});
}; //link
return {
restrict: 'E',
require: ['?^map','?^ngMap'],
link: linkFunc
};
}; // infoWindow
infoWindow.$inject =
['Attr2MapOptions', '$compile', '$q', '$templateRequest', '$timeout', '$parse', 'NgMap'];
angular.module('ngMap').directive('infoWindow', infoWindow);
})();
/**
* @ngdoc directive
* @name kml-layer
* @param Attr2MapOptions {service} convert html attribute to Google map api options
* @description
* renders Kml layer on a map
* Requires: map directive
* Restrict To: Element
*
* @attr {Url} url url of the kml layer
* @attr {KmlLayerOptions} KmlLayerOptions
* (https://developers.google.com/maps/documentation/javascript/reference#KmlLayerOptions)
* @attr {String} <KmlLayerEvent> Any KmlLayer events,
* https://developers.google.com/maps/documentation/javascript/reference
* @example
* Usage:
* <map MAP_ATTRIBUTES>
* <kml-layer ANY_KML_LAYER ANY_KML_LAYER_EVENTS"></kml-layer>
* </map>
*
* Example:
*
* <map zoom="11" center="[41.875696,-87.624207]">
* <kml-layer url="https://gmaps-samples.googlecode.com/svn/trunk/ggeoxml/cta.kml" >
* </kml-layer>
* </map>
*/
(function() {
'use strict';
angular.module('ngMap').directive('kmlLayer', [
'Attr2MapOptions', function(Attr2MapOptions) {
var parser = Attr2MapOptions;
var getKmlLayer = function(options, events) {
var kmlLayer = new google.maps.KmlLayer(options);
for (var eventName in events) {
google.maps.event.addListener(kmlLayer, eventName, events[eventName]);
}
return kmlLayer;
};
return {
restrict: 'E',
require: ['?^map','?^ngMap'],
link: function(scope, element, attrs, mapController) {
mapController = mapController[0]||mapController[1];
var orgAttrs = parser.orgAttributes(element);
var filtered = parser.filter(attrs);
var options = parser.getOptions(filtered, {scope: scope});
var events = parser.getEvents(scope, filtered);
void 0;
var kmlLayer = getKmlLayer(options, events);
mapController.addObject('kmlLayers', kmlLayer);
mapController.observeAttrSetObj(orgAttrs, attrs, kmlLayer); //observers
element.bind('$destroy', function() {
mapController.deleteObject('kmlLayers', kmlLayer);
});
}
}; // return
}]);
})();
/**
* @ngdoc directive
* @name map-data
* @param Attr2MapOptions {service}
* convert html attribute to Google map api options
* @description
* set map data
* Requires: map directive
* Restrict To: Element
*
* @wn {String} method-name, run map.data[method-name] with attribute value
* @example
* Example:
*
* <map zoom="11" center="[41.875696,-87.624207]">
* <map-data load-geo-json="https://storage.googleapis.com/maps-devrel/google.json"></map-data>
* </map>
*/
(function() {
'use strict';
angular.module('ngMap').directive('mapData', [
'Attr2MapOptions', 'NgMap', function(Attr2MapOptions, NgMap) {
var parser = Attr2MapOptions;
return {
restrict: 'E',
require: ['?^map','?^ngMap'],
link: function(scope, element, attrs, mapController) {
mapController = mapController[0] || mapController[1];
var filtered = parser.filter(attrs);
var options = parser.getOptions(filtered, {scope: scope});
var events = parser.getEvents(scope, filtered, events);
void 0;
NgMap.getMap(mapController.map.id).then(function(map) {
//options
for (var key in options) {
var val = options[key];
if (typeof scope[val] === "function") {
map.data[key](scope[val]);
} else {
map.data[key](val);
}
}
//events
for (var eventName in events) {
map.data.addListener(eventName, events[eventName]);
}
});
}
}; // return
}]);
})();
/**
* @ngdoc directive
* @name map-lazy-load
* @param Attr2Options {service} convert html attribute to Google map api options
* @description
* Requires: Delay the initialization of map directive
* until the map is ready to be rendered
* Restrict To: Attribute
*
* @attr {String} map-lazy-load
* Maps api script source file locat