Source: render/kekule.render.boundInfoRecorder.js

/**
 * @fileoverview
 * This file contains a helper class to record all bound infos of basic object
 * during the rendering of chem object.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /core/kekule.common.js
 * requires /utils/kekule.utils.js
 * requires /render/kekule.render.utils.js
 * requires /render/kekule.render.base.js
 */

/**
 * A helper class to record all bound info during the renderering of a renderer or painter.
 *
 * NOTE: current implementation of this class use may hidden details of TwoTupleMapEx.
 * Should changed in the future.
 * @class
 * @augments ObjectEx
 * @param {Object} rendererOrPainter Renderer or painter that do the actual render job.
 *
 * @property {Array} boundInfos Bound infos recorded.
 * @property {Object} targetContext If this property is set, only bound info on this context will be recorded.
 */
/**
 * Invoked when a basic object (node, connector, glyph...) is drawn, updated or removed by renderer.
 *   event param of it has fields: {obj, parentObj, boundInfo, updateType}
 *   where boundInfo provides the bound box information of this object on context. It has the following fields:
 *   {
 *     context: drawing context object
 *     obj: drawn object
 *     parentObj: parent of drawn object
 *     boundInfo: a hash containing info of bound, including fields:
 *     {
	 *     shapeType: value from {@link Kekule.Render.MetaShapeType} or {@link Kekule.Render.Meta3DShapeType}.
 *     coords: [Array of coords]
 *   }
 *     updateType: add, modify or remove
 *   }
 *   boundInfo may also be a array for complex situation (such as multicenter bond): [boundInfo1, boundInfo2, boundInfo3...].
 *   Note that in removed event, boundInfo may be null.
 * @name Kekule.Render.BoundInfoRecorder#updateBasicDrawObject
 * @event
 */
Kekule.Render.BoundInfoRecorder = Class.create(ObjectEx,
/** @lends Kekule.Render.BoundInfoRecorder# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Render.BoundInfoRecorder',
	/** @constructs */
	initialize: function($super, rendererOrPainter)
	{
		$super();
		this.setPropStoreFieldValue('boundInfos', new Kekule.TwoTupleMapEx(true));
		this._renderer = rendererOrPainter;  // used internally
		//console.log('boundInfoCreated', rendererOrPainter);
		this.installEventListener(rendererOrPainter);
	},
	/** @ignore */
	finalize: function($super)
	{
		var infos = this.getPropStoreFieldValue('boundInfos');
		if (infos)
			infos.clear();
		$super();
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('boundInfos', {'dataType': 'Kekule.TwoTupleMapEx', 'serializable': false, 'setter': null});
		this.defineProp('targetContext', {'dataType': DataType.OBJECT, 'serializable': false});
	},

	/** @private */
	needRecordOnContext: function(context)
	{
		var target = this.getTargetContext();
		return (!target) || (target === context);
	},

	/** @private */
	installEventListener: function(renderer)
	{
		renderer.addEventListener('updateBasicDrawObject', function(e)
			{
				var OT = Kekule.Render.ObjectUpdateType;
				if (this.needRecordOnContext(e.context))
				{
					var utype = e.updateType;
					if (utype === OT.ADD)
						this.add(e.context, e.obj, e.parentObj, e.boundInfo, e.target);
					else if (utype === OT.MODIFY)
						this.modify(e.context, e.obj, e.parentObj, e.boundInfo, e.target);
					else if (utype === OT.REMOVE)
					{
						//console.log('object removed', e.obj.getClassName());
						this.remove(e.context, e.obj);
					}
					else if (utype === OT.CLEAR)
						this.clear(e.context);
					//console.log(utype, e.context.canvas.parentNode.className, e.obj.getClassName(), e.parentObj.getClassName(), e.boundInfo);
					/*
					if (!e.boundInfo)
						console.log('no boundInfo', e);
					*/
					this.invokeEvent('updateBasicDrawObject', e);
				}
			}, this
		);
		renderer.addEventListener('clear', function(e)
			{
				//console.log('[RECEIVE CLEAR]', e.target.getClassName());
				if (this.needRecordOnContext(e.context))
					//this.clear(e.context);
					this.clearOnRenderer(e.context, e.target);
				var infos = this.getAllRecordedInfoOfContext(e.context);
				//console.log(infos.length, infos);
			}, this
		);
	},

	/**
	 * Returns recorded info item of obj in context.
	 * @param {Object} context
	 * @param {Object} obj
	 * @returns {Object}
	 */
	getInfo: function(context, obj)
	{
		return this.getBoundInfos().get(context, obj);
	},
	/**
	 * Returns recorded info items based on object, or based on all children of parent object.
	 * @param {Object} context
	 * @param {Object} objOrParentObj
	 * @returns {Array}
	 */
	getBelongedInfos: function(context, objOrParentObj)
	{
		var info = this.getInfo(context, objOrParentObj);
		if (info)
			return [info];
		else  // info map to obj not found, search all infos and check parent objects
		{
			var result = [];
			var infos = this.getAllRecordedInfoOfContext(context);
			for (var i = 0, l = infos.length; i < l; ++i)
			{
				var info = infos[i];
				var currObj = info.obj;
				if (currObj.isChildOf(objOrParentObj))
				{
					result.push(info);
				}
			}
			return result;
		}
	},
	/**
	 * Returns recorded bound info of obj in context.
	 * If direct bound not found, this method will return the container box of all bound info of obj's children.
	 * @param {Object} context
	 * @param {Object} obj
	 * @returns {Object}
	 */
	getBound: function(context, obj)
	{
		var info = this.getInfo(context, obj);
		var result = info? info.boundInfo: null;

		if (!result)
		{
			var BU = Kekule.BoxUtils;
			var MU = Kekule.Render.MetaShapeUtils;
			var infos = this.getBelongedInfos(context, obj);
			var containerBox = null;
			for (var i = 0, l = infos.length; i < l; ++i)
			{
				var info = infos[i];
				var box = MU.getContainerBox(info.boundInfo, 0);
				containerBox = containerBox? BU.getContainerBox(containerBox, box): box;
			}
			if (containerBox)
			{
				result = MU.createShapeInfo(Kekule.Render.MetaShapeType.RECT,
					[{'x': containerBox.x1, 'y': containerBox.y1},
					{'x': containerBox.x2, 'y': containerBox.y2}]
				);
				//console.log(containerBox, result, MU.getContainerBox(result, 0));
			}
		}

		return result;
	},

	/**
	 * Clear recorded info of renderer on context.
	 * @param {Object} context
	 * @param {Object} renderer
	 */
	clearOnRenderer: function(context, renderer)
	{
		var infos = this.getBoundInfos();
		var map = infos.getSecondLevelMap(context);
		if (map)
		{
			var objs = map.getKeys();
			var infos = map.getValues();
			//console.log('before remove: ', objs.length);
			//var count = 0;
			for (var i = objs.length - 1; i >= 0; --i)
			{
				var obj = objs[i];
				var info = infos[i];
				if (info.renderer === renderer)
				{
					map.remove(obj);
					//++count;
					//console.log('<clear info on renderer>', renderer.getClassName(), obj.getClassName());
				}
			}
			//console.log('after remove: ', objs.length, count);
		}
	},
	/**
	 * Clear bound infos in a context.
	 * @param {Object} context
	 */
	clear: function(context)
	{
		var infos = this.getBoundInfos();
		var map = infos.getSecondLevelMap(context);
		if (map)
			map.clear();
		return this;
	},
	/**
	 * Clear bound infos in all context.
	 */
	clearAll: function()
	{
		this.getBoundInfos().clear();  // clear all info
		return this;
	},
	/**
	 * Add an info item to list.
	 * @param {Object} context
	 * @param {Object} obj
	 * @param {Object} parentObj
	 * @param {Object} boundInfo
	 * @param {Object} renderer
	 */
	add: function(context, obj, parentObj, boundInfo, renderer)
	{
		this.getBoundInfos().set(context, obj, {'obj': obj, 'parentObj': parentObj, 'boundInfo': boundInfo, 'renderer': renderer});
		return this;
	},
	/**
	 * Modify bound info in list.
	 * @param {Object} context
	 * @param {Object} obj
	 * @param {Object} parentObj
	 * @param {Object} boundInfo
	 * @param {Object} renderer
	 */
	modify: function(context, obj, parentObj, boundInfo, renderer)
	{
		var item = this.getBoundInfos().get(context, obj);
		if (!item)
		{
			item = {};
			this.getBoundInfos.set(context, obj, item);
		}
		//this.getBoundInfos().set(context, obj, boundInfo);
		var notUnset = Kekule.ObjUtils.notUnset;
		if (notUnset(parentObj))
			item.parentObj = parentObj;
		if (notUnset(boundInfo))
			item.boundInfo = boundInfo;
		if (notUnset(renderer))
			item.renderer = renderer;
		return this;
	},
	/**
	 * Remove an info item in map.
	 * @param {Object} context
	 * @param {Object} obj
	 * @param {Object} parentObj
	 */
	remove: function(context, obj, parentObj)
	{
		this.getBoundInfos().remove(context, obj);
		return this;
	},

	/**
	 * Returns an array that contains bound / obj / parentObj / renderer information in a certain context.
	 * @param {Object} context
	 * @returns {Array}
	 */
	getAllRecordedInfoOfContext: function(context)
	{
		var infos = this.getBoundInfos();
		var map = infos.getSecondLevelMap(context);
		if (!map)
			return [];
		else
		{
			// since this is not a weak map, we can get its array properties
			return map.getValues();
		}
	},
	/**
	 * Returns an bound info array in a certain context.
	 * @param {Object} context
	 * @returns {Array}
	 */
	getAllBoundInfosOfContext: function(context)
	{
		var infos = this.getAllRecordedInfoOfContext(context);
		var result = [];
		for (var i = 0, l = infos.length; i < l; ++i)
		{
			var boundInfo = infos[i].boundInfo;
			if (boundInfo)
				result.push(boundInfo);
		}
		return result;
	},
	/**
	 * Returns a minimal box that can contains all drawn elements in context.
	 * @param {Object} context
	 * @returns {Hash}
	 */
	getContainerBox: function(context)
	{
		var BU = Kekule.BoxUtils;
		var MU = Kekule.Render.MetaShapeUtils;
		var result = null;
		var infos = this.getAllBoundInfosOfContext(context);
		for (var i = 0, l = infos.length; i < l; ++i)
		{
			var box = MU.getContainerBox(info[i], 0);
			result = BU.getContainerBox(result, box);
		}
		return result;
	},

	/**
	 * Get all intersected bound and related informations on coord.
	 * @param {Object} context
	 * @param {Hash} coord
	 * @param {Hash} refCoord This value is used in 3D mode. Passes null to it in 2D mode.
	 * @param {Int} inflation
	 * @returns {Array}
	 */
	getIntersectionInfos: function(context, coord, refCoord, inflation, filterFunc)
	{
		var result = [];
		// TODO: now only handles 2D itersection
		var infos = this.getAllRecordedInfoOfContext(context);

		for (var i = 0, l = infos.length; i < l; ++i)
		{
			var bound = infos[i].boundInfo;
			if (filterFunc && !filterFunc(infos[i]))
			{
				//console.log('filtered out', infos[i]);
				continue;
			}
			if (bound)
			{
				if (Kekule.Render.MetaShapeUtils.isCoordInside(coord, bound, inflation))
					result.push(infos[i]);
			}
		}
		return result;
	}
});