/**
* Copyright 2009 Franson Technology AB, Sweden, All Rights Reserved<br />
* http://franson.com, http://gpsgate.com
* <br />
* author Fredrik Blomqvist
*
* @module Map
*
*/
var Franson = Franson || {};
Franson.Map = Franson.Map || {};
/**
* basic interface very similar to GMap, should be possible to implement ontop of most underlying map implementations.
* todo: create an instantiator (plugin factory) for all map-types
* todo: more event forwarding
* see Franson.GUtil for other GMap specific helpers
* @class Franson.Map.GMap
* @extends Franson.Map.IMap
* @constructor
* @param {DOM|string} div (is supplied a div it must have an .id)
* @param {literal} [options] see Franson.GUtil.setupMap:options for spec.
*/
Franson.Map.GMap = function(div, options)
{
/**
* @type string
* @default 'gmap'
*/
this.id = 'gmap'; // for state-saving (if using multiple instances this should be made unique)
/**
* @private @type GMap2
*/
this._map = Franson.GUtil.setupMap(div, options);
/**
* @private @type Franson.Map.MapSurface
*/
this._mapSurface = new Franson.Map.MapSurface(Franson.GUtil.getProjection(this._map));
/**
* should rarely need to be accessed
* @private @type Franson.Map.GMapGraphicsOverlay
*/
this._gmapOverlay = new Franson.Map.GMapGraphicsOverlay(this._mapSurface);
// add to GMap (this will call gmapOverlay.initialize() which will call mapSurface.init() followed by a .redraw() call for both)
this._map.addOverlay(this._gmapOverlay);
/**
* @type GEvent[]
* @private
*/
this._events = [];
//--------
this._events.push(
GEvent.addListener(this._map, 'maptypechanged', function()
{
if (Franson.GUtil.in3dMode(this))
{
log('GMap in 3D mode, markers and tracks will currently not display'); // or alert?
// todo: replace all markers and polylines with google's native ones as a fallback? could be done as a plugin! :)
// todo: shouldn't it be possible to simply link the KML-feed from the server to the earth-instance? (!)
}
}),
GEvent.addListener(this._map, 'movestart', partial(signal, this, 'onmovestart')),
GEvent.addListener(this._map, 'moveend', partial(signal, this, 'onmoveend'))
// todo: fwd more events: click, zoom, drag, move, mouseover etc (do in __connect__) (! many/all mouse events can be handled by mapSurface directly!)
);
// tell listeners we're done
signal(this, 'onload');
}; // Franson.GMapAdapter
// methods
Franson.Map.GMap.prototype =
{
/**
* @type string
* @default 'gmap'
* @final
* @static
*/
type: 'gmap',
/**
* @method getSurface
* @return {Franson.Map.MapSurface}
*/
getSurface: function()
{
return this._mapSurface;
},
/**
* @method getNativeMap
* @return {GMap2}
*/
getNativeMap: function()
{
return this._map;
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.isLoaded">GMap.isLoaded</a>
* todo: should fire the 'onload' signal also (though not sure if we can trust GMap to re-fire it.. (can only be done if we create a complete wrapper))
* @method isLoaded
* @return {boolean}
*/
isLoaded: function()
{
return this._map.isLoaded();
},
/**
* @private
*/
__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)();
}
},
/**
* destructor
* @method destroy
*/
destroy: function()
{
forEach(this._events, GEvent.removeListener);
this._events = null;
GEvent.clearInstanceListeners(this._map);
this._map.removeOverlay(this._gmapOverlay);
//this._map.clearOverlays();
this._gmapOverlay.destroy();
disconnectAll(this);
this._map = null;
// since we can have several instances we Don'r run GUnload here (run in page_unload)
// though I guess we could use a ref-count?
},
//--- Common Map Interface (mimics GMap but with "our" external types) ---------------
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.getContainer">GMap.getContainer</a>
* @method getContainer
* @return {DOM}
*/
getContainer: function()
{
return this._map.getContainer();
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.getSize">GMap.getSize</a>
* @method getSize
* @return {(w, h)}
*/
getSize: function()
{
return this.getSurface().getSize();
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.getCenter">GMap.getCenter</a>
* @method getCenter
* @return {LatLng}
*/
getCenter: function()
{
var center = this._map.getCenter();
return { lat: center.lat(), lng: center.lng() };
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.getBounds">GMap.getBounds</a>
* @method getBounds
* @return {Franson.Geo.Bounds}
*/
getBounds: function()
{
// todo: this should be possible to do using only mapsurface..
var gbounds = this._map.getBounds();
var sw = gbounds.getSouthWest();
var ne = gbounds.getNorthEast();
return new Franson.Geo.Bounds(
{ lat: sw.lat(), lng: sw.lng() },
{ lat: ne.lat(), lng: ne.lng() }
);
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.getZoom">GMap.getZoom</a>
* @method getZoom
* @return {number}
*/
getZoom: function()
{
return this._map.getZoom();
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.zoomIn">GMap.zoomIn</a>
* @method zoomIn
*/
zoomIn: function()
{
this._map.zoomIn();
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.zoomOut">GMap.zoomOut</a>
* @method zoomOut
*/
zoomOut: function()
{
this._map.zoomOut();
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.setZoom">GMap.setZoom</a>
* @method setZoom
* @param {number} zoom
*/
setZoom: function(zoom)
{
this._map.setZoom(zoom);
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.setCenter">GMap.setCenter</a>
* @method setCenter
* @param {LatLng} center
* @param {number} [zoom]
*/
setCenter: function(center, zoom)
{
var latlng = new GLatLng(center.lat, center.lng);
this._map.setCenter(latlng, zoom);
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.panTo">GMap.panTo</a>
* @method panTo
* @param {LatLng} center
*/
panTo: function(center)
{
this._map.panTo(new GLatLng(center.lat, center.lng));
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.panBy">GMap.panBy</a>
* @method panBy
* @param {(w,h)} distance
*/
panBy: function(distance)
{
this._map.panBy(new GSize(distance.w || 0, distance.h || 0));
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.checkResize">GMap.checkResize</a>
* @method checkResize
*/
checkResize: function()
{
this._map.checkResize();
var size = this._map.getSize();
this.getSurface().resize({ w: size.width, h: size.height }); // can't call this.getSize since it calls surface getSize..
// todo: wouldn't it be useful for overlays to have a 'onresize' event fire here? (not in GMap API though)
// todo: should we force a redraw here? (check GMap)
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.openInfoWindow">GMap.openInfoWindo</a>
* note that we don't support tabs etc (and currently not the html version)
* (also note that this is currently not supported for custom maps)
* @method openInfoWindow
* @param {LatLng} latlng
* @param {DOM} node
* @param {object} [options]
*/
openInfoWindow: function(latlng, node, options)
{
// todo: replace with our own implementation using mapsurface-gfx
// todo: support more options
if (options && typeof(options.pixelOffset) != 'undefined')
{
options.pixelOffset = new GSize(options.pixelOffset.w, options.pixelOffset.h);
}
this._map.openInfoWindow(new GLatLng(latlng.lat, latlng.lng), node, options);
signal(this, 'oninfowindowopen');
},
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.closeInfoWindow">GMap.closeInfoWindow</a>
* (note that this is not supported for custom maps yet (uses native GMap infowin))
* @method closeInfoWindow
*/
closeInfoWindow: function()
{
// todo: replace with our own implementation using mapsurface-gfx
this._map.closeInfoWindow();
signal(this, 'oninfowindowclose'); // todo: do we need 'onbeforeinfowindowclose' etc?
},
// --- todo: addControl etc
/**
* @method getProjection
* @return {Projection}
*/
getProjection: function()
{
return this.getSurface().getProjection(); // necessary to use the surface version since it handles our margins in screen-space.
},
// todo: maptype? (rely on the native controls)
/**
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2.getBoundsZoomLevel">GMap.getBoundsZoomLevel</a>
* or just expose a zoomToFit(bounds) ?
* todo: we need some way of signalling wheter the bounds/zoom fit our not.. (other maps will quite often "fail" here due to lack of resolution..)
* @method getBoundsZoomLevel
* @param {Franson.Geo.Bounds} bounds
* @return {number}
*/
getBoundsZoomLevel: function(bounds)
{
var sw = bounds.getSouthWest();
var ne = bounds.getNorthEast();
// todo: seems like the upcoming v2.158 will expose the long awaited: G_NORMAL_MAP.getMaxZoomAtLatLng(latlng, callback). Use it to clamp the values here.
return this._map.getBoundsZoomLevel(new GLatLngBounds(new GLatLng(sw.lat, sw.lng), new GLatLng(ne.lat, ne.lng)));
},
// ------ layer handling ----------
/**
* fired in addLayer
* @event onaddlayer
* @param {Franson.Map.Layer} layer the added layer
*/
/**
* fired in removeLayer
* @event onremovelayer
* @param {Franson.Map.Layer} layer the removed layer
*/
/**
* @method addLayer
* @param {Franson.Map.ILayer} layer
*/
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);
},
/**
* todo: should this be able to take both a layer and a string? (id)
* @method removeLayer
* @param {Franson.Map.ILayer} 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...
},
/**
* @method getLayer
* @param {string} id
* @return {Franson.Map.ILayer}
*/
getLayer: function(id)
{
return this.getSurface().getLayer(id);
},
/**
* @method getLayers
* @return {Franson.Map.ILayer[]}
*/
getLayers: function()
{
return this.getSurface().getLayers();
},
/**
* @method clearLayers
*/
clearLayers: function()
{
this.getSurface().clearLayers();
signal(this, 'onclearlayers');
},
// todo: layer ordering (z-index) ? (or add moveToFront/Back on layers? (or map.moveLayerToBack(layer)?)
//-------------
/**
* for StateManager
* @method _saveState
* @protected
*/
_saveState: function()
{
return {
id: this.id,
type: this.type,
mapType: this._map.getCurrentMapType().getName(),
center: this.getCenter(),
zoom: this.getZoom()
};
},
/**
* for StateManager
* @method _restoreState
* @protected
*/
_restoreState: function(state)
{
if (state.type == this.type)
{
// todo: log if type not found
forEach(this._map.getMapTypes(), function(mapType)
{
if (mapType.getName() == state.mapType)
{
this._map.setMapType(mapType);
//this.setCenter(state.center, state.zoom); // todo: map needs to check if the zoom is within range to avoid low-level projection problems..
if (isFinite(state.center.lat) && isFinite(state.center.lng)) // protect against NaN..
this.setCenter(state.center);
throw MochiKit.Iter.StopIteration; // break
}
}, this);
}
}
}; // Franson.GMapAdapter.prototype
/**
* implements the <a href="http://code.google.com/apis/maps/documentation/reference.html#GOverlay">GMap.GOverlay</a> interface.
*
* Connects ontop of google as a google overlay and maintains state of a map-type agnostic map-surface layer.<br />
* All our other graphics and layers then use this (instance) (via getSurface).</br >
* In practice this should only be instantiated from within the Franson.GMapAdapter.
* @param {Franson.Map.MapSurface} mapSurface
* @extends GMap.GOverlay
* @class Franson.Map.GMapGraphicsOverlay
* @constructor
* @protected
*/
Franson.Map.GMapGraphicsOverlay = function(mapSurface)
{
/**
* @private @type GMap2
*/
this._map = null;
/**
* @private @type Franson.Map.MapSurface
*/
this._mapSurface = mapSurface; // todo: expose getter? getSurface()?
/**
* @private @type Array[GEvent]
*/
this._events = [];
/**
* to keep track of when the projection has changed and we need to do a complete refresh of the surface
* @private @type boolean
*/
this._dirty = true;
};
// inherit from GOverlay
// todo: perhaps put all of this inside a function? (lazy loading) (now this requires the GMap script to be loaded even if this isn't used.)
Franson.Map.GMapGraphicsOverlay.prototype = MochiKit.Base.merge(new GOverlay(),
{
/**
* @method getSurface
* @return {Franson.Graphics.MapSurface}
*/
getSurface: function()
{
return this._mapSurface;
},
/**
* destructor
* @method destroy
*/
destroy: function()
{
this.remove();
this._map = null;
disconnectAll(this); // though we currently don't expose any signals
},
/**
* GOverlay interface
* todo: there's actually two cases we could split the flag in. repojection and re-centering of div&SVG
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GOverlay.redraw">GOverlay.redraw</a>
* note that this is called on every move-increment(sync=false), not just on end of move.
* @method redraw
* @param {boolean} force
*/
redraw: function(force)
{
// todo: it should be (is) possible to maintain the "divPixelOffset" ourselves by doing relative calcs from initial position (either pixel or screen<->latlng) but
// grabbing from GMap should be more exact?
this._dirty = !force; // signal wheter the recenter code needs to be called
// if (force) // todo: should remove the need for this check (mapSurface should not re-center div until a larger bounds is touched)
// logDebug('force redraw (reproject) by GMap');
this.getSurface().redraw(Franson.GUtil.getDivPixelOffset(this._map), force);
},
/**
* GOverlay interface
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GOverlay.initialize">GOverlay.initialize</a>
* @method initialize
* @param {GMap2} map
*/
initialize: function(map)
{
this._map = map;
var size = this._map.getSize();
// todo: ok pane? attach to maker_pane? (currently we end up below the traffic-info...)
this._mapSurface.init(map.getPane(G_MAP_OVERLAY_LAYER_PANE), { w: size.width, h: size.height });
this._events.push(
// todo: should allow a more "fuzzy" margin by letting the containing div move around more (not resetting pos on every update?).
GEvent.bind(this._map, 'moveend', this, function()
{
if (this._dirty) // to skip calling reprojection if a "real" redraw has just been called ('moveend' event is called on zoom-changes also)
{
// logDebug('non-force redraw (reposition) by GMapGraphicsOverlay');
this.redraw(true);
}
})
);
},
/**
* Remove the main DIV from the map pane
* GOverlay interface
* see <a href="http://code.google.com/apis/maps/documentation/reference.html#GOverlay.remove">GOverlay.remove</a>
* @method remove
*/
remove: function()
{
forEach(this._events, GEvent.removeListener);
this._events = [];
this.getSurface().remove();
},
/**
* GOverlay interface
* (is this fn really used by GMap anymore? doesn't seem so..) -> seems to be used (only?) when mirroring overlays to the "showmapblowup" mini-map in infowindows.
* <a href="http://code.google.com/apis/maps/documentation/reference.html#GOverlay.copy">GOverlay.copy</a>
* @method copy
*/
copy: function()
{
return new Franson.Graphics.GMapGraphicsOverlay(); // ...
}
}); // Franson.Map.GMapGraphicsOverlay.prototype