GpsGate Server JavaScript API

GpsGate  1.0.0

GpsGate Server JavaScript API > GpsGate > graphics.js (source view)
Search:
 
Filters
/**
 * Copyright Franson Technology AB, Sweden, 2009 <br />
 * http://franson.com, http://gpsgate.com
 * <br />
 * author Fredrik Blomqvist
 * <p>
 * low-level graphics using <a href="http://docs.dojocampus.org/dojox/gfx">Dojo Gfx (dojo.dojox.gfx)</a> (SVG/VML) <br />
 * intended as a complement to dojox.gfx, filling in functionality we need, i.e Not an API wrapper.
 * </p>
 * Will probably not work in Canvas due to some getNode usage etc..
 * (Canvas doesn't support events either, should probably remove from profile altogether..)
 * todo: Silverlight should be possible to get working if hide/show etc where adapted
 *
 * note: methods here ignore the pass-through convention used in dojo.gfx
 * note: we currently use a custom-build of dojo with only the components needed for graphics included,
 * thus you shouldn't expect more than the basics of the dojo API to be available.
 *
 * @module Graphics
 *
 */

var Franson = Franson || {};
/**
 * namespace
 * @class Franson.Graphics
 * @static
 */
Franson.Graphics = Franson.Graphics || {};


// helpers for dealing with dojox.gfx vector graphics
MochiKit.Base.update(Franson.Graphics, (function()
{
	/**
	 * helper surface attached to a hidden DOM node ('__franson_dojox_gfx_helper')
	 * used as a shape factory (this will be the same even if you are using multiple surfaces).
	 * @private
	 * @type dojox.gfx.Surface
	 */
	var _helperSurface = null;

	/**
	 * assumes the DOM is ready
	 * (lazily called from getCreator, thus not exposed in the public API)
	 * @method _init
	 * @private
	 */
	function _init()
	{
		if (_helperSurface === null)
		{
			var id = '__franson_dojox_gfx_holder_helper';
			var helperDiv = $(id);

			if (helperDiv == null)
			{
				helperDiv = MochiKit.DOM.DIV({
					id: id,
					style: {
						// try to make sure the div is not visible
						'visibility': 'hidden' // can't use "display: none" (all nodes below become invisible)
						,'position': 'absolute'
						,'padding': '0px'
						,'width': '0px'
						,'height': '0px'
						,'z-index': 0
						,'overflow': 'hidden'
					}
				});

				var body = Franson.DOM.body();
				body.insertBefore(helperDiv, body.lastChild); // just doing a body.appendChild results in this consuming layout space anyway..

				_helperSurface = dojox.gfx.createSurface(helperDiv, 1, 1); // 0x0 fails in IE/VML
			}
		}
	}


	/**
	 * returns the global shape factory
	 * Very convenient since you can create objects without having access to the surface/creator.
	 * @method getCreator
	 * @return {dojox.gfx.Creator} (actually a gfx.Surface)
	 */
	function getCreator()
	{
		_init();
		return _helperSurface;
	}

	/**
	 * destroys the helper surface.
	 * Call if you're destroying your <i>last</i> surface (restarting your gfx)
	 * @method destroy
	 */
	function destroy()
	{
		if (_helperSurface != null)
		{
			_helperSurface.destroy();
			_helperSurface = null;
			MochiKit.DOM.removeElement('__franson_dojox_gfx_holder_helper');
		}
	}

	/**
	 * todo: integrate this in calcBounds
	 * see dojox.gfx._getTextBox also
	 * @method getTextBoundingBox
	 * @param {dojox.gfx.Text} textShape
	 * @return {(x, y, w, h)} untransformed bounding rectangle
	 */
	function getTextBoundingBox(textShape)
	{
		var shp = textShape.getShape();

		var w = textShape.getTextWidth(); // this seems to be veery slow in FF2 (hangs..). hmm seems to be Courier font only(?) hopefully dojo fixes this.. (or we need to add workaround here)
		var h = dojox.gfx.normalizedLength(textShape.getFont().size);

		var x = shp.x;
		var y = h - shp.y; // invert y-anchor to get a standard upper-left origo

		return {
			x: x, y: y,
			w: w, h: h
		};
	}

	/**
	 * similar to code in dojo.gfx.shape.setStroke()
	 * @method normalizeStroke
	 * @param {string|number[]|dojo.Color|dojox.gfx.Stroke} stroke
	 * @return {dojox.gfx.Stroke}
	 */
	function normalizeStroke(stroke)
	{
		// let the null case return the defaultStroke
	//	if (!stroke)
	//		return null;

		if (typeof(stroke) == "string" || dojo.isArray(stroke) || stroke instanceof dojo.Color)
			stroke = { color: dojox.gfx.normalizeColor(stroke) };

		stroke = dojox.gfx.makeParameters(dojox.gfx.defaultStroke, stroke);
		stroke.color = dojox.gfx.normalizeColor(stroke.color);

		return stroke;
	}

	/**
	 * similar to code in dojo.gfx.shape.setFill()
	 * note that in contrast to stroke the returned object is not uniform, either a color or a gradient obj is returned
	 * @method normalizeFill
	 * @param {object|string|dojox.gfx.Fill} fill
	 * @return {dojo.Color|dojox.gfx.Gradient}
	 */
	function normalizeFill(fill)
	{
		if (!fill)
			return null; // => transparent (no)fill

		var f = null;
		if (typeof(fill) == "object" && "type" in fill)
		{
			// gradient or pattern
			switch (fill.type)
			{
				// todo: normalize the colors in the colors[] array inside fill also?
				case "linear":
					f = dojox.gfx.makeParameters(dojox.gfx.defaultLinearGradient, fill);
					break;
				case "radial":
					f = dojox.gfx.makeParameters(dojox.gfx.defaultRadialGradient, fill);
					break;
				case "pattern":
					f = dojox.gfx.makeParameters(dojox.gfx.defaultPattern, fill);
					break;
			}
		} else {
			// color object
			f = dojox.gfx.normalizeColor(fill);
		}
		return f;
	}

	/**
	 * @method getDOMContainer
	 * @param {dojox.gfx.Shape|dojox.gfx.Surface} shapeOrSurface
	 * @return {DOM}
	 */
	function getDOMContainer(shapeOrSurface)
	{
		var s = shapeOrSurface;
		while (typeof(s.getParent) == 'function'/* && s.getParent() != null*/)
			s = s.getParent();
		return s._parent;
	}

	/**
	 * todo: isn't it possible to get this correctly without the ref to the surface/node!? howto extract stable source or target node?
	 * (in SVG should be possible to walk e.target.parentNode->..)
	 * @method getMouseSurfacePosition
	 * @param {dojox.gfx.Shape|dojox.gfx.Surface} shapeOrSurface
	 * @param {Event} e
	 * @return {(x,y)}
	 */
	function getMouseSurfacePosition(shapeOrSurface, e)
	{
		var ev = Franson.Event.normalize(e);

		var div = getDOMContainer(shapeOrSurface);
		var offset = MochiKit.Style.getElementPosition(div);

		var mousePos = ev.mouse().page;
		var pos = { x: mousePos.x - offset.x, y: mousePos.y - offset.y };
		return pos;
	}

	/**
	 * todo: fix for silverlight
	 * @method hide
	 * @param {dojox.gfx.Shape} shape
	 */
	function hide(shape)
	{
		try
		{
			shape.getNode().style.display = 'none';
		}
		catch (e)
		{
			log("Current gfx layer doesn't support the style.display attribute");
		}
	}

	/**
	 * todo: fix for silverlight
	 * @method show
	 * @param {dojox.gfx.Shape} shape
	 */
	function show(shape)
	{
		try
		{
			shape.getNode().style.display = 'block';
		}
		catch (e)
		{
			log("Current gfx layer doesn't support the style.display attribute");
		}
	}

	/**
	 * todo: fix for silverlight
	 * @method isHidden
	 * @param {dojox.gfx.Shape} shape
	 * @return {boolean}
	 */
	function isHidden(shape)
	{
		try
		{
			//return shape.getNode().offsetWidth === 0 || shape.getNode().offsetHeight === 0; // todo: test this method
		//	// todo: implement as moving to/from to a hidden-group of the helper surface perhaps?
			return shape.getNode().style.display == 'none'; // == return !Franson.DOM.isBlockVisible(shape.getNode()); // (will not work in Silverlight or Canvas)
		//	//return shape.getNode().style.visibility = 'hidden';
		}
		catch (e)
		{
			log("Current gfx layer doesn't support the style.display attribute");
			return false;
		}
	}


	/**
	 * note that this is not guaranteed to work/show (similar to html title-attribute), thus allowed to silently fail.
	 * todo: works in FF & IE but not in Safari, Chrome or Opera (and will not work in Silverlight or Canvas)
	 * todo: implement using a custom mouse-over popup graphics object instead
	 * @method setTitle
	 * @param {dojox.gfx.Shape} shape
	 * @param {string} title
	 */
	function setTitle(shape, title)
	{
		// allow to silently fail (hmm, mochi already does this..)
		try
		{
			MochiKit.DOM.setNodeAttribute(shape.getNode(), 'title', title);
		}
		catch(e)
		{
			log("Current gfx layer doesn't support the title attribute");
		}
	}

	/**
	 * same limitations as setTitle
	 * @method setCursor
	 * @param {dojox.gfx.Shape} shape
	 * @param {string} cursor
	 */
	function setCursor(shape, cursor)
	{
		// todo: add IE workarounds for cursor type here? ('pointer'->'hand' etc)

		try // allow silent failure
		{
			MochiKit.Style.setStyle(shape.getNode(), { 'cursor': cursor });
		}
		catch(e)
		{
			log("Current gfx layer doesn't support the cursor attribute");
		}
	}


	/**
	 * convenience, also handles shapes with no transform set
	 * note that this is still the relative position, i.e not the fully evaluated world-position (see getScreenPosition)
	 * @method getPosition
	 * @param {dojox.gfx.Shape} shape
	 * @return {x,y}
	 */
	function getPosition(shape)
	{
		var m = shape.getTransform() || dojox.gfx.matrix.identity; // no transform behaves as identity but returns null
		return { x: m.dx, y: m.dy };
	}

	/**
	 * @method setPosition
	 * @param {dojox.gfx.Shape} shape
	 * @param {(x,y)} pos
	 */
	function setPosition(shape, pos)
	{
		var m = shape.getTransform() || {};
		m.dx = pos.x || 0;
		m.dy = pos.y || 0;
		shape.setTransform(m);
	}

	/**
	 * todo: create fromShapeToScreenCoord() and fromScreenToShapeCoord() functions using _realmatrix?
	 * @method getScreenPosition
	 * @param {dojox.gfx.Shape} shape
	 * @return {(x,y)}
	 */
	function getScreenPosition(shape) // hmm, getAbsolutePosition?, getSurfacePosition? (getWorldPosition?)
	{
		var m = shape._getRealMatrix(); // do we need an identity fallback here also?
		return { x: m.dx, y: m.dy };
	}


	/**
	 * updates the screen representation if the properties of the base-shape has changed.
	 * assumes shape is connected at a single level (todo: traverse entire structure? optional?)
	 * @method refreshShape
	 * @param {dojox.gfx.Shape}
	 */
	function refreshShape(shape)
	{
		shape.setShape(shape.getShape());
	}

	/**
	 * set Position, Rotation and Scale (sometimes called TRS (Translation))
	 * @method setPRS
	 * @param {dojxo.gfx.Shape} shape
	 * @param {(x,y)} pos
	 * @param {number} [rot=0] radians
	 * @param {number|(x,y)} [scale=1]
	 */
	function setPRS(shape, pos, rot, scale)
	{
		rot = rot || 0;
		scale = scale || 1; // (though scale can be an x,y obj also)

		var m = dojox.gfx.matrix.multiply(
			dojox.gfx.matrix.translate(pos),
			// todo: could perhaps skip the multiplication in the default case
			dojox.gfx.matrix.rotate(rot),
			dojox.gfx.matrix.scale(scale)
		);

		shape.setTransform(m);
	}


	/**
	 * to make all objects look like they have child nodes (interface) (DOM style)
	 * @method getChildren
	 * @param {dojox.gfx.Shape} shape
	 * @return {dojox.gfx.Shape[]}
	 */
	function getChildren(shape) // rename getChildNodes?
	{
		return (shape instanceof dojox.gfx.Group || shape instanceof dojox.gfx.Surface) ? shape.children : []; // or just shape.children || [] ?
	}


	/**
	 * see <a href="Franson.Iter.html#method_treePreOrderIter">Franson.Iter.treePreOrderIter</a>
	 * @method preOrderIter
	 * @param {dojox.gfx.Shape} shape
	 * @return {Iterable[dojox.gfx.Shape]}
	 */
	function preOrderIter(shape)
	{
		return Franson.Iter.treePreOrderIter(shape, getChildren);
	}

	/**
	 * see <a href="Franson.Iter.html#method_treeLevelOrderIter">Franson.Iter.treeLevelOrderIter</a>
	 * @method levelOrderIter
	 * @param {dojox.gfx.Shape} shape
	 * @return {Iterable[dojox.gfx.Shape]}
	 */
	function levelOrderIter(shape)
	{
		return Franson.Iter.treeLevelOrderIter(shape, getChildren);
	}

	/**
	 * see <a href="Franson.Iter.html#method_treePostOrderIter">Franson.Iter.treePostOrderIter</a>
	 * @method postOrderIter
	 * @param {dojox.gfx.Shape} shape
	 * @return {Iterable[dojox.gfx.Shape]}
	 */
	function postOrderIter(shape)
	{
		return Franson.Iter.treePostOrderIter(shape, getChildren);
	}

	/**
	 * child->parent traversal
	 * see <a href="Franson.Iter.html#method_leafParentIter">Franson.Iter.leafParentIter</a>
	 * @method parentIter
	 * @param {dojox.gfx.Shape} shape
	 * @return {Iterable[dojox.gfx.Shape]}
	 */
	function parentIter(shape)
	{
		return Franson.Iter.leafParentIter(shape, MochiKit.Base.methodcaller('getParent'));
	}


	/**
	 * deliberately Not named getBounds since it potentially does a lot of work.. (should be cached)
	 * todo: hmm, should the origin of groups be considered bounds extending? make optional?
	 * todo: calcBoundingSphere also?
	 * (note: radius of "safe" bounding sphere that allows full rotation of shape around any point within the OBB is: 1.5*Vec2.length(Vec2.sub(BB.max, BB.min)) )
	 * todo: needs special case for textShapes(?)
	 * @method calcBounds
	 * @param {dojox.gfx.Shape} shape
	 * @return {(x,y,w,h)}
	 */
	function calcBounds(shape)
	{
		var min = { x: +Number.MAX_VALUE, y: +Number.MAX_VALUE }; // todo: hmm, should be better to use the first elem's value for both (iterator gets messier though)
		var max = { x: -Number.MAX_VALUE, y: -Number.MAX_VALUE };

		forEach(preOrderIter(shape), function(shp) // could use any traversal order
		{
		/*	var bb = shp.getBoundingBox();
			if (bb != null) // todo: 'dojox.gfx.Text' needs special handling etc (all that return null..)
			{
				min.x = Math.min(min.x, bb.x);
				min.y = Math.min(min.y, bb.y);

				max.x = Math.max(max.x, bb.x + bb.width);
				max.y = Math.max(max.y, bb.y + bb.height);
			}
		*/
			// turn the OBB into an AABB (since OBB doesn't take scale into account this is necesssary..)
			// todo: this is not same (worse) as AABB with correct scale..
			var bb = shp.getTransformedBoundingBox();
			if (bb != null)
			{
				for (var i = 0; i < 4; ++i)
				{
					min.x = Math.min(min.x, bb[i].x);
					min.y = Math.min(min.y, bb[i].y);

					max.x = Math.max(max.x, bb[i].x);
					max.y = Math.max(max.y, bb[i].y);
				}
			}
		});

		var origo = getPosition(shape); // sometimes you want to subtract this, sometimes not..

		// todo: or return Franson.Geometry.Bounds? (same public properties)
		var bounds = {
			x: min.x - origo.x, y: min.y - origo.y,
		//	x: min.x, y: min.y,
			w: max.x - min.x, h: max.y - min.y
		};

		return bounds;
	}

	/**
	 * mostly to rebuild structures top->down to work around IE/VML bugs.. sigh..
	 * @method cloneShape
	 * @param {dojox.gfx.Shape} shape
	 * @param {dojox.gfx.Group|dojox.gfx.Surface} [parent=getCreator()] to help IE you need to supply the final destination here, otherwise it is optional (added to the getCreator() node)
	 * @return {dojox.gfx.Shape}
	 */
	function cloneShape(shape, parent) // rename deepClone? or just clone?
	{
		parent = parent || getCreator();
		// similar to dojox.gfx.utils.deserialize

 		var clone = (shape instanceof dojox.gfx.Group) ? parent.createGroup() : parent.createShape(shape.getShape());

		if (typeof(shape.getTransform) == 'function')
			clone.setTransform(shape.getTransform());

		if (typeof(shape.getStroke) == 'function')
			clone.setStroke(shape.getStroke());

		if (typeof(shape.getFill) == 'function')
			clone.setFill(shape.getFill());

		if (typeof(shape.getFont) == 'function')
			clone.setFont(shape.getFont());

		// copy possibly custom properties added by client (note that these are copied, not deep-cloned). ok?
		// hmm, skip functions? closures can't be re-bound..
		// (necessary to do this?)
		MochiKit.Base.setdefault(clone, shape);

		var children = getChildren(shape);
		for (var i = 0; i < children.length; ++i)
			cloneShape(clone, children[i]);

		return clone;
	}


	// public API
	return {
		getCreator: getCreator,
		destroy: destroy,

		getTextBoundingBox: getTextBoundingBox,

		normalizeStroke: normalizeStroke,
		normalizeFill: normalizeFill,

		hide: hide,
		show: show,
		isHidden: isHidden,

		getPosition: getPosition,
		setPosition: setPosition,

		getScreenPosition: getScreenPosition,

		refreshShape: refreshShape,

		getDOMContainer: getDOMContainer,
		getMouseSurfacePosition: getMouseSurfacePosition,

		setPRS: setPRS,

		setTitle: setTitle,
		setCursor: setCursor,

		getChildren: getChildren,

		preOrderIter: preOrderIter,
		levelOrderIter: levelOrderIter,
		postOrderIter: postOrderIter,
		parentIter: parentIter,

		calcBounds: calcBounds,

		cloneShape: cloneShape,

		/**
		 * IE specific. this is in practice invisible *but will still catch mouse events in IE* (which alpha=0 doesn't do..) (all other browsers handle a=0)
		 * @property smallestAlphaWithEvents
		 * @type float
		 * @final
		 */
		smallestAlphaWithEvents: 0.002 // todo: or actually detect IE and set it to 0 otherwise?
	};

})()); // Franson.Graphics

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