/**
* 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 = "▼"; // previously " ? "
//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: "> 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 + "'>  </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