/**
* Copyright Franson Technology AB, Sweden, 2009
* http://gpsgate.com, http://franson.com
* <br />
* author Fredrik Blomqvist
*
* @module Map
*
* @requires dojo.dojox.gfx
*
*/
var Franson = Franson || {};
/*
* namespace
* //class Franson.Map
* //static
*/
Franson.Map = Franson.Map || {};
/**
* generic map graphics surface <br />
* (no GMap or other map-type coupling, only projection), full setup is made in the .init() method.
* @param {Franson.Map.IProjection} projection
* @param {literal} [options]
* @class Franson.Map.MapSurface
* @constructor
*/
Franson.Map.MapSurface = function(projection, options) // todo: rename Franson.Map.GfxSurface? or just Franson.Map.Surface
{
dojo.require('dojox.gfx');
this._options = MochiKit.Base.setdefault(options, { // todo: expose a getOptions?
margin: 400 // todo: should set proportional to the current map window size (since it is (more) proportional to the average pan- & drag-length)
});
/**
* wrap the incoming projection to
* handle our margins (essentially creating our own divpixels version)
* @private
*/
this._projection = {
fromContainerPixelToLatLng: function(pos)
{
return projection.fromContainerPixelToLatLng(pos);
},
fromLatLngToContainerPixel: function(latlng)
{
return projection.fromLatLngToContainerPixel(latlng);
},
//------
fromDivPixelToLatLng: method(this, function(pos)
{
// todo: should allow a more "fuzzy" margin by letting the containing div move around more (not resetting pos on every update?).
// then the margin here should come from the actual div pos (GMap does that(?))
return projection.fromContainerPixelToLatLng({ x: pos.x - this._options.margin, y: pos.y - this._options.margin });
}),
fromLatLngToDivPixel: method(this, function(latlng)
{
var pix = projection.fromLatLngToContainerPixel(latlng);
return { x: pix.x + this._options.margin, y: pix.y + this._options.margin };
})
};
/**
* @private
* @type DOM
*/
this._div = null;
/*
* todo: maintain a viewport for better margin handling
* @private
* @type Franson.Geometry.Bounds
*/
// this._viewPort = null;
/**
* @private
* @type dojox.gfx.Surface
*/
this._surface = null;
/**
* @private
* @type Franson.Map.ILayer[]
*/
this._layers = [];
// todo: keep an _events[] for GC (todo: such a pattern could be turned into a mixin style)
};
// methods
Franson.Map.MapSurface.prototype =
{
/**
* @method getContainer
* @return {DOM}
*/
getContainer: function()
{
return this._div; // hmm, or should we return div.parentNode? (thats the div we attached to in .init)
},
/**
* @method getGfxSurface
* @return {dojox.gfx.Surface}
*/
getGfxSurface: function()
{
return this._surface;
},
/**
* @method getRoot
* @return {dojox.gfx.Creator(Group)} root group
*/
getRoot: function()
{
return this._surface;
},
/**
* normally (but not necessarily) same as map's projection
* @method getProjection
* @return {Projection}
*/
getProjection: function()
{
return this._projection;
},
/**
* @method init
* @param {DOM|string} parentDivOrId
* @param {(w,h)} [size=size(parentDivOrId)]
*/
init: function(parentDivOrId, size) // rename initialize?
{
var parentDiv = $(parentDivOrId);
size = size || MochiKit.Style.getElementDimensions(parentDiv);
this._div = MochiKit.DOM.DIV({
id: '__franson_gfx_holder_' + Franson.Util.getUniqueId(),
style: {
position: 'absolute',
left: -this._options.margin + 'px',
top : -this._options.margin + 'px',
'z-index': 5 // necessary to not end up below tile-layers (traffic etc). todo: allow configuration? necessary if we end up as first sibling for example..)
}
});
parentDiv.appendChild(this._div);
this._surface = dojox.gfx.createSurface(this._div, size.w + 2*this._options.margin, size.h + 2*this._options.margin);
log('Vector graphics renderer:', dojox.gfx.renderer);
// this._setupEventHandlers();
},
/**
* Called by the map when the map display has changed.
* @method redraw
* @param {(x,y)} offset distance from the parent DIV's(pane) to the visible viewport. // todo: or should we've taken a function (similar to Projection) that returns the offset?
* @param {boolean} [force=true] <code>true</code> if the zoom level or the pixel offset of the map view has changed, so that the pixel coordinates need to be recomputed.
*/
redraw: function(offset, force)
{
// force = typeof(force) == 'boolean' ? force : true; // todo: hmm, we could be "smart" and detect zoom-changes and set force automatically!
force = true; // !
// We only need to reproject if the coordinate system has changed (i.e typically on zoom for most projections)
if (force)
{
offset = Franson.Vec2.sub(offset, new Franson.Vec2.Vec2(this._options.margin, this._options.margin));
MochiKit.Style.setElementPosition(this.getContainer(), offset);
}
//-- disabled. layers hide themselves now (if VML) (or should we do this in the SVG case?)
// if (force)
// this.hide(); // todo: should store wasHidden state
forEach(this._layers, MochiKit.Base.methodcaller('redraw', force));
// if (force)
// this.show();
},
/**
* @method _setupEventHandlers
* @private
*/
_setupEventHandlers: function()
{
// todo: should be able to synthesize most of the mouse-events using only this surface + the projection code!
// => even less work for when porting a map api!
var getMouseLatLng = method(this, function(e)
{
var mousePos = Franson.Graphics.getMouseSurfacePosition(this.getGfxSurface(), e); // todo: shouldn't need to pass the surface...
var prj = this.getProjection();
var latlng = prj.fromContainerPixelToLatLng(mousePos);
return latlng;
});
// todo: keyboard handling (incl events) should be possible to add like this also!
// todo: click should Not be signalled if a user drags also! (or holds down button too long)
this._surface.connect('onclick', this, function(e)
{
var latlng = getMouseLatLng(e);
logDebug('mapsurface onclick');
logDebug('lat:', latlng.lat, 'lng:', latlng.lng);
// todo: GMap style signal needs overlays etc in event..
signal(this, 'onclick', null, latlng, null);
});
this._surface.connect('onmousedown', this, function(e) // test (synthesize onclick using mousedown/up? (delay + move offset))
{
var latlng = getMouseLatLng(e);
logDebug('mapsurface onmousedown');
logDebug('lat:', latlng.lat, 'lng:', latlng.lng);
});
this._surface.connect('ondblclick', this, function(e) // on google this is preceeded with two 'onclicks'.
{
var latlng = getMouseLatLng(e);
logDebug('mapsurface dblclick');
logDebug('lat:', latlng.lat, 'lng:', latlng.lng);
});
this._surface.connect('onmouseover', this, function(e)
{
var latlng = getMouseLatLng(e);
logDebug('mapsurface onmouseover');
logDebug('lat:', latlng.lat, 'lng:', latlng.lng);
signal(this, 'onmouseover', latlng);
});
this._surface.connect('onmouseout', this, function(e) // todo: this gets fired after dblclick also??
{
var latlng = getMouseLatLng(e);
logDebug('mapsurface onmouseout');
logDebug('lat:', latlng.lat, 'lng:', latlng.lng);
signal(this, 'onmouseout', latlng);
});
this._surface.connect('onmousemove', this, function(e)
{
var latlng = getMouseLatLng(e);
// logDebug('mapsurface onmouseover');
// logDebug('lat:', latlng.lat, 'lng:', latlng.lng);
signal(this, 'onmousemove', latlng);
});
// todo: ondrag etc
},
/**
* should be the same as the map's size
* @method getSize
* @return {(w,h)} pixels
*/
getSize: function()
{
var dim = this._surface.getDimensions();
return { w: dim.width - 2*this._options.margin, h: dim.height - 2*this._options.margin }; // subtract margin
},
/**
* should be the same as the map's center
* @method getCenter
* @return {LatLng}
*/
getCenter: function()
{
// todo: not ok..? our div(container) might be moving.. and with margins.. (and this will not work if surface is hidden (using div.hide, see below))
var pixBounds = Franson.DOM.getElementBounds(this.getContainer()); // err!
var pixCenter = { x: pixBounds.x + pixBounds.w / 2, y: pixBounds.y + pixBounds.h / 2};
var geoCenter = this.getProjection().fromDivPixelToLatLng(pixCenter);
return geoCenter;
},
/**
* should be the same as the map's bounds (viewport bounds)
* @method getBounds
* @return {Franson.Geo.Bounds}
*/
getBounds: function()
{
// todo: ! need to maintain the position separately. this will fail if surface is hidden!
var divPos = MochiKit.Style.getElementPosition(this.getContainer());
var pos = Franson.Vec2.add(divPos, { x: this._options.margin, y: this._options.margin }); // remove margin
var size = this.getSize();
var bounds = Franson.Geo.pixelBoundsToGeoBounds(this.getProjection(), {
x: pos.x, y: pos.y,
w: size.w, h: size.h
});
//return bounds;
//-----
// hackish way to get hold of map..
var mapBounds = this._layers[0].getMap().getBounds();
//return mapBounds;
var mc = this._layers[0].getMap().getContainer();
var _bounds = Franson.Geo.pixelBoundsToGeoBounds(this.getProjection(), Franson.DOM.getElementBounds(mc));
//----
return bounds;
},
/**
* bounds including the margin
* @private
* @method _getCullBounds
* @return {Franson.Geo.Bounds}
*/
_getCullBounds: function()
{
// todo: ! need to maintain the position separately. this will fail if surface is hidden!
var pos = MochiKit.Style.getElementPosition(this.getContainer());
var size = this._surface.getDimensions();
var bounds = Franson.Geo.pixelBoundsToGeoBounds(this.getProjection(), {
x: pos.x, y: pos.y,
w: size.width, h: size.height
});
//return bounds;
//---
var _bounds = this.getBounds();
var cullBounds = Franson.Geo.scaleBounds(_bounds, 2);
return cullBounds;
//----
//----
var bb = Franson.DOM.getElementBounds(this.getContainer());
var dbounds = Franson.Geo.pixelBoundsToGeoBounds(this.getProjection(), bb);
return dbounds;
//----
return bounds;
},
/**
* @method hide
*/
hide: function()
{
MochiKit.Style.hideElement(this._div);
},
/**
* @method show
*/
show: function()
{
MochiKit.Style.showElement(this._div);
},
/**
* @method isHidden
* @return {boolean}
*/
isHidden: function()
{
return this._div.style.display == 'none';
},
/**
* @method remove
*/
remove: function()
{
if (this._div != null && this._div.parentNode != null)
MochiKit.DOM.removeElement(this._div);
},
/**
* destructor
* @method destroy
*/
destroy: function()
{
forEach(this._layers, MochiKit.Base.methodcaller('destroy'));
this._layers = null;
this.remove();
disconnectAll(this); // ok?
this._surface.destroy();
this._surface = null;
this._div = null;
},
/**
* todo: or skip this? (should use clearOverlays, clearLayers)
* @method clear
*/
clear: function()
{
// todo: dojo.disconnect..?
this._surface.clear();
},
/**
* @method resize
* @param {(w,h)} size
*/
resize: function(size)
{
size = { w: size.w + 2*this._options.margin, h: size.h + 2*this._options.margin };
this._surface.setDimensions(size.w, size.h);
MochiKit.Style.setElementDimensions(this._div, size);
},
//--------
/**
* @method addLayer
* @param {Franson.Map.ILayer} layer
*/
addLayer: function(layer)
{
var layerRoot = this.getRoot().createGroup(); // todo: store these to be able to sort etc? (or just grab from layer, i.e demand layer.getRoot?)
try
{
layerRoot.getNode().id = layer.id; // just for debuggablility (doesn't work in Silverlight)
}
catch (e)
{
log('Current graphics layer doesn\'t allow id attribute'); // necessary?
}
layer.initialize(this, layerRoot);
this._layers.push(layer);
layer.redraw(true);
},
/**
* currently O(N)
* @method removeLayer
* @param {Franson.Map.ILayer|string} layer a layer object or a string (layer-id)
*/
removeLayer: function(layer)
{
if (typeof(layer) == 'string')
layer = this.getLayer(layer); // could just do a layer = { id: layer } also
for (var i = 0; i < this._layers.length; ++i)
{
if (this._layers[i].id == layer.id)
{
this._layers.splice(i, 1);
break;
}
}
layer.remove();
disconnectAll(layer);
},
/**
* currently O(N)
* @method getLayer
* @param {string} id
* @return {Franson.Map.ILayer}
*/
getLayer: function(id)
{
for (var i = 0; i < this._layers.length; ++i)
{
if (this._layers[i].id == id)
return this._layers[i];
}
return null;
},
/**
* note that this doesn't .destroy the layers (but it disconnect events from layers)
* @method clearLayers
*/
clearLayers: function()
{
forEach(this._layers, function(layer) // ! can't use forEach(.. this.removeLayer) since it modifies the list!
{
layer.remove();
disconnectAll(layer);
});
this._layers = [];
},
/**
* @method getLayers
* @return {Franson.Map.ILayer[]}
*/
getLayers: function()
{
return this._layers;
}
// todo: layer ordering (z-index) etc
// insertLayerAt(layer, index) (before/after?)
// getLayerIndex(layer);
}; // Franson.Map.MapSurface.prototype