/**
* 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