GpsGate Server JavaScript API

Graphics  1.0.0

GpsGate Server JavaScript API > Graphics > gutil.js (source view)
Search:
 
Filters
/**
 * Copyright Franson Technology AB, Sweden, 2009
 * http://gpsgate.com, http://franson.com
 *
 * author Fredrik Blomqvist
 *
 * @module Map
 *
 */

var Franson = Franson || {};
/**
 * <p>namespace</p>
 * Google Map utilities
 *
 * todo: split in separate modules, many functions here are
 * perfect examples of functionality that could be added
 * as CustomScripts plugins.
 *
 * todo: rename Franson.GMap? (and put in a Franson.Map sub module?)
 *
 * @class Franson.GUtil
 * @static
 */
Franson.GUtil = Franson.GUtil || {};


MochiKit.Base.update(Franson.GUtil,
{
	/**
	 * coordinates of UL (origo) of the viewport relative to the overlay div.
	 * @method getDivPixelOffset
	 * @param {GMap2} map
	 * @return {(x,y)}
	 */
	getDivPixelOffset: function(map) // rename getViewportOffset/Position?
	{
		var center = map.fromLatLngToDivPixel(map.getCenter());
		var size = map.getSize();

		return { x: center.x - size.width / 2, y: center.y - size.height / 2 };
	},

	/**
	 * zoom control but without the panning arrows (does anyone use them?)
	 * (must always reside in top-left corner)
	 * @method addPartialZoomControl
	 * @param {GMap2} map
	 * @param {boolean} [smallControl=false]
	 */
	addPartialZoomControl: function(map, smallControl)
	{
		smallControl = smallControl || false;

		map.getContainer().style.overflow = 'hidden'; // should be safe to do(?)    //	MochiKit.Style.setStyle(map.getContainer(), { 'overflow': 'hidden' });
		if (smallControl)
		{
			map.addControl(new GSmallMapControl()
				, new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(-6, -54)) // hack to hide the pan-control part and only show the zoom (needs "overflow: hidden" set for the main div)
			);
		}
		else // large (default)
		{
			map.addControl(new GLargeMapControl()
				, new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(-15, -60)) // hack to hide the pan-control part and only show the zoom (needs "overflow: hidden" set for the main div)
			);
		}
	},

	/**
	 * front-end to <a href="http://code.google.com/p/gmaps-utility-library/">DragZoom</a>
	 * todo: should write our own, only using map-surface.
	 * @method enableDragZoom
	 * @param {GMap2} map
	 */
	enableDragZoom: function(map)
	{
		map.addControl(
			new DragZoomControl(
				{
					opacity: 0.2,
					border: '2px solid yellow'
				},
				{
					buttonHTML: '<img src="Images/MapControl/btn_zoom_area.png" class="png-fix" title="' + localize('VT_VEHICLETRACKER_MAPICON_ZOOMAREA') + '" />',
					buttonZoomingHTML: '<img src="Images/MapControl/btn_zoom_area_activated.png" class="png-fix" />',
					buttonStartingStyle: {
						position: 'absolute',
						width: '30px',
						height: '29px'
					},
					// don't change any background color when clicking
					buttonStyle: {},
					buttonZoomingStyle: {},

					overlayRemoveTime: 0
					// minDragSize: 4 ?
				},
				{}
			),
			new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(28, 4)) // positioning assume we're using the partialZoomControl
		);
	},


	/**
	 * enables the <a href="http://code.google.com/apis/maps/documentation/reference.html#GControlImpl.GOverviewMapControl">GOverviewMapControl</a>
	 * but makes it initially hidden
	 * @method enableOverviewMap
	 * @param {GMap2} map
	 */
	enableOverviewMap: function(map)
	{
		// !? if creating a new GMap instance this fails here already (claiming a .show() method doesn't exist..)
		var overviewMap = new GOverviewMapControl(); // todo: the constructor can apparently take a GSize also (undocumented). expose?
		// todo: tag the map if already having an overviewMap?
		map.addControl(overviewMap);
return; // auto-hide doesn't work in 2.153+ !

		// add a small delay before minimizing (seems Safari needs it sometimes)
		setTimeout(
			function()
			{
				try
				{
					// initially show the overviewMap minimized (undocumented method, thus the try-catch)
					// todo: has this stopped working in GMap v2.149+??
					// see http://groups.google.com/group/Google-Maps-API/browse_thread/thread/1d855f075e174f2f/86000b0799f37900?lnk=gst&q=overview+hide#86000b0799f37900
					overviewMap.hide(true);
				}
				catch (e)
				{
					logWarning('OverviewControl hide() failed');
				}
			},
			50
		);
	},

	/**
	 * same as GEvent.addListener but automatically unhooks on first call
	 * todo: support context obj?
	 * see <a href="Franson.Event.html#method_listenOnce">Franson.Event.listenOnce()</a>
	 * @method listenOnce
	 * @param {GObject} source
	 * @param {string} event
	 * @param {function} callback
	 * @return {EventHandler} typically ignored (the purpose of this function..). but might be useful if cancelling is needed.
	 */
	listenOnce: function(source, event, callback)
	{
		var eh = GEvent.addListener(source, event,
			function(/*arguments*/)
			{
				GEvent.removeListener(eh);
				callback.apply(this, arguments);
			}
		);
		return eh;
	},

	/**
	 * note that this doesn't need an instantiated GMap to work
	 * needs GMap v2.133d+
	 * todo: create a separate rev-geocode package that wraps several impl. (geonames etc) (and then move to .Geo module)
	 * @method reverseGeocode
	 * @param {LatLng} pos
	 * @return {Deferred}
	 */
	reverseGeocode: function(pos)
	{
		Franson.GUtil._geocoder = Franson.GUtil._geocoder || new GClientGeocoder();

		var d = new MochiKit.Async.Deferred();

		Franson.GUtil._geocoder.getLocations(new GLatLng(pos.lat, pos.lng),
			function(result)
			{
				// todo: hmm, perhaps pass both values to the .callback? (and only _real_ errs to .errback..)

				if (result.Status.code == G_GEO_SUCCESS)
				{
					var placemark = result.Placemark[0]; // todo: can be multiple placemarks.. but 0 should be closest

					// normalize result
					var ret = {
						address: placemark.address, // todo: this should be split in it's components!
						accuracy: placemark.AddressDetails.Accuracy, // hmm, perhaps treat 0 (or < 3?) as an error?
						addressPos: { lat: placemark.Point.coordinates[1], lng: placemark.Point.coordinates[0] }, // there's also a alt field in [2] but that seems to be 0 everywhere (expose anyway? not sure how common alt is among other geocoders?)
						requestPos: pos // available in result.name also
					};

					d.callback(ret);
				}
				else // error..
				{
					if (result.Status.code == G_GEO_TOO_MANY_QUERIES)
					{
						// see http://code.google.com/apis/maps/faq.html#geocoder_limit
						// can potentially lead to google blocking us => we disable ourselves for this session.
						// todo: hmm, perhaps we initially should try to simply delay next request, in case this error is due to too frequent calls.

						errorMessage = 'Reverse geocoder: Google reports too many/frequent geocode requests from this IP. Geocoder will be disabled for the current session.';
						logWarning(errorMessage);

						Franson.GUtil.reverseGeocode = function(pos) // todo: hmm, or perhaps just use a flag instead (this won't help if a client has made an alias copy of this function..)
						{
							logWarning(errorMessage);
							return MochiKit.Async.fail(new Error(errorMessage));
						};
						Franson.GUtil._geocoder = null;
					}
					else
					{
						errorMessage = 'Reverse geocoder: Failed to find an address';
					}

					var error = new Error(errorMessage);
					error.requestPos = pos;
					d.errback(error);
				}
			}
		);

		return d;
	},

	/**
	 * convenience to setup "everything" at once
	 * a mix of the standard flags and settings and our custom functionality
	 * note: also handles the GUnload on page-unload.
	 * @method setupMap
	 * @param {DOM|string} div (a div must have an id)
	 * @param {literal} [options]  (todo: document)
	 * @return {GMap2} Google Map object  (todo: perhaps return GMapAdapter? (or wrap another level))
	 */
	setupMap: function(div, options)
	{
		// todo: need to double-check GMap's defaults, if these aren't same we
		// would need to call the corresponding 'disable'-function in GMap below also (which we don't now)
		options = MochiKit.Base.setdefault(options,
		{
			center: { lat: 56.36, lng: 13.97 }, // approximate center of Europe
			zoom: 4,
			//------------
			startMapType: false, // set to false here to Not call. from outside it should be G_NORMAL_MAP etc
			enableDragging: true,
			enableContinuousZoom: true, // GMap uses false by default
			enableScrollWheelZoom: true, // GMap uses false by default
			enablePartialZoomControl: false,
			enableDragZoom: false,
			enableDoubleClickZoom: true,
			enableKeyboard: false,
			enableTrafficView: false,
			enableOverviewMap: false,
			enableScaleControl: true,
			enableGoogleBar: false
		});

		var map = new GMap2($(div));
		map.setCenter(new GLatLng(options.center.lat, options.center.lng)); // must be the first thing done

		if (typeof(options.zoom) == 'number') // can't use '||' since it can be 0..
			map.setZoom(options.zoom);

		if (options.enableDragging)
		{
			map.enableDragging();
		}
		else
		{
			map.disableDragging();
		}

		if (options.enableKeyboard)
		{
			var keyboard = new GKeyboardHandler(map); // no need to store the keyboard object, just a dummy
		}

		// always add this
		map.addMapType(G_PHYSICAL_MAP);

		// heh, this was cool :) but currently not really usable, would also need to add another interface around our gfx-stuff..
	 	//map.addMapType(G_SATELLITE_3D_MAP);

		if (options.startMapType) map.setMapType(options.startMapType);

		map.addControl(new GMenuMapTypeControl(), // always use the drop-down list type to save space for other buttons etc (or add this as option?)
			new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(5, 5)) // move slightly up and right
		);

		if (options.enableTrafficView)
		{
			map.addControl(new Franson.GUtil.TrafficButtonControl(),
				new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(87, 5)) // positioning assumes MenuMapTypeControl is used
			);
		}
		// todo: if using two of these dynamically visible buttons we should move them so no gap shows.. (need a "screen-space allocator"?)
//		if (options.enableStreetView)
//		{
//			map.addControl(new Franson.GUtil.StreetviewButtonControl(), new GControlPosition(...));
//		}

		if (options.enableContinuousZoom)
		{
			map.enableContinuousZoom();
		}
		else
		{
			map.disableContinuousZoom();
		}

		if (options.enableScrollWheelZoom)
		{
			map.enableScrollWheelZoom();
		}
		else
		{
			map.disableScrollWheelZoom();
		}

		if (options.enablePartialZoomControl)
		{
			Franson.GUtil.addPartialZoomControl(map); // todo: take a flag indication 'large' or 'small'
		}
		// .. else default?

		if (options.enableDragZoom) Franson.GUtil.enableDragZoom(map); // todo: the positioning here assumes a partial zoom-control

		if (options.enableDoubleClickZoom)
		{
			map.enableDoubleClickZoom();
		}
		else
		{
			map.disableDoubleClickZoom();
		}

		if (options.enableOverviewMap) Franson.GUtil.enableOverviewMap(map);

		if (options.enableScaleControl)
		{
			if (options.enableGoogleBar)
			{
				map.addControl(new GScaleControl()
					, new GControlPosition(G_ANCHOR_BOTTOM_LEFT, new GSize(95, 4)) // need to move scale-control to the right if googlebar is enabled
				);
			}
			else
			{
				map.addControl(new GScaleControl());
			}
		}

		// todo: expose GOOGLEBAR_TYPE_LOCALONLY_RESULTS also (checkbox?)
		if (options.enableGoogleBar) map.enableGoogleBar();

		// attach the global cleanup function
		Franson.Event.connectOnce(window, 'onunload', GUnload);

		log('Google Maps', 'v 2.' + G_API_VERSION, 'started');

		return map;
	},

	/**
	 * is map currently set to use the Google Earth plugin?
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMapType.G_SATELLITE_3D_MAP">G_SATELLITE_3D_MAP</a> and
	 * <a href="http://code.google.com/apis/earth/">Google Earth API</a>
	 * @param {GMap2} map
	 * @return {boolean}
	 */
	in3dMode: function(map)
	{
		return map.getCurrentMapType().getName() == 'Earth';  // enough? (check GE for instance also?)
	},

	/**
	 * extracts the <a href="http://code.google.com/apis/maps/documentation/reference.html#GProjection">GProjection</a> object as a closure (for use in GMapGraphicsAdapter etc)
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.fromContainerPixelToLatLng">fromContainerPixelToLatLng</a> and
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GProjection.fromLatLngToPixel">fromLatLngToPixel</a>
	 * @method getProjection
	 * @param {GMap2} map
	 * @return {Franson.Map.IProjection}
	 */
	getProjection: function(map)
	{
		// see also http://groups.google.com/group/Google-Maps-API/browse_thread/thread/2f6509269d74551c/84adab4bcf2a90ec?lnk=raot&fwc=1&pli=1
		return {
			fromContainerPixelToLatLng: function(pos)
			{
				var latLng = map.fromContainerPixelToLatLng(new GPoint(pos.x, pos.y));
				return { lat: latLng.lat(), lng: latLng.lng() };
			},
			fromLatLngToContainerPixel: function(latlng)
			{
				var pix = map.fromLatLngToContainerPixel(new GLatLng(latlng.lat, latlng.lng));
				return { x: pix.x, y: pix.y };
			},
			//------ note: the current mapsurface can do without these two now
			fromDivPixelToLatLng: function(pos)
			{
				var latLng = map.fromDivPixelToLatLng(new GPoint(pos.x, pos.y));
				return { lat: latLng.lat(), lng: latLng.lng() };
			},
			fromLatLngToDivPixel: function(latlng)
			{
				var pix = map.fromLatLngToDivPixel(new GLatLng(latlng.lat, latlng.lng));
				return { x: pix.x, y: pix.y };
			}
		};
	},

	/**
	 * normalizes the version so it can more easily be compared
	 * example <code>[2, 153, 'c']</code>
	 * @method getVersion
	 * @param {string} [version=G_API_VERSION]
	 * @return {[number, number, string]}
	 */
	getVersion: function(version)
	{
		version = version || G_API_VERSION;
		// transform example "153c" => [2, 153, 'c']
		var ver = version.match(/(\d+)(\D*)/);
		return [ 2, parseInt(ver[1]), ver[2] ];
	},

	/**
	 * @method isGMapScriptLoaded
	 * @param {string} [minVersion]
	 * @return {boolean}
	 */
	isGMapScriptLoaded: function(minVersion) // todo: hmm, standardize the name of this function? (should be part of all Map-packages I'd say, just call it isScriptLoaded?)
	{
		return (
			typeof(GMap2) != 'undefined' &&
			typeof(G_API_VERSION) != 'undefined' &&
			typeof(google) != 'undefined' && typeof(google.maps) != 'undefined' &&
			//---
			typeof(minVersion) != 'undefined' ? (Franson.GUtil.getVersion(minVersion) >= Franson.GUtil.getVersion()) : true
		);
	}

});



//------ todo: this should be put in a separate trafficbutton.js script and included as a CustomScript plugin instead --

var Franson = Franson || {};
Franson.GUtil = Franson.GUtil || {};

/**
 * Copyright Franson Technology AB, Sweden, 2009
 * http://gpsgate.com, http://franson.com
 *
 * author Fredrik Blomqvist
 * initially based on code found in ExtMapTypeControl Class v1.3, Copyright (c) 2007, Google, Author: Pamela Fox, others.
 *
 * todo: localize
 * todo: expose hide/show methods?
 * todo: enable dynamic hide/show of the incident-markers? (checkbox in the menu)
 * todo: remove/destroy method? (incl. cleanup)
 * todo: IE DOM leaks
 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GControl">GControl</a>
 * @extends GControl
 * @class Franson.GUtil.TrafficButtonControl
 * @constructor
 */
Franson.GUtil.TrafficButtonControl = function(options)
{
	this.options = MochiKit.Base.setdefault(options, {
		showTrafficKey: true,	// legend
		showIncidents: true // todo: enable user toggling of this (checkbox?)
	});

	this._trafficInfo = null; // not really necessary to store here
};

// inherit from GControl
Franson.GUtil.TrafficButtonControl.prototype = MochiKit.Base.merge(new GControl(),
{
	/**
	 * Required by GMap API for controls.
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GControl.getDefaultPosition">GControl.getDefaultPosition()</a>
	 * @method getDefaultPosition
	 * @return {GControlPosition} default location for control
	 */
	getDefaultPosition: function()
	{
		return new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(87, 5)); // default position assumes GMenuMapTypeControl is used
	},

	/**
	 * Required by GMap API for controls.
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GControl.initialize">GControl.initialize()</a>
	 * @method initialize
	 * @param {GMap2} map
	 */
	initialize: function(map)
	{
		//--- local functions
		// todo: externalize these (should use same as templates if creating other similar buttons (streetviewbutton etc))
		function _setButtonStyle(button)
		{
			button.style.color = "#000000";
			button.style.backgroundColor = "white";
			button.style.font = "small Arial";
			button.style.border = "1px solid black";
			button.style.padding = "0px";
			button.style.margin = "0px";
			button.style.textAlign = "center";
			button.style.fontSize = "12px";
			button.style.cursor = "pointer";
		}

		function _createButton(text)
		{
			var buttonDiv = document.createElement("div");
			buttonDiv.id = '_gmapTrafficButton';
			_setButtonStyle(buttonDiv);
			buttonDiv.style.cssFloat = "left";
			buttonDiv.style.styleFloat = "left";
			var textDiv = document.createElement("div");
			textDiv.appendChild(document.createTextNode(text));
			textDiv.style.width = "6em";
			buttonDiv.appendChild(textDiv);
			return buttonDiv;
		}

		function _toggleButton(div, boolCheck)
		{
			div.style.fontWeight = boolCheck ? "bold" : "";
			div.style.border = "1px solid white";
			var shadows = boolCheck ? ["Top", "Left"] : ["Bottom", "Right"];
			for (var j = 0; j < shadows.length; ++j) {
				div.style["border" + shadows[j]] = "1px solid #b0b0b0";
			}
		}
		//----

		// todo: localize
		var trafficDiv = _createButton("Traffic");
		//trafficDiv.setAttribute('title', 'Show Traffic'); // todo: localize
		trafficDiv.style.marginRight = "8px";
		trafficDiv.style.visibility = 'hidden';
		trafficDiv.firstChild.style.cssFloat = "left";
		trafficDiv.firstChild.style.styleFloat = "left";

		this._trafficInfo = new GTrafficOverlay({
			hide: true, // initially hidden
			incidents: this.options.showIncidents // todo: expose a checkbox so user can toggle this (unfinished code below..)
		});
		this._trafficInfo._hidden = true;

		// We have to do this so that we can sense if traffic is in view
		GEvent.addListener(this._trafficInfo, "changed", function(hasTrafficInView)
		{
			trafficDiv.style.visibility = hasTrafficInView ? 'visible' : 'hidden';
		});
		map.addOverlay(this._trafficInfo);

		GEvent.bindDom(trafficDiv.firstChild, "click", this, function()
		{
			if (this._trafficInfo._hidden) {
				this._trafficInfo._hidden = false;
				this._trafficInfo.show();
			} else {
				this._trafficInfo._hidden = true;
				this._trafficInfo.hide();
			}
			_toggleButton(trafficDiv.firstChild, !this._trafficInfo._hidden);
		});

		if (this.options.showTrafficKey)
		{
			var keyDiv = document.createElement("div");
			keyDiv.style.cssFloat = "left";
			keyDiv.style.styleFloat = "left";
			keyDiv.innerHTML = "&#9660;";  // previously "&nbsp;?&nbsp;"
			//keyDiv.setAttribute('title', 'Legend'); // todo: localize

			var keyExpandedDiv = document.createElement("div");
			keyExpandedDiv.style.clear = "both";
			keyExpandedDiv.style.padding = "2px";

	/*    	// incident markers are available in gmap v2.121+
			// todo: for the checkbox to work we need to add/remove the entire overlay, not just hide/show it inside extmaptypecontrol..
			keyExpandedDiv.innerHTML += "<div style='text-align: left'><input type='checkbox' checked='checked' id='showTrafficIncidents'></input><span>incidents</span></div>";
			keyExpandedDiv.innerHTML += "<div style='text-align: center'><span>--- legend ---</span></div>";
	*/
			// todo: localize (including units)
			var keyInfo = [
				{ color: "#30ac3e", text: "&gt; 45 MPH" }, // green
				{ color: "#ffcf00", text: "25-45 MPH" }, // yellow
				{ color: "#ff0000", text: "10-25 MPH" }, // red
				{ color: "#000000", text: "0-10 MPH" }, // black (should actually be interleaved red/black(?))
				{ color: "#c0c0c0", text: "No data" } // grey
			];

			for (var i = 0; i < keyInfo.length; ++i) {
				keyExpandedDiv.innerHTML += "<div style='text-align: left'><span style='background-color: " + keyInfo[i].color + "'>&nbsp;&nbsp</span>"
					+  "<span style='color: " + keyInfo[i].color + "'> " + keyInfo[i].text + " </span>" + "</div>";
			}
			keyExpandedDiv.style.display = "none";

			GEvent.addDomListener(keyDiv, "click", function()
			{
				if (keyDiv._keyExpanded) {
					keyDiv._keyExpanded = false;
					keyExpandedDiv.style.display = "none";
				} else {
					keyDiv._keyExpanded = true;
					keyExpandedDiv.style.display = "block";
				}

				_toggleButton(keyDiv, keyDiv._keyExpanded);
			});

			_toggleButton(keyDiv, keyDiv._keyExpanded);
		}

		var separatorDiv = document.createElement("div"); // necessary?
		separatorDiv.style.clear = "both";

		if (this.options.showTrafficKey)
			trafficDiv.appendChild(keyDiv);
		trafficDiv.appendChild(separatorDiv);
		if (this.options.showTrafficKey)
			trafficDiv.appendChild(keyExpandedDiv);
		_toggleButton(trafficDiv.firstChild, false);

		var container = document.createElement("div"); // !? without this extra level of indirection the infowindows seems to trigger visibility of the button!? (report as GMap bug?)
		container.appendChild(trafficDiv);

		map.getContainer().appendChild(container);

		return container;
	}

}); // TrafficButtonControl.prototype

//------ TrafficButtonControl

Copyright © 2009 Franson Technology AB, Sweden. All rights reserved.