source : custom-marker.js

/**
 * @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') {
        console.log(_this.getProjection());
        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';
      console.log("custom-marker options", options);
      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);
        console.log('customMarker', customMarker, 'classNames', classNames);
        if (!(options.position instanceof google.maps.LatLng)) {
          NgMap.getGeoLocation(options.position).then(
            function(latlng) {
              customMarker.setPosition(latlng);
            }
          );
        }
      });
      console.log("custom-marker events", events);
      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) {
        console.log('el', element);
        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);
})();