// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/maps/RouteModel.js?ver=2.8.0 
function CMMRM_RouteModel(data, waypointsString, locations) {
	
//	console.log(data);
//	console.log('waypointsString', waypointsString);
	
	this.data = data;
	
	this.waypointsString = (typeof waypointsString == 'string' ? waypointsString : '');
	this.waypointsCoords = [];
	this.waypoints = [];
	this.decodeWaypoints();
	
	this.locations = [];
	this.addLocations(locations);
	
}

CMMRM_RouteModel.prototype.addLocations = function(locations) {
	for (var i=0; i<locations.length; i++) {
		this.addLocation(locations[i]);
	}
};

CMMRM_RouteModel.prototype.addLocation = function(locationData) {
	var locationModel = new CMMRM_LocationModel(locationData, this);
	var that = this;
	this.locations.push(locationModel);
	jQuery(this).trigger('RouteModel:addLocation', {locationData: locationData, locationModel: locationModel});
	jQuery(locationModel).bind('LocationModel:remove', function() {
		that.removeLocationByModel(this);
	});
	return locationModel;
};


CMMRM_RouteModel.prototype.removeLocationByIndex = function(index) {
	var removed = this.locations.splice(index, 1);
	return (removed.length == 1);
};


CMMRM_RouteModel.prototype.removeLocationByModel = function(obj) {
	for (var i=0; i<this.locations.length; i++) {
		if (this.locations[i] === obj) {
			return this.removeLocationByIndex(i);
		}
	}
	return false;
};


CMMRM_RouteModel.prototype.addWaypoints = function(waypoints) {
	var that = this;
	for (var i=0; i<waypoints.length; i++) {
		this.addWaypoint(waypoints[i]);
	}
	return this;
};


CMMRM_RouteModel.prototype.removeWaypointByIndex = function(index) {
	var removed = this.waypointsCoords.splice(index, 1);
	return (removed.length == 1);
};


CMMRM_RouteModel.prototype.removeWaypointByModel = function(obj) {
	for (var i=0; i<this.waypoints.length; i++) {
		if (this.waypoints[i] === obj) {
			return this.removeWaypointByIndex(i);
		}
	}
	return false;
};


CMMRM_RouteModel.prototype.decodeWaypoints = function() {
	if (typeof this.waypointsString == 'string') {
		this.waypointsCoords = google.maps.geometry.encoding.decodePath(this.waypointsString);
	} else {
		this.waypointsCoords = [];
	}
	return this;
//	return this.addWaypoints(coords);
};

CMMRM_RouteModel.prototype.addWaypoint = function(waypointData, index) {
	if (Object.prototype.toString.call(waypointData) == '[object Array]') {
//		waypointData = [waypointData.lat(), waypointData.lng()];
		waypointData = new google.maps.LatLng(waypointData[0], waypointData[1]);
	}
	
	if (typeof index == 'undefined') {
		index = this.waypointsCoords.length;
		this.waypointsCoords.push(waypointData);
	} else {
		this.waypointsCoords.splice(index, 0, waypointData);
	}
	
	jQuery(this).trigger('RouteModel:addWaypoint', {waypointData: waypointData});
//	google.maps.event.addListener(waypointData, 'WaypointModel:remove', function() {
//		that.removeWaypointByIndex(index);
//	});
	
	return this;
	
//	var that = this;
//	var waypointModel = new CMMRM_WaypointModel(waypointData, this);
//	
//	if (typeof index == 'undefined') this.waypoints.push(waypointModel);
//	else this.waypoints.splice(index, 0, waypointModel);
//	
//	jQuery(this).trigger('RouteModel:addWaypoint', {waypointData: waypointData, waypointModel: waypointModel});
//	jQuery(waypointModel).bind('WaypointModel:remove', function() {
////		console.log('CCCCCCCCC')
//		that.removeWaypointByModel(this);
//	});
//	return waypointModel;
};


CMMRM_RouteModel.prototype.getTravelMode = function() {
	return this.data.travelMode;
};

CMMRM_RouteModel.prototype.setTravelMode = function(mode) {
	var old = this.data.travelMode;
	this.data.travelMode = mode;
	jQuery(this).trigger('RouteModel:setTravelMode', {old: old, travelMode: mode});
	return this;
};

CMMRM_RouteModel.prototype.getLocations = function() {
	return this.locations;
};

CMMRM_RouteModel.prototype.getWaypoints = function() {
	return this.waypoints;
};


CMMRM_RouteModel.prototype.getWaypointsString = function() {
	return this.waypointsString;
};


CMMRM_RouteModel.prototype.setWaypointsString = function(val) {
	var old = this.waypointsString;
	this.waypointsString = val;
	jQuery(this).trigger('RouteModel:setWaypointsString', {old: old, waypointsString: val});
	return this;
};

CMMRM_RouteModel.prototype.getWaypointsCoords = function() {
	return this.waypointsCoords;
//	var coords = [];
//	for (var i=0; i<this.waypoints.length; i++) {
//		coords.push([this.waypoints[i].getLat(), this.waypoints[i].getLng()]);
//	}
//	return coords;
};


CMMRM_RouteModel.prototype.getWaypointsGoogleLatLng = function() {
	var coords = [];
	for (var i=0; i<this.waypoints.length; i++) {
		coords.push(this.waypoints[i].getGoogleLatLng());
	}
	return coords;
};

CMMRM_RouteModel.prototype.getPolylineString = function() {
	return this.data.overviewPath;
};


CMMRM_RouteModel.prototype.setPolylineString = function(str) {
	var old = this.data.overviewPath;
	this.data.overviewPath = str;
	jQuery(this).trigger('RouteModel:setPolylineString', {old: old, polylineString: str});
	return this;
};


CMMRM_RouteModel.prototype.getPolylineCoords = function() {
	var str = this.getPolylineString();
	if (str) {
		return google.maps.geometry.encoding.decodePath(str);
	} else {
		return [];
	}
};


CMMRM_RouteModel.prototype.getPathColor = function() {
	return (this.data.pathColor ? this.data.pathColor : '#3377FF');
};

CMMRM_RouteModel.prototype.showDirectionalArrows = function() {
	return this.data.showDirectionalArrows;
};


CMMRM_RouteModel.prototype.getIcon = function() {
	return null;
};


CMMRM_RouteModel.prototype.getName = function() {
	return this.data.name;
};


CMMRM_RouteModel.prototype.getBounds = function() {
	var coords = [];
	
	// Add waypoints
//	var waypoints = this.getWaypoints();
//	for (var i=0; i<waypoints.length; i++) {
//		coords.push(waypoints[i].getPosition());
//	}
	
	// Add locations
	var locations = this.getLocations();
	for (var i=0; i<locations.length; i++) {
		coords.push(locations[i].getPosition());
	}
	
	// Add polyline
	var polyline = this.getPolylineCoords();
	for (var i=0; i<polyline.length; i++) {
		coords.push(polyline[i]);
	}
	
	// Add default lat lng
	var latLng = this.getGoogleLatLng();
	if (latLng) {
		coords.push(latLng);
	}
	
	return coords;
};


CMMRM_RouteModel.prototype.setRouteParam = function(name, val) {
	var old = this.data[name];
	this.data[name] = val;
	jQuery(this).trigger('RouteModel:setRouteParam', {name: name, old: old, value: val});
	return this;
};

CMMRM_RouteModel.prototype.getRouteParam = function(name) {
	return this.data[name];
};

CMMRM_RouteModel.prototype.updateWaypointsString = function() {
//	var waypoints = this.getWaypointsCoords();
	if (google && google.maps && google.maps.geometry && google.maps.geometry.encoding) {
		var coords = this.getWaypointsCoords();
//		for (var i=0; i<waypoints.length; i++) {
//			coords.push(new google.maps.LatLng(waypoints[i].getLat(), waypoints[i].getLng()));
//		}
//		console.log(coords);
		var path =  google.maps.geometry.encoding.encodePath(coords);
//		console.log(path);
		this.setWaypointsString(path);
	}
};


CMMRM_RouteModel.prototype.getGoogleLatLng = function() {
//	console.log(this.data.lat, this.data.long);
	if (this.data.lat && this.data.long) {
		return new google.maps.LatLng(this.data.lat, this.data.long);
	}
};
// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/maps/RequestTrail.js?ver=2.8.0 
function CMMRM_RequestTrail(travelMode, waypointsCoords) {
	this.travelMode = travelMode;
	this.waypointsCoords = waypointsCoords;
	this.response = null;
	this.status = null;
}


CMMRM_RequestTrail.prototype.run = function(mapRenderer, callback) {
	if ('DIRECT' == this.travelMode) {
		this.requestTrailDirect(mapRenderer, callback);
	} else {
		this.requestTrailGoogle(callback);
	}
};


CMMRM_RequestTrail.prototype.getDistance = function() {
	var totalDistance = 0;
	var legs = this.response.routes[0].legs;
	for (var i=0; i<legs.length; ++i) {
		totalDistance += legs[i].distance.value;
	}
	return totalDistance;
};


CMMRM_RequestTrail.prototype.getResponse = function() {
	return this.response;
};

CMMRM_RequestTrail.prototype.requestTrailGoogle = function(callback) {
	var that = this;
	var requestWaypoints = [];
	var coords = this.waypointsCoords;
//	console.log(coords);
	for (var i=1; i<coords.length-1; i++) {
		requestWaypoints.push({
			location: coords[i],
			stopover: true,
		});
	}
	
	var directionsService = new google.maps.DirectionsService();
	var origin = coords[0];
	var destination = coords[coords.length-1];
	
	directionsService.route({
		origin: origin,
		destination: destination,
		waypoints: requestWaypoints,
		travelMode: this.travelMode,
		optimizeWaypoints: false
	  }, function(response, status) {
		  that.response = response;
		  that.status = status;
		  callback(response, status);
//		  that.requestTrailCallback(response, status);
	});
	
};


CMMRM_RequestTrail.prototype.requestTrailDirect = function(mapRenderer, callback) {
	
	var overview_path = [];
	var legs = [];
	var newLeg = {duration: {value: 0}, distance: {value: 0}, steps: [{path: []}]};
	var leg = jQuery.extend(true, {}, newLeg);
	var step = null;
	var lastCoord = null;
	var coords = this.waypointsCoords;
//	console.log(coords);
	for (var i=0; i<coords.length; i++) {
		
		var coord = coords[i];
//		var coord = new google.maps.LatLng(location.lat, location.long);
		overview_path.push(coord);
		
		if (lastCoord) {
			var distance = CMMRM_GoogleMap.prototype.calculateDistance(lastCoord, coord);
			leg.distance.value += distance;
			leg.duration.value += distance * (3600/4000);
			leg.duration.text = leg.duration.value.toString();
		}
		
		if (i > 0) {
			leg.steps[0].path.push(coord);
			leg.steps[0].distance = {text: distance.toString(), value: distance};
			if (coords.length < CMMRM_Map_Settings.editorWaypointsLimit) {
				// Created new legs only if it's simple trail to avoid too many legs for the imported GPX files
				legs.push(leg);
			}
		}
		if (coords.length < CMMRM_Map_Settings.editorWaypointsLimit) {
			// Created new legs only if it's simple trail to avoid too many legs for the imported GPX files
			leg = jQuery.extend(true, {}, newLeg); // commented to avoid too many legs for imported files
		}
		
		leg.steps[0].path.push(coord);
		lastCoord = coord;
		
	}
	
	legs.push(leg); // added to create only one leg for direct travel mode
	
	this.status = google.maps.DirectionsStatus.OK;
	this.response = {
		routes: [{
		   overview_path: overview_path,
		   overview_polyline: google.maps.geometry.encoding.encodePath(overview_path),
		   legs: legs
		}]
	};
	
	callback(this.response, this.status);
//	this.requestTrailCallback('DIRECT', response, status);
	
};



CMMRM_RequestTrail.prototype.createTrailPolylines = function(map, color, showDirectionalArrows) {
	var result = [];
	var legs = this.response.routes[0].legs;
	for (var legIndex=0; legIndex<legs.length; legIndex++) {
		var path = [];
		var steps = legs[legIndex].steps;
		for (var j=0; j<steps.length; j++) {
			path = path.concat(steps[j].path);
		}
		result.push(this.createTrailPolyline(path, legIndex, map, color, showDirectionalArrows));
	}
	return result;
};



CMMRM_RequestTrail.prototype.createTrailPolyline = function(path, legIndex, map, color, showDirectionalArrows) {
//	console.log(this.pathColor);
	
	var icons = [];
	if (showDirectionalArrows) {
		var icon = {
          icon: {
                  path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
                  strokeColor:color,
                  strokeOpacity: 0,
                  fillColor: color,
                  fillOpacity: 1,
                  offset: '0'
                },
          repeat:'100px',
          path:[]
        };
		icons.push(icon);
	}
	
	var p = new google.maps.Polyline({
		path: path,
		strokeColor: color,
		opacity: 0.1,
		map: map,
		icons: icons
	});
	p.legIndex = legIndex;
	return p;
};


CMMRM_RequestTrail.prototype.getDirectionErrorMessage = function(status) {
	switch (status) {
	case google.maps.DirectionsStatus.INVALID_REQUEST:
		return 'Invalid request';
	case google.maps.DirectionsStatus.MAX_WAYPOINTS_EXCEEDED:
		return 'This website has reached the limit of the Google Maps API waypoints number per directions request. '
				+ 'The total allowed waypoints is 23, plus the origin and destination. '
				+ 'Please use the Direct Travel Mode if using more waypoints.';
	case google.maps.DirectionsStatus.NOT_FOUND:
		return 'At least one of the origin, destination, or waypoints could not be geocoded.';
	case google.maps.DirectionsStatus.OVER_QUERY_LIMIT:
		return 'This website has gone over the requests limit in too short a period of time.';
	case google.maps.DirectionsStatus.REQUEST_DENIED:
		return 'The webpage is not allowed to use the directions service.';
	case google.maps.DirectionsStatus.ZERO_RESULTS:
		return 'No route could be found between the origin and destination.';
	case google.maps.DirectionsStatus.UNKNOWN_ERROR:
	default:
		return 'A directions request could not be processed due to a server error. The request may succeed if you try again.';
	}
};


CMMRM_RequestTrail.prototype.getDuration = function() {
	var totalDuration = 0;
	var legs = this.response.routes[0].legs;
	for (var i=0; i<legs.length; ++i) {
		totalDuration += legs[i].duration.value;
	}
	return totalDuration; // seconds
};


CMMRM_RequestTrail.prototype.getOverviewPath = function() {
	if (typeof this.response.routes == 'object' && this.response.routes.length > 0) {
		return this.response.routes[0].overview_path;
	} else {
		return '';
	}
};

CMMRM_RequestTrail.prototype.getOverviewPolyline = function() {
	if (typeof this.response.routes == 'object' && this.response.routes.length > 0) {
		return this.response.routes[0].overview_polyline;
	} else {
		return '';
	}
};
// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/maps/markerclusterer.js?ver=2.8.0 
/**
 * @name MarkerClusterer for Google Maps v3
 * @version version 1.0.1
 * @author Luke Mahe
 * @fileoverview
 * The library creates and manages per-zoom-level clusters for large amounts of
 * markers.
 * <br/>
 * This is a v3 implementation of the
 * <a href="http://gmaps-utility-library-dev.googlecode.com/svn/tags/markerclusterer/"
 * >v2 MarkerClusterer</a>.
 */

/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


/**
 * A Marker Clusterer that clusters markers.
 *
 * @param {google.maps.Map} map The Google map to attach to.
 * @param {Array.<google.maps.Marker>=} opt_markers Optional markers to add to
 *   the cluster.
 * @param {Object=} opt_options support the following options:
 *     'gridSize': (number) The grid size of a cluster in pixels.
 *     'maxZoom': (number) The maximum zoom level that a marker can be part of a
 *                cluster.
 *     'zoomOnClick': (boolean) Whether the default behaviour of clicking on a
 *                    cluster is to zoom into it.
 *     'averageCenter': (boolean) Whether the center of each cluster should be
 *                      the average of all markers in the cluster.
 *     'minimumClusterSize': (number) The minimum number of markers to be in a
 *                           cluster before the markers are hidden and a count
 *                           is shown.
 *     'styles': (object) An object that has style properties:
 *       'url': (string) The image url.
 *       'height': (number) The image height.
 *       'width': (number) The image width.
 *       'anchor': (Array) The anchor position of the label text.
 *       'textColor': (string) The text color.
 *       'textSize': (number) The text size.
 *       'backgroundPosition': (string) The position of the backgound x, y.
 * @constructor
 * @extends google.maps.OverlayView
 */
function MarkerClusterer(map, opt_markers, opt_options) {
  // MarkerClusterer implements google.maps.OverlayView interface. We use the
  // extend function to extend MarkerClusterer with google.maps.OverlayView
  // because it might not always be available when the code is defined so we
  // look for it at the last possible moment. If it doesn't exist now then
  // there is no point going ahead :)
  this.extend(MarkerClusterer, google.maps.OverlayView);
  this.map_ = map;

  /**
   * @type {Array.<google.maps.Marker>}
   * @private
   */
  this.markers_ = [];

  /**
   *  @type {Array.<Cluster>}
   */
  this.clusters_ = [];

  this.sizes = [53, 56, 66, 78, 90];

  /**
   * @private
   */
  this.styles_ = [];

  /**
   * @type {boolean}
   * @private
   */
  this.ready_ = false;

  var options = opt_options || {};

  /**
   * @type {number}
   * @private
   */
  this.gridSize_ = options['gridSize'] || 60;

  /**
   * @private
   */
  this.minClusterSize_ = options['minimumClusterSize'] || 2;


  /**
   * @type {?number}
   * @private
   */
  this.maxZoom_ = options['maxZoom'] || null;

  this.styles_ = options['styles'] || [];

  /**
   * @type {string}
   * @private
   */
  this.imagePath_ = options['imagePath'] ||
      this.MARKER_CLUSTER_IMAGE_PATH_;

  /**
   * @type {string}
   * @private
   */
  this.imageExtension_ = options['imageExtension'] ||
      this.MARKER_CLUSTER_IMAGE_EXTENSION_;

  /**
   * @type {boolean}
   * @private
   */
  this.zoomOnClick_ = true;

  if (options['zoomOnClick'] != undefined) {
    this.zoomOnClick_ = options['zoomOnClick'];
  }

  /**
   * @type {boolean}
   * @private
   */
  this.averageCenter_ = false;

  if (options['averageCenter'] != undefined) {
    this.averageCenter_ = options['averageCenter'];
  }

  this.setupStyles_();

  this.setMap(map);

  /**
   * @type {number}
   * @private
   */
  this.prevZoom_ = this.map_.getZoom();

  // Add the map event listeners
  var that = this;
  google.maps.event.addListener(this.map_, 'zoom_changed', function() {
    // Determines map type and prevent illegal zoom levels
    var zoom = that.map_.getZoom();
    var minZoom = that.map_.minZoom || 0;
    var maxZoom = Math.min(that.map_.maxZoom || 100,
                         that.map_.mapTypes[that.map_.getMapTypeId()].maxZoom);
    zoom = Math.min(Math.max(zoom,minZoom),maxZoom);

    if (that.prevZoom_ != zoom) {
      that.prevZoom_ = zoom;
      that.resetViewport();
    }
  });

  google.maps.event.addListener(this.map_, 'idle', function() {
    that.redraw();
  });

  // Finally, add the markers
  if (opt_markers && (opt_markers.length || Object.keys(opt_markers).length)) {
    this.addMarkers(opt_markers, false);
  }
}


/**
 * The marker cluster image path.
 *
 * @type {string}
 * @private
 */
MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = '../images/m';


/**
 * The marker cluster image path.
 *
 * @type {string}
 * @private
 */
MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png';


/**
 * Extends a objects prototype by anothers.
 *
 * @param {Object} obj1 The object to be extended.
 * @param {Object} obj2 The object to extend with.
 * @return {Object} The new extended object.
 * @ignore
 */
MarkerClusterer.prototype.extend = function(obj1, obj2) {
  return (function(object) {
    for (var property in object.prototype) {
      this.prototype[property] = object.prototype[property];
    }
    return this;
  }).apply(obj1, [obj2]);
};


/**
 * Implementaion of the interface method.
 * @ignore
 */
MarkerClusterer.prototype.onAdd = function() {
  this.setReady_(true);
};

/**
 * Implementaion of the interface method.
 * @ignore
 */
MarkerClusterer.prototype.draw = function() {};

/**
 * Sets up the styles object.
 *
 * @private
 */
MarkerClusterer.prototype.setupStyles_ = function() {
  if (this.styles_.length) {
    return;
  }

  for (var i = 0, size; size = this.sizes[i]; i++) {
    this.styles_.push({
      url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_,
      height: size,
      width: size
    });
  }
};

/**
 *  Fit the map to the bounds of the markers in the clusterer.
 */
MarkerClusterer.prototype.fitMapToMarkers = function() {
  var markers = this.getMarkers();
  var bounds = new google.maps.LatLngBounds();
  for (var i = 0, marker; marker = markers[i]; i++) {
    bounds.extend(marker.getPosition());
  }

  this.map_.fitBounds(bounds);
};


/**
 *  Sets the styles.
 *
 *  @param {Object} styles The style to set.
 */
MarkerClusterer.prototype.setStyles = function(styles) {
  this.styles_ = styles;
};


/**
 *  Gets the styles.
 *
 *  @return {Object} The styles object.
 */
MarkerClusterer.prototype.getStyles = function() {
  return this.styles_;
};


/**
 * Whether zoom on click is set.
 *
 * @return {boolean} True if zoomOnClick_ is set.
 */
MarkerClusterer.prototype.isZoomOnClick = function() {
  return this.zoomOnClick_;
};

/**
 * Whether average center is set.
 *
 * @return {boolean} True if averageCenter_ is set.
 */
MarkerClusterer.prototype.isAverageCenter = function() {
  return this.averageCenter_;
};


/**
 *  Returns the array of markers in the clusterer.
 *
 *  @return {Array.<google.maps.Marker>} The markers.
 */
MarkerClusterer.prototype.getMarkers = function() {
  return this.markers_;
};


/**
 *  Returns the number of markers in the clusterer
 *
 *  @return {Number} The number of markers.
 */
MarkerClusterer.prototype.getTotalMarkers = function() {
  return this.markers_.length;
};


/**
 *  Sets the max zoom for the clusterer.
 *
 *  @param {number} maxZoom The max zoom level.
 */
MarkerClusterer.prototype.setMaxZoom = function(maxZoom) {
  this.maxZoom_ = maxZoom;
};


/**
 *  Gets the max zoom for the clusterer.
 *
 *  @return {number} The max zoom level.
 */
MarkerClusterer.prototype.getMaxZoom = function() {
  return this.maxZoom_;
};


/**
 *  The function for calculating the cluster icon image.
 *
 *  @param {Array.<google.maps.Marker>} markers The markers in the clusterer.
 *  @param {number} numStyles The number of styles available.
 *  @return {Object} A object properties: 'text' (string) and 'index' (number).
 *  @private
 */
MarkerClusterer.prototype.calculator_ = function(markers, numStyles) {
  var index = 0;
  var count = markers.length;
  var dv = count;
  while (dv !== 0) {
    dv = parseInt(dv / 10, 10);
    index++;
  }

  index = Math.min(index, numStyles);
  return {
    text: count,
    index: index
  };
};


/**
 * Set the calculator function.
 *
 * @param {function(Array, number)} calculator The function to set as the
 *     calculator. The function should return a object properties:
 *     'text' (string) and 'index' (number).
 *
 */
MarkerClusterer.prototype.setCalculator = function(calculator) {
  this.calculator_ = calculator;
};


/**
 * Get the calculator function.
 *
 * @return {function(Array, number)} the calculator function.
 */
MarkerClusterer.prototype.getCalculator = function() {
  return this.calculator_;
};


/**
 * Add an array of markers to the clusterer.
 *
 * @param {Array.<google.maps.Marker>} markers The markers to add.
 * @param {boolean=} opt_nodraw Whether to redraw the clusters.
 */
MarkerClusterer.prototype.addMarkers = function(markers, opt_nodraw) {
  if (markers.length) {
    for (var i = 0, marker; marker = markers[i]; i++) {
      this.pushMarkerTo_(marker);
    }
  } else if (Object.keys(markers).length) {
    for (var marker in markers) {
      this.pushMarkerTo_(markers[marker]);
    }
  }
  if (!opt_nodraw) {
    this.redraw();
  }
};


/**
 * Pushes a marker to the clusterer.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @private
 */
MarkerClusterer.prototype.pushMarkerTo_ = function(marker) {
  marker.isAdded = false;
  if (marker['draggable']) {
    // If the marker is draggable add a listener so we update the clusters on
    // the drag end.
    var that = this;
    google.maps.event.addListener(marker, 'dragend', function() {
      marker.isAdded = false;
      that.repaint();
    });
  }
  this.markers_.push(marker);
};


/**
 * Adds a marker to the clusterer and redraws if needed.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @param {boolean=} opt_nodraw Whether to redraw the clusters.
 */
MarkerClusterer.prototype.addMarker = function(marker, opt_nodraw) {
  this.pushMarkerTo_(marker);
  if (!opt_nodraw) {
    this.redraw();
  }
};


/**
 * Removes a marker and returns true if removed, false if not
 *
 * @param {google.maps.Marker} marker The marker to remove
 * @return {boolean} Whether the marker was removed or not
 * @private
 */
MarkerClusterer.prototype.removeMarker_ = function(marker) {
  var index = -1;
  if (this.markers_.indexOf) {
    index = this.markers_.indexOf(marker);
  } else {
    for (var i = 0, m; m = this.markers_[i]; i++) {
      if (m == marker) {
        index = i;
        break;
      }
    }
  }

  if (index == -1) {
    // Marker is not in our list of markers.
    return false;
  }

  marker.setMap(null);

  this.markers_.splice(index, 1);

  return true;
};


/**
 * Remove a marker from the cluster.
 *
 * @param {google.maps.Marker} marker The marker to remove.
 * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
 * @return {boolean} True if the marker was removed.
 */
MarkerClusterer.prototype.removeMarker = function(marker, opt_nodraw) {
  var removed = this.removeMarker_(marker);

  if (!opt_nodraw && removed) {
    this.resetViewport();
    this.redraw();
    return true;
  } else {
   return false;
  }
};


/**
 * Removes an array of markers from the cluster.
 *
 * @param {Array.<google.maps.Marker>} markers The markers to remove.
 * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
 */
MarkerClusterer.prototype.removeMarkers = function(markers, opt_nodraw) {
  var removed = false;

  for (var i = 0, marker; marker = markers[i]; i++) {
    var r = this.removeMarker_(marker);
    removed = removed || r;
  }

  if (!opt_nodraw && removed) {
    this.resetViewport();
    this.redraw();
    return true;
  }
};


/**
 * Sets the clusterer's ready state.
 *
 * @param {boolean} ready The state.
 * @private
 */
MarkerClusterer.prototype.setReady_ = function(ready) {
  if (!this.ready_) {
    this.ready_ = ready;
    this.createClusters_();
  }
};


/**
 * Returns the number of clusters in the clusterer.
 *
 * @return {number} The number of clusters.
 */
MarkerClusterer.prototype.getTotalClusters = function() {
  return this.clusters_.length;
};


/**
 * Returns the google map that the clusterer is associated with.
 *
 * @return {google.maps.Map} The map.
 */
MarkerClusterer.prototype.getMap = function() {
  return this.map_;
};


/**
 * Sets the google map that the clusterer is associated with.
 *
 * @param {google.maps.Map} map The map.
 */
MarkerClusterer.prototype.setMap = function(map) {
  this.map_ = map;
};


/**
 * Returns the size of the grid.
 *
 * @return {number} The grid size.
 */
MarkerClusterer.prototype.getGridSize = function() {
  return this.gridSize_;
};


/**
 * Sets the size of the grid.
 *
 * @param {number} size The grid size.
 */
MarkerClusterer.prototype.setGridSize = function(size) {
  this.gridSize_ = size;
};


/**
 * Returns the min cluster size.
 *
 * @return {number} The grid size.
 */
MarkerClusterer.prototype.getMinClusterSize = function() {
  return this.minClusterSize_;
};

/**
 * Sets the min cluster size.
 *
 * @param {number} size The grid size.
 */
MarkerClusterer.prototype.setMinClusterSize = function(size) {
  this.minClusterSize_ = size;
};


/**
 * Extends a bounds object by the grid size.
 *
 * @param {google.maps.LatLngBounds} bounds The bounds to extend.
 * @return {google.maps.LatLngBounds} The extended bounds.
 */
MarkerClusterer.prototype.getExtendedBounds = function(bounds) {
  var projection = this.getProjection();

  // Turn the bounds into latlng.
  var tr = new google.maps.LatLng(bounds.getNorthEast().lat(),
      bounds.getNorthEast().lng());
  var bl = new google.maps.LatLng(bounds.getSouthWest().lat(),
      bounds.getSouthWest().lng());

  // Convert the points to pixels and the extend out by the grid size.
  var trPix = projection.fromLatLngToDivPixel(tr);
  trPix.x += this.gridSize_;
  trPix.y -= this.gridSize_;

  var blPix = projection.fromLatLngToDivPixel(bl);
  blPix.x -= this.gridSize_;
  blPix.y += this.gridSize_;

  // Convert the pixel points back to LatLng
  var ne = projection.fromDivPixelToLatLng(trPix);
  var sw = projection.fromDivPixelToLatLng(blPix);

  // Extend the bounds to contain the new bounds.
  bounds.extend(ne);
  bounds.extend(sw);

  return bounds;
};


/**
 * Determins if a marker is contained in a bounds.
 *
 * @param {google.maps.Marker} marker The marker to check.
 * @param {google.maps.LatLngBounds} bounds The bounds to check against.
 * @return {boolean} True if the marker is in the bounds.
 * @private
 */
MarkerClusterer.prototype.isMarkerInBounds_ = function(marker, bounds) {
  return bounds.contains(marker.getPosition());
};


/**
 * Clears all clusters and markers from the clusterer.
 */
MarkerClusterer.prototype.clearMarkers = function() {
  this.resetViewport(true);

  // Set the markers a empty array.
  this.markers_ = [];
};


/**
 * Clears all existing clusters and recreates them.
 * @param {boolean} opt_hide To also hide the marker.
 */
MarkerClusterer.prototype.resetViewport = function(opt_hide) {
  // Remove all the clusters
  for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
    cluster.remove();
  }

  // Reset the markers to not be added and to be invisible.
  for (var i = 0, marker; marker = this.markers_[i]; i++) {
    marker.isAdded = false;
    if (opt_hide) {
      marker.setMap(null);
    }
  }

  this.clusters_ = [];
};

/**
 *
 */
MarkerClusterer.prototype.repaint = function() {
  var oldClusters = this.clusters_.slice();
  this.clusters_.length = 0;
  this.resetViewport();
  this.redraw();

  // Remove the old clusters.
  // Do it in a timeout so the other clusters have been drawn first.
  window.setTimeout(function() {
    for (var i = 0, cluster; cluster = oldClusters[i]; i++) {
      cluster.remove();
    }
  }, 0);
};


/**
 * Redraws the clusters.
 */
MarkerClusterer.prototype.redraw = function() {
  this.createClusters_();
};


/**
 * Calculates the distance between two latlng locations in km.
 * @see http://www.movable-type.co.uk/scripts/latlong.html
 *
 * @param {google.maps.LatLng} p1 The first lat lng point.
 * @param {google.maps.LatLng} p2 The second lat lng point.
 * @return {number} The distance between the two points in km.
 * @private
*/
MarkerClusterer.prototype.distanceBetweenPoints_ = function(p1, p2) {
  if (!p1 || !p2) {
    return 0;
  }

  var R = 6371; // Radius of the Earth in km
  var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
  var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
  var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
    Math.sin(dLon / 2) * Math.sin(dLon / 2);
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  var d = R * c;
  return d;
};


/**
 * Add a marker to a cluster, or creates a new cluster.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @private
 */
MarkerClusterer.prototype.addToClosestCluster_ = function(marker) {
  var distance = 40000; // Some large number
  var clusterToAddTo = null;
  var pos = marker.getPosition();
  for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
    var center = cluster.getCenter();
    if (center) {
      var d = this.distanceBetweenPoints_(center, marker.getPosition());
      if (d < distance) {
        distance = d;
        clusterToAddTo = cluster;
      }
    }
  }

  if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
    clusterToAddTo.addMarker(marker);
  } else {
    var cluster = new Cluster(this);
    cluster.addMarker(marker);
    this.clusters_.push(cluster);
  }
};


/**
 * Creates the clusters.
 *
 * @private
 */
MarkerClusterer.prototype.createClusters_ = function() {
  if (!this.ready_) {
    return;
  }

  // Get our current map view bounds.
  // Create a new bounds object so we don't affect the map.
  var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(),
      this.map_.getBounds().getNorthEast());
  var bounds = this.getExtendedBounds(mapBounds);

  for (var i = 0, marker; marker = this.markers_[i]; i++) {
    if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) {
      this.addToClosestCluster_(marker);
    }
  }
};


/**
 * A cluster that contains markers.
 *
 * @param {MarkerClusterer} markerClusterer The markerclusterer that this
 *     cluster is associated with.
 * @constructor
 * @ignore
 */
function Cluster(markerClusterer) {
  this.markerClusterer_ = markerClusterer;
  this.map_ = markerClusterer.getMap();
  this.gridSize_ = markerClusterer.getGridSize();
  this.minClusterSize_ = markerClusterer.getMinClusterSize();
  this.averageCenter_ = markerClusterer.isAverageCenter();
  this.center_ = null;
  this.markers_ = [];
  this.bounds_ = null;
  this.clusterIcon_ = new ClusterIcon(this, markerClusterer.getStyles(),
      markerClusterer.getGridSize());
}

/**
 * Determins if a marker is already added to the cluster.
 *
 * @param {google.maps.Marker} marker The marker to check.
 * @return {boolean} True if the marker is already added.
 */
Cluster.prototype.isMarkerAlreadyAdded = function(marker) {
  if (this.markers_.indexOf) {
    return this.markers_.indexOf(marker) != -1;
  } else {
    for (var i = 0, m; m = this.markers_[i]; i++) {
      if (m == marker) {
        return true;
      }
    }
  }
  return false;
};


/**
 * Add a marker the cluster.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @return {boolean} True if the marker was added.
 */
Cluster.prototype.addMarker = function(marker) {
  if (this.isMarkerAlreadyAdded(marker)) {
    return false;
  }

  if (!this.center_) {
    this.center_ = marker.getPosition();
    this.calculateBounds_();
  } else {
    if (this.averageCenter_) {
      var l = this.markers_.length + 1;
      var lat = (this.center_.lat() * (l-1) + marker.getPosition().lat()) / l;
      var lng = (this.center_.lng() * (l-1) + marker.getPosition().lng()) / l;
      this.center_ = new google.maps.LatLng(lat, lng);
      this.calculateBounds_();
    }
  }

  marker.isAdded = true;
  this.markers_.push(marker);

  var len = this.markers_.length;
  if (len < this.minClusterSize_ && marker.getMap() != this.map_) {
    // Min cluster size not reached so show the marker.
    marker.setMap(this.map_);
  }

  if (len == this.minClusterSize_) {
    // Hide the markers that were showing.
    for (var i = 0; i < len; i++) {
      this.markers_[i].setMap(null);
    }
  }

  if (len >= this.minClusterSize_) {
    marker.setMap(null);
  }

  this.updateIcon();
  return true;
};


/**
 * Returns the marker clusterer that the cluster is associated with.
 *
 * @return {MarkerClusterer} The associated marker clusterer.
 */
Cluster.prototype.getMarkerClusterer = function() {
  return this.markerClusterer_;
};


/**
 * Returns the bounds of the cluster.
 *
 * @return {google.maps.LatLngBounds} the cluster bounds.
 */
Cluster.prototype.getBounds = function() {
  var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
  var markers = this.getMarkers();
  for (var i = 0, marker; marker = markers[i]; i++) {
    bounds.extend(marker.getPosition());
  }
  return bounds;
};


/**
 * Removes the cluster
 */
Cluster.prototype.remove = function() {
  this.clusterIcon_.remove();
  this.markers_.length = 0;
  delete this.markers_;
};


/**
 * Returns the center of the cluster.
 *
 * @return {number} The cluster center.
 */
Cluster.prototype.getSize = function() {
  return this.markers_.length;
};


/**
 * Returns the center of the cluster.
 *
 * @return {Array.<google.maps.Marker>} The cluster center.
 */
Cluster.prototype.getMarkers = function() {
  return this.markers_;
};


/**
 * Returns the center of the cluster.
 *
 * @return {google.maps.LatLng} The cluster center.
 */
Cluster.prototype.getCenter = function() {
  return this.center_;
};


/**
 * Calculated the extended bounds of the cluster with the grid.
 *
 * @private
 */
Cluster.prototype.calculateBounds_ = function() {
  var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
  this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds);
};


/**
 * Determines if a marker lies in the clusters bounds.
 *
 * @param {google.maps.Marker} marker The marker to check.
 * @return {boolean} True if the marker lies in the bounds.
 */
Cluster.prototype.isMarkerInClusterBounds = function(marker) {
  return this.bounds_.contains(marker.getPosition());
};


/**
 * Returns the map that the cluster is associated with.
 *
 * @return {google.maps.Map} The map.
 */
Cluster.prototype.getMap = function() {
  return this.map_;
};


/**
 * Updates the cluster icon
 */
Cluster.prototype.updateIcon = function() {
  var zoom = this.map_.getZoom();
  var mz = this.markerClusterer_.getMaxZoom();

  if (mz && zoom > mz) {
    // The zoom is greater than our max zoom so show all the markers in cluster.
    for (var i = 0, marker; marker = this.markers_[i]; i++) {
      marker.setMap(this.map_);
    }
    return;
  }

  if (this.markers_.length < this.minClusterSize_) {
    // Min cluster size not yet reached.
    this.clusterIcon_.hide();
    return;
  }

  var numStyles = this.markerClusterer_.getStyles().length;
  var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles);
  this.clusterIcon_.setCenter(this.center_);
  this.clusterIcon_.setSums(sums);
  this.clusterIcon_.show();
};


/**
 * A cluster icon
 *
 * @param {Cluster} cluster The cluster to be associated with.
 * @param {Object} styles An object that has style properties:
 *     'url': (string) The image url.
 *     'height': (number) The image height.
 *     'width': (number) The image width.
 *     'anchor': (Array) The anchor position of the label text.
 *     'textColor': (string) The text color.
 *     'textSize': (number) The text size.
 *     'backgroundPosition: (string) The background postition x, y.
 * @param {number=} opt_padding Optional padding to apply to the cluster icon.
 * @constructor
 * @extends google.maps.OverlayView
 * @ignore
 */
function ClusterIcon(cluster, styles, opt_padding) {
  cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView);

  this.styles_ = styles;
  this.padding_ = opt_padding || 0;
  this.cluster_ = cluster;
  this.center_ = null;
  this.map_ = cluster.getMap();
  this.div_ = null;
  this.sums_ = null;
  this.visible_ = false;

  this.setMap(this.map_);
}


/**
 * Triggers the clusterclick event and zoom's if the option is set.
 */
ClusterIcon.prototype.triggerClusterClick = function() {
  var markerClusterer = this.cluster_.getMarkerClusterer();

  // Trigger the clusterclick event.
  google.maps.event.trigger(markerClusterer, 'clusterclick', this.cluster_);

  if (markerClusterer.isZoomOnClick()) {
    // Zoom into the cluster.
    this.map_.fitBounds(this.cluster_.getBounds());
  }
};


/**
 * Adding the cluster icon to the dom.
 * @ignore
 */
ClusterIcon.prototype.onAdd = function() {
  this.div_ = document.createElement('DIV');
  if (this.visible_) {
    var pos = this.getPosFromLatLng_(this.center_);
    this.div_.style.cssText = this.createCss(pos);
    this.div_.innerHTML = this.sums_.text;
  }

  var panes = this.getPanes();
  panes.overlayMouseTarget.appendChild(this.div_);

  var that = this;
  google.maps.event.addDomListener(this.div_, 'click', function(ev) {
	  ev.preventDefault();
	  ev.stopPropagation();
    that.triggerClusterClick();
  });
};


/**
 * Returns the position to place the div dending on the latlng.
 *
 * @param {google.maps.LatLng} latlng The position in latlng.
 * @return {google.maps.Point} The position in pixels.
 * @private
 */
ClusterIcon.prototype.getPosFromLatLng_ = function(latlng) {
  var pos = this.getProjection().fromLatLngToDivPixel(latlng);
  pos.x -= parseInt(this.width_ / 2, 10);
  pos.y -= parseInt(this.height_ / 2, 10);
  return pos;
};


/**
 * Draw the icon.
 * @ignore
 */
ClusterIcon.prototype.draw = function() {
  if (this.visible_) {
    var pos = this.getPosFromLatLng_(this.center_);
    this.div_.style.top = pos.y + 'px';
    this.div_.style.left = pos.x + 'px';
  }
};


/**
 * Hide the icon.
 */
ClusterIcon.prototype.hide = function() {
  if (this.div_) {
    this.div_.style.display = 'none';
  }
  this.visible_ = false;
};


/**
 * Position and show the icon.
 */
ClusterIcon.prototype.show = function() {
  if (this.div_) {
    var pos = this.getPosFromLatLng_(this.center_);
    this.div_.style.cssText = this.createCss(pos);
    this.div_.style.display = '';
  }
  this.visible_ = true;
};


/**
 * Remove the icon from the map
 */
ClusterIcon.prototype.remove = function() {
  this.setMap(null);
};


/**
 * Implementation of the onRemove interface.
 * @ignore
 */
ClusterIcon.prototype.onRemove = function() {
  if (this.div_ && this.div_.parentNode) {
    this.hide();
    this.div_.parentNode.removeChild(this.div_);
    this.div_ = null;
  }
};


/**
 * Set the sums of the icon.
 *
 * @param {Object} sums The sums containing:
 *   'text': (string) The text to display in the icon.
 *   'index': (number) The style index of the icon.
 */
ClusterIcon.prototype.setSums = function(sums) {
  this.sums_ = sums;
  this.text_ = sums.text;
  this.index_ = sums.index;
  if (this.div_) {
    this.div_.innerHTML = sums.text;
  }

  this.useStyle();
};


/**
 * Sets the icon to the the styles.
 */
ClusterIcon.prototype.useStyle = function() {
  var index = Math.max(0, this.sums_.index - 1);
  index = Math.min(this.styles_.length - 1, index);
  var style = this.styles_[index];
  this.url_ = style['url'];
  this.height_ = style['height'];
  this.width_ = style['width'];
  this.textColor_ = style['textColor'];
  this.anchor_ = style['anchor'];
  this.textSize_ = style['textSize'];
  this.backgroundPosition_ = style['backgroundPosition'];
};


/**
 * Sets the center of the icon.
 *
 * @param {google.maps.LatLng} center The latlng to set as the center.
 */
ClusterIcon.prototype.setCenter = function(center) {
  this.center_ = center;
};


/**
 * Create the css text based on the position of the icon.
 *
 * @param {google.maps.Point} pos The position.
 * @return {string} The css style text.
 */
ClusterIcon.prototype.createCss = function(pos) {
  var style = [];
  style.push('background-image:url(' + this.url_ + ');');
  var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0';
  style.push('background-position:' + backgroundPosition + ';');

  if (typeof this.anchor_ === 'object') {
    if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 &&
        this.anchor_[0] < this.height_) {
      style.push('height:' + (this.height_ - this.anchor_[0]) +
          'px; padding-top:' + this.anchor_[0] + 'px;');
    } else {
      style.push('height:' + this.height_ + 'px; line-height:' + this.height_ +
          'px;');
    }
    if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 &&
        this.anchor_[1] < this.width_) {
      style.push('width:' + (this.width_ - this.anchor_[1]) +
          'px; padding-left:' + this.anchor_[1] + 'px;');
    } else {
      style.push('width:' + this.width_ + 'px; text-align:center;');
    }
  } else {
    style.push('height:' + this.height_ + 'px; line-height:' +
        this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;');
  }

  var txtColor = this.textColor_ ? this.textColor_ : 'black';
  var txtSize = this.textSize_ ? this.textSize_ : 11;

  style.push('cursor:pointer; top:' + pos.y + 'px; left:' +
      pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' +
      txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold');
  return style.join('');
};
// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/maps/RouteRenderer.js?ver=2.8.0 
function CMMRM_RouteRenderer(widget, routeModel) {
	
	this.widget = widget;
	this.routeModel = routeModel;
	this.polylineCache = {};
	this.polylines = [];
	
	this.renderPolylines();
	this.locationRenderers = this.renderLocations();
	
	var that = this;
	
	jQuery(this.routeModel).bind('RouteModel:setTravelMode', function() {
		that.renderPolylines();
	});
	
	jQuery(this.routeModel).bind('RouteModel:setWaypointsString', function() {
		that.renderPolylines();
	});
	
	setTimeout(function() {
		that.widget.map.extendBounds(that.routeModel.getBounds()).center();
	}, 500);
	
	jQuery(this).trigger('RouteRenderer:ready');
	
}


CMMRM_RouteRenderer.prototype.renderLocations = function() {
	var locations = this.routeModel.getLocations();
	var renderers = [];
//	var markers = [];
	for (var i=0; i<locations.length; i++) {
		var renderer = new (this.widget.resolve('LocationRenderer'))(this.widget, locations[i]);
		renderers.push(renderer);
//		markers.push(renderer.getMarker());
	}
	
	return renderers;
	
};



CMMRM_RouteRenderer.prototype.renderPolylines = function() {
	var that = this;
//	console.log('renderPolylines')
	var waypointsCoords = this.routeModel.getWaypointsCoords();
	if (waypointsCoords.length == 0) return;
	var request = new CMMRM_RequestTrail(this.routeModel.getTravelMode(), waypointsCoords);
	request.run(this, function(response, status) {
		if (status !== google.maps.DirectionsStatus.OK) {
			var errorMsg = request.getDirectionErrorMessage(status);
			window.CMMRM.Utils.toast(errorMsg, null, Math.ceil(errorMsg.length/10));
			console.log(status);
			console.log(response);
		} else {
//			console.log(response);
			that.removeTrailPolylines();
			that.polylines = request.createTrailPolylines(that.widget.map.map, that.routeModel.getPathColor(), that.routeModel.showDirectionalArrows());
			that.routeModel.setPolylineString(response.routes[0].overview_polyline);
			jQuery(that).trigger('RouteRenderer:trailRequestSuccess', {request: request});
		}
	});
};


CMMRM_RouteRenderer.prototype.removeTrailPolylines = function() {
	for (var i=0; i<this.polylines.length; i++) {
		this.polylines[i].setMap(null);
	}
	this.polylines = [];
};


CMMRM_RouteRenderer.prototype.getLocationRenderers = function() {
	return this.locationRenderers;
};
// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/maps/LocationModel.js?ver=2.8.0 
function CMMRM_LocationModel(data, routeModel) {
	this.data = data;
	this.routeModel = routeModel;
	jQuery(this).trigger('LocationModel:ready');
}

CMMRM_LocationModel.prototype.getId = function() {
	return this.data.id;
};

CMMRM_LocationModel.prototype.getLat = function() {
	return this.data.lat;
};

CMMRM_LocationModel.prototype.getLng = function() {
	return this.data.lng;
};

CMMRM_LocationModel.prototype.getName = function() {
	return this.data.name;
};


CMMRM_LocationModel.prototype.getPosition = function() {
	return [this.getLat(), this.getLng()];
};

CMMRM_LocationModel.prototype.getGoogleLatLng = function() {
	return new google.maps.LatLng(this.getLat(), this.getLng());
};

CMMRM_LocationModel.prototype.setPosition = function(lat, lng) {
	this.data.lat = lat;
	this.data.lng = lng;
	jQuery(this).trigger('LocationModel:setPosition', {lat: lat, lng: lng});
	return this;
};

CMMRM_LocationModel.prototype.remove = function() {
	jQuery(this).trigger('LocationModel:remove');
};

CMMRM_LocationModel.prototype.getRoute = function() {
	return this.routeModel;
};

CMMRM_LocationModel.prototype.getIcon = function() {
	return this.data.icon;
};


CMMRM_LocationModel.prototype.getIconSize = function() {
	return this.data.iconSize;
};


CMMRM_LocationModel.prototype.getAddress = function() {
	return this.data.address;
};

CMMRM_LocationModel.prototype.setAddress = function(address) {
	this.data.address = address;
	jQuery(this).trigger('LocationModel:setAddress', {address: address});
	return this;
};

CMMRM_LocationModel.prototype.getDescription = function() {
	return this.data.description;
};

CMMRM_LocationModel.prototype.getImages = function() {
	if (typeof this.data.images == 'object') {
		return this.data.images;
	} else {
		return [];
	}
};

CMMRM_LocationModel.prototype.getInfoWindowContent = function() {
	return this.data.infoWindowContent;
};
// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/maps/LocationRenderer.js?ver=2.8.0 
function CMMRM_LocationRenderer(widget, locationModel) {
	
	this.widget = widget;
	this.locationModel = locationModel;
	
	this.marker = this.createMarker();
	
	var that = this;
	
	jQuery(this.locationModel).bind('LocationModel:remove', function() {
		that.remove();
	});
	
	jQuery(this).trigger('LocationRenderer:ready');
	
}

CMMRM_LocationRenderer.prototype.createMarker = function() {
	return new CMMRM_Marker(this.widget.map,
			this.locationModel.getGoogleLatLng(),
			this.getMarkerIconOptions(),
			this.getMarkerLabelOptions()
		);
};


CMMRM_LocationRenderer.prototype.getMarkerIconOptions = function() {
	return {draggable: false, style: 'cursor:pointer;', icon: this.locationModel.getIcon(), iconSize: this.locationModel.getIconSize()};
};


CMMRM_LocationRenderer.prototype.getMarkerLabelOptions = function() {
	return {text: this.getLabelText(), style: 'cursor:pointer;'};
};


CMMRM_LocationRenderer.prototype.getLabelText = function() {
	return this.locationModel.getName();
};


CMMRM_LocationRenderer.prototype.remove = function() {
	this.marker.setMap(null);
};


CMMRM_LocationRenderer.prototype.getMarker = function() {
	return this.marker;
};
// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/maps/WaypointModel.js?ver=2.8.0 
function CMMRM_WaypointModel(data, routeModel) {
	this.data = data;
	this.routeModel = routeModel;
}

CMMRM_WaypointModel.prototype.getLat = function() {
	return this.data[0];
};

CMMRM_WaypointModel.prototype.getLng = function() {
	return this.data[1];
};

CMMRM_WaypointModel.prototype.getPosition = function() {
	return [this.getLat(), this.getLng()];
};

CMMRM_WaypointModel.prototype.getGoogleLatLng = function() {
	return new google.maps.LatLng(this.getLat(), this.getLng());
};

CMMRM_WaypointModel.prototype.setPosition = function(lat, lng) {
	this.data[0] = lat;
	this.data[1] = lng;
	jQuery(this).trigger('WaypointModel:setPosition', {lat: lat, lng: lng});
	return this;
};

CMMRM_WaypointModel.prototype.remove = function() {
	jQuery(this).trigger('WaypointModel:remove');
};

CMMRM_WaypointModel.prototype.getRoute = function() {
	return this.routeModel;
};
// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/maps/WaypointRenderer.js?ver=2.8.0 
function CMMRM_WaypointRenderer(widget, coords, index) {
	
	this.index = index;
	this.widget = widget;
	this.coords = coords;
	this.marker = this.createMarker();
	
	var that = this;
	
	google.maps.event.addListener(this.marker, 'dragend', function() {
		var pos = that.marker.getPosition();
		that.updatePosition(pos);
//	    that.waypointModel.setPosition(pos.lat(), pos.lng());
	});
	
	google.maps.event.addListener(this.marker, 'click', function(ev) {
		that.remove();
	});
	
//	google.maps.event.addListener(this.marker, 'rightclick', function(ev) {
//		var pos = that.marker.getPosition();
//		that.widget.addLocation(pos.lat(), pos.lng());
//		that.waypointModel.remove();
//	});
	
	jQuery(this.waypointModel).bind('WaypointModel:remove', function() {
		that.remove();
		that.widget.routeModel.updateWaypointsString();
		that.widget.routeRenderer.renderPolylines();
	});
	
//	this.widget.routeRenderer.renderPolylines();
	
	jQuery(this).trigger('WaypointRenderer:ready');
	
}


CMMRM_WaypointRenderer.prototype.createMarker = function() {
	var markerImage = new google.maps.MarkerImage('https://maps.gstatic.com/mapfiles/dd-via.png',
		    new google.maps.Size(11, 11), //size
		    new google.maps.Point(0, 0), //origin point
		    new google.maps.Point(6, 5)); // offset point
	var marker = new google.maps.Marker({
		position: this.coords,
		map: this.widget.map.map,
		icon: markerImage,
		draggable: true,
	});
	return marker;
};


CMMRM_WaypointRenderer.prototype.remove = function() {
	this.marker.setMap(null);
	jQuery(this).trigger('WaypointRenderer:remove');
};


CMMRM_WaypointRenderer.prototype.updatePosition = function(coords) {
	this.coords = coords;
	jQuery(this).trigger('WaypointRenderer:updatePosition', {coords: coords});
};



CMMRM_WaypointRenderer.prototype.getWaypointCoords = function() {
	return this.coords;
};

CMMRM_WaypointRenderer.prototype.getWaypointIndex = function() {
	return this.index;
};

CMMRM_WaypointRenderer.prototype.setWaypointIndex = function(index) {
	this.index = index;
	return this.index;
};
// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/utils.js?ver=2.8.0 
(function($) {

window.CMMRM = {};
window.CMMRM.Utils = {
		
	addSingleHandler: function(handlerName, selector, action, func) {
		var obj;
		if (typeof selector == 'string') obj = $(selector);
		else obj = selector;
		obj.each(function() {
			var obj = $(this);
			if (obj.data(handlerName) != '1') {
				obj.data(handlerName, '1');
				obj.on(action, func);
			}
		});
	},
	
	leftClick: function(func) {
		return function(e) {
			// Allow to use middle-button to open thread in a new tab:
			if (e.which > 1 || e.shiftKey || e.altKey || e.metaKey || e.ctrlKey) return;
			func.apply(this, [e]);
			return false;
		}
	},
	
	
	toast: function(msg, className, duration) {
		if (typeof className != 'string') className = 'info';
		if (typeof duration == 'undefined') duration = 5;
		var toast = $('<div/>', {"class":"cmmrm-toast "+ className, "style":"display:none"});
		toast.text(msg);
		$('body').append(toast);
		toast.fadeIn(500, function() {
			setTimeout(function() {
				toast.fadeOut(500);
			}, duration*1000);
		});
	}
		
};


$('.cmmrm-delete-confirm').click(function(ev) {
	if (!confirm(CMMRM_Utils.deleteConfirmText)) {
		ev.stopPropagation();
		ev.preventDefault();
	}
});


})(jQuery);
// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/maps/Marker.js?ver=2.8.0 
CMMRM_Marker.prototype = new google.maps.OverlayView();

function CMMRM_Marker(mapObj, position, markerOptions, labelOptions) {

	this.mapObj = mapObj;
	var map = mapObj.map;

	if (typeof markerOptions != 'object')
		markerOptions = {};
	if (typeof markerOptions.color == 'undefined')
		markerOptions.color = '#ff6666';
	if (typeof markerOptions.style == 'undefined')
		markerOptions.style = '';
	if (typeof markerOptions.draggable == 'undefined')
		markerOptions.draggable = false;
	if (typeof markerOptions.icon != 'string')
		markerOptions.icon = '';
	if (typeof markerOptions.title != 'string')
		markerOptions.title = '';

	if (typeof labelOptions != 'object')
		labelOptions = {};
	if (typeof labelOptions.style == 'undefined')
		labelOptions.style = '';
	if (typeof labelOptions.text == 'undefined')
		labelOptions.text = '';

	this.set('position', position);
	this._labelOptions = labelOptions;
	this._markerOptions = markerOptions;
	this.setMap(map);
	
	this._createContainer();

};


CMMRM_Marker.prototype._createContainer = function() {
	var that = this;
	var container = document.createElement('div');
	google.maps.event.addDomListener(container, 'click', function(ev) {
		google.maps.event.trigger(that, 'click', ev);
	});
	google.maps.event.addDomListener(container, 'mouseenter', function(ev) {
		google.maps.event.trigger(that, 'mouseenter', ev);
	});
	google.maps.event.addDomListener(container, 'mouseleave', function(ev) {
		google.maps.event.trigger(that, 'mouseleave', ev);
	});
	this.set('container', container);
	return container;
};


/**
 * onAdd is called when the map's panes are ready and the overlay has been added
 * to the map.
 */
CMMRM_Marker.prototype.onAdd = function() {

	var container = this.getContainer();
	container.style.position = 'absolute';
	container.draggable = true;
	
	var markerHTML;
	
	if (this._markerOptions.icon.length > 0) {
		var iconUrl = this._markerOptions.icon;
		if (iconUrl.substr(0, 4) != 'http' && iconUrl.substr(0, 2) != '//') {
			iconUrl = 'https://maps.google.com/mapfiles/kml/shapes/' + iconUrl +'.png';
		}
		var height = 40;
		var size = 'normal';
		if (typeof this._markerOptions.iconSize == 'string' && this._markerOptions.iconSize.length > 0) {
			size = this._markerOptions.iconSize;
		}
		markerHTML = '<img src="'+ iconUrl +'" class="cmmrm-marker-icon-size-'+ size +'" style="position:relative;" />';
	} else {
		markerHTML = '<div style="z-index:200"><div class="cmmrm-pin" style="background:'
			+ this._markerOptions.color + '"></div>'
			+ '<div class="cmmrm-pin-triangle" style="border-top-color:'
			+ this._markerOptions.color + '"></div>'
			+ '<div class="cmmrm-pin-dot"></div></div>';
	}
	
	container.innerHTML = '<div class="cmmrm-custom-marker" style="'
			+ this._markerOptions.style
			+ '">'+ markerHTML +'</div>';
	if (this._labelOptions.text.length > 0) {
		var labelLeft = 12 - this.getTextWidth(
				this._labelOptions.text, 10);
		container.innerHTML += '<div class="cmmrm-map-label" style="left:'
				+ labelLeft + 'px;z-index:300;' + this._labelOptions.style + '">'
				+ this._labelOptions.text + '</div>';
	}
	
	if (this._markerOptions.draggable) {
		this.setDragEvents(container);
	}

//	console.log(this._markerOptions);
	if (typeof this._markerOptions.title == 'string' && this._markerOptions.title.length > 0) {
		container.title = this._markerOptions.title;
	}
//	this.set('container', container)
	this.getPanes().floatPane.appendChild(container);
	
};


CMMRM_Marker.prototype.getTextWidth = function(text, fontSize) {
	var narrow = '1tiIfjJl';
	var wide = 'WODGKXZBM';
	var result = 0;
	for (var i=0; i<text.length; i++) {
		var letter = text.substr(i, 1);
		var rate = 1.0 + (0.5*(wide.indexOf(letter) >= 0 ? 1 : 0)) - (0.5*(narrow.indexOf(letter) >= 0 ? 1 : 0));
//		console.log(letter +' : '+ rate);
		result += rate;
	}
	return result * fontSize*0.7/2;
};




CMMRM_Marker.prototype.setDragEvents = function(container) {

	var dragging = false;
	var that = this;

	google.maps.event.addDomListener(this.get('map').getDiv(), 'mouseleave',
			function() {
				google.maps.event.trigger(container, 'mouseup');
			});

	google.maps.event
			.addDomListener(
					container,
					'mousedown',
					function(e) {
//						console.log('mousedown');
						that.mapObj.suspendAddWaypoints = true;
						dragging = true;
						this.style.cursor = 'move';
						that.map.set('draggable', false);
						that.set('origin', e);

						that.moveHandler = google.maps.event
								.addDomListener(
										that.get('map').getDiv(),
										'mousemove',
										function(e) {
											var origin = that.get('origin')
											var left = origin.clientX - e.clientX;
											var top = origin.clientY - e.clientY;
											var pos = that.getProjection().fromLatLngToDivPixel(that.get('position'));
											var latLng = that.getProjection().fromDivPixelToLatLng(new google.maps.Point(pos.x - left, pos.y - top));
											that.set('origin', e);
											that.set('position', latLng);
											that.draw();
										});

					});

	google.maps.event.addDomListener(container, 'mouseup', function(ev) {
//		console.log('mouseup');
		if (ev) {
			if (ev.preventDefault) {
				ev.preventDefault();
			}
			ev.cancelBubble = true;
			if (ev.stopPropagation) {
				ev.stopPropagation();
			}
		}
		if (that.map) {
			that.map.set('draggable', true);
		}
		this.style.cursor = 'default';
		google.maps.event.removeListener(that.moveHandler);
		google.maps.event.removeListener(that.clickHandler);
		if (dragging) {
			google.maps.event.trigger(that, 'dragend');
			google.maps.event.trigger(that, 'positionUpdated');
		}
		dragging = false;
		setTimeout(function() {
			that.mapObj.suspendAddWaypoints = false;
		}, 500);
	});


};



CMMRM_Marker.prototype.draw = function() {
	var pos = this.getProjection().fromLatLngToDivPixel(this.get('position'));
	this.get('container').style.left = (pos.x - 11) + 'px';
	this.get('container').style.top = (pos.y - 30 - 12) + 'px';
	return this;
};

CMMRM_Marker.prototype.onRemove = function() {
	this.get('container').parentNode.removeChild(this.get('container'));
	this.set('container', null)
};

CMMRM_Marker.prototype.getPosition = function() {
	return this.get('position');
};

CMMRM_Marker.prototype.setPosition = function(pos) {
	this.set('position', pos);
	this.draw();
	google.maps.event.trigger(this, 'positionUpdated');
	return this;
};


CMMRM_Marker.prototype.getContainer = function() {
	var container = this.get('container');
	if (!container) {
		container = this._createContainer();
	}
	return container;
};
// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/maps/ElevationGraph.js?ver=2.8.0 
function CMMRM_ElevationGraph(widget, routeModel) {
	
	this.widget = widget;
	this.routeModel = routeModel;
	this.results = null;
	
	this.maxElevation = 0;
	this.minElevation = 99999;
	this.elevationGain = 0;
	this.elevationDescent = 0;
	
	this.graph = null;
	this.graphData = null;
	
	if (this.routeModel.getTravelMode() == 'DIRECT') {
		this.calculateElevationAlongPath(this.routeModel.getWaypointsCoords());
		this.initPolylineMouseEventListeners();
	}
	
	var that = this;
	var timeout = null;
	jQuery(this.routeModel).bind('RouteModel:setPolylineString', function() {
		var route = this;
		clearTimeout(timeout);
		timeout = setTimeout(function() {
			that.calculateElevationAlongPath(route.getPolylineCoords());
		}, 500);
	});
	
	jQuery(widget.routeRenderer).bind('RouteRenderer:trailRequestSuccess', function() {
		that.initPolylineMouseEventListeners();
	});
	
}


CMMRM_ElevationGraph.prototype.calculateElevationAlongPath = function(path) {
	
	if (path.length < 2) return;
	
	var elevator = new google.maps.ElevationService;
	var that = this;
//	var dist = 0;
//	dist = this.widget.map.calculateDistance(path[0], path[path.length-1]);
	var samples = 450; //Math.min(450, Math.max(2, Math.floor(dist/5)));
//	console.log('dist = '+ dist + ' samples = '+ samples);
	path = this.reduceCoordsNumber(path, samples);
	
	elevator.getElevationAlongPath({
		'path': path,
		'samples': samples,
	  }, function(results, status) {
//		  console.log(results);
		  that.results = results;
		  that.status = status;
		  that.processElevationResults(results, status);
	  });
};


CMMRM_ElevationGraph.prototype.reduceCoordsNumber = function(path, max) {
//	console.log('path.lengt = ', path.length);
//	console.log('max = ', max);
	if (path.length < max) return path;
	var result = [];
	var i = 0;
	var step = path.length/max;
	while (i < path.length) {
		result.push(path[Math.floor(i)]);
		i += step;
	}
//	console.log('result.len = ', result.length);
	return result;
};



CMMRM_ElevationGraph.prototype.processElevationResults = function(results, status) {
	if (status !== google.maps.ElevationStatus.OK) {
		console.error('[CMMRM_ElevationGraph] Elevation service failed due to: ' + status);
		return;
	}
	
	this.maxElevation = 0;
	this.minElevation = 99999;
	this.elevationGain = 0;
	this.elevationDescent = 0;
	
	var prev = null;
	for (var i=0; i<results.length; i++) {
		var elevation = results[i].elevation;
		if (elevation > this.maxElevation) {
			this.maxElevation = elevation;
		}
		if (elevation < this.minElevation) {
			this.minElevation = elevation;
		}
//		console.log('elev '+ elevation +' --- '+(elevation-prev));
		if (typeof prev == 'number') {
			if (elevation-prev > 0) {
				this.elevationGain += (elevation-prev);
			} else {
				this.elevationDescent += (prev-elevation);
			}
		}
		prev = elevation;
	}
	
	if (this.minElevation == 99999) {
		this.minElevation = 0;
	}
	
	this.showElevationGraph(results);
	jQuery(this).trigger('ElevationGraph:successResponse', {results: results});
	
};


CMMRM_ElevationGraph.prototype.showElevationGraph = function(elevations) {
//	console.log('showElevationGraph');
	var graphDiv = this.getGraphCanvasContainer();
//	console.log(this.widget);
	if (graphDiv.length == 0 || typeof google == 'undefined' || typeof google.visualization == 'undefined' || typeof google.visualization.ColumnChart == 'undefined') {
		console.error('[CMMRM_ElevationGraph] Missing library: google.visualization.ColumnChart');
		return;
	}
	var graph = new google.visualization.ColumnChart(graphDiv[0]);
	this.graph = graph;
	var data = new google.visualization.DataTable();
	this.graphData = data;
	var unit = ('feet' == CMMRM_Map_Settings.lengthUnits ? 'ft' : 'm');
//	data.addColumn('number', 'Sample');
	data.addColumn('number', 'Elevation');
	for (var i = 0; i < elevations.length; i++) {
		var num = elevations[i].elevation / (unit == 'ft' ? CMMRM_Map_Settings.feetToMeter : 1);
		data.addRow([num]);
	}
	graph.draw(data, {
	    height: 150,
	    legend: 'none',
	    titleY: 'Elevation ('+ unit +')',
	    crosshair: null,
	  });
	
	var marker = new google.maps.Marker({
//		position: new google.maps.LatLng(location.lat, location.long),
//		map: this.map,
		icon: 'https://maps.gstatic.com/mapfiles/dd-via.png',
		draggable: false,
	});
	
	var googleMapObj = this.widget.map.map;
	google.visualization.events.addListener(graph, 'onmouseover', function(ev) {
		if (typeof elevations[ev.row] != 'undefined') {
			marker.setMap(googleMapObj);
			marker.setPosition(elevations[ev.row].location);
		}
	});
	
	graphDiv.mouseout(function() {
		marker.setMap(null);
	});
	
};


CMMRM_ElevationGraph.prototype.getGraphCanvasContainer = function() {
	return jQuery(this.widget.getWidgetElement()).find('.cmmrm-elevation-graph-canvas');
};


CMMRM_ElevationGraph.prototype.getGraphWrapper = function() {
	return jQuery(this.widget.getWidgetElement()).find('.cmmrm-elevation-graph');
};


CMMRM_ElevationGraph.prototype.removeElevationGraph = function() {
	this.getGraphCanvasContainer().html('');
};


CMMRM_ElevationGraph.prototype.getMaxElevation = function() {
	return this.maxElevation;
};

CMMRM_ElevationGraph.prototype.getMinElevation = function() {
	return this.minElevation;
};

CMMRM_ElevationGraph.prototype.getElevationGain = function() {
	return this.elevationGain;
};

CMMRM_ElevationGraph.prototype.getElevationDescent = function() {
	return this.elevationDescent;
};


CMMRM_ElevationGraph.prototype.initPolylineMouseEventListeners = function() {
	
	// @TODO maybe in future
	return;
	
	var renderer = this.widget.routeRenderer;
	var polylines = renderer.polylines;
	var that = this;
	for (var i=0; i<polylines.length; i++) {
		google.maps.event.addListener(polylines[i], "mouseover", function(ev) {
			var polyline = polylines[i];
			
			// Find closest elevation result to the current location
			var closestResultIndex = that.findClosestResult(ev.latLng);
			if (closestResultIndex != null) {
				that.showCrosshair(closestResultIndex);
			}
			
		});
	}
};


CMMRM_ElevationGraph.prototype.findClosestResult = function(coords) {
	var minDistance = -1;
	var closestResultIndex = null;
	for (var i=0; i<this.results.length; i++) {
		var result = this.results[i];
		var d = CMMRM_GoogleMap.prototype.calculateDistance(coords, result.location);
		if (minDistance == -1 || d < minDistance) {
			minDistance = d;
			closestResultIndex = i;
		}
	}
	return closestResultIndex;
};


CMMRM_ElevationGraph.prototype.showCrosshair = function(columnIndex) {
	
	var cli = this.graph.getChartLayoutInterface();
    var chartArea = cli.getChartAreaBoundingBox();
    // "Zombies" is element #5.
    var wrapper = this.getGraphWrapper();
    
    var xDiv = jQuery('.cmmrm-elevation-graph-crosshair-x', wrapper);
    var x = Math.floor(cli.getXLocation(columnIndex)) - 10
    xDiv.css('left', x + "px");
    xDiv.show();
    
    var yDiv = jQuery('.cmmrm-elevation-graph-crosshair-y', wrapper);
    var y = Math.floor(cli.getYLocation(dataTable.getValue(columnIndex, 1))) - 50;
    yDiv.css('top', y + "px");
    yDiv.show();
    
	
};
// source --> https://geschichtenhaus.com/wp-content/plugins/cm-route-manager/asset/js/maps/Tooltip.js?ver=2.8.0 
CMMRM_Tooltip.prototype = new google.maps.OverlayView();

function CMMRM_Tooltip(widget, position, content, style) {

	this.widget = widget;
	this.content = content;
	this.position = position;
	
	this.offsetTop = 0;
	this.offsetLeft = 0;
	
	if (typeof style != 'object') style = {};
	this.style = style;
	
//	this.setMap(widget.map.map);

}

CMMRM_Tooltip.prototype.onAdd = function() {

//	console.log('CMMRM_Tooltip.prototype.onAdd');
	
	var div = document.createElement('div');
	div.setAttribute('class', 'cmmrm-map-tooltip');
	div.style.borderStyle = 'solid';
	div.style.borderColor = 'black';
	div.style.borderWidth = '1px';
	div.style.backgroundColor = '#ffff66';
	div.style.padding = '0.2em 0.5em';
	div.style.zIndex = '99999';
	div.style.position = 'absolute';
	
	for (var param in this.style) {
		div.style[param] = this.style[param];
	}
	
	div.style.color = this.getContrastColor(div.style.backgroundColor);
	
	div.innerHTML = this.content;

	this.div_ = div;

	// Add the element to the "overlayLayer" pane.
	var panes = this.getPanes();
	panes.overlayLayer.appendChild(div);
	
};

CMMRM_Tooltip.prototype.draw = function() {

//	console.log('CMMRM_Tooltip.prototype.draw');
	var pos = this.getProjection().fromLatLngToDivPixel(this.position);
	this.div_.style.left = (pos.x + this.offsetLeft) + 'px';
	this.div_.style.top = (pos.y + this.offsetTop) + 'px';
	return this;
	
};

// The onRemove() method will be called automatically from the API if
// we ever set the overlay's map property to 'null'.
CMMRM_Tooltip.prototype.onRemove = function() {
	this.div_.parentNode.removeChild(this.div_);
	this.div_ = null;
};


CMMRM_Tooltip.prototype.getContrastColor = function(color) {
    var d = 0;
    
    var hexToRgb = function(hex) {
	    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
	    return result ? {
	        R: parseInt(result[1], 16),
	        G: parseInt(result[2], 16),
	        B: parseInt(result[3], 16)
	    } : null;
	};
    
    var componentToHex = function(c) {
	    var hex = c.toString(16);
	    return hex.length == 1 ? "0" + hex : hex;
	};

	var rgbToHex = function(r, g, b) {
	    return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
	}
    
	if (color.substr(0, 1) == '#') {
		color = hexToRgb(color);
		if (!color) {
			return '#000000';
		}
	} else {
//		console.log(color);
		var result = color.match(/[0-9]+/g);
		if (result) {
			color = {R: result[0], G: result[1], B: result[2]};
		} else {
			return '#000000';
		}
	}

    // Counting the perceptive luminance - human eye favors green color... 
    var a = 1 - ( 0.299 * color.R + 0.587 * color.G + 0.114 * color.B)/255;

    if (a < 0.5)
       d = 0; // bright colors - black font
    else
       d = 255; // dark colors - white font

    return  rgbToHex(d, d, d);
    
};