Source: chemdoc/kekule.attachedMarkers.js

/**
 * @fileoverview
 * Implementation of property markers (e.g. charge mark, lone pair, etc.) binding to the chem object.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /core/kekule.common.js
 * requires /core/kekule.structures.js
 * requires /core/kekule.chemUtils.js
 */

(function(){
"use strict";

var AU = Kekule.ArrayUtils;

/**
 * A recommended namespace for all attach marker classes
 * @namespace
 */
Kekule.ChemMarker = {};

// extend chemObject, enable associate descriptive glyphs to it
/** @ignore */
ClassEx.extend(Kekule.ChemObject,
/** @lends Kekule.ChemObject# */
{
	/**
	 * Returns whether current object is an attachedMarker of parent object.
	 * @returns {Bool}
	 */
	isAttachedMarker: function()
	{
		var p = this.getParent();
		return (p && p.hasMarker(this));
	},
	/**
	 * Notify {@link Kekule.ChemObject#attachedMarkers} property has been changed
	 * @private
	 */
	notifyAttachedMarkersChanged: function()
	{
		this.notifyPropSet('attachedMarkers', this.getPropStoreFieldValue('attachedMarkers'));
	},
	/** @private */
	_attachedMarkerAdded: function(marker)
	{
		// do nothing here
	},
	/** @private */
	_attachedMarkerRemoved: function(marker)
	{
		// do nothing here
	},

	/**
	 * Return count of attached markers.
	 * @returns {Int}
	 */
	getMarkerCount: function()
	{
		var markers = this.getPropStoreFieldValue('attachedMarkers');
		return markers? markers.length: 0;
	},
	/**
	 * Get attached marker at index.
	 * @param {Int} index
	 * @returns {Kekule.ChemObject}
	 */
	getMarkerAt: function(index)
	{
		var markers = this.getPropStoreFieldValue('attachedMarkers');
		return markers? markers[index]: null;
	},
	/**
	 * Returns the first child marker of a specified class type.
	 * @param {Class} classType
	 * @param {Bool} exactMatch If true, only marker of classType (not its descendants) should be returned.
	 * @returns {Kekule.ChemObject}
	 */
	getMarkerOfType: function(classType, exactMatch)
	{
		for (var i = 0, l = this.getMarkerCount(); i < l; ++i)
		{
			var marker = this.getMarkerAt(i);
			if ((exactMatch && marker.getClass() === classType) || (marker instanceof classType))
				return marker;
		}
		return null;
	},
	/**
	 * Returns all child markers of a specified class type.
	 * @param {Class} classType
	 * @param {Bool} exactMatch If true, only markers of classType (not its descendants) should be returned.
	 * @returns {Array}
	 */
	getMarkersOfType: function(classType, exactMatch)
	{
		var result = [];
		for (var i = 0, l = this.getMarkerCount(); i < l; ++i)
		{
			var marker = this.getMarkerAt(i);
			if ((exactMatch && marker.getClass() === classType) || (marker instanceof classType))
				result.push(marker);
		}
		return result;
	},
	/**
	 * Get attached marker of classType. If no such a marker currently and canCreate is true, a new marker will be created.
	 * @param {Class} classType
	 * @param {Bool} canCreate
	 * @param {Bool} exactMatch If true, only marker of classType (not its descendants) should be returned.
	 * @param {Hash} defProps If create a new marker, those prop values will be applied.
	 * @returns {Kekule.ChemObject}
	 */
	fetchMarkerOfType: function(classType, canCreate, exactMatch, defProps)
	{
		var result = this.getMarkerOfType(classType, exactMatch);
		if (!result && canCreate)
		{
			result = new classType();
			result.beginUpdate();
			try
			{
				if (defProps)
				{
					result.setPropValues(defProps);
				}
				//console.log('fetch create on', this.getId());
				this.appendMarker(result);
			}
			finally
			{
				result.endUpdate();
			}
		}
		return result;
	},
	/**
	 * Returns markers that has no coord on coordMode
	 * @param {Int} coordMode
	 * @return {Array}
	 */
	getUnplacedMarkers: function(coordMode)
	{
		var result = [];
		for (var i = 0, l = this.getMarkerCount(); i < l; ++i)
		{
			var marker = this.getMarkerAt(i);
			if (!marker.getCoordOfMode(coordMode))
				result.push(marker);
		}
		return result;
	},
	/**
	 * Get index of attached marker in marker array.
	 * @param {Kekule.ChemObject} marker
	 * @returns {Int}
	 */
	indexOfMarker: function(marker)
	{
		var markers = this.getPropStoreFieldValue('attachedMarkers');
		return markers? markers.indexOf(marker): -1;
	},
	/**
	 * Check if a marker has been attached to this object.
	 * @param {Kekule.ChemObject} marker
	 * @returns {Bool}
	 */
	hasMarker: function(marker)
	{
		return this.indexOfMarker(marker) >= 0;
	},
	/**
	 * Returns whether there exists child markers of a specified class type.
	 * @param {Class} classType
	 * @param {Bool} exactMatch If true, only markers of classType (not its descendants) should be considered.
	 * @returns {Bool}
	 */
	hasMarkerOfType: function(classType, exactMatch)
	{
		return !!this.getMarkerOfType(classType, exactMatch);
	},
	/**
	 * Attach a marker to this object. If marker already exists, nothing will be done.
	 * @param {Kekule.ChemObject} marker
	 */
	appendMarker: function(marker)
	{
		var index = this.indexOfMarker(marker);
		if (index >= 0) // already exists
			return index;// do nothing
		else
		{
			var result = this.getAttachedMarkers(true).push(marker);
			marker.beginUpdate();
			try
			{
				if (marker.setOwner)
					marker.setOwner(this.getOwner());
				if (marker.setParent)
					marker.setParent(this);
				this._attachedMarkerAdded(marker);
			}
			finally
			{
				marker.endUpdate();
			}
			this.notifyAttachedMarkersChanged();
			return result;
		}
	},
	/**
	 * Insert marker before refMarker in marker list. If refMarker is null or does not exists, marker will be append to tail of list.
	 * @param {Kekule.ChemObject} marker
	 * @param {Kekule.ChemObject} refMarker
	 * @return {Int} Index of obj after inserting.
	 */
	insertMarkerBefore: function(marker, refMarker)
	{
		var refIndex = this.indexOfMarker(refMarker);
		return this.insertMarkerAt(marker, refIndex);
	},
	/**
	 * Insert marker to index. If index is not set, marker will be inserted to the tail of the marker array.
	 * @param {Kekule.ChemObject} marker
	 * @param {Int} index
	 */
	insertMarkerAt: function(marker, index)
	{
		var i = this.indexOfMarker(marker);
		var markers = this.getAttachedMarkers(true);
		if (Kekule.ObjUtils.isUnset(index) || (index < 0))
			index = markers.length;
		if (i >= 0)  // already inside, adjust position
		{
			markers.splice(i, 1);
			markers.splice(index, 0, marker);
		}
		else // new one
		{
			markers.splice(index, 0, marker);
			if (marker.setOwner)
				marker.setOwner(this.getOwner());
			if (marker.setParent)
				marker.setParent(this);
			this._attachedMarkerAdded(marker);
		}
		this.notifyAttachedMarkersChanged();
		return index;
	},
	/**
	 * Change index of marker.
	 * @param {Kekule.ChemObject} marker
	 * @param {Int} index
	 */
	setMarkerIndex: function(marker, index)
	{
		var i = this.indexOfMarker(marker);
		if (i >= 0)  // already inside, adjust position
		{
			var markers = this.getPropStoreFieldValue('attachedMarkers'); // this.getAttachedMarkers();
			markers.splice(i, 1);
			markers.splice(index, 0, marker);
		}
	},
	/**
	 * Remove marker at index in attached marker list.
	 * @param {Int} index
	 */
	removeMarkerAt: function(index)
	{
		var marker = this.getMarkerAt(index);
		if (marker)
		{
			var result = this.getAttachedMarkers(true).splice(index, 1);
			if (marker.setOwner)
				marker.setOwner(null);
			if (marker.setParent)
				marker.setParent(null);
			this._attachedMarkerRemoved(marker);
			this.notifyAttachedMarkersChanged();
			return result;
		}
	},
	/**
	 * Remove an attached marker.
	 * @param {Kekule.ChemObject} marker
	 */
	removeMarker: function(marker)
	{
		var index = this.indexOfMarker(marker);
		if (index >= 0)
			return this.removeMarkerAt(index);
	},
	/**
	 * Replace oldMarker with new one.
	 * @param {Kekule.ChemObject} oldMarker
	 * @param {Kekule.ChemObject} newMarker
	 */
	replaceMarker: function(oldMarker, newMarker)
	{
		var oldIndex = this.indexOfMarker(oldMarker);
		if (oldIndex < 0)  // old marker not exists
		{
			return this;
		}
		else
		{
			this.removeMarkerAt(oldIndex);
			this.insertMarkerAt(newMarker, oldIndex);
			return this;
		}
	},
	/**
	 * Remove all attached markers.
	 */
	clearMarkers: function()
	{
		//var oldMarkers = AU.clone(this.getAttachedMarkers());
		var oldMarkers = this.getPropStoreFieldValue('attachedMarkers');
		if (oldMarkers)
			oldMarkers = AU.clone(oldMarkers);

		this.setPropStoreFieldValue('attachedMarkers', null);

		if (oldMarkers)
		{
			for (var i = 0, l = oldMarkers.length; i < l; ++i)
			{
				var marker = oldMarkers[i];
				if (marker.setOwner)
					marker.setOwner(null);
				if (marker.setParent)
					marker.setParent(null);
				this._attachedMarkerRemoved(marker);
			}
		}

		this.notifyAttachedMarkersChanged();
		return this;
	},

	autoSetMarker2DPos: function(marker, offset, allowCoordBorrow, avoidDirectionAngles)
	{
		if (this.hasMarker(marker))
		{
			var angle = Kekule.ChemStructureUtils.getMostEmptyDirection2DAngleOfObj(this, [marker], allowCoordBorrow, true, false, avoidDirectionAngles);
			var coord = {'x': offset * Math.cos(angle), 'y': offset * Math.sin(angle)};
			//console.log('angle2', coord);
			marker.setCoord2D(coord);
		}
	},

	/** @private */
	_updateAttachedMarkersOwner: function(owner)
	{
		if (!owner)
			owner = this.getOwner();
		for (var i = 0, l = this.getMarkerCount(); i < l; ++i)
		{
			var marker = this.getMarkerAt(i);
			if (marker.setOwner)
				marker.setOwner(owner);
		}
	},
	/** @private */
	_updateAttachedMarkersParent: function(parent)
	{
		if (!parent)
			parent = this;
		/*
		if (this.getMarkerCount() > 0)
			console.log('set parent', parent.getClassName());
    */
		for (var i = 0, l = this.getMarkerCount(); i < l; ++i)
		{
			var marker = this.getMarkerAt(i);
			if (marker.setParent)
				marker.setParent(parent);
		}
	}
});
ClassEx.extendMethod(Kekule.ChemObject, 'ownerChanged', function($origin, newOwner){
	$origin(newOwner);
	this._updateAttachedMarkersOwner(newOwner);
});
ClassEx.extendMethod(Kekule.ChemObject, 'removeChild', function($origin, child){
	//console.log('remove child', child.getClassName(), child.getId());
	var result = $origin(child);
	if (!result)
		result = this.removeMarker(child) || $origin(child);
	return result;
});
ClassEx.extendMethod(Kekule.ChemObject, 'insertBefore', function($origin, child, refChild){
	var result = $origin(child, refChild);
	if (result < 0)
	{
		if (refChild && this.hasMarker(refChild) || child instanceof Kekule.ChemMarker.BaseMarker)
			result = this.insertMarkerBefore(child, refChild);
	}
	return result;
});

ClassEx.defineProp(Kekule.ChemObject, 'attachedMarkers',
{
	'dataType': DataType.ARRAY, 'scope':  Class.PropertyScope.PUBLISHED,
	'getter': function(autoCreate)
	{
		var result = this.getPropStoreFieldValue('attachedMarkers');
		if (!result && autoCreate)
		{
			result = [];
			this.setPropStoreFieldValue('attachedMarkers', result);
		}
		return result;
	},
	'setter': function(value)
	{
		//console.log('set markers', value, this.getClassName());
		this.clearMarkers();
		this.setPropStoreFieldValue('attachedMarkers', value);
		this._updateAttachedMarkersOwner();
		this._updateAttachedMarkersParent();
		//console.log('after set', this.getAttachedMarkers());
	}
});
/*
// if true, position of newly added marker will be set automatically
ClassEx.defineProp(Kekule.ChemObject, 'autoSetAttachedMarkerPos',
{
	'dataType': DataType.BOOL, 'scope':  Class.PropertyScope.PUBLISHED
});
*/


})();