/**
* @class
* Displays a label on the border of a Google Map.
* If multiple labels are to be displayed you are recommended to use a LimbFactory.
*
* @constructor
* @param {lucid.maps.limbs.LimbOptions|google.maps.Marker} optionsOrMarker Settings for the LIMB to be rendered or the Marker to be tracked.
*/
lucid.maps.limbs.Limb = function( optionsOrMarker )
{
var thisRenderer = this;
var limbOptions = (optionsOrMarker.constructor == (new google.maps.Marker()).constructor) ? { marker: optionsOrMarker } : optionsOrMarker;
var hidden;
var inTheBorder;
var marker;
var iconDiv;
var mapViewChangeListener;
var syncClickWithMarker;
var layoutManager;
var generateTooltip;
// This initialise function is called at the end of the constructor.
function init()
{
hidden = limbOptions.hidden;
marker = limbOptions.marker;
syncClickWithMarker = false;
layoutManager = (typeof limbOptions.layout === "object") ? limbOptions.layout : new lucid.maps.limbs.layout.DefaultLayoutStrategy();
generateTooltip = (typeof limbOptions.tooltipFactory === "function") ? limbOptions.tooltipFactory : lucid.maps.limbs.tooltip.standardTooltip;
if (limbOptions.independent !== false)
{
mapViewChangeListener = google.maps.event.addListener( thisRenderer.getMap(), "bounds_changed", handleRefresh );
}
createIconDiv();
thisRenderer.refresh();
}
/**
* Destroy this instance and any associated resources.
* This method should be called when the instance is no longer required.
*/
this.destroy = function()
{
marker = null;
if (mapViewChangeListener)
{
google.maps.event.removeListener( mapViewChangeListener );
}
removeIconDiv();
};
function createIconDiv()
{
var icon = (limbOptions.icon) ? limbOptions.icon : marker.getIcon();
iconDiv = jQuery( "<div></div>" );
iconDiv.css( "background", "url( '" + icon.url + "' ) no-repeat center center" );
iconDiv.css( "position", "absolute" );
iconDiv.css( "width", icon.size.width );
iconDiv.css( "height", icon.size.height );
iconDiv.css( "zIndex", layoutManager.getMinZIndex() );
iconDiv.hide();
if (limbOptions.clickable !== false)
{
if (marker.getTitle() != null)
{
iconDiv.attr( "title", marker.getTitle() );
}
iconDiv.css( "cursor", "pointer" );
iconDiv.click( handleClick );
}
iconDiv.appendTo( getLimbElement() );
}
function removeIconDiv()
{
if (iconDiv)
{
iconDiv.remove();
iconDiv = null;
}
}
/**
* Temporarily hide the label.
* This does not remove the LIMB, it just takes it off display.
* Call this if you hide the map element.
*/
this.hide = function()
{
hidden = true;
if (iconDiv)
{
iconDiv.hide();
}
};
/**
* Re-display the label after being hidden with a call to 'hide'.
*/
this.show = function()
{
hidden = false;
if (iconDiv)
{
if (inTheBorder === true)
{
iconDiv.show();
}
}
};
/**
* @return {google.maps.Map} The map this LIMB is associated with.
*/
this.getMap = function()
{
return marker.getMap();
};
/**
* @return {google.maps.Marker} The marker this LIMB is associated with.
*/
this.getMarker = function()
{
return marker;
};
function getMapElement()
{
return jQuery( thisRenderer.getMap().getDiv() );
}
function getLimbElement()
{
return getMapElement().parent();
}
function handleClick()
{
google.maps.event.trigger( thisRenderer, "click", thisRenderer );
if (syncClickWithMarker === true)
{
google.maps.event.trigger( marker, "click" );
}
}
/**
* Set whether the click event on the LIMB should cause an effective click on the associated marker.
*
* @param {boolean} synchonised Whether the click events should be synchonised.
*/
this.setSyncClickWithMarker = function( synchonised )
{
syncClickWithMarker = synchonised;
};
function handleRefresh()
{
thisRenderer.refresh();
}
/**
* Refresh the display of the LIMB on the page.
*/
this.refresh = function()
{
var map = this.getMap();
var mapElement = getMapElement();
var markerLocation = marker.getPosition();
var mapBounds = map.getBounds();
if ( (typeof mapBounds === "undefined") // The getBounds method returns undefined when the map is still initialising.
|| (mapBounds.contains( markerLocation )) )
{
inTheBorder = false;
iconDiv.hide();
return;
}
else
{
inTheBorder = true;
if (hidden === true)
{
iconDiv.hide();
}
else
{
iconDiv.show();
}
}
var viewCentre = map.getCenter();
var heading = computeHeading();
var angle = 90 - heading;
var angleInRadians = angle * Math.PI / 180;
var mapWidth = mapElement.width();
var mapHeight = mapElement.height();
var intersection = computeIntersectionOnLeftOrRightEdge();
if (Math.abs( intersection.y ) > (mapHeight / 2))
// The intersection hits the top/bottom edge before the left/right edge.
{
intersection = computeIntersectionOnTopOrBottomEdge();
}
// The intersection coords are relative to the centre of the map.
// Offset this to an origin in the top-left corner.
// Also reverse the y-axis (positive values point up the Maths plane, but point down the screen).
var intersectionFromTopLeft = { x: (mapWidth / 2) + intersection.x,
y: (mapHeight / 2) - intersection.y };
// Centre the icon on that position by applying an offset.
// TODO Use the marker's icon's offset.
var display = {};
display.x = Math.round( intersectionFromTopLeft.x - (iconDiv.width() / 2) );
display.y = Math.round( intersectionFromTopLeft.y - (iconDiv.height() / 2) );
// The origin of these display coords are in the map element.
// Offset these coords against the mapElement position to position the LIMB correctly on the screen.
var mapPosition = mapElement.position();
var outerWidthOffset = ( mapElement.outerWidth( true ) - mapElement.innerWidth() ) / 2;
var outerHeightOffset = ( mapElement.outerHeight( true ) - mapElement.innerHeight() ) / 2;
display.x = mapPosition.left + outerWidthOffset + display.x;
display.y = mapPosition.top + outerHeightOffset + display.y;
var mapDetails = { "viewCentre": viewCentre,
"markerLocation": markerLocation,
"heading": heading };
layoutManager.layout( iconDiv, display, mapDetails );
if (limbOptions.clickable !== false)
{
var limbLocation = computeLimbLocation();
var distance = google.maps.geometry.spherical.computeDistanceBetween( limbLocation, markerLocation );
iconDiv.attr( "title", generateTooltip( marker, distance, display, mapDetails ) );
}
// else: the LIMB does not respond to mouse hover; a tooltip is not needed
function computeHeading()
{
var heading = google.maps.geometry.spherical.computeHeading( viewCentre, markerLocation );
// The function above can return a negative value.
// The computeIntersectionOnLeftOrRightEdge/computeIntersectionOnTopOrBottomEdge rely on values being in the range 0 - 360.
// So normalise the value now.
while (heading > 360)
heading -= 360;
while (heading < 0)
heading += 360;
return heading;
}
function computeIntersectionOnLeftOrRightEdge()
{
// The intersection will be on either the left or right edge.
var intersectionX = (heading < 180) ? (mapWidth / 2) : -(mapWidth / 2);
var distanceToIntersection = intersectionX / Math.cos( angleInRadians );
var intersectionY = distanceToIntersection * Math.sin( angleInRadians );
return { x: intersectionX, y: intersectionY };
}
function computeIntersectionOnTopOrBottomEdge()
{
// The intersection will be on either the top or bottom edge.
var intersectionY = ((heading > 90) && (heading < 270)) ? -(mapHeight / 2) : (mapHeight / 2);
var distanceToIntersection = intersectionY / Math.sin( angleInRadians );
var intersectionX = distanceToIntersection * Math.cos( angleInRadians );
return { x: intersectionX, y: intersectionY };
}
function computeLimbLocation()
{
// We do this by converting the intersection coords from pixels to distance.
// This is done by computing the scaling-factor between the element dimensions and the real-world distance.
var mapHeightDistance = mapBounds.getNorthEast().lat() - mapBounds.getSouthWest().lat();
var mapHeightScale = mapHeightDistance / mapHeight;
var limbLocationLat = viewCentre.lat() + (intersection.y * mapHeightScale);
var mapWidthDistance = mapBounds.getNorthEast().lng() - mapBounds.getSouthWest().lng();
var mapWidthScale = mapWidthDistance / mapWidth;
var limbLocationLng = viewCentre.lng() + (intersection.x * mapWidthScale);
return new google.maps.LatLng( limbLocationLat, limbLocationLng );
}
}
init();
};
/**
* Indicates when the LIMB is clicked.
*
* @event lucid.maps.limbs.Limb#click
* @type {object}
*/
/**
* @type {object}
* @property {boolean} [hidden] Whether the LIMB is initially hidden. If so, it can be made visible with a call to show().
* @property {boolean} [independent] Whether this LIMB is independent of a lucid.maps.limbs.LimbFactory.
* It is more efficient for a group of LIMBs to be managed by a manager, but if
* a single LIMB is being displayed then the Limb will manage itself if
* you set this property to true. The default is true.
* @property {google.maps.Marker} marker The marker on the map which is to be displayed in the map border
* when the marker is outside the map's viewport.
* @property {google.maps.Icon} [icon] Icon specification which must contain the URL and size of the icon image to display.
* @property {boolean} [clickable] Whether the icon in the map border is clickable. If not defined the clickable setting is taken from the marker.
* @property {lucid.maps.limbs.layout.LayoutStrategy} [layout] A strategy for positioning the LIMB.
* If not defined a DefaultLayoutStrategy will be used.
* @property {function} [tooltipFactory] A strategy for generating the tooltip shown when the user mouses-over the LIMB.
* NOTE: A tooltip is only shown if the LIMB is clickable.
*/
lucid.maps.limbs.LimbOptions = {};
// TODO Use the google.maps.Icon to specify the icon graphic - would need to support sprite origin and scaled size. Also read the size from a loaded image if it's not defined.
/**
* Copy lucid.maps.limbs.LimbOptions from one instance to another.
* Only the settings defined in the 'optionsToApply' object will be copied onto the 'options' object.
*
* @param {lucid.maps.limbs.LimbOptions} options The target instance.
* @param {lucid.maps.limbs.LimbOptions} optionsToApply Options that take precedence and should be copied into the target object.
*/
lucid.maps.limbs.applyLimbOptions = function( options, optionsToApply )
{
if (typeof optionsToApply.hidden !== "undefined")
options.hidden = optionsToApply.hidden;
if (typeof optionsToApply.independent !== "undefined")
options.independent = optionsToApply.independent;
if (typeof optionsToApply.marker !== "undefined")
options.marker = optionsToApply.marker;
if (typeof optionsToApply.icon !== "undefined")
options.icon = optionsToApply.icon;
if (typeof optionsToApply.clickable !== "undefined")
options.clickable = optionsToApply.clickable;
if (typeof optionsToApply.layout !== "undefined")
options.layout = optionsToApply.layout;
};