/**
* Copyright Franson Technology AB, Sweden, 2009
* http://gpsgate.com, http://franson.com
*
* author Fredrik Blomqvist
*
* @module Map
*
*/
var Franson = Franson || {};
/**
* namespace
* @class Franson.Map
* @static
*/
Franson.Map = Franson.Map || {};
/**
* note that we don't document the return value, treat it as a black-box.
* todo: this should rather be a function the different low-level maps should
* provide (and we should only manage the command queue)
* @method saveMapState
* @param {Franson.Map.IMap} map
* @return {object} (JSON-serializable)
*/
Franson.Map.saveMapState = function(map)
{
// create a state-blob. could add more stuff?
return {
type: map.type,
center: map.getCenter(),
zoom: map.getZoom()
};
};
/**
* @method restoreMapState
* @param {Franson.Map.IMap} map
* @param {object} state obtained from <a href="#method_saveMapState">saveMapState()</a> (JSON-serializable)
*/
Franson.Map.restoreMapState = function(map, state)
{
map.setCenter(state.center, state.zoom);
};
/**
* @method zoomToFit
* @param {Franson.Map.IMap} map
* @param {Franson.Geo.Bounds} bounds
*/
Franson.Map.zoomToFit = function(map, bounds) // rename zoomToBounds?
{
// todo: hmm, make this detect if the zooming suceeded? necessary for custom maps to be able to use a fallback panTo...
map.setCenter(bounds.getCenter(), map.getBoundsZoomLevel(bounds));
};
/**
* workaround to not scroll entire page when scrolling over map
* (no longer needed for current versions of GMap)
* todo: verify if it works in Opera and Safari also
* @method disablePageScrollOnMouseWheel
* @param {Franson.Map.IMap} map
*/
Franson.Map.disablePageScrollOnMouseWheel = function(map)
{
Franson.Event.connectOnce(map.getContainer(), 'onmousewheel', Franson.Event.preventDefault);
};
// todo: should make this a pattern to force all interfaces to supply a verificator functions
// todo: these should be loaded conditionally as part of a DEBUG-mode.
/**
* used to validate overlays added to MapSurface (& maps), mostly for debugging/testing
* (since the interface is the same it could be used to verify GMap::GOverlay interfaces also)
* @method isOverlayLike
* @param {object} obj object to test
* @return {boolean} <code>true</code> if obj fulfils the Franson.Map.IOverlay interface
*/
Franson.Map.isOverlayLike = function(obj)
{
// todo: create a util func to scan for function-types
return (
typeof(obj) == 'object' &&
obj !== null &&
typeof(obj.initialize) == 'function' &&
typeof(obj.redraw) == 'function' &&
typeof(obj.remove) == 'function'
// + copy?
// && typeof(obj.destroy) == 'function' // ok? (not in GMap)
// hide/show?
);
};
/**
* todo: turn into control class?
* @method enableCrosshair
* @param {Franson.Map.IMap} map
* @param {literal} [options]
*/
Franson.Map.enableCrosshair = function(map, options)
{
options = MochiKit.Base.setdefault(options, {
hideTime: 400 // ms
});
var id = 'mapCrosshair_' + Franson.Util.getUniqueId();
// todo: hide onmouseover? (to avoid hiding clicks)
map.getContainer().appendChild(
MochiKit.DOM.IMG({
'id': id,
'src': 'Images/MapControl/crosshair.gif', // todo: be able to configure this
'style': {
'position': 'absolute',
'width': '19px',
'height': '19px',
'display': options.hideTime != -1 ? 'none' : 'block',
'z-index': 200
}
})
);
// todo: hook these to zoom-start/end also?
var crossHideTimeoutHandler = null;
connect(map, 'onmovestart', function()
{
if (crossHideTimeoutHandler !== null)
{
clearTimeout(crossHideTimeoutHandler);
crossHideTimeoutHandler = null;
}
var divSize = MochiKit.Style.getElementDimensions(this.getContainer());
var crossPos = { x: divSize.w / 2 - 9, y: divSize.h / 2 - 9 }; // adjust to center of crosshair
MochiKit.Style.showElement(id);
MochiKit.Style.setElementPosition(id, crossPos);
});
connect(map, 'onmoveend', function()
{
// MochiKit.Base.partial(Franson.DOM.blockHide, 'mapCrosshairs'),
crossHideTimeoutHandler = setTimeout(
function()
{
crossHideTimeoutHandler = null;
MochiKit.Style.hideElement(id);
},
options.hideTime
);
});
};
/**
* auguments a Map class with the methods and events from the "layerManager" in MapSurface. mixin-style.
* Mostly for convenience. Typically call this right after your class definition.
* Requires that the mapType exposes getSurface() (part of IMap interface).
* todo: we should be able to add more functionality like this..
* @method injectLayerManagerInterface
* @param {MapType} mapType note: a <i>class</i> Not an object instance.
* @return {MapType} mapType with injected layer methods (allow chaining)
* @chainable
*/
Franson.Map.injectLayerManagerInterface = function(mapType)
{
// see the Franson.Map.IMap for specifications of the methods injected here
// todo: hmm, or rather separate this into a ILayerManager interface?
// use setdefault to not overwrite an existing method
MochiKit.Base.setdefault(mapType.prototype, {
addLayer: function(layer)
{
layer._map = this; // force in map reference (for now, should rather change layer.initialize call to receive the map instead of mapsurface)
this.getSurface().addLayer(layer);
signal(this, 'onaddlayer', layer);
},
removeLayer: function(layer)
{
this.getSurface().removeLayer(layer);
signal(this, 'onremovelayer', layer); // if we're allowing a string as input this needs to be normalized to alway return layer anyway...
},
getLayer: function(id)
{
return this.getSurface().getLayer(id);
},
getLayers: function()
{
return this.getSurface().getLayers();
},
clearLayers: function()
{
this.getSurface().clearLayers();
signal(this, 'onclearlayers');
}
});
return mapType;
};
/**
* adds many of the "convenience" methods to mapType (mixin-style)
* @method injectCommonMethods
* @param {MapType} mapType note: a <i>class</i> Not an object instance.
* @return {MapType} mapType with injected layer methods (allow chaining)
* @chainable
*/
Franson.Map.injectCommonMethods = function(mapType)
{
// see the Franson.IMap.MapAdapter for specifications of the methods injected here
// todo: include mousewheel and keyboard support etc?
Franson.Map.injectLayerManagerInterface(mapType);
MochiKit.Base.setdefault(mapType.prototype, {
getProjection: function()
{
return this.getSurface().getProjection(); // necessary to use the surface version since it handles our margins in screen-space.
},
// default.impl assumes the setZoom handles out of range zoom-levels silently (clamping) (or add a try-catch?)
zoomIn: function() // todo: hmm, google zoomIn can take a latlng..
{
// default impl.
var oldLevel = this.getZoom();
this.setZoom(this.getZoom() + 1);
var newLevel = this.getZoom(); // we don't just use oldLevel+1 to account for possible clamping
signal(this, 'onzoomend', oldLevel, newLevel);
},
// default.impl assumes the setZoom handles out of range zoom-levels silently (clamping)
zoomOut: function() // todo: hmm, google zoomOut can take a latlng..
{
// default impl.
var oldLevel = this.getZoom();
this.setZoom(this.getZoom() - 1);
var newLevel = this.getZoom(); // we don't just use oldLevel-1 to account for possible clamping
signal(this, 'onzoomend', oldLevel, newLevel);
},
panTo: function(center)
{
// default impl. (if map has no smooth-pan function)
this.setCenter(center);
},
panBy: function(distance)
{
// default impl.
var center = this.getCenter();
var centerPx = this.getProjection().fromLatLngToContainerPixel(center);
var newCenterPx = Franson.Vec2.sub(centerPx, { x: distance.w || 0, y: distance.h || 0 });
var newCenter = this.getProjection().fromContainerPixelToLatLng(newCenterPx);
this.panTo(newCenter);
},
panDirection: function(dx, dy)
{
// default impl.
var size = this.getSize();
this.panBy({ w: dx*size.w/2, h: dy*size.h/2 });
},
// default impl. assumes map don't need a check-resize (or add a poller by default?)
checkResize: function()
{
// ! if user overrides this the lines below must still be added!
var size = this.getSize();
this.getSurface().resize(size);
},
// make this optional? can be difficult to ensure map is ready for state handling
_saveState: function()
{
return Franson.Map.saveMapState(this);
},
_restoreState: function(state)
{
// if (this.type == state.type)
Franson.Map.restoreMapState(this, state);
},
// todo: expose the trivial savePosition/returnToSavedPosition also?
__connect__: function(ident, eventName, objOrFunc, funcOrStr)
{
// support case of attaching after onload has initially fired
if (eventName == 'onload' && this.isLoaded())
{
// immediately notify the handler
bind(funcOrStr, objOrFunc)();
}
},
openInfoWindow: function(latlng, node, options)
{
// not implemented by mapsurface yet..
this.getSurface().openInfoWindow(latlng, node, options);
signal(this, 'oninfowindowopen'); // or fwd these events by actually hooking to them?
},
closeInfoWindow: function()
{
// not implemented by mapsurface yet..
this.getSurface().closeInfoWindow();
signal(this, 'oninfowindowclose'); // todo: do we need 'onbeforeinfowindowclose' etc?
}
});
return mapType;
};