GpsGate Server JavaScript API

Util  1.0.0

GpsGate Server JavaScript API > Util > mapsurface.js (source view)
Search:
 
Filters
/**
 * 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

Copyright © 2009 Franson Technology AB, Sweden. All rights reserved.