UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

4,497 lines 146 kB
(function(){

"use strict";

var PS = Class.PropertyScope;
var AU = Kekule.ArrayUtils;
var KUnit = Kekule.Unit;

Kekule.globalOptions.add('spectrum', {
	spectrumInfo: {
		enablePrefixOmissionInGetter: true
	},
	data: {
		allowedComparisonErrorRate: 5e-8
	}
});

/**
 * A comparison flag, comparing the key properties and data of spectrum only.
 * @ignore
 */
Kekule.ComparisonMethod.SPECTRUM_DATA = 30;

/**
 * Base namespace for spectra.
 * @namespace
 */
Kekule.Spectroscopy = {};

/**
 * Enumeration of data mode of spectrum variable.
 * @enum
 */
Kekule.Spectroscopy.DataMode = {
	/** Value points are continuous, e.g. in IR data table. */
	CONTINUOUS: 1,
	/** Value points are discrete, e.g. in MS peak table. */
	PEAK: 2
};

/**
 * Enumeration of peak position in spectrum data curve.
 * @enum
 */
Kekule.Spectroscopy.DataPeakPosition = {
	MAX: 0,
	MIN: 1
};

/**
 * Some util methods about spectroscopy.
 * @class
 */
Kekule.Spectroscopy.Utils = {
	/**
	 * Returns the preferred peak position of spectrum.
	 * @param {Variant} spectrumOrDataSection
	 * @returns {Int} Value from {@link Kekule.Spectroscopy.DataPeakPosition}.
	 */
	getSpectrumPeakPosition: function(spectrumOrDataSection)
	{
		var stype = (spectrumOrDataSection.getSpectrumType)? spectrumOrDataSection.getSpectrumType():
			(spectrumOrDataSection.getParentSpectrum() && spectrumOrDataSection.getParentSpectrum().getSpectrumType());
		return (stype === Kekule.Spectroscopy.SpectrumType.IR)?
			Kekule.Spectroscopy.DataPeakPosition.MIN:
			Kekule.Spectroscopy.DataPeakPosition.MAX;
	},
	/**
	 * Expand range with new values.
	 * @param {Hash} range
	 * @param {Array} values
	 */
	expandDataRange: function(range, values)
	{
		var vs = AU.toArray(values);
		var r = range || {};
		for (var i = 0, ii = vs.length; i < ii; ++i)
		{
			var value = vs[i];
			var fields = Kekule.ObjUtils.getOwnedFieldNames(value);
			for (var j = 0, jj = fields.length; j < jj; ++j)
			{
				var fieldName = fields[j];
				var fieldValue = value[fieldName];
				if (!r[fieldName])
					r[fieldName] = {'min': fieldValue, 'max': fieldValue};
				else
				{
					if (fieldValue < r[fieldName].min)
						r[fieldName].min = fieldValue;
					else if (fieldValue > r[fieldName].max)
						r[fieldName].max = fieldValue;
				}
			}
		}
		return r;
	},
	/**
	 * Merge two data ranges.
	 * Each item is a hash like {x: {min: minValue, max: maxValue}, y: {min: minValue, max: maxValue}}.
	 * @param {Hash} r1
	 * @param {Hash} r2
	 * @returns {Hash}
	 */
	mergeDataRange: function(r1, r2)
	{
		var result = {};
		var vars = AU.clone(Kekule.ObjUtils.getOwnedFieldNames(r1));
		AU.pushUnique(vars, Kekule.ObjUtils.getOwnedFieldNames(r2));
		for (var i = 0, l = vars.length; i < l; ++i)
		{
			var varSymbol =vars[i];
			if (!r1[varSymbol])
				result[varSymbol] = Object.extend(r2[varSymbol]);
			else if (!r2[varSymbol])
				result[varSymbol] = Object.extend(r1[varSymbol]);
			else
			{
				result[varSymbol] = {
					'min': (r1[varSymbol].min < r2[varSymbol].min)? r1[varSymbol].min: r2[varSymbol].min,
					'max': (r1[varSymbol].max > r2[varSymbol].max)? r1[varSymbol].max: r2[varSymbol].max
				}
			}
		}
		return result;
	},

	/**
	 * Returns scale point information for a data range.
	 * @param {Number} dataRangeMin
	 * @param {Number} dataRangeMax
	 * @param {Int} preferredScaleSectionCount
	 * @returns {Hash}
	 */
	calcScalePointInfo: function(dataRangeMin, dataRangeMax, preferredScaleSectionCount)
	{
		if (preferredScaleSectionCount <= 0)
			preferredScaleSectionCount = 10;   // avoid exception, set a default count value here
		var digitCounts = [Math.log10(Math.abs(dataRangeMin)), Math.log10(Math.abs(dataRangeMax))];
		var digitCountMax = Math.floor(Math.max(digitCounts[0], digitCounts[1]));
		var digitCountMin = (Math.sign(dataRangeMin) === Math.sign(dataRangeMax))? Math.floor(Math.min(digitCounts[0], digitCounts[1], 0)): -Infinity;
		var useSciForm = (digitCountMax > 6);  // need to use sci form if the digit num is very large to compact space

		var dataDelta = dataRangeMax - dataRangeMin;
		var deltaBetweenScales = dataDelta / preferredScaleSectionCount;
		var deltaBetweenScalesDigitCount = Math.max(Math.floor(Math.log10(Math.abs(deltaBetweenScales))), digitCountMin);
		var scaleBase = Math.pow(10, deltaBetweenScalesDigitCount);
		var actualDeltaBetweenScales;
		if (actualDeltaBetweenScales < 10 && dataDelta > 0.5)  // major scale should be even number in 1-10 scope
		{
			actualDeltaBetweenScales = Math.ceil(actualDeltaBetweenScales / scaleBase / 2) * 2 * scaleBase;
		}
		else
		{
			actualDeltaBetweenScales = Math.ceil(deltaBetweenScales / scaleBase) * scaleBase;
		}
		var scaleFrom = Math.ceil(dataRangeMin / actualDeltaBetweenScales) * actualDeltaBetweenScales;
		var scaleTo = Math.floor(dataRangeMax / actualDeltaBetweenScales) * actualDeltaBetweenScales;
		var result = {
			'useSciForm': useSciForm,
			'scaleFrom': scaleFrom,
			'scaleTo': scaleTo,
			'scaleSectionCount': Math.round((scaleTo - scaleFrom) / actualDeltaBetweenScales),
			'scaleValues': [],
			'scaleBase': scaleBase,
			'scaleFromOnBase': scaleFrom / scaleBase,
			'scaleToOnBase': scaleTo / scaleBase,
			'fixDigitsCountAfterPoint': Math.max(-deltaBetweenScalesDigitCount, 0)  // record the recommended digits to appear after the decimal point
		};
		for (var i = 0, l = result.scaleSectionCount + 1; i < l; ++i)
		{
			result.scaleValues.push(Math.round(i * actualDeltaBetweenScales / scaleBase) * scaleBase + scaleFrom);
		}
		//console.log(result, scaleBase);
		return result;
	},

	/*
	 * Calculate the standardized distance square between two data points.
	 * The srcData/targetData/dataRanges should have the same dimension.
	 * @param {Array} srcData
	 * @param {Array} targetData
	 * @param {Array} dataRangeValues Array of {min, max}.
	 * @returns {Float}
	 */
	/*
	calcStandardizedDistanceSqr: function(srcData, targetData, dataRangeValues)
	{
		var length = dataRanges.length;
		for (var i = 0; i < length; ++i)
		{

		}
	},
	*/

	/**
	 * Generate a suitable label key for storing the spectrum info(meta/condition/parameter...) value.
	 * @param {String} name The core name of key, e.g. observedFrequency.
	 * @param {String} spectrumType The spectrum type, e.g. 'NMR'.
	 * @param {String} namespace The namespace of this label, e.g. 'jcamp', 'cml'.
	 * @returns {String}
	 */
	generateInfoKey: function(name, spectrumType, namespace)
	{
		var prefix = spectrumType || namespace;
		return prefix? prefix + MetaPropNamespace.DELIMITER + name: name;
	},

	/**
	 * Returns the default class to storing extra information for a data item in dataSection.
	 * @param {Kekule.Spectroscopy.SpectrumDataSection} dataSection
	 */
	getDefaultDataExtraInfoClass: function(dataSection)
	{
		var mode = dataSection.getMode();
		return (mode === Kekule.Spectroscopy.DataMode.PEAK)? Kekule.Spectroscopy.SpectrumPeakDetails: Kekule.Spectroscopy.SpectrumDataDetails;
	},
	/**
	 * Create a default object to storing extra information for a data item in dataSection.
	 * @param {Kekule.Spectroscopy.SpectrumDataSection} dataSection
	 */
	createDefaultDataExtraInfoObject: function(dataSection)
	{
		var c = Kekule.Spectroscopy.Utils.getDefaultDataExtraInfoClass(dataSection);
		return c && new c();
	}
};

/**
 * Manager for namespaces of spectrum info property name.
 * @enum
 */
Kekule.Spectroscopy.MetaPropNamespace = {
	/** Namespace for custom properties */
	CUSTOM: 'custom',
	/** Delimiter for namespace parts */
	DELIMITER: '.',
	/** @private */
	_namespaces: [],
	/**
	 * Register namespace(s).
	 * @param {Variant} namespace
	 */
	register: function(namespace)
	{
		AU.pushUnique(MetaPropNamespace._namespaces, namespace);
	},
	/**
	 * Unregister namespace(s).
	 * @param {Variant} namespace
	 */
	unregister: function(namespace)
	{
		MetaPropNamespace._namespaces = AU.exclude(MetaPropNamespace._namespaces, namespace);
	},
	/**
	 * Returns all registered namespaces.
	 * @returns {Array}
	 */
	getNamespaces: function()
	{
		return AU.clone(MetaPropNamespace._namespaces);
	},
	/**
	 * Returns a namespaced name of property.
	 * @param {String} namespace
	 * @param {String} coreName
	 * @returns {string}
	 */
	createPropertyName: function(namespace, coreName)
	{
		return (namespace || '') + MetaPropNamespace.DELIMITER + coreName;
	},
	/**
	 * Returns the namespace/core part of a prop name.
	 * @param {String} propName
	 * @returns {Hash} A hash of {namespace, coreName}.
	 */
	getPropertyNameDetail: function(propName)
	{
		var namespace, coreName;
		var p = propName.lastIndexOf(MetaPropNamespace.DELIMITER);
		if (p >= 0)
		{
			namespace = propName.substring(0, p);
			coreName = propName.substr(p + 1);
		}
		else
		{
			coreName = propName;
		}
		return {'namespace': namespace, 'coreName': coreName};
	},
	/**
	 * Returns the core part of a prop name.
	 * @param {String} propName
	 * @returns {String}
	 */
	getPropertyCoreName: function(propName)
	{
		return MetaPropNamespace.getPropertyNameDetail(propName).coreName;
	}
};
/** @ignore */
var MetaPropNamespace = Kekule.Spectroscopy.MetaPropNamespace;

MetaPropNamespace.register(MetaPropNamespace.CUSTOM);

/**
 * A util object to manage the registered spectrum data value converters.
 * These converters are used to convert raw spectrum value from one unit to another (e.g., Hz to ppm in NMR).
 * @class
 */
Kekule.Spectroscopy.DataValueConverterManager = {
	/** @private */
	_converters: [],
	/**
	 * Register a converter object.
	 * The converter object should implement the following methods:
	 * {
	 *   convert: function(value, varDef, fromUnitObj, toUnitObj, spectrumDataSection, spectrum) => newValue,
	 *   canConvert: function(value, varDef, fromUnitObj, toUnitObj, spectrumDataSection, spectrum) => Bool,
	 *   getAltUnits: function(varDef, fromUnitObj, spectrumDataSection, spectrum) -> array (optional), returns the recommended alternative unitObjs for spectrum
	 * }
	 * @param {Object} converter
	 */
	register: function(converter)
	{
		DCM._converters.push(converter);
	},
	/**
	 * Unregister a converter.
	 * @param {Object} converter
	 */
	unregister: function(converter)
	{
		var index = DMC._converters.indexOf(converter);
		if (index >= 0)
			DMC._converters.splice(index, 1);
	},

	/** @private */
	doConvert: function(value, varDef, fromUnit, toUnit, spectrumDataSection, spectrum)
	{
		if (fromUnit === toUnit)
			return value;
		if (!Kekule.NumUtils.isNormalNumber(value))
			return value;
		var converters = DCM._converters;
		if (converters.length)
		{
			var fromUnitObj = Kekule.Unit.getUnit(fromUnit);
			var toUnitObj = Kekule.Unit.getUnit(toUnit);
			if (fromUnitObj && toUnitObj)
			{
				for (var i = converters.length - 1; i >= 0; --i)
				{
					var converter = converters[i];
					if (converter.canConvert(value, varDef, fromUnitObj, toUnitObj, spectrumDataSection, spectrum))
						return converter.convert(value, varDef, fromUnitObj, toUnitObj, spectrumDataSection, spectrum);
				}
			}
		}
		// no available converter found, can not convert
		Kekule.error(Kekule.$L('ErrorMsg.UNABLE_TO_CONVERT_BETWEEN_UNITS').format(fromUnitObj.getKey(), toUnitObj.getKey()));
		return null;
	},
	/** @private */
	getAltUnits: function(varDef, fromUnit, spectrumDataSection, spectrum)
	{
		var result = [];
		var converters = DCM._converters;
		if (converters.length)
		{
			var fromUnitObj = Kekule.Unit.getUnit(fromUnit);
			if (fromUnitObj)
			{
				for (var i = converters.length - 1; i >= 0; --i)
				{
					var converter = converters[i];
					var subResult = converter.getAltUnits(varDef, fromUnitObj, spectrumDataSection, spectrum) || [];
					AU.pushUnique(result, subResult);
				}
			}
		}
		return result;
	}
};
/** @ignore */
var DCM = Kekule.Spectroscopy.DataValueConverterManager;

// register the default data value converter
DCM.register({
	convert: function(value, varDef, fromUnitObj, toUnitObj, spectrumDataSection, spectrum)
	{
		return fromUnitObj.convertValueTo(value, toUnitObj);
	},
  canConvert: function(value, varDef, fromUnitObj, toUnitObj, spectrumDataSection, spectrum)
	{
		return fromUnitObj.canConvertValueTo(toUnitObj);
	},
	getAltUnits: function(varDef, fromUnitObj, spectrumDataSection, spectrum)
	{
		var category = fromUnitObj.category;
		return category.getConvertableUnits();
	}
});
// register a converter to convert between NMR frequency and ppm
DCM.register({
	convert: function(value, varDef, fromUnitObj, toUnitObj, spectrumDataSection, spectrum)
	{
		var observeFreq = spectrum.getParameter('NMR.ObserveFrequency');
		if (fromUnitObj.category === KUnit.Frequency)  // from Hz to ppm
		{
			var freq = fromUnitObj.convertValueTo(value, observeFreq.getUnit());
			var pureRatio = freq / observeFreq.getValue();  // in ppm * 1e10, in another word, the pure ratio
			return KUnit.Dimensionless.ONE.convertValueTo(pureRatio, toUnitObj);
		}
		else if (fromUnitObj.category === Kekule.Unit.Dimensionless)  // from ppm to Hz
		{
			var value2 = fromUnitObj.convertValueToStandard(value);
			var freq = value2 * observeFreq.getValue();
			var freqUnit = KUnit.getUnit(observeFreq.getUnit());
			return freqUnit.convertValueTo(freq, toUnitObj);
		}
	},
	canConvert: function(value, varDef, fromUnitObj, toUnitObj, spectrumDataSection, spectrum)
	{
		if (spectrum.getSpectrumType() === Kekule.Spectroscopy.SpectrumType.NMR)
		{
			var observeFreq = spectrum.getParameter('NMR.ObserveFrequency');
			var freqNum = observeFreq && observeFreq.getValue();
			if (observeFreq && freqNum && Kekule.NumUtils.isNormalNumber(freqNum) && Kekule.Unit.getUnit(observeFreq.getUnit()).category === Kekule.Unit.Frequency)
			{
				return (fromUnitObj.category === Kekule.Unit.Frequency && toUnitObj.category === Kekule.Unit.Dimensionless)
					|| (fromUnitObj.category === Kekule.Unit.Dimensionless && toUnitObj.category === Kekule.Unit.Frequency);
			}
		}
		return false;
	},
	getAltUnits: function(varDef, fromUnitObj, spectrumDataSection, spectrum)
	{
		var result = [];
		if (spectrum.getSpectrumType() === Kekule.Spectroscopy.SpectrumType.NMR)
		{
			var observeFreq = spectrum.getParameter('NMR.ObserveFrequency');
			var freqNum = observeFreq && observeFreq.getValue();
			if (observeFreq && freqNum && Kekule.NumUtils.isNormalNumber(freqNum) && Kekule.Unit.getUnit(observeFreq.getUnit()).category === Kekule.Unit.Frequency)
			{
				if (fromUnitObj.category === Kekule.Unit.Frequency)
					result.push(Kekule.Unit.Dimensionless.PARTS_PER_MILLION);
				else if (fromUnitObj.category === Kekule.Unit.Dimensionless)
					result = result.concat(Kekule.Unit.Frequency.getConvertableUnits());
			}
		}
		return result;
	}
});
// register a converter to convert between IR wave length and wave number
DCM.register({
	convert: function(value, varDef, fromUnitObj, toUnitObj, spectrumDataSection, spectrum)
	{
		if (fromUnitObj.category === KUnit.Length)   // from wave length to wave number
		{
			var standardWaveLengthScalar = fromUnitObj.convertValueToStandardEx(value);
			var standardWaveNumber = 1 / standardWaveLengthScalar.value;
			return toUnitObj.convertValueFromStandard(standardWaveNumber);
		}
		else if (fromUnitObj.category === KUnit.WaveNumber)  // from wave number to wave length
		{
			var standardWaveNumberScalar = fromUnitObj.convertValueToStandardEx(value);
			var standardWaveLength = 1 / standardWaveNumberScalar.value;
			return toUnitObj.convertValueFromStandard(standardWaveLength);
		}
	},
	canConvert: function(value, varDef, fromUnitObj, toUnitObj, spectrumDataSection, spectrum)
	{
		if (spectrum.getSpectrumType() === Kekule.Spectroscopy.SpectrumType.IR)
		{
			return (fromUnitObj.category === Kekule.Unit.Length && toUnitObj.category === Kekule.Unit.WaveNumber)
				|| (fromUnitObj.category === Kekule.Unit.WaveNumber && toUnitObj.category === Kekule.Unit.Length);
		}
		return false;
	},
	getAltUnits: function(varDef, fromUnitObj, spectrumDataSection, spectrum)
	{
		var result;
		if (spectrum.getSpectrumType() === Kekule.Spectroscopy.SpectrumType.IR)
		{
			if (fromUnitObj.category === Kekule.Unit.Length)
				result = [Kekule.Unit.WaveNumber.RECIPROCAL_CENTIMETER];
			else if (fromUnitObj.category === Kekule.Unit.WaveNumber)
				result = [Kekule.Unit.Length.getConvertableUnits()];
		}
		return result;
	}
});

/**
 * Variable used in spectrum.
 * @class
 * @augments Kekule.VarDefinition
 *
 * @property {String} internalUnit Unit that used in internal data storage.
 * @property {String} externalUnit Unit that used to expose data to public.
 */
Kekule.Spectroscopy.SpectrumVarDefinition = Class.create(Kekule.VarDefinition,
/** @lends Kekule.Spectroscopy.SpectrumVarDefinition# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Spectroscopy.SpectrumVarDefinition',
	initProperties: function()
	{
		this.defineProp('internalUnit', {'dataType': DataType.STRING, 'serializable': false,
			'getter': function() { return this.getUnit(); },
			'setter': function(value) { this.setUnit(value); }
		});
		this.defineProp('externalUnit', {'dataType': DataType.STRING});
	},
	/**
	 * Returns the actual external unit of var.
	 * Usually this function returns the value of {@link Kekule.Spectroscopy.SpectrumVarDefinition.externalUnit}
	 * If it is not set, the result will be the same as internalUnit.
	 * @returns {String}
	 */
	getActualExternalUnit: function()
	{
		return this.getExternalUnit() || this.getInternalUnit();
	},
	/**
	 * Whether the external unit setting of this var differs from the internal unit.
	 * @returns {Bool}
	 */
	hasDifferentExternalUnit: function()
	{
		var externalUnit = this.getExternalUnit();
		return !!(externalUnit && externalUnit !== this.getInternalUnit());
	}
});

/**
 * Represent part of data in a spectrum.
 * @class
 *
 * @param {String} name
 * @param {Kekule.Spectroscopy.SpectrumData} parent Parent spectrum data object.
 * @param {Array} localVariables Array of variable definition objects or symbols.
 *
 * @property {Kekule.Spectroscopy.SpectrumData} parent Parent spectrum data object.
 * @property {Array} localVarInfos Stores the local variable information. Each item is a hash containing fields {'symbol', 'range'(optional)}.
 * @property {Array} varSymbols Array of variable symbols such as ['X', 'Y'].
 * @property {Int} mode Data mode of section, continuous or peak.
 * @property {Date} modifiedTime Time that do the last modification to data.
 * @property {Hash} peakRoot
 * @property {String} name
 * @property {String} title
 */
Kekule.Spectroscopy.SpectrumDataSection = Class.create(Kekule.ChemObject,
/** @lends Kekule.Spectroscopy.SpectrumDataSection# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Spectroscopy.SpectrumDataSection',
	/** @private */
	DATAITEM_SRC_FIELD_NAME: '_src',
	/** @private */
	DATAITEM_EXTRA_FIELD_NAME: '_extra',
	/** @private */
	initialize: function(name, parent, localVariables)
	{
		this.updateDataModifiedTime();
		this.setPropStoreFieldValue('name', name);
		this.setPropStoreFieldValue('localVarInfos', []);
		this.setPropStoreFieldValue('dataItems', []);
		this.setPropStoreFieldValue('parent', parent);
		this.tryApplySuper('initialize', []);
		//this.setLocalVarSymbols(localVariables);
		if (localVariables)
			this.setLocalVariables(localVariables);
		this.setDataSorted(true);
		this._cache = {};  // private
		//this.setPropStoreFieldValue('variables', variables? AU.clone(variables): []);
	},
	doFinalize: function()
	{
		if (this.getParent() && this.getParent().removeChild)
		{
			// remove item in parent first
			this.getParent().removeChild(this);
		}
		this.clear();
		var variables = this.getVariables();
		for (var i = 0, l = variables.length; i < l; ++i)
		{
			variables[i].finalize();
		}
		this.setPropStoreFieldValue('localVarInfos', null);
		this.tryApplySuper('doFinalize');
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('parent', {'dataType': 'Kekule.MapEx', 'setter': null, 'serializable': false});
		this.defineProp('name', {'dataType': DataType.STRING});
		this.defineProp('title', {'dataType': DataType.STRING});
		/*
		this.defineProp('variables', {'dataType': DataType.ARRAY});
		this.defineProp('varSymbols', {'dataType': DataType.ARRAY, 'setter': null, 'scope': PS.PRIVATE,
			'getter': function() {
				var result = [];
				var list = this.getVariables();
				for (var j = 0, jj = list.length; j < jj; ++j)
				{
					var varDef = list[j];
					result.push(varDef.getSymbol());
				}
				return result;
			}});
		*/
		this.defineProp('localVarInfos', {'dataType': DataType.ARRAY, 'setter': null});
		this.defineProp('localVarSymbols', {'dataType': DataType.ARRAY, 'scope': PS.PRIVATE, 'serializable': false,
			'getter': function() {
				var result = [];
				var list = this.getActualLocalVarInfos();
				if (list && list.length)
				{
					for (var j = 0, jj = list.length; j < jj; ++j)
					{
						var info = list[j];
						//result.push(info.varDef.getSymbol());
						result.push(info.symbol);
					}
				}
				/*
				else // localVarInfos is not initialized yet, read from the storage
					result = this.getPropStoreFieldValue('localVarSymbols');
				*/
				return result;
			},
			'setter': function(value)
			{
				var v = value || [];
				//this.setPropStoreFieldValue('localVarSymbols', v);
				this._updateLocalVarInfosFromSymbols(v);
			}
		});
		this.defineProp('mode', {'dataType': DataType.INT, 'enumSource': Kekule.Spectroscopy.DataMode,
			'setter': function(value)
			{
				if (this.getMode() !== value)
				{
					//console.log('set mode', value);
					this.setPropStoreFieldValue('mode', value);
					this.notifyDataChange();
				}
			}
		});
		this.defineProp('defPeakRoot', {'dataType': DataType.Hash});
		this.defineProp('modifiedTime', {'dataType': DataType.DATE, 'setter': null});
		// private, stores the data items, each item is a hash, e.g. {x: 1, y: 10, w: 2}
		this.defineProp('dataItems', {'dataType': DataType.ARRAY, 'setter': null, 'scope': PS.PRIVATE});
	},
	/** @ignore */
	initPropValues: function()
	{
		this.tryApplySuper('initPropValues');
		this.setMode(Kekule.Spectroscopy.DataMode.CONTINUOUS);
	},

	/** @ignore */
	getAutoIdPrefix: function()
	{
		return 'sec';
	},

	/** @ignore */
	ownerChanged: function(newOwner, oldOwner)
	{
		// change the owner of all extra info objects if possible
		for (var i = 0, l = this.getDataCount(); i < l; ++i)
		{
			var extra = this.getExtraInfoAt(i);
			if (extra && extra.setOwner)
				extra.setOwner(newOwner);
		}
		this.tryApplySuper('ownerChanged', [newOwner, oldOwner]);
	},

	// custom save / load method
	/** @ignore */
	doSaveProp: function(obj, prop, storageNode, options, serializer, rootObj, rootStorageNode, handledObjs)
	{
		if (!prop.serializable)
			return;
		var propName = prop.name;
		if (propName === 'dataItems')
		{
			var node = serializer.createChildStorageNode(storageNode, serializer.propNameToStorageName('dataItems'), false);
			var subNode = serializer.createChildStorageNode(node, serializer.propNameToStorageName('values'), true); // create sub node for array
			serializer.saveObj(obj.getDataItems(), subNode, options, rootObj, rootStorageNode, handledObjs);  // save array values in this sub node
			// extract all extra info of data array and save them
			var extraInfos = obj._extractAllExtraInfoOfDataItems();
			if (extraInfos.length)
			{
				var subNode = serializer.createChildStorageNode(node, serializer.propNameToStorageName('extras'), true);
				serializer.saveObj(extraInfos, subNode, options, rootObj, rootStorageNode, handledObjs);
			}
			return true;  // this property is handled, do not use default save method
		}
		else
			return false;  // use the default method
	},
	/** @ignore */
	doLoadProp: function(obj, prop, storageNode, serializer, rootObj, rootStorageNode, handledObjs)
	{
		if (!prop.serializable)
			return;
		var propName = prop.name;
		if (propName === 'dataItems')
		{
			var items = [];
			var node = serializer.getChildStorageNode(storageNode, serializer.propNameToStorageName('dataItems'));
			var subNode = serializer.getChildStorageNode(node, serializer.propNameToStorageName('values')); // get sub node for array
			serializer.loadObj(items, subNode, rootObj, rootStorageNode, handledObjs);
			obj.setPropStoreFieldValue('dataItems', items);
			// then the extra info
			var subNode = serializer.getChildStorageNode(node, serializer.propNameToStorageName('extras'));
			if (subNode)
			{
				var extras = [];
				serializer.loadObj(extras, subNode, rootObj, rootStorageNode, handledObjs);
				obj._writeExtraInfoOfDataItems(extras);
			}
			return true;
		}
		else
			return false;  // use the default method
	},
	/** @private */
	_setSysFieldOfDataItem: function(dataItem, fieldName, fieldValue)
	{
		try
		{
			if (Object.defineProperty)
			{
				Object.defineProperty(dataItem, fieldName, {
					'value': fieldValue,
					'configurable': true,
					'writable': true,
					'enumerable': false
				});
			}
			else
				dataItem[fieldName] = fieldValue;
		}
		catch(e)
		{
			dataItem[fieldName] = fieldValue;
		}
	},
	/** @private */
	_extractAllExtraInfoOfDataItems: function()
	{
		var result = [];
		for (var i = 0, l = this.getDataCount(); i < l; ++i)
		{
			var info = this.getExtraInfoAt(i);
			if (info)
				result.push({'index': i, 'info': info});
		}
		return result;
	},
	/** @private */
	_writeExtraInfoOfDataItems: function(extras)
	{
		for (var i = 0, l = extras.length; i < l; ++i)
		{
			var info = extras[i];
			this.setExtraInfoAt(info.index, info.info);
		}
	},

	/* @ignore */
	/*
	parentChanged: function(newParent, oldParent)
	{
		//console.log('parent changed', newParent && newParent.getClassName(), oldParent);
		var result = this.tryApplySuper('parentChanged', newParent, oldParent);
		// after changing of parent the local var info may be changed as well
		this._updateLocalVarInfosFromSymbols(this.getLocalVarSymbols());
		return result;
	},
	*/

	/** @ignore */
	isEmpty: function()
	{
		return this.getDataCount() <= 0;
	},

	/**
	 * Returns whether this data section containing the peak data.
	 * @returns {Bool}
	 */
	isPeakSection: function()
	{
		return this.getMode() === Kekule.Spectroscopy.DataMode.PEAK;
	},
	/**
	 * Returns whether this data section containing the peak data and assignments.
	 * @returns {Bool}
	 */
	hasPeakAssignments: function()
	{
		if (this.isPeakSection())
		{
			var result = false;
			for (var i = 0, l = this.getDataCount(); i < l; ++i)
			{
				var extra = this.getExtraInfoAt(i);
				if (extra && extra && extra.hasAssignments && extra.hasAssignments())
				{
					result = true;
					break;
				}
			}
			return result;
		}
		else
			return false;
	},

	/**
	 * Returns the actual parent SpectrumData object.
	 * @returns {Kekule.Spectroscopy.Spectrum}
	 * @private
	 */
	getParentSpectrum: function()
	{
		var p = this.getParent();
		while (p && !(p instanceof Kekule.Spectroscopy.Spectrum) && p.getParent)
		{
			p = p.getParent();
		}
		return p;
	},
	/**
	 * Returns the variable definition of parent spectrum data.
	 * @returns {Array}
	 */
	getParentVariables: function()
	{
		var parent = this.getParentSpectrum();
		return (parent && parent.getVariables()) || [];
	},
	/**
	 * Returns the actual local variable infos.
	 * User should use this method rather than ref to localVarInfos property.
	 * @returns {Array}
	 */
	getActualLocalVarInfos: function()
	{
		var result = AU.clone(this.getLocalVarInfos());
		if (!result || !result.length)  // inherit all from parent spectrum
		{
			var vars = this.getParentVariables();
			for (var i = 0, l = vars.length; i < l; ++i)
			{
				result.push({'symbol': vars[i].symbol});
			}
		}
		return result;
	},
	/** @private */
	_updateLocalVarInfosFromSymbols: function(varSymbols, silent)
	{
		var v = varSymbols || [];
		var infos = [];
		var parent = this.getParentSpectrum();
		for (var i = 0, l = v.length; i < l; ++i)
		{
			var item = v[i];
			this._pushLocalVariable(parent, item, infos);
		}
		//console.log('update local var infos', varSymbols, infos, parent);
		this.setPropStoreFieldValue('localVarInfos', infos);
		//this.setLocalVarInfos(infos);
		this.notifyPropSet('localVarInfos', infos, silent);
	},
	/** @private */
	_pushLocalVariable: function(parent, varSymbol, targetArray)
	{
		if (!targetArray)
			targetArray = this.getLocalVarInfos();
		//var parent = this.getParent();
		if (parent && parent.getVariable)
		{
			var varDef = parent.getVariable(varSymbol);
			if (varDef)
			{
				targetArray.push({/*'varDef': varDef,*/ 'symbol': varSymbol});
			}
		}
	},
	/**
	 * Set the local variable symbols or definitions.
	 * @param {Array} variables Array of var defintion or symbols.
	 */
	setLocalVariables: function(variables)
	{
		var localVar;
		var varDefs = [], varSymbols = [];
		for (var i = 0, l = variables.length; i < l; ++i)
		{
			localVar = variables[i];
			if (typeof(localVar) === 'string')  // a var symbol
			{
				varSymbols.push(localVar);
			}
			else // var definition
			{
				varDefs.push(localVar);
			}
		}
		if (varDefs.length)
		{
			this.setPropStoreFieldValue('localVarInfos', varDefs);
			this.notifyPropSet('localVarInfos', varDefs);
		}
		else if (varSymbols.length)
		{
			this._updateLocalVarInfosFromSymbols(varSymbols);
		}
	},
	/**
	 * Returns the local variable information index of variable.
	 * @param {Variant} varIndexOrNameOrDef
	 * @returns {Int}
	 */
	getLocalVarInfoIndex: function(varIndexOrNameOrDef)
	{
		var result = -1;
		var localVarInfos = this.getActualLocalVarInfos();
		if (typeof (varIndexOrNameOrDef) === 'number')
			result = varIndexOrNameOrDef;
		else // if (varIndexOrNameOrDef instanceof Kekule.Spectroscopy.SpectrumVarDefinition)
		{
			var symbol = varIndexOrNameOrDef.getSymbol? varIndexOrNameOrDef.getSymbol(): varIndexOrNameOrDef;
			for (var i = 0, l = localVarInfos.length; i < l; ++i)
			{
				/*
				var varDef = localVarInfos[i].varDef;
				if (varDef === varIndexOrNameOrDef || varDef.getSymbol() === varIndexOrNameOrDef)
				{
					result = i;
					break;
				}
				*/
				if (symbol === localVarInfos[i].symbol)
				{
					result = i;
					break;
				}
			}
		}
		return result;
	},
	/**
	 * Returns the local information of variable.
	 * @param {Variant} varIndexOrNameOrDef
	 * @returns {Hash}
	 */
	getLocalVarInfo: function(varIndexOrNameOrDef)
	{
		var index = this.getLocalVarInfoIndex(varIndexOrNameOrDef);
		var result = (index >= 0)? this.getActualLocalVarInfos()[index]: null;
		/*
		if (result)
		{
			var parent = this.getParentSpectrum();
			if (parent)
			{
				var symbol = result.symbol;
				result = Object.create(result);  // avoid affect the original hash object
				result.varDef = parent.getVariable(symbol);
			}
		}
		*/
		return result;
		/*
		var result;
		var localVarInfos = this.getActualLocalVarInfos();
		if (typeof (varIndexOrNameOrDef) === 'number')
			result = localVarInfos[varIndexOrNameOrDef];
		else // if (varIndexOrNameOrDef instanceof Kekule.Spectroscopy.SpectrumVarDefinition)
		{
			for (var i = 0, l = localVarInfos.length; i < l; ++i)
			{
				var varDef = localVarInfos[i].varDef;
				if (varDef === varIndexOrNameOrDef || varDef.getSymbol() === varIndexOrNameOrDef)
				{
					result = localVarInfos[i];
					break;
				}
			}
		}
		return result;
		*/
	},
	/**
	 * Returns the local information value of a variable.
	 * @param {Variant} varIndexOrNameOrDef
	 * @param {String} key
	 * @returns {Variant}
	 */
	getLocalVarInfoValue: function(varIndexOrNameOrDef, key)
	{
		var info = this.getLocalVarInfo(varIndexOrNameOrDef);
		return info && info[key];
	},
	/**
	 * Set a local information of variable.
	 * @param {Variant} varIndexOrNameOrDef
	 * @param {String} key
	 * @param {Variant} value
	 */
	setLocalVarInfoValue: function(varIndexOrNameOrDef, key, value)
	{
		var info = this.getLocalVarInfo(varIndexOrNameOrDef);
		info[key] = value;
	},
	/**
	 * Returns the variable definition of a local variable.
	 * @param {Variant} varIndexOrNameOrDef
	 * @returns {Kekule.Spectroscopy.SpectrumVarDefinition}
	 */
	getLocalVarDef: function(varIndexOrNameOrDef)
	{
		//return this.getLocalVarInfoValue(varIndexOrNameOrDef, 'varDef');
		var symbol = this.getLocalVarInfoValue(varIndexOrNameOrDef, 'symbol');
		var parent = this.getParentSpectrum();
		return parent && parent.getVariable(symbol);
	},

	/**
	 * Returns the local variable info of certain dependency.
	 * @param {Int} dependency
	 * @returns {Array}
	 */
	getLocalVarInfoOfDependency: function(dependency)
	{
		var result = [];
		var localVarInfos = this.getActualLocalVarInfos();
		for (var i = 0, l = localVarInfos.length; i < l; ++i)
		{
			var varDef = this.getLocalVarDef(i);
			if (varDef.getDependency() === dependency)
			{
				var info = Object.extend({}, localVarInfos[i]);
				info.varDef = varDef;
				result.push(info);
			}
		}
		return result;
	},
	/**
	 * Returns the local variable symbols of certain dependency.
	 * @param {Int} dependency
	 * @returns {Array}
	 */
	getLocalVarSymbolsOfDependency: function(dependency)
	{
		var varInfos = this.getLocalVarInfoOfDependency(dependency);
		var result = [];
		for (var i = 0, l = varInfos.length; i < l; ++i)
		{
			result.push(varInfos[i].varDef.getSymbol());
		}
		return result;
	},

	/**
	 * Returns the from/to value of a continuous variable.
	 * Note the return values of this function are all based on internal var unit.
	 * @param {Variant} varNameOrIndexOrDef
	 * @returns {Hash} Hash of {fromValue, toValue}
	 */
	getContinuousVarRange: function(varIndexOrNameOrDef)
	{
		var parent = this.getParent();
		var varInfo = this.getLocalVarInfo(varIndexOrNameOrDef);
		return varInfo.continuousRange || (parent && parent.getContinuousVarRange && parent.getContinuousVarRange(varInfo.symbol));
		/*
		var result = this.getLocalVarInfoValue(varIndexOrNameOrDef, 'continuousRange');
		if (!result)
		{
			var parent = this.getParent();
			result = parent && parent.getContinuousVarRange(varInfo.varDef);
		}
		return result;
		*/
	},
	/**
	 * Set the from/to value of a variable and mark it as a continuous one.
	 * @param {Variant} varNameOrIndexOrDef
	 * @param {Number} fromValue
	 * @param {Number} toValue
	 */
	setContinuousVarRange: function(varIndexOrNameOrDef, fromValue, toValue)
	{
		/*
		var varInfo = this.getLocalVarInfo(varIndexOrNameOrDef);
		varInfo.range = {'fromValue': fromValue, 'toValue': toValue};
		*/
		this.setLocalVarInfoValue(varIndexOrNameOrDef, 'continuousRange', {'fromValue': fromValue, 'toValue': toValue});
		return this;
	},
	/**
	 * Remove the continuous information of a variable.
	 * @param {Variant} varIndexOrNameOrDef
	 */
	clearContinuousVarRange: function(varIndexOrNameOrDef)
	{
		/*
		var varInfo = this.getLocalVarInfo(varIndexOrNameOrDef);
		varInfo.range = null;
		*/
		this.setLocalVarInfoValue(varIndexOrNameOrDef, 'continuousRange', null);
		return this;
	},
	/**
	 * Set the local default value of a variable when the concrete value in spectrum is absent.
	 * @param {Variant} varIndexOrNameOrDef
	 * @param {Number} value
	 */
	setDefaultVarValue: function (varIndexOrNameOrDef, value)
	{
		this.setLocalVarInfoValue(varIndexOrNameOrDef, 'defaultValue', value);
		return this;
	},
	/**
	 * Clear the local default value of a variable.
	 * @param {Variant} varIndexOrNameOrDef
	 */
	clearDefaultVarValue: function(varIndexOrNameOrDef)
	{
		return this.setDefaultVarValue(varIndexOrNameOrDef, null);
	},
	/**
	 * Get the local default value of a variable when the concrete value in spectrum is absent.
	 * @param {Variant} varIndexOrNameOrDef
	 * @returns {Number}
	 */
	getDefaultVarValue: function(varIndexOfNameOrDef)
	{
		var result = this.getLocalVarInfoValue(varIndexOfNameOrDef, 'defaultValue');
		if (Kekule.ObjUtils.isUnset(result))
		{
			var varInfo = this.getLocalVarInfo(varIndexOfNameOrDef);
			var parent = this.getParent();
			result = parent && parent.getDefaultVarValue(varInfo.symbol);
		}
		return result;
	},
	/**
	 * Returns the range when displaying spectrum of a variable.
	 * @param {Variant} varNameOrIndexOrDef
	 * @param {Hash} options May include fields:
	 *  {
	 *    autoCalc: Bool. If true, when explicit display range is not set, the number range of variable will be calculated and returned.
	 *    basedOnInternalUnit: Bool. If true, the returned value will be based on internal unit rather than the external unit of variable.
	 *  }
	 * @returns {Hash} Hash of {min, max}
	 */
	getVarDisplayRange: function(varIndexOrNameOrDef, options)
	{
		var op = options || {};
		//var varDef = this.getVar
		var varIndex = this.getLocalVarInfoIndex(varIndexOrNameOrDef);
		var info = this.getLocalVarInfo(varIndex);
		var result = info.displayRange? Object.extend({}, info.displayRange): null;  // avoid affect the original values
		if (!result)  // check the var definition
		{
			//var varDef = info.varDef;
			var varDef = this.getLocalVarDef(varIndex);
			var varDefRange = varDef.getInfoValue('displayRange');
			if (varDefRange)
				result = Object.extend({}, varDefRange);   // avoid affecting the original values
		}

		if (!result && op.autoCalc)
			result = this.calcDataRange(varIndex, {basedOnInternalUnit: true})[info.symbol];  // get range with internal unit first
			//result = this.calcDataRange(varIndexOrNameOrDef)[info.varDef.getSymbol()];
		// do not forget to do unit conversion if necessary
		if (!op.basedOnInternalUnit)
		{
			result = this._convertDataRangeToExternalUnit(result, varIndex);
			/*
			var fieldNames = Kekule.ObjUtils.getOwnedFieldNames(result);
			for (var i = 0, l = fieldNames.length; i < l; ++i)
			{
				var fname = fieldNames[i];
				//result[fname] = this._convertVarValueToExternal(result[fname], varIndex);
			}
			// after conversion, the min/max values may be reversed
			if (result && result.min > result.max)
			{
				var temp = result.min;
				result.min = result.max;
				result.max = temp;
			}
			*/
		}
		return result;
	},
	/**
	 * Set the range when displaying spectrum of a variable.
	 * @param {Variant} varNameOrIndexOrDef
	 * @param {Number} minValue
	 * @param {Number} maxValue
	 * @param {Hash} options Extra options, may include fields:
	 *   {
	 *     basedOnExternalUnit: Bool
	 *   }
	 */
	setVarDisplayRange: function(varIndexOrNameOrDef, minValue, maxValue, options)
	{
		var op = options || {};
		var range = {'min': minValue, 'max': maxValue};
		if (op.basedOnExternalUnit)  // need to convert values to internal unit first
		{
			var varIndex = this.getLocalVarInfoIndex(varIndexOrNameOrDef);
			range = this._convertDataRangeToInternalUnit(range, varIndex);
		}
		this.setLocalVarInfoValue(varIndexOrNameOrDef, 'displayRange', range);
		return this;
	},
	/**
	 * Remove the display range information of a variable.
	 * @param {Variant} varIndexOrNameOrDef
	 */
	clearVarDisplayRange: function(varIndexOrNameOrDef)
	{
		this.setLocalVarInfoValue(varIndexOrNameOrDef, 'displayRange',null);
		return this;
	},
	/**
	 * Returns display range of variables.
	 * @param {Array} targetVariables Array of variable definition or symbol.
	 *   If not set, all variables will be considered.
	 * @param {Hash} options May include fields:
	 *  {
	 *    autoCalc: Bool. If true, when explicit display range is not set, the number range of variable will be calculated and returned.
	 *    basedOnInternalUnit: Bool. If true, the returned value will be based on internal unit rather than the external unit of variable.
	 *  }
	 * @returns {Hash}
	 */
	getDisplayRangeOfVars: function(targetVariables, options)
	{
		var result = {};
		if (!targetVariables)
			targetVariables = this.getLocalVarSymbols();
		for (var i = 0, l = targetVariables.length; i < l; ++i)
		{
			var symbol = this._varToVarSymbol(targetVariables[i]);
			result[symbol] = this.getVarDisplayRange(targetVariables[i], options);
		}
		return result;
	},

	/** @private */
	_varToVarSymbol: function(targetVar)
	{
		/*
		var info = this.getLocalVarInfo(targetVar);
		if (info)
			return info.varDef.getSymbol();
		*/
		var varDef = this.getLocalVarDef(targetVar);
		if (varDef)
			return varDef.getSymbol();
		else
			return null;
	},
	/** @private */
	_varToVarSymbols: function(targetVariables)
	{
		var targetVarSymbols = [];
		var vars = targetVariables? AU.toArray(targetVariables): null;
		if (!vars)
			targetVarSymbols = this.getLocalVarSymbols();
		else
		{
			for (var i = 0, l = vars.length; i < l; ++i)
			{
				targetVarSymbols.push(this._varToVarSymbol(vars[i]))
			}
		}
		return targetVarSymbols;
	},
	/** @private */
	_getDefaultPeakRoot: function()
	{
		var result = {};
		var varInfos = this.getActualLocalVarInfos();
		for (var i = 0, l = varInfos.length; i < l; ++i)
		{
			//var varDef = varInfos[i].varDef;
			var varDef = this.getLocalVarDef(i);
			if (varDef.getDependency() !== Kekule.VarDependency.INDEPENDENT)
			{
				result[varDef.getSymbol()] = 0;
			}
		}
		return result;
	},
	/**
	 * Iterate all data items and calculate the min/max value of each variable.
	 * Note this function will always returns the value based on internal unit,
	 * regardless of whether the external unit is set or not.
	 * @param {Array} targetVariables Array of variable definition or symbol.
	 *   If not set, all variables will be calculated.
	 * @param {Hash} options Extra calculation options, may include fields:
	 *   {
	 *    basedOnInternalUnit: Bool. If true, the returned value will be based on internal unit rather than the external unit of variable.
	 *    ignorePeakRoot: Bool. If true, the peak root value will be ignored during calculation.
	 *  }
	 * @returns {Hash}
	 */
	calcDataRange: function(targetVariables, options)
	{
		var op = options || {};
		// since calculation of data range is a time-consuming job, here we cache the result
		var targetVarSymbols = this._varToVarSymbols(targetVariables);

		var notNum = function (v) {
			return !Kekule.NumUtils.isNormalNumber(v);
		};

		var ranges = {};
		var rangeCache = this._cache.ranges;
		if (!rangeCache)
		{
			rangeCache = {};
			this._cache.ranges = rangeCache;
		}
		var remainingVarSymbols = [];
		for (var i = 0, l = targetVarSymbols.length; i < l; ++i)
		{
			var symbol = targetVarSymbols[i];
			if (rangeCache[symbol])  // cached
			{
				// console.log('got range from cache', symbol);
				ranges[symbol] = Object.extend({}, rangeCache[symbol]);
			}
			else
				remainingVarSymbols.push(symbol);
		}

		if (remainingVarSymbols.length)
		{
			var self = this;
			var isPeakData = this.isPeakSection();
			this.forEach(function (dataValue, index) {
				for (var i = 0, l = remainingVarSymbols.length; i < l; ++i)
				{
					var symbol = remainingVarSymbols[i];
					if (notNum(dataValue[symbol]))
						continue;
					if (!ranges[symbol])
						ranges[symbol] = {};

					ranges[symbol].min = notNum(ranges[symbol].min) ? dataValue[symbol] : Math.min(ranges[symbol].min, dataValue[symbol]);
					ranges[symbol].max = notNum(ranges[symbol].max) ? dataValue[symbol] : Math.max(ranges[symbol].max, dataValue[symbol]);

					// consider peak root value
					if (isPeakData && !op.ignorePeakRoot)
					{
						var peakRootValue = self.getPeakRootValueOf(dataValue);
						if (peakRootValue && !notNum(peakRootValue[symbol]))
						{
							ranges[symbol].min = notNum(ranges[symbol].min) ? peakRootValue[symbol] : Math.min(ranges[symbol].min, peakRootValue[symbol]);
							ranges[symbol].max = notNum(ranges[symbol].max) ? peakRootValue[symbol] : Math.max(ranges[symbol].max, peakRootValue[symbol]);
						}
					}
				}
			}, null, {basedOnInternalUnit: true}); // here we use the internal unit, to keep the cache with the same unit

			// cache the range values
			for (var i = 0, l = remainingVarSymbols.length; i < l; ++i)
			{
				var symbol = remainingVarSymbols[i];
				rangeCache[symbol] = Object.extend({}, ranges[symbol]);
			}
		}

		/*
		if (this.getMode() === Kekule.Spectroscopy.DataMode.PEAK)  // consider the peak root
		{
			var peakRoot = this.getDefPeakRoot() || this._getDefaultPeakRoot();
			for (var i = 0, l = targetVarSymbols.length; i < l; ++i)
			{
				var symbol = targetVarSymbols[i];
				var rootValue = peakRoot[symbol];
				if (!notNum(rootValue))
				{
					ranges[symbol].min = Math.min(ranges[symbol].min, rootValue);
					ranges[symbol].max = Math.max(ranges[symbol].max, rootValue);
				}
			}
		}
		*/
		//console.log(this.getMode(), peakRoot, ranges);

		if (!op.basedOnInternalUnit)
		{
			for (var i = 0, l = targetVarSymbols.length; i < l; ++i)
			{
				var symbol = targetVarSymbols[i];
				ranges[symbol] = this._convertDataRangeToExternalUnit(ranges[symbol], i);
			}
		}

		return ranges;
	},
	/** @private */
	_convertDataRangeToExternalUnit: function(range, varIndex)
	{
		if (!range)
			return range;
		var fieldNames = ['min', 'max'];
		for (var i = 0, l = fieldNames.length; i < l; ++i)
		{
			var fname = fieldNames[i];
			range[fname] = this._convertVarValueToExternal(range[fname], varIndex);
		}
		// after conversion, the min/max values may be reversed
		if (range.min > range.max)
		{
			var temp = range.min;
			range.min = range.max;
			range.max = temp;
		}
		return range;
	},
	/** @private */
	_convertDataRangeToInternalUnit: function(range, varIndex)
	{
		if (!range)
			return range;
		var fieldNames = ['min', 'max'];
		for (var i = 0, l = fieldNames.length; i < l; ++i)
		{
			var fname = fieldNames[i];
			range[fname] = this._convertVarValueToInternal(range[fname], varIndex);
		}
		// after conversion, the min/max values may be reversed
		if (range.min > range.max)
		{
			var temp = range.min;
			range.min = range.max;
			range.max = temp;
		}
		return range;
	},

	/**
	 * Iterate all data items and calculate the average value of each variable.
	 * Note this function will always returns the value based on internal unit,
	 * regardless of whether the external unit is set or not.
	 * @param {Array} targetVariables Array of variable definition or symbol.
	 *   If not set, all variables will be calculated.
	 * @param {Hash} options Extra calculation options, may include fields:
	 *  {
	 *    basedOnInternalUnit: Bool. If true, the returned value will be based on internal unit rather than the external unit of variable.
	 *  }
	 * @returns {Hash}
	 */
	calcDataAverage: function(targetVariables, options)
	{
		var op = options || {};
		var targetVarSymbols = this._varToVarSymbols(targetVariables);
		var averages = {};
		var averageCache = this._cache.averages;

		var notNum = function (v) {
			return !Kekule.NumUtils.isNormalNumber(v);
		};

		if (!averageCache)
		{
			averageCache = {};
			this._cache.averages = averageCache;
		}
		var remainingVarSymbols = [];
		for (var i = 0, l = targetVarSymbols.length; i < l; ++i)
		{
			var symbol = targetVarSymbols[i];
			if (!notNum(averageCache[symbol]))  // cached
			{
				averages[symbol] = averageCache[symbol];
			}
			else
				remainingVarSymbols.push(symbol);
		}

		if (remainingVarSymbols.length)
		{
			var sums = {};
			var counts = {};
			for (var i = 0, l = remainingVarSymbols.length; i < l; ++i)
			{
				sums[remainingVarSymbols[i]] = 0;
				counts[remainingVarSymbols[i]] = 0;
			}
			this.forEach(function (dataValue, index) {
				for (var i = 0, l = remainingVarSymbols.length; i < l; ++i)
				{
					var symbol = remainingVarSymbols[i];
					var value = dataValue[symbol];
					if (notNum(value))
						continue;
					sums[symbol] += value;
					++counts[symbol];
				}
			}, null, {basedOnInternalUnit: true});

			// cache the average values
			for (var i = 0, l = remainingVarSymbols.length; i < l; ++i)
			{
				var symbol = remainingVarSymbols[i];
				averages[symbol] = sums[symbol] / counts[symbol];
				averageCache[symbol] = averages[symbol];
			}
		}

		if (!op.basedOnInternalUnit)
		{
			for (var i = 0, l = targetVarSymbols.length; i < l; ++i)
			{
				var symbol = targetVarSymbols[i]
				averages[symbol] = this._convertVarValueToExternal(averages[symbol, i]);
			}
		}

		return averages;
	},

	/**
	 * Returns the symbols of continuous variable.
	 * @returns {Array}
	 */
	getContinuousVarSymbols: function()
	{
		var result = [];
		var varInfos = this.getActualLocalVarInfos();
		for (var i = 0, l = varInfos.length; i < l; ++i)
		{
			if (this.getContinuousVarRange(i))
				result.push(varInfos[i].symbol);
		}
		return result;
	},

	/** @private */
	_itemHashToArray: function(hashValue)
	{
		if (!hashValue)
			return null;
		var result = [];
		var symbols = this.getLocalVarSymbols();
		for (var i = 0, l = symbols.length; i < l; ++i)
		{
			result.push(hashValue[symbols[i]]);
		}
		var src = this._getDataValueSrc(hashValue);
		if (src)
		{
			this._setSysFieldOfDataItem(result, this.DATAITEM_SRC_FIELD_NAME, src);
		}

		// then the extra fields
		var extra;
		if (src && src[this.DATAITEM_EXTRA_FIELD_NAME])
		{
			//result._extra = hashValue._extra;
			extra = src[this.DATAITEM_EXTRA_FIELD_NAME];
		}
		else
		{
			// then the remaining fields of hashValue, storing in _extra field of array item
			var remainingFields = AU.exclude(
				Kekule.ObjUtils.getOwnedFieldNames(hashValue, false),
				[this.DATAITEM_SRC_FIELD_NAME, this.DATAITEM_EXTRA_FIELD_NAME].concat(symbols));
			if (remainingFields.length)
				extra = {};
			for (var i = 0, l = remainingFields.length; i < l; ++i)
			{
				extra[remainingFields[i]] = hashValue[remainingFields[i]];
			}
		}
		if (extra)
			//this.setExtraInfoOf(result, extra);
			this._setSysFieldOfDataItem(src || result, this.DATAITEM_EXTRA_FIELD_NAME, extra);
		return result;
	},
	/** @private */
	_convArrayToSymboledHash: function(arrayValue, symbols)
	{
		var result = {};
		for (var i = 0, l = Math.min(symbols.length, arrayValue.length); i < l; ++i)
		{
			var value = arrayValue[i];
			result[symbols[i]] = value;
		}
		return result;
	},
	/** @private */
	_itemArrayToHash: function(arrayValue, options)
	{
		if (!arrayValue)
			return null;
		var result = {};
		var symbols = this.getLocalVarSymbols();
		for (var i = 0, l = Math.min(symbols.length, arrayValue.length); i < l; ++i)
		{
			var value;
			if (!options.basedOnInternalUnit)
				value = this._convertVarValueToExternal(arrayValue[i], i);
			else
				value = arrayValue[i];
			result[symbols[i]] = value;
		}//
		var src = this._getDataValueSrc(arrayValue);

		if (src[this.DATAITEM_EXTRA_FIELD_NAME])
		{
			//result = Object.extend(result, arrayValue._extra);
			//result._extra = arrayValue._extra;
			this._setSysFieldOfDataItem(result, this.DATAITEM_EXTRA_FIELD_NAME, arrayValue[this.DATAITEM_EXTRA_FIELD_NAME]);
		}

		this._setSysFieldOfDataItem(result, this.DATAITEM_SRC_FIELD_NAME, src || arrayValue);
		//result._raw = arrayValue;
		return result;
	},
	/** @private */
	_convertVarValueToNewUnit: function(value, varDef, fromUnit, toUnit)
	{
		if (!Kekule.NumUtils.isNormalNumber(value))  // not a number, usually can not be converted
			return value;
		//return Kekule.UnitUtils.convertValue(value, fromUnit, toUnit);
		return Kekule.Spectroscopy.DataValueConverterManager.doConvert(value, varDef, fromUnit, toUnit, this, this.getParent());
	},
	/**
	 * Convert a raw value (storaged value) to the one exposed to external with a different unit.
	 * @param {Number} value
	 * @param {Int} varIndex
	 * @returns {Number} value
	 * @private
	 */
	_convertVarValueToExternal: function(value, varIndex)
	{
		var result = value;
		var varDef = this.getLocalVarDef(varIndex);
		if (varDef && varDef.hasDifferentExternalUnit && varDef.hasDifferentExternalUnit())  // need to do a value conversion
		{
			result = this._convertVarValueToNewUnit(value, varDef, varDef.getInternalUnit(), varDef.getActualExternalUnit());
		}
		return result;
	},
	/**
	 * Convert a value with external unit to the one with internal unit.
	 * @param {Number} value
	 * @param {Int} varIndex
	 * @returns {Number} value
	 * @private
	 */
	_convertVarValueToInternal: function(value, varIndex)
	{
		var result = value;
		var varDef = this.getLocalVarDef(varIndex);
		if (varDef && varDef.hasDifferentExternalUnit && varDef.hasDifferentExternalUnit())  // need to do a value conversion
		{
			result = this._convertVarValueToNewUnit(value, varDef, varDef.getActualExternalUnit(), varDef.getInternalUnit());
		}
		return result;
	},

	/**
	 * Returns whether data in this section has been sorted.
	 * @returns {Bool}
	 */
	isDataSorted: function()
	{
		return this._sorted || this.getDataCount() <= 1;
	},
	/**
	 * Manually set the sorted state of data.
	 * @param {Bool} value
	 */
	setDataSorted: function(value)
	{
		this._sorted = !!value;
		return this;
	},
	/**
	 * Sort all data items.
	 * @param {Func} func Optional, func(hash1, hash2). If not set, data items will be sorted by default method.
	 */
	sort: function(func)
	{
		if (this.isDataSorted())
			return;
		var self = this;
		var sortFunc = func?
			function(a1, a2) { return func(self._itemArrayToHash(a1), self._itemArrayToHash(a2)); }:
			function(a1, a2) { return AU.compare(a1, a2); }
		this.getDataItems().sort(sortFunc);
		this.setDataSorted(true);
		return this;
	},

	/**
	 * Returns the order of an independent var in current sorted data array.
	 * @param {String} varSymbol
	 * @returns {Int} 1 for ascending order, -1 for descending order and 0 for unknown.
	 */
	getIndependentVarValueOrder: function(varSymbol)
	{
		var result = 0;
		if (this.isDataSorted())
		{
			var self = this;
			var _getValidVarValueEx = function(varSymbol, fromIndex, toIndex, delta)
			{
				var currIndex = fromIndex;
				var info = {};
				var passed = false;
				do
				{
					info.value = self.getHashValueAt(currIndex)[varSymbol];
					info.index = currIndex;
					currIndex += delta;
					passed = Kekule.NumUtils.isNormalNumber(info.value);
				}
				while (!passed && ((delta > 0 && currIndex <= maxIndex) || (delta < 0 && currIndex >= toIndex)));
				return (passed)? info: null;
			}
			var maxIndex = this.getDataCount() - 1;
			var infoFrom = _getValidVarValueEx(varSymbol, 0, maxIndex, 1);
			var infoTo = _getValidVarValueEx(varSymbol, maxIndex, 0, -1)
			if (infoFrom && infoTo && infoFrom.index < infoTo.index)
				result = Math.sign(infoTo.value - infoFrom.value);
		}
		return result;
	},


	/**
	 * Returns the count of data items.
	 * @returns {Int}
	 */
	getDataCount: function()
	{
		return this.getDataItems().length;
	},

	/** @private */
	clearCache: function()
	{
		this._cache = {};
	},
	/**
	 * Notify the data of this section has been changed.
	 * @private
	 */
	notifyDataChange: function()
	{
		var items = this.getDataItems();
		this.setDataSorted(false);
		this.clearCache();
		this.updateDataModifiedTime();
		this.notifyPropSet('dataItems', items);
		this.invokeEvent('dataChange', {'data': items})
	},
	/** @private */
	updateDataModifiedTime: function()
	{
		this.setPropStoreFieldValue('modifiedTime', new Date());
	},
	/**
	 * Clear all data items.
	 */
	clear: function()
	{
		this.setDataItems([]);
		this.notifyDataChange();
		this.setDataSorted(true);  // empty data are always sorted
	},
	/**
	 * Returns the index of data item in this section. The item is can be a hash or an array.
	 * If it is a hash, the hash fields must matches {@link Kekule.Spectroscopy.SpectrumData.independentVars} and {@link Kekule.Spectroscopy.SpectrumData.dependentVars}.
	 * @param {Variant} item
	 * @returns {Int}
	 */
	indexOfDataItem: function(item)
	{
		var dataItem = item[this.DATAITEM_SRC_FIELD_NAME] || item;
		var items = this.getDataItems();
		return items.indexOf(dataItem);
	},
	/**
	 * Add new data item. The item is can be a hash or an array.
	 * If it is a hash, the hash fields must matches {@link Kekule.Spectroscopy.SpectrumData.independentVars} and {@link Kekule.Spectroscopy.SpectrumData.dependentVars}.
	 * If it is an array, the values in array will automatically mapped to independent and dependent vars.
	 * @param {Variant} item
	 */
	appendData: function(item)
	{
		var d;
		if (!DataType.isArrayValue(item))  // is hash value, convert it to array first
			d = this._itemHashToArray(item);
		else
			d = item;
		if (d)
		{
			var items = this.getDataItems();
			items.push(d);
			if (d[this.DATAITEM_EXTRA_FIELD_NAME])
				this._extraInfoAdded(d[this.DATAITEM_EXTRA_FIELD_NAME]);
			this.notifyDataChange();
			return d;
		}
	},
	/**
	 * Remove a data item.
	 * @param {Array} item
	 */
	removeData: function(item)
	{
		var items = this.getDataItems();
		var index = items.indexOf(item);
		return this.removeDataItemAt(index);
	},
	/**
	 * Remove a data item at index.
	 * @param {Int} index
	 */
	removeDataAt: function(index)
	{
		var result = this.getDataItems().splice(index, 1);
		this.notifyDataChange();
		return result;
	},
	/**
	 * Get the data value at index.
	 * @param {Int} index
	 * @returns {Array} The arrayed form of value.
	 */
	getRawValueAt: function(index)
	{
		var srcValue = this.getDataItems()[index];
		if (srcValue)
		{
			var result = AU.clone(srcValue);
			result[this.DATAITEM_SRC_FIELD_NAME] = srcValue;
			var isContinousData = this.getMode() === Kekule.Spectroscopy.DataMode.CONTINUOUS;
			//if (this.getMode() === Kekule.Spectroscopy.DataMode.CONTINUOUS)
			{
				var dataIntervalCount = this.getDataCount() - 1;
				// check if there are omitted values
				for (var i = 0, l = result.length; i < l; ++i)
				{
					var v = result[i];
					if (DataType.isUndefinedValue(v) || DataType.isNullValue(v))  // maybe omitted? check if it is a continous variable or it has a default value
					{
						var defValue = this.getDefaultVarValue(i);
						if (Kekule.ObjUtils.notUnset(defValue))
							v = defValue;
						else if (isContinousData)
						{
							var range = this.getContinuousVarRange(i);
							if (range)
							{
								v = (dataIntervalCount > 1)? ((index / dataIntervalCount) * (range.toValue - range.fromValue) + range.fromValue): range.fromValue;
								//console.log('adjusted v', v, range);
							}
						}
					}
					result[i] = v;
				}
			}
			if (srcValue[this.DATAITEM_EXTRA_FIELD_NAME])  // copy the extra properties
			{
				//result._extra = rawValue._extra;
				this._setSysFieldOfDataItem(result, this.DATAITEM_EXTRA_FIELD_NAME, srcValue._extra);
			}
			return result;
		}
		else
			return null;
	},
	/** @private */
	getHashValueAt: function(index, options)
	{
		return this._itemArrayToHash(this.getRawValueAt(index), options || {});
	},
	/**
	 * Get the data value at index.
	 * @param {Int} index
	 * @returns {Hash} The hashed form of value.
	 *   The hash value's ._raw field stores the original array form value.
	 *   It may also containing a ._extra field storing the extra information of spectrum data.
	 */
	getValueAt: function(index, options)
	{
		return this.getHashValueAt(index, options);
	},
	/**
	 * Set the data value at index.
	 * @param {Int} index
	 * @param {Array} The arrayed form of value.
	 */
	setRawValueAt: function(index, value)
	{
		var oldValue = this.getDataItems()[index];
		if (oldValue && oldValue[this.DATAITEM_EXTRA_FIELD_NAME])
			this._extraInfoRemoved(oldValue[this.DATAITEM_EXTRA_FIELD_NAME]);
		this.getDataItems()[index] = value;
		if (value[this.DATAITEM_EXTRA_FIELD_NAME])
		{
			this._setSysFieldOfDataItem(value, this.DATAITEM_EXTRA_FIELD_NAME, value[this.DATAITEM_EXTRA_FIELD_NAME]);  // redefine the value._extra field with special descriptors
			this._extraInfoAdded(value[this.DATAITEM_EXTRA_FIELD_NAME]);
		}
		this.notifyDataChange();
		return this;
	},
	/** @private */
	setHashValueAt: function(index, value, options)
	{
		var aValue = this._itemHashToArray(value);
		this.setRawValueAt(index, aValue);
		return this;
	},
	/**
	 * Set the data value at index.
	 * @param {Int} index
	 * @param {Variant} value Value in hash or array form.
	 */
	setValueAt: function(index, value, options)
	{
		var d;
		if (!DataType.isArrayValue(value))  // is hash value, convert it to array first
			d = this._itemHashToArray(value);
		else
			d = value;
		this.setRawValueAt(index, d);
		return this;
	},

	/** @private */
	_getDataValueSrc: function(value)
	{
		if (!value)
			return null;
		else if (DataType.isArrayValue(value))
			return value[this.DATAITEM_SRC_FIELD_NAME] || value;
		else
			return value[this.DATAITEM_SRC_FIELD_NAME];
	},

	/**
	 * Create a new extra info object for value or index.
	 * If no default object can be created, null will be returned.
	 * Descendants may override this method.
	 * @param {Variant} valueOrIndex
	 * @returns {Object}
	 */
	createDefaultExtraInfoObjectFor: function(valueOrIndex)
	{
		var infoClass = (this.isPeakSection())? Kekule.Spectroscopy.SpectrumPeakDetails: null;
		var result = infoClass? new infoClass(): null;
		if (result && Kekule.ObjUtils.notUnset(valueOrIndex))
		{
			if (typeof(valueOrIndex) === 'number')  // index
				this.setExtraInfoAt(valueOrIndex, result);
			else if (valueOrIndex)
				this.setExtraInfoOf(valueOrIndex, result);
		}
		return result;
	},
	/**
	 * Get the extra information of a data value.
	 * @param {Variant} value Data value in hash or array form.
	 * @returns {Hash}
	 */
	getExtraInfoOf: function(value)
	{
		var src = this._getDataValueSrc(value) || value
		return src[this.DATAITEM_EXTRA_FIELD_NAME] || value[this.DATAITEM_EXTRA_FIELD_NAME];
	},
	/**
	 * Set the extra information of a data value.
	 * @param {Variant} value Data value in hash or array form.
	 * @param {Hash} info
	 */
	setExtraInfoOf: function(value, info)
	{
		var target = this._getDataValueSrc(value) || value;
		/*
		if (!DataType.isArrayValue(value))  // is hash value, get its _raw field first
			src = value[this.DATAITEM_SRC_FIELD_NAME];
		else
			src = value;
		*/
		if (target[this.DATAITEM_EXTRA_FIELD_NAME])
			this._extraInfoRemoved(target[this.DATAITEM_EXTRA_FIELD_NAME]);
		this._setSysFieldOfDataItem(target, this.DATAITEM_EXTRA_FIELD_NAME, info);
		this._extraInfoAdded(info);
		this.notifyDataChange();
		return this;
	},
	/**
	 * Get the extra information of data value at index.
	 * @param {Int} index
	 * @returns {Hash}
	 */
	getExtraInfoAt: function(index)
	{
		var d = this.getDataItems()[index];
		return d && d[this.DATAITEM_EXTRA_FIELD_NAME];
	},
	/**
	 * Set the extra information of data value at index.
	 * @param {Int} index
	 * @param {Hash} info
	 */
	setExtraInfoAt: function(index, info)
	{
		var d = this.getDataItems()[index];
		if (d[this.DATAITEM_EXTRA_FIELD_NAME])
			this._extraInfoRemoved(d[this.DATAITEM_EXTRA_FIELD_NAME]);
		this._setSysFieldOfDataItem(d, this.DATAITEM_EXTRA_FIELD_NAME, info);
		this._extraInfoAdded(info);
		this.notifyDataChange();
		return this;
	},
	/** @private */
	_extraInfoAdded: function(extraInfo)
	{
		if (extraInfo && extraInfo instanceof Kekule.ChemObject)
		{
			extraInfo.setParent(this);
			extraInfo.setOwner(this.getOwner());
		}
	},
	/** @private */
	_extraInfoRemoved: function(extraInfo)
	{
		if (extraInfo && extraInfo instanceof Kekule.ChemObject && extraInfo.getParent() === this)
		{
			extraInfo.setParent(null);
			extraInfo.setOwner(null);
		}
	},

	/*
	 * Get the extra peak detail information (based on {@link Kekule.Spectroscopy.SpectrumPeakDetails}).
	 * @param {Variant} value Data value in hash or array form.
	 * @param {Bool} autoCreate If the peak is without extra info, whether automatically create an instance of {@link Kekule.Spectroscopy.SpectrumPeakDetails}.
	 * @returns {Kekule.Spectroscopy.SpectrumPeakDetails}
	 */
	/*
	getPeakDetailsOf: function(value, autoCreate)
	{
		var extra = this.getExtraInfoOf(value);
		if (!extra && autoCreate)
		{
			extra = new Kekule.Spectroscopy.SpectrumPeakDetails();
			this.setExtraInfoOf(value, extra);
		}
		return extra && ((extra instanceof Kekule.Spectroscopy.SpectrumPeakDetails)? extra: null);
	},
	*/
	/*
	 * Get the extra peak detail information (based on {@link Kekule.Spectroscopy.SpectrumPeakDetails}).
	 * @param {Int} index
	 * @param {Bool} autoCreate If the peak is without extra info, whether automatically create an instance of {@link Kekule.Spectroscopy.SpectrumPeakDetails}.
	 * @returns {Kekule.Spectroscopy.SpectrumPeakDetails}
	 */
	/*
	getPeakDetailsAt: function(index, autoCreate)
	{
		var value = this.getRawValueAt(index);
		if (value)
			return this.getPeakDetailsOf(value, autoCreate);
		else
			return null;
	},
	*/
	/**
	 * Returns the peak root value of data item value.
	 * @param {Hash} value
	 * @returns {Hash}
	 */
	getPeakRootValueOf: function(value)
	{
		if (this.getMode() !== Kekule.Spectroscopy.DataMode.PEAK)
			return null;
		else
		{
			var pr = this.getDefPeakRoot() || this._getDefaultPeakRoot();
			return Object.extend(Object.extend({}, value), pr);
		}
	},
	/**
	 * Returns the peak root value of data item at index.
	 * @param {Int} index
	 * @returns {Hash}
	 */
	getPeakRootValueAt: function(index)
	{
		return this.getPeakRootValueOf(this.getValueAt(index));
	},

	/**
	 * Get the data item indexes that locates around independent variable values.
	 * This method is recommended to be used in peak mode spectrum data section.
	 * @param {Variant} independentValues A hash value with symbol: number map, or an array of values, or a single number value.
	 * @param {Hash} extraOptions
	 * @returns {Array}
	 */
	getSurroundingDataItemIndexesFromIndependent: function(independentValues, extraOptions)
	{
		// TODO: unfinished
	},
	/**
	 * Get the data item index from independent variable values.
	 * If the independentValues not pointed to a data item, -1 will be returned.
	 * This method is recommended to be used in peak mode spectrum data section.
	 * @param {Variant} independentValues A hash value with symbol: number map, or an array of values, or a single number value.
	 * @param {Hash} extraOptions
	 * @returns {Int}
	 */
	getDataItemIndexFromIndependent: function(independentValues, extraOptions)
	{
		var indepHashValues = {};
		if (typeof(independentValues) === 'number')  // a direct number value,
			indepHashValues = this._convArrayToSymboledHash([independentValues], this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.INDEPENDENT));
		else if (DataType.isArrayValue(independentValues))
			indepHashValues = this._convArrayToSymboledHash(independentValues, this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.INDEPENDENT));
		else
			indepHashValues = independentValues;

		var indepVarSymbols = Kekule.ObjUtils.getOwnedFieldNames(indepHashValues);
		var varDataRange = this.calcDataRange(indepVarSymbols);
		var allowedErrorRate = extraOptions && extraOptions.allowedErrorRate;
		var allowedError = {};
		for (var i = 0, l = indepVarSymbols.length; i < l; ++i)
		{
			var symbol = indepVarSymbols[i];
			var indepSearchValue = indepHashValues[symbol];
			if (indepSearchValue < varDataRange[symbol].min || indepSearchValue > varDataRange[symbol].max)
				return -1;
			else
				allowedError[symbol] = allowedErrorRate && ((varDataRange[symbol].max - varDataRange[symbol].min) * allowedErrorRate);
		}

		var resultDetails = {'index': -1, 'distanceSqr': 0};
		for (var i = 0, l = this.getDataCount(); i < l; ++i)
		{
			var value = this.getHashValueAt(i);
			var distanceSqr;
			var peakMatch = true;
			for (var j = 0, k = indepVarSymbols.length; j < k; ++j)
			{
				var symbol = indepVarSymbols[j];
				var indepSearchValue = indepHashValues[symbol];
				var currValue = value[symbol];
				var indepDistance = Math.abs(currValue - indepSearchValue);
				if (indepDistance <= allowedError[symbol])
				{
					distanceSqr += Math.sqr(indepDistance / varDataRange[symbol]);
				}
				else // indep value not match to this peak
				{
					peakMatch = false;
					break;
				}
			}
			if (peakMatch)  // found the candicate peak, compare to the prev candicate one, chose with the min distance
			{
				if (resultDetails.index < 0 || resultDetails.distanceSqr > distanceSqr)
				{
					resultDetails.index = i;
					resultDetails.distanceSqr = distanceSqr;
				}
			}
		}
		return resultDetails.index;
	},
	/**
	 * Returns the data hash value item from independent variable values.
	 * If the independentValues not pointed to a data value item, null will be returned.
	 * This method is recommended to be used in peak mode spectrum data section.
	 * @param {Variant} independentValues A hash value with symbol: number map, or an array of values, or a single number value.
	 * @param {Hash} extraOptions
	 * @returns {Int}
	 */
	getDataValueFromIndependent: function(independentValues, extraOptions)
	{
		var index = this.getDataItemIndexFromIndependent(independentValues, extraOptions);
		return (index >= 0)? this.getValueAt(index): null;
	},
	/**
	 * Calculate values of dependant variable values from independent variable values.
	 * @param {Variant} independentValues A hash value with symbol: number map, or an array of values, or a single number value.
	 * @param {Hash} extraOptions
	 * @returns {Hash}
	 */
	calcValueFromIndependent: function(independentValues, extraOptions)
	{
		var indepHashValues = {};
		if (typeof(independentValues) === 'number')  // a direct number value,
			indepHashValues = this._convArrayToSymboledHash([independentValues], this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.INDEPENDENT));
		else if (DataType.isArrayValue(independentValues))
			indepHashValues = this._convArrayToSymboledHash(independentValues, this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.INDEPENDENT));
		else
			indepHashValues = independentValues;
		return this.doCalcValueFromIndependent(indepHashValues, extraOptions);
	},
	/**
	 * Do actual work of {@link Kekule.Spectroscopy.SpectrumData.calcValueFromIndependent}.
	 * Descendants may override this method.
	 * @param {Hash} independentValues
	 * @param {Hash} extraOptions
	 * @returns {Hash}
	 * @private
	 */
	doCalcValueFromIndependent: function(independentValues, extraOptions)
	{
		var mode = this.getMode();
		return (mode === Kekule.Spectroscopy.DataMode.PEAK)?
			this._doCalcValueFromIndependentInPeakMode(independentValues, extraOptions):
			this._doCalcValueFromIndependentInContinousMode(independentValues, extraOptions);
	},
	/** @private */
	_doCalcValueFromIndependentInPeakMode: function(independentValues, extraOptions)
	{
		/*
		// TODO: currently only handles data with one one independent var, but this approach can handle most of the spectrum cases
		var indepVarSymbols = this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.INDEPENDENT);
		if (indepVarSymbols.length > 1)
			return null;
		var indepVarSymbol = indepVarSymbols[0];
		var indepDataRange = this.calcDataRange([indepVarSymbol])[indepVarSymbol];
		var indepSearchValue = independentValues[indepVarSymbol];
		if (indepSearchValue < indepDataRange.min || indepSearchValue > indepDataRange.max)
			return null;

		var allowedError = ((extraOptions && extraOptions.allowedErrorRate) || Kekule.globalOptions.spectrum.data.allowedComparisonErrorRate)
			* (indepDataRange.max - indepDataRange.min);
		var resultDetails = {'value': null, 'distance': indepDataRange.max - indepDataRange.min};
		for (var i = 0, l = this.getDataCount(); i < l; ++i)
		{
			var value = this.getHashValueAt(i);
			var indepDistance = Math.abs(value[indepVarSymbol] - indepSearchValue);
			if (indepDistance <= allowedError)
			{
				if (indepDistance < resultDetails.distance)
				{
					resultDetails.value = value;
					resultDetails.distance = indepDistance;
				}
			}
		}
		if (!resultDetails.value)  // use the default peak root value
			resultDetails.value = this.getPeakRootValueOf(independentValues);

		//delete resultDetails.value[indepVarSymbol];  // remove the indepedent var, only use the dependent ones
		return resultDetails.value;
		*/
		var peakIndex = this.getDataItemIndexFromIndependent(independentValues, extraOptions);
		if (peakIndex >= 0)  // found the peak
			return this.getHashValueAt(peakIndex);
		else  // use the default peak root value
			return this.getPeakRootValueOf(independentValues);
	},
	/** @private */
	_doCalcValueFromIndependentInContinousMode: function(independentValues, extraOptions)
	{
		var resultEx = this._doCalcValueFromIndependentInContinousModeEx(independentValues, extraOptions);
		return resultEx && resultEx.value;
	},
	/** @private */
	_doCalcValueFromIndependentInContinousModeEx: function(independentValues, extraOptions)
	{
		// TODO: currently only handles data with one one independent var, but this approach can handle most of the spectrum cases
		var indepVarSymbols = this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.INDEPENDENT);
		var depVarSymbols = this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.DEPENDENT);
		if (indepVarSymbols.length > 1)
			return null;
		var indepVarSymbol = indepVarSymbols[0];
		var indepVarValue = independentValues[indepVarSymbol];
		var totalDataRange = this.calcDataRange([indepVarSymbol])[indepVarSymbol];
		if (indepVarValue < totalDataRange.min || indepVarValue > totalDataRange.max)
			return null;

		var resultValue;
		var dataIndexFloor = -1, dataIndexCeil = -1;
		var useContinuousVarRange = true;
		var varDef = this.getLocalVarDef(indepVarSymbol);
		var dataCount = this.getDataCount();
		useContinuousVarRange = !varDef.hasDifferentExternalUnit();  // the value get from getContinuousVarRange function in based on internal unit
		if (useContinuousVarRange)
		{
			var indepVarRange = this.getContinuousVarRange(indepVarSymbol);
			useContinuousVarRange = !!indepVarRange;
		}
		if (useContinuousVarRange)
		{
			var dataIndex = (indepVarValue - indepVarRange.fromValue) / (indepVarRange.toValue - indepVarRange.fromValue) * (dataCount - 1);
			dataIndexFloor = Math.max(Math.floor(dataIndex), 0);
			dataIndexCeil = Math.min(Math.ceil(dataIndex), dataCount - 1);
		}
		else
		{
			//indepVarRange = this.calcDataRange([indepVarSymbols])[indepVarSymbol];
			var indepOrder = this.getIndependentVarValueOrder(indepVarSymbol);
			if (indepOrder === 0)  // var values are not ordered, can not calculate
			{
				// do nothing, keep dataIndexFloor/dataIndexCeil < 0
			}
			else
			{
				var reversedOrder = indepOrder < 0;
				var indexes = this._calcNeighborDataIndexesToIndependentValue(indepVarSymbol, indepVarValue, 0, dataCount - 1, reversedOrder);
				dataIndexFloor = indexes[0];
				dataIndexCeil = indexes[1];
			}
		}

		if (dataIndexFloor < 0 || dataIndexCeil < 0)
			return null;
		if (dataIndexFloor === dataIndexCeil)
			//return this.getHashValueAt(dataIndexFloor);
			resultValue = this.getHashValueAt(dataIndexFloor);
		else
		{
			/*
			var valueFloor = this.getHashValueAt(dataIndexFloor);
			var valueCeil = this.getHashValueAt(dataIndexCeil);
			var ratio = (indepVarValue - valueFloor[indepVarSymbol]) / (valueCeil[indepVarSymbol] - valueFloor[indepVarSymbol]);
			var result = {};
			var symbols =  Kekule.ObjUtils.getOwnedFieldNames(valueFloor);
			for (var i = 0, l = symbols.length; i < l; ++i)
			{
				var symbol = symbols[i];
				if (symbol === indepVarSymbol)
					result[symbol] = indepVarValue;
				else
					result[symbol] = valueFloor[symbol] + (valueCeil[symbol] - valueFloor[symbol]) * ratio;
			}
			*/
			//var valueFloor = this.getHashValueAt(dataIndexFloor);
			//var depVarSymbols = Kekule.ObjUtils.getOwnedFieldNames(valueFloor);
			//AU.remove(depVarSymbols, indepVarSymbol);

			var depValues = this._calcIntermediateDependentVarValuesBetween(depVarSymbols, indepVarSymbol, indepVarValue, dataIndexFloor, dataIndexCeil);
			resultValue = {};
			resultValue[indepVarSymbol] = indepVarValue;
			resultValue = Object.extend(resultValue, depValues);  // ensure the independent value first
			//console.log('calc', result, depVarSymbols, indepVarSymbol, indepVarValue, dataIndexFloor, dataIndexCeil);
			//return result;
		}
		return {
			'value': resultValue,
			'dataIndexes': [dataIndexFloor, dataIndexCeil]
		};
	},
	/** @private */
	_calcNeighborDataIndexesToIndependentValue: function(indepVarSymbol, indepValue, fromIndex, toIndex, reversedOrder)
	{
		if (toIndex - fromIndex <= 2)
			return [fromIndex, toIndex];
		var halfIndex = Math.round((fromIndex + toIndex) / 2);
		var halfValue = this.getHashValueAt(halfIndex)[indepVarSymbol];
		if (halfValue === indepValue)
			return [halfIndex, halfIndex];
		else if (halfValue <= indepValue)
			return !reversedOrder?
				this._calcNeighborDataIndexesToIndependentValue(indepVarSymbol, indepValue, halfIndex, toIndex, reversedOrder):
				this._calcNeighborDataIndexesToIndependentValue(indepVarSymbol, indepValue, fromIndex, halfIndex, reversedOrder);
		else
			return !reversedOrder?
				this._calcNeighborDataIndexesToIndependentValue(indepVarSymbol, indepValue, fromIndex, halfIndex, reversedOrder):
				this._calcNeighborDataIndexesToIndependentValue(indepVarSymbol, indepValue, halfIndex, toIndex, reversedOrder);
	},
	/** @private */
	_calcIntermediateDependentVarValuesBetween: function(depVarSymbols, indepVarSymbol, indepVarValue, floorIndex, ceilIndex)
	{
		if (floorIndex === ceilIndex)
		{
			var value = this.getHashValueAt(floorIndex);
			var result = {};
			for (var i = 0, l = depVarSymbols.length; i < l; ++i)
			{
				result[depVarSymbols[i]] = value[depVarSymbols[i]];
			}
			return result;
		}

		var valueFloor = this.getHashValueAt(floorIndex);
		var valueCeil = this.getHashValueAt(ceilIndex);

		var floorIndepValue = valueFloor[indepVarSymbol];
		var ceilIndepValue = valueCeil[indepVarSymbol];
		if (!Kekule.NumUtils.isNormalNumber(floorIndepValue))
		{
			if (floorIndex > 0)
				return this._calcIntermediateDependentVarValuesBetween(depVarSymbols, indepVarSymbol, indepVarValue, floorIndex - 1, ceilIndex);
			else
				return this._calcIntermediateDependentVarValuesBetween(depVarSymbols, indepVarSymbol, indepVarValue, ceilIndex, ceilIndex);
		}
		else if (!Kekule.NumUtils.isNormalNumber(ceilIndepValue))
		{
			if (ceilIndex < this.getDataCount() - 1)
				return this._calcIntermediateDependentVarValuesBetween(depVarSymbols, indepVarSymbol, indepVarValue, floorIndex, ceilIndex + 1);
			else
				return this._calcIntermediateDependentVarValuesBetween(depVarSymbols, indepVarSymbol, indepVarValue, floorIndex, floorIndex);
		}
		else
		{
			var ratio = (indepVarValue - valueFloor[indepVarSymbol]) / (valueCeil[indepVarSymbol] - valueFloor[indepVarSymbol]);
			var result = {};
			for (var i = 0, l = depVarSymbols.length; i < l; ++i)
			{
				var symbol = depVarSymbols[i];
				var floorDepValue = valueFloor[symbol];
				var ceilDepValue = valueCeil[symbol];
				var resultValue = false;
				if (!Kekule.NumUtils.isNormalNumber(floorDepValue))
				{
					if (floorIndex > 0)
						resultValue = this._calcIntermediateDependentVarValuesBetween([symbol], indepVarSymbol, indepVarValue, floorIndex - 1, ceilIndex);
					else
						resultValue = ceilDepValue[symbol];
				}
				else if (!Kekule.NumUtils.isNormalNumber(ceilDepValue))
				{
					if (ceilIndex < this.getDataCount() - 1)
						resultValue = this._calcIntermediateDependentVarValuesBetween([symbol], indepVarSymbol, indepVarValue, floorIndex, ceilIndex + 1);
					else
						resultValue = floorDepValue[symbol];
				}
				else
					resultValue = floorDepValue + (ceilDepValue - floorDepValue) * ratio;
				result[symbol] = resultValue;
			}
			return result;
		}

		/*
		var symbols =  Kekule.ObjUtils.getOwnedFieldNames(valueFloor);
		for (var i = 0, l = symbols.length; i < l; ++i)
		{
			var symbol = symbols[i];
			if (symbol === indepVarSymbol)
				result[symbol] = indepVarValue;
			else
				result[symbol] = valueFloor[symbol] + (valueCeil[symbol] - valueFloor[symbol]) * ratio;
		}
		//var valueFloor
		var ratio = (indepVarValue - valueFloor[indepVarSymbol]) / (valueCeil[indepVarSymbol] - valueFloor[indepVarSymbol]);
		*/
	},

	/**
	 * Calculate value ranges of dependant variables from independent variable value ranges.
	 * @param {Variant} independentRanges A hash value with {symbol: {min, max}} map, or an array of {min, max}.
	 * @param {Hash} extraOptions
	 * @returns {Hash} Including the following fields: {range, dataIndexes, dataValues}
	 */
	calcValueRangeFromIndependentRangeEx: function(independentRanges, extraOptions)
	{
		var indepHashRanges = {};
		if (DataType.isArrayValue(independentRanges))
			indepHashRanges = this._convArrayToSymboledHash(independentRanges, this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.INDEPENDENT));
		else
			indepHashRanges = independentRanges;
		return this.doCalcValueRangeFromIndependentRangeEx(indepHashRanges, extraOptions);
	},
	/** @private */
	doCalcValueRangeFromIndependentRangeEx: function(independentRanges, extraOptions)
	{
		var indepVarSymbols = this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.INDEPENDENT);
		var depVarSymbols = this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.DEPENDENT);
		var totalDataRange = this.calcDataRange(indepVarSymbols);
		var findNearest = extraOptions && extraOptions.findNearest;

		var actualIndepRanges = {};
		var centerIndepValue = {};
		for (var i = 0, l = indepVarSymbols.length; i < l; ++i)
		{
			var indepVarSymbol = indepVarSymbols[i];
			if (independentRanges[indepVarSymbol])
			{
				var indepVarRange = Object.extend({}, independentRanges[indepVarSymbol]);
				var currTotalDataRange = totalDataRange[indepVarSymbol];

				if (indepVarRange.min < currTotalDataRange.min)
					indepVarRange.min = currTotalDataRange.min;
				if (indepVarRange.max > currTotalDataRange.max)
					indepVarRange.max = currTotalDataRange.max;
				actualIndepRanges[indepVarSymbol] = indepVarRange;
				centerIndepValue[indepVarSymbol] = (indepVarRange.min + indepVarRange.max) / 2;
			}
		}

		var mode = this.getMode();
		var result = (mode === Kekule.Spectroscopy.DataMode.PEAK)?
			this._doCalcValueRangeFromIndependentRangeInPeakModeEx(actualIndepRanges, centerIndepValue, indepVarSymbols, depVarSymbols, extraOptions):
			this._doCalcValueRangeFromIndependentRangeInContinousModeEx(actualIndepRanges, centerIndepValue, indepVarSymbols, depVarSymbols, extraOptions);
		if (result && result.range)  // check if no legal range returned
		{
			if (Kekule.ObjUtils.getOwnedFieldNames(result.range) <= 0)
				result.range = null;
		}
		return result;
	},
	/** @private */
	_doCalcValueRangeFromIndependentRangeInPeakModeEx: function(independentRanges, centerIndepValue, indepVarSymbols, depVarSymbols, extraOptions)
	{
		var findNearest = !!centerIndepValue;  // if centerIndepValue is passed, we need to find the nearest value
		var minDistanceSqr;
		var result = {'dataIndexes': [], 'dataValues': [], 'range':{}};
		var indexFrom, indexTo, indexStep;
		if (extraOptions.findInReversedOrder)
		{
			indexFrom = this.getDataCount() - 1;
			indexTo = 0;
			indexStep = -1;
		}
		else
		{
			indexFrom = 0;
			indexTo = this.getDataCount() - 1;
			indexStep = 1;
		}
		//for (var i = 0, l = this.getDataCount(); i < l; ++i)
		for (var i = indexFrom; extraOptions.findInReversedOrder? (i >= indexTo): (i <= indexTo); i += indexStep)
		{
			var value = this.getHashValueAt(i);
			var passed = true;
			var distanceSqr = 0;
			for (var j = 0, jj = indepVarSymbols.length; j < jj; ++j)
			{
				var indepSymbol = indepVarSymbols[j];
				passed = (value[indepSymbol] >= independentRanges[indepSymbol].min)
					&& (value[indepSymbol] <= independentRanges[indepSymbol].max);
				if (!passed)
					break;
				else
				{
					if (findNearest)
						distanceSqr += Math.sqr(value[indepSymbol] - centerIndepValue[indepSymbol]);
				}
			}
			if (passed)
			{
				result.dataIndexes.push(i);
				result.dataValues.push(value);
				if (findNearest)
				{
					if (minDistanceSqr === undefined || distanceSqr < minDistanceSqr)
					{
						minDistanceSqr = distanceSqr;
						result.nearest = {'dataIndex': i, 'dataValue': value};
					}
				}
			}
		}
		result.range = Kekule.Spectroscopy.Utils.expandDataRange(result.range, result.dataValues);
		return result;
	},
	/** @private */
	_doCalcValueRangeFromIndependentRangeInContinousModeEx: function(independentRanges, centerIndepValue, indepVarSymbols, depVarSymbols, extraOptions)
	{
		// TODO: currently only handles data with one one independent var, but this approach can handle most of the spectrum cases
		//var indepVarSymbols = this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.INDEPENDENT);
		//var depVarSymbols = this.getLocalVarSymbolsOfDependency(Kekule.VarDependency.DEPENDENT);
		if (indepVarSymbols.length > 1)
			return null;
		var indepVarSymbol = indepVarSymbols[0];
		var indepVarRange = independentRanges[indepVarSymbol];
		/*
		var totalDataRange = this.calcDataRange([indepVarSymbol])[indepVarSymbol];
		if (indepVarRange.min < totalDataRange.min)
			indepVarRange.min = totalDataRange.min;
		if (indepVarRange.max > totalDataRange.max)
			indepVarRange.max = totalDataRange.max;
		*/
		var indepV1 = {}, indepV2 = {};
		indepV1[indepVarSymbol] = indepVarRange.min;
		indepV2[indepVarSymbol] = indepVarRange.max;
		var valueEx1 = this._doCalcValueFromIndependentInContinousModeEx(indepV1, extraOptions);
		var valueEx2 = this._doCalcValueFromIndependentInContinousModeEx(indepV2, extraOptions);
		if (!valueEx1 || !valueEx2)
			return null;

		var isIndepValueReversed = (valueEx1.dataIndexes[0] > valueEx2.dataIndexes[0]);
		var rangeDataIndexes = {}
		if (isIndepValueReversed)
		{
			rangeDataIndexes.fromIndex = valueEx2.dataIndexes[1];
			rangeDataIndexes.toIndex = valueEx1.dataIndexes[0];
		}
		else
		{
			rangeDataIndexes.fromIndex = valueEx1.dataIndexes[1];
			rangeDataIndexes.toIndex = valueEx2.dataIndexes[0];
		}

		//var dataValues = [];

		var result = {'range': {}, 'dataIndexes': [], 'dataValues': []};
		for (var i = rangeDataIndexes.fromIndex; i <= rangeDataIndexes.toIndex; ++i)
		{
			var value = this.getHashValueAt(i)
			result.dataValues.push(value);
			result.dataIndexes.push(i);
			result.range = Kekule.Spectroscopy.Utils.expandDataRange(result.range, value);
		}
		result.range = Kekule.Spectroscopy.Utils.expandDataRange(result.range, [valueEx1.value, valueEx2.value]);
		// the leading and tailing values between data points
		if (isIndepValueReversed)
		{
			result.dataValues.push(valueEx1.value);
			result.dataValues.unshift(valueEx2.value);
		}
		else
		{
			result.dataValues.push(valueEx2.value);
			result.dataValues.unshift(valueEx1.value);
		}
		result.dataIndexes.push(-1);
		result.dataIndexes.unshift(-1);

		return result;
	},

	/**
	 * Returns an iterator to iterate all data in this object.
	 * If iterator is not available, null should be returned.
	 * Otherwise, the return value should be an object with method next(): {done, value}.
	 * @returns {Object}
	 */
	getIterator: function()
	{
		return this.doGetIterator();
	},
	/**
	 * Do actual work of {@link Kekule.Spectroscopy.SpectrumData.getIterator}.
	 * Desendants may override this method.
	 * @returns {Object}
	 * @private
	 */
	doGetIterator: function()
	{
		var dataItems = this.getDataItems();
		var self = this;
		var result = {
			index: 0,
			next: function(options)
			{
				if (this.index >= dataItems.length)
					return {'done': true};
				else
				{
					var ret = {'done': false, 'value': /*self._itemArrayToHash(dataItems[this.index])*/self.getHashValueAt(this.index, options)};
					++this.index;
					return ret;
				}
			}
		};
		return result;
	},

	/**
	 * Call function to each data item.
	 * @param {Func} func With params: (hashValue [, index, options]).
	 */
	forEach: function(func, thisArg, options)
	{
		var iterator = this.getIterator();
		if (iterator)
		{
			//var dataItems = this.getDataItems();
			var index = 0;
			var nextItem = iterator.next(options);
			while (!nextItem.done)
			{
				func.apply(thisArg, [nextItem.value, index]);
				++index;
				nextItem = iterator.next();
			}
		}
		return this;
	},

	// methods about comparison
	/** @ignore */
	getComparisonPropNames: function(options)
	{
		var result = this.tryApplySuper('getComparisonPropNames', [options]);
		if (result)
		{
			result = AU.exclude(result, ['parent', 'owner']);  // parent is a spectrum object, causing recursions in comparison
			AU.pushUnique(result, 'dataItems');   // the data items property is private, and will not be added to comparison by default
		}
		return result;
	},
	/** @ignore */
	doGetComparisonPropNames: function(options)
	{
		var result = this.tryApplySuper('doGetComparisonPropNames', [options]);
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			result = (result || []).concat(/*'name', 'title',*/  'defPeakRoot', 'localVarInfos', 'mode', 'dataItems');
		}
		return result;
	},
	/** @ignore */
	doCompareProperty: function(targetObj, propName, options)
	{
		if (propName === 'dataItems')
		{
			//console.log('compare dataItems', this.doCompareDataItems(this, targetObj, options));
			// the values in dataItems are float, so they need to be compared with NumUtils
			return this.doCompareDataItems(this, targetObj, options);
		}
		else if (propName === 'localVarInfos')
		{
			//console.log('compare localVarInfos', this.doCompareLocalVarInfos(this, targetObj, options));
			return this.doCompareLocalVarInfos(this, targetObj, options);
		}
		else
		{
			//console.log('compare property', propName, this.tryApplySuper('doCompareProperty', [targetObj, propName, options]));
			return this.tryApplySuper('doCompareProperty', [targetObj, propName, options]);
		}
	},
	/** @private */
	doCompareLocalVarInfos: function(section1, section2, options)
	{
		var varInfos1 = section1.getActualLocalVarInfos();
		var varInfos2 = section2.getActualLocalVarInfos();
		var result = varInfos1.length - varInfos2.length;
		if (!result && !(options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)) // local var info should not affect spectrum data greatly
		{
			for (var i = 0, l = varInfos1.length; i < l; ++i)
			{
				var info1 = varInfos1[i];
				var info2 = varInfos2[i];
				result = Kekule.ObjComparer.compare(info1, info2, options);
				if (result)
					break;
			}
		}
		//var result = this.doCompareOnValue(varInfos1, varInfos2, options);
		//console.log('varinfo', result, varInfos1, varInfos2);
		if (!result)  // further compare the var definitions of
		{
			for (var i = 0, l = varInfos1.length; i < l; ++i)
			{
				var def1 = section1.getLocalVarDef(i);
				var def2 = section1.getLocalVarDef(i);
				result = this.doCompareOnValue(def1, def2, options);
				if (result)
					break;
			}
		}
		return result;
	},
	/** @private */
	doCompareDataItems: function(section1, section2, options)
	{
		var getArrayValueAt = function(section, i)
		{
			var hashValue = section.getHashValueAt(i);
			return section._itemHashToArray(hashValue);
		};
		var result = section1.getDataCount() - section2.getDataCount();
		//console.log('dataCount', section1.getDataCount(), section2.getDataCount());
		if (!result)
		{
			for (var i = 0, l = section1.getDataCount(); i < l; ++i)
			{
				/*
				var rawValue1 = section1.getRawValueAt(i);
				var rawValue2 = section2.getRawValueAt(i);
				result = this._compareDataRawValue(rawValue1, rawValue2, options);
				*/
				var value1 = getArrayValueAt(section1, i);
				var value2 = getArrayValueAt(section1, i);
				result = this._compareDataRawValue(value1, value2, options);
				// and the extra info of value
				if (!result)
				{
					var extra1 = section1.getExtraInfoAt(i) || null;
					var extra2 = section2.getExtraInfoAt(i) || null;
					// empty spectrum peak details should be regarded as containing no information
					if (extra1 && extra1.isEmpty())
						extra1 = null;
					if (extra2 && extra2.isEmpty())
						extra2 = null;
					result = this.doCompareOnValue(extra1, extra2, options);
					//console.log('compare extra', i, extra1, extra2, result);
				}
				if (result)
				{
					//console.log('compare', i, value1, value2, result);
					break;
				}
			}
		}
		return result;
	},
	/** @private */
	_compareDataRawValue: function(v1, v2, options)
	{
		var result;
		if (!v1 && !v2)
			return 0;
		else if (v1 && !v2)
			return 1;
		else if (!v1 && v2)
			return -1;
		else
		{
			result = (v1.length || -1) - (v2.length || -1);
			if (!result)  // compare each values
			{
				var NumUtils = Kekule.NumUtils;
				var errorRate = options.allowedComparisonErrorRate || Kekule.globalOptions.spectrum.data.allowedComparisonErrorRate || 5e-8;
				for (var i = 0, l = v1.length; i < l; ++i)
				{
					var value1 = v1[i];
					var value2 = v2[i];
					if (NumUtils.isNormalNumber(value1) && NumUtils.isNormalNumber(value2))
					{
						if (NumUtils.isFloatEqual(value1, value2, Math.abs(value1 * errorRate)))  // TODO: the error value is fixed now
							result = 0;
						else
						{
							result = Math.sign(value1 - value2);
							//console.log('float diff', value1, value2);
						}
					}
					else
						result = this.doCompareOnValue(v1, v2, options)
					if (result)
						break;
				}
			}
		}
		return result;
	}
});

/**
 * The base spectrum data class.
 * The concrete data can be stored in different forms, implemented in different descendant classes.
 * @class
 * @augments ObjectEx
 *
 * @param {String} id
 * @param {Array} variables Array of variables of data, each item is {@link Kekule.Spectroscopy.SpectrumVarDefinition}.
 *
 * @property {Array} variables Array of variables of data, each item is {@link Kekule.Spectroscopy.SpectrumVarDefinition}.
 * @property {Kekule.ChemObjList} sections Child data sections.
 * @property {Kekule.Spectroscopy.SpectrumData} activeSection Active data section to read/write data.
 * @property {Bool} autoCreateSection Whether create a initial data section automatically when inserting data.
 */
Kekule.Spectroscopy.SpectrumData = Class.create(ObjectEx,
/** @lends Kekule.Spectroscopy.SpectrumData# */
	{
		/** @private */
		CLASS_NAME: 'Kekule.Spectroscopy.SpectrumData',
		/** @private */
		initialize: function (id, variables, parent) {
			//this.setPropStoreFieldValue('dataItems', []);
			this.tryApplySuper('initialize', [id]);
			this.setPropStoreFieldValue('variables', variables ? AU.clone(variables) : []);
			var sections = new Kekule.ChemObjList(null, Kekule.Spectroscopy.SpectrumDataSection, true);
			this.setPropStoreFieldValue('sections', sections);
			this.setParent(parent);
			sections.setOwner(this.getOwner());
			//this.createSection(this.getVariables());  // create a default section
		},
		doFinalize: function ()
		{
			//this.clear();
			this.getSections().finalize();
			var variables = this.getVariables() || [];
			for (var i = 0, l = variables.length; i < l; ++i) {
				variables[i].finalize();
			}
			this.setPropStoreFieldValue('variables', null);
			this.tryApplySuper('doFinalize');
		},
		/** @private */
		initProperties: function ()
		{
			this.defineProp('owner', {
				'dataType': 'Kekule.ChemSpace',
				'serializable': false,
				'scope': Class.PropertyScope.PUBLIC,
				'getter': function()
				{
					return this.getPropStoreFieldValue('owner') || this._getDefaultOwner();
				},
				'setter': function(value)
				{
					if (value !== this.getPropStoreFieldValue('owner'))
					{
						this.setPropStoreFieldValue('owner', value);
						var newOwner = this.getOwner();
						var sections = this.getSections();
						if (sections)
							sections.setOwner(newOwner);
					}
				}
			});
			this.defineProp('parent', {
				'dataType': 'Kekule.Spectroscopy.Spectrum', 'serializable': false,
				'setter': function (value) {
					this.setPropStoreFieldValue('parent', value);
					var sections = this.getSections();
					if (sections)
						sections.setParent(this.getParent() || this);
				}
			});
			this.defineProp('sections', {
				'dataType': 'Kekule.ChemObjList',
				'setter': function (value) {
					var old = this.getSections();
					if (old !== value)
					{
						if (old)
						{
							old.finalize();
						}
						if (value)
						{
							value._transparent = true;  // force the obj list be transparent
							value.setParent(this.getParent() || this);
							value.setOwner(this.getOwner());
						}
						this.setPropStoreFieldValue('sections', value);
					}
				}
			});
			this.defineProp('autoCreateSection', {'dataType': DataType.BOOL});
			this.defineProp('activeSectionIndex', {
				'dataType': DataType.INT,
				'getter': function () {
					if (this.getSectionCount() <= 0)
						return -1;
					else if (this.getSectionCount() === 1)  // only one section, it should be activated by default
						return 0;
					else
						return this.getPropStoreFieldValue('activeSectionIndex');
				},
				'setter': function (value) {
					if (value >= 0 && value <= this.getSectionCount())
						this.setPropStoreFieldValue('activeSectionIndex', value);
				}
			});
			this.defineProp('activeSection', {
				'dataType': 'Kekule.Spectroscopy.SpectrumDataSection', 'serializable': false,
				'getter': function () {
					var result = this.getSectionAt(this.getActiveSectionIndex() || 0);
					if (!result && this.getSectionCount() <= 0 && this.getAutoCreateSection()) {
						result = this.createSection(this.getVariables());
						//console.log('auto create');
					}
					return result;
				},
				'setter': function (value) {
					this.setActiveSectionIndex(this.indexOfSection(value));
				}
			});
			/*
      this.defineProp('variables', {'dataType': DataType.ARRAY, 'setter': null, 'serializable': false,
        'getter': function()
        {
          var result = [];
          for (var i = 0, l = this.getSectionCount(); i < l; ++i)
          {
            var vars = this.getSectionAt(i).getVariables();
            AU.pushUnique(result, vars);
          }
          return result;
        }
      });
      */
			this.defineProp('variables', {'dataType': DataType.ARRAY/*, 'setter': null*/});
			// private, stores the data items, each item is a hash, e.g. {x: 1, y: 10, w: 2}
			//this.defineProp('dataItems', {'dataType': DataType.ARRAY, 'setter': null, 'scope': PS.PRIVATE});
			// private, cache all variable names
			this.defineProp('varSymbols', {
				'dataType': DataType.ARRAY, 'setter': null, 'scope': PS.PRIVATE,
				'getter': function () {
					var result = [];
					var list = this.getVariables() || [];
					for (var j = 0, jj = list.length; j < jj; ++j) {
						var varDef = list[j];
						result.push(varDef.getSymbol());
					}
					return result;
				}
			});
			this.defineProp('mode', {'dataType': DataType.INT, 'enumSource': Kekule.Spectroscopy.DataMode});
		},
		/** @ignore */
		initPropValues: function () {
			this.tryApplySuper('initPropValues');
			this.setAutoCreateSection(true);
			this.setMode(Kekule.Spectroscopy.DataMode.CONTINUOUS);
		},

		/** @private */
		_getDefaultOwner: function()
		{
			var parent = this.getParent();
			return parent && parent.getOwner && parent.getOwner();  // always returns the owner of parent spectrum
		},
		/** @private */
		getHigherLevelObj: function ()
		{
			return this.getParent();
		},
		/** @ignore */
		getChildHolder: function ()
		{
			return this.getSections();
		},

		/** @ignore */
		loaded: function(rootObj)
		{
			var sections = this.getSections();
			if (sections)
			{
				sections.parentChanged(this);
				sections.ownerChanged(this.getOwner());
			}
			this.tryApplySuper('loaded', [rootObj]);
		},

		/**
		 * Check if there are concrete data in child data sections.
		 * @returns {Bool}
		 */
		isEmpty: function()
		{
			var result = true;
			for (var i = 0, l = this.getSectionCount(); i < l; ++i)
			{
				if (!this.getSectionAt(i).isEmpty())
					return false;
			}
			return result;
		},

		/**
		 * Create and append a new {@link Kekule.Spectroscopy.SpectrumDataSection}.
		 * @param {Array} variables Array of local variable symbol or definition used by secion.
		 * @param {Int} mode
		 * @returns {Kekule.Spectroscopy.SpectrumDataSection}
		 */
		createSection: function (variables, mode) {
			var result = new Kekule.Spectroscopy.SpectrumDataSection(undefined, this, variables);
			//result.setVariables(variables);
			result.setMode(mode || this.getMode());
			this.getSections().appendChild(result);
			return result;
		},
		/**
		 * Remove all data sections.
		 */
		clearSection: function () {
			var sections = this.getChildren();
			for (var i = 0, l = sections.length; i < l; ++i) {
				sections[i].clear();
				sections[i].setParent(null);
				sections[i].finalize();
			}
			this.getSections().clear();
		},
		/**
		 * Get count of child data sections.
		 * @returns {Int}
		 */
		getSectionCount: function () {
			return this.getSections().getChildCount();
		},
		/**
		 * Get child data sectionb at index.
		 * @param {Int} index
		 * @returns {Kekule.Spectroscopy.SpectrumDataSection}
		 */
		getSectionAt: function (index) {
			return this.getSections().getItemAt(index);
		},
		/**
		 * Get the index of child section in children list.
		 * @param {Kekule.Spectroscopy.SpectrumDataSection} section
		 * @returns {Int} Index of section or -1 when not found.
		 */
		indexOfSection: function (section) {
			return this.getSections().indexOfItem(section);
		},
		/**
		 * Check if section is in this spectrum data.
		 * @param {Kekule.Spectroscopy.SpectrumDataSection} section
		 * @returns {Bool}
		 */
		hasSection: function (section) {
			return this.indexOfSection(section) >= 0;
		},
		/**
		 * Remove a data section at index.
		 * @param {Int} index
		 * @returns {Kekule.Spectroscopy.SpectrumDataSection} Child section removed.
		 */
		removeSectionAt: function (index) {
			return this.getSections().removeItemAt(index);
		},
		/**
		 * Remove a child data section.
		 * @param {Kekule.Spectroscopy.SpectrumDataSection} section
		 * @returns {Kekule.Spectroscopy.SpectrumDataSection} Section object removed.
		 */
		removeSection: function (section) {
			return this.getSections().removeItem(section);
		},
		/**
		 * Insert a new section to index.
		 * @param {Kekule.Spectroscopy.SpectrumDataSection} section
		 * @param {Int} index
		 * @return {Int} Index of section after insertion.
		 */
		insertSectionAt: function (section, index) {
			return this.getSections().insertItemAt(section, index);
		},
		/**
		 * Insert a data section before refSection in data section list.
		 * @param {Kekule.Spectroscopy.SpectrumDataSection} obj
		 * @param {Kekule.Spectroscopy.SpectrumDataSection} refChildr
		 * @return {Int} Index of section after insertion.
		 */
		insertSectionBefore: function (section, refSection) {
			return this.getSections().insertItemBefore(section, refSection);
		},
		/**
		 * Add new data section to the tail of section list.
		 * @param {Kekule.Spectroscopy.SpectrumDataSection} section
		 * @return {Int} Index of obj after appending.
		 */
		appendSection: function (section) {
			return this.getSections().appendChild(section);
		},
		/**
		 * Returns whether multiple sections exists in this spectrum data.
		 * @returns {Bool}
		 */
		hasMultipleSections: function () {
			return this.getSections().getChildCount() > 1;
		},

		/**
		 * Iterate all data items in a section and calculate the min/max value of each variable.
		 * @param {Kekule.Spectroscopy.SpectrumDataSection} section
		 * @param {Array} targetVariables Array of variable definition or symbol.
		 *   If not set, all variables will be calculated.
		 * @returns {Hash}
		 */
		calcDataRangeOfSection: function (section, targetVariables) {
			return section.calcDataRange(targetVariables);
		},
		/**
		 * Iterate all data items in a set of sections and calculate the min/max value of each variable.
		 * @param {Array} sections
		 * @param {Array} targetVariables Array of variable definition or symbol.
		 *   If not set, all variables will be calculated.
		 * @param {Hash} options Extra calculation options, same as the options in {@link Kekule.Spectroscopy.SpectrumDataSection.calcDataRange}.
		 * @returns {Hash}
		 */
		calcDataRangeOfSections: function (sections, targetVariables, options) {
			var result = {};
			for (var i = 0, l = sections.length; i < l; ++i) {
				var range = sections[i].calcDataRange(targetVariables, options);
				result = Kekule.Spectroscopy.Utils.mergeDataRange(result, range);
			}
			return result;
		},
		/**
		 * Returns the display range of a section.
		 * @param {Kekule.Spectroscopy.SpectrumDataSection} section
		 * @param {Array} targetVariables Array of variable definition or symbol.
		 *   If not set, all variables will be calculated.
		 * @param {Hash} options May include fields:
		 *  {
		 *    autoCalc: Bool. If true, when explicit display range is not set, the number range of variable will be calculated and returned.
		 *    basedOnInternalUnit: Bool. If true, the returned value will be based on internal unit rather than the external unit of variable.
		 *  }
		 * @returns {Hash}
		 */
		getDisplayRangeOfSection: function (section, targetVariables, options) {
			return section.getDisplayRangeOfVars(targetVariables, options);
		},
		/**
		 * Returns the display range of a set of sections.
		 * @param {Array} sections
		 * @param {Array} targetVariables Array of variable definition or symbol.
		 *   If not set, all variables will be calculated.
		 * @param {Hash} options May include fields:
		 *  {
		 *    autoCalc: Bool. If true, when explicit display range is not set, the number range of variable will be calculated and returned.
		 *    basedOnInternalUnit: Bool. If true, the returned value will be based on internal unit rather than the external unit of variable.
		 *  }
		 * @returns {Hash}
		 */
		getDisplayRangeOfSections: function (sections, targetVariables, options) {
			var result = {};
			for (var i = 0, l = sections.length; i < l; ++i) {
				var range = sections[i].getDisplayRangeOfVars(targetVariables, options);
				result = Kekule.Spectroscopy.Utils.mergeDataRange(result, range);
			}
			return result;
		},

		/**
		 * Returns count of all variables.
		 * @returns {Int}
		 */
		getVariableCount: function () {
			return (this.getVariables() || []).length;
		},
		/**
		 * Returns the variable definition by a index or variable name.
		 * @param {Variant} varIndexOrNameOrDef
		 * @returns {Kekule.Spectroscopy.SpectrumVarDefinition}
		 */
		getVariable: function (varIndexOrNameOrDef) {
			var varDef = (varIndexOrNameOrDef instanceof Kekule.VarDefinition) ? varIndexOrNameOrDef :
				(typeof (varIndexOrNameOrDef) === 'number') ? this.getVariables()[varIndexOrNameOrDef] :   // index
					this.getVariables()[this.getVarSymbols().indexOf(varIndexOrNameOrDef)];   // name
			return varDef;
		},
		/**
		 * Returns the index of a variable definition.
		 * @param {Kekule.Spectroscopy.SpectrumVarDefinition} varDef
		 * @returns {Int}
		 */
		indexOfVariable: function (varDef) {
			return this.getVariables().indexOf(varDef);
		},
		/**
		 * Insert a new variable definition at a specified position.
		 * @param {Kekule.Spectroscopy.SpectrumVarDefinition} varDef
		 * @param {Int} index
		 */
		insertVariableAt: function (varDef, index) {
			if (index >= 0)
				this.getVariables().splice(index, 0, varDef);
			else
				this.getVariables().push(varDef);
			return this;
		},
		/**
		 * Insert a new variable definition before ref.
		 * @param {Kekule.Spectroscopy.SpectrumVarDefinition} varDef
		 * @param {Kekule.Spectroscopy.SpectrumVarDefinition} ref
		 */
		insertVariableBefore: function (varDef, ref) {
			var index = ref ? this.indexOfVarDefinition(ref) : -1;
			return this.insertVarDefinitionAt(varDef, index);
		},
		/**
		 * Append a new variable definition.
		 * @param {Kekule.Spectroscopy.SpectrumVarDefinition} varDef
		 */
		appendVariable: function (varDef) {
			return this.insertVariableAt(varDef, -1);
		},
		/**
		 * Remove a variable definition at index.
		 * @param {Int} index
		 */
		removeVariableAt: function (index) {
			this.getVariables().splice(index, 1);
			return this;
		},
		/**
		 * Remove a variable definition.
		 * @param {Kekule.Spectroscopy.SpectrumVarDefinition} varDef
		 */
		removeVariable: function (varDef) {
			var index = this.indexOfVariable(varDef);
			if (index >= 0)
				this.removeVariableAt(index);
			return this;
		},

		/**
		 * Returns variables of certain dependency.
		 * @param {Int} dependency Value from {@link Kekule.VarDependency}
		 * @returns {Array} Array of var definition.
		 */
		getVariablesOfDependency: function (dependency) {
			var result = [];
			for (var i = 0, l = this.getVariableCount(); i < l; ++i) {
				var varDef = this.getVariable(i);
				if (varDef && varDef.getDependency() === dependency)
					result.push(varDef);
			}
			return result;
		},

		/**
		 * Returns the first/last value of a continuous variable.
		 * @param {Variant} varNameOrIndexOrDef
		 * @returns {Hash} Hash of {firstValue, lastValue}
		 */
		getContinuousVarRange: function (varIndexOrNameOrDef) {
			var varDef = this.getVariable(varIndexOrNameOrDef);
			var info = varDef && varDef.getInfo();
			if (info) {
				if (info.continuous) {
					//var count = this.getDateItemCount();
					return {
						'fromValue': info.fromValue,
						'toValue': info.toValue /*, 'interval': (info.lastValue - info.firstValue) / count */
					};
				}
			}
			return null;
		},
		/**
		 * Set the first/last value of a variable and mark it as a continuous one.
		 * @param {Variant} varNameOrIndexOrDef
		 * @param {Number} fromValue
		 * @param {Number} toValue
		 */
		setContinuousVarRange: function (varIndexOrNameOrDef, fromValue, toValue) {
			var varDef = this.getVariable(varIndexOrNameOrDef);
			var info = varDef && varDef.getInfo(true);
			info.continuous = true;
			info.fromValue = fromValue;
			info.toValue = toValue;
			return this;
		},
		/**
		 * Remove the continuous information of a variable.
		 * @param {Variant} varIndexOrNameOrDef
		 */
		clearContinuousVarRange: function (varIndexOrNameOrDef) {
			var varDef = this.getVariable(varIndexOrNameOrDef);
			var info = varDef.getInfo();
			if (info && info.continuous)
				info.continuous = false;
			return this;
		},
		/**
		 * Set the default value of a variable when the concrete value in spectrum is absent.
		 * E.g., in many NMR peak spectrums, y value will be omitted, and this method will provide a default one for it.
		 * @param {Variant} varIndexOrNameOrDef
		 * @param {Number} value
		 */
		setDefaultVarValue: function (varIndexOrNameOrDef, value)
		{
			var varDef = this.getVariable(varIndexOrNameOrDef);
			var info = varDef && varDef.getInfo(true);
			info.defaultValue = value;
			return this;
		},
		/**
		 * Clear the default value of a variable.
		 * @param {Variant} varIndexOrNameOrDef
		 */
		clearDefaultVarValue: function(varIndexOrNameOrDef)
		{
			return this.setDefaultVarValue(varIndexOrNameOrDef, null);
		},
		/**
		 * Get the default value of a variable when the concrete value in spectrum is absent.
		 * E.g., in many NMR peak spectrums, y value will be omitted, and this method will provide a default one for it.
		 * @param {Variant} varIndexOrNameOrDef
		 * @returns {Number}
		 */
		getDefaultVarValue: function(varIndexOrNameOrDef)
		{
			var varDef = this.getVariable(varIndexOrNameOrDef);
			var info = varDef && varDef.getInfo();
			if (info)
			{
				return info.defaultValue;
			}
		},

	/**
	 * Iterate all child sections and execute function.
	 * @param {Function} func Function with param (section, index).
	 */
	iterateSections: function(func)
	{
		for (var i = 0, l = this.getSectionCount(); i < l; ++i)
		{
			func(this.getSectionAt(i), i);
		}
	},
	/**
	 * Sort all data items.
	 * @param {Func} func Optional, func(hash1, hash2). If not set, data items will be sorted by default method.
	 */
	sort: function(func)
	{
		this.iterateSections(function(c){
			c.sort(func);
		});
	},

	/**
	 * Returns the count of data items.
	 * @returns {Int}
	 */
	getDataCount: function()
	{
		var result = 0;
		this.iterateSections(function(c){
			result += c.getDataCount();
		});
		return result;
	},
	/**
	 * Clear all data items in all data sections.
	 */
	clearData: function()
	{
		this.iterateSections(function(c){
			c.clear();
		});
	},
	/**
	 * Add new data item to active data section. The item is can be a hash or an array.
	 * If it is a hash, the hash fields must matches {@link Kekule.Spectroscopy.SpectrumData.independentVars} and {@link Kekule.Spectroscopy.SpectrumData.dependentVars}.
	 * If it is an array, the values in array will automatically mapped to independent and dependent vars.
	 * @param {Variant} item
	 */
	appendData: function(item)
	{
		return this.getActiveSection().appendData(item);
	},
	/**
	 * Remove a data item from active data section.
	 * @param {Array} item
	 */
	removeData: function(item)
	{
		return this.getActiveSection().removeData(item);
	},
	/**
	 * Remove a data item at index in current active section.
	 * @param {Int} index
	 */
	removeDataAt: function(index)
	{
		return this.getActiveSection().removeDataAt(index);
	},
	/**
	 * Get the data value at index in current active section.
	 * @param {Int} index
	 * @returns {Array} The arrayed form of value.
	 */
	getRawValueAt: function(index)
	{
		return this.getActiveSection().getRawValueAt(index);
	},
	/** @private */
	getHashValueAt: function(index)
	{
		return this.getActiveSection().getHashValueAt(index);
	},
	/**
	 * Get the data value at index in current active section.
	 * @param {Int} index
	 * @returns {Hash} The hashed form of value.
	 */
	getValueAt: function(index)
	{
		return this.getHashValueAt(index);
	},
	/**
	 * Set the data value at index in current active section.
	 * @param {Int} index
	 * @param {Array} The array form of value.
	 */
	setRawValueAt: function(index, value)
	{
		this.getActiveSection().setRawValueAt(index, value);
		return this;
	},
	/** @private */
	setHashValueAt: function(index, value, options)
	{
		this.getActiveSection().setHashValueAt(index, value, options);
		return this;
	},
	/**
	 * Set the data value at index in current active section.
	 * @param {Int} index
	 * @param {Variant} value Value in hash or array form.
	 */
	setValueAt: function(index, value, options)
	{
		this.getActiveSection().setValueAt(index, value);
		return this;
	},
	/**
	 * Get the extra information of a data value in current active section.
	 * @param {Variant} value Data value in hash or array form.
	 * @returns {Hash}
	 */
	getExtraInfoOf: function(value)
	{
		return this.getActiveSection().getExtraInfoOf(value);
	},
	/**
	 * Set the extra information of a data value in current active section.
	 * @param {Variant} value Data value in hash or array form.
	 * @param {Hash} info
	 */
	setExtraInfoOf: function(value, info)
	{
		this.getActiveSection().setExtraInfoOf(value, info);
		return this;
	},
	/**
	 * Get the extra information of data value at index of current active section.
	 * @param {Int} index
	 * @returns {Hash}
	 */
	getExtraInfoAt: function(index)
	{
		return this.getActiveSection().getExtraInfoAt(index);
	},
	/**
	 * Set the extra information of data value at index of current active section.
	 * @param {Int} index
	 * @param {Hash} info
	 */
	setExtraInfoAt: function(index, info)
	{
		this.getActiveSection().setExtraInfoAt(index, info);
		return this;
	},
	/**
	 * Returns an iterator to iterate all data in this object.
	 * If iterator is not available, null should be returned.
	 * Otherwise, the return value should be an object with method next(): {done, value}.
	 * @returns {Object}
	 */
	getIterator: function()
	{
		return this.doGetIterator();
	},
	/**
	 * Do actual work of {@link Kekule.Spectroscopy.SpectrumData.getIterator}.
	 * Desendants may override this method.
	 * @returns {Object}
	 * @private
	 */
	doGetIterator: function()
	{
		//var dataItems = this.getDataItems();
		var sections = this.getSections().getItems();
		var self = this;
		var result = {
			sectionIndex: 0,
			index: 0,
			next: function()
			{
				var self = this;
				var outOfRange = function()
				{
					return (self.sectionIndex >= sections.length || (self.sectionIndex === sections.length - 1 && self.index >= sections[sections.length - 1].getDataCount()));
				}
				if (outOfRange())
					return {'done': true};
				else
				{
					if (this.index < sections[this.sectionIndex].getDataCount())
					{
						var ret = {'done': false, 'value': sections[this.sectionIndex].getValueAt(this.index)};
						++this.index;
					}
					else
					{
						do
						{
							++this.sectionIndex;
							this.index = 0;
						}
						while(this.index >= sections[this.sectionIndex].getDataCount() || self.sectionIndex >= sections.length);
						if (outOfRange())
							return {'done': true};
						else
							return {'done': false, 'value': sections[this.sectionIndex].getValueAt(this.index)};
					}
					return ret;
				}
			}
		};
		return result;
	},

	/**
	 * Call function to each data item.
	 * @param {Func} func With params: (hashValue [, index]).
	 */
	forEach: function(func, thisArg)
	{
		var iterator = this.getIterator();
		if (iterator)
		{
			var index = 0;
			var nextItem = iterator.next();
			while (!nextItem.done)
			{
				func.apply(thisArg, [nextItem.value, index]);
				++index;
				nextItem = iterator.next();
			}
		}
		return this;
	}
});

/**
 * Spectrum peak shape enumeration.
 * @enum
 */
Kekule.Spectroscopy.PeakShape = {
	SHARP: 'sharp',
	BROAD: 'broad'
};
/**
 * Spectrum peak multiplicity enumeration.
 * @enum
 */
Kekule.Spectroscopy.PeakMultiplicity = {
	UNKNOWN: 0,
	SINGLET: 1,
	DOUBLET: 2,
	TRIPLET: 3,
	QUARTET: 4,
	QUINTET: 5,
	SEXTUPLET: 6,
	DOUBLE_DOUBLET: 22,
	TRIPLE_DOUBLET: 32,
	MULTIPLET: 255
};

/**
 * A special class to store the extra information of spectrum data value (e.g. the assignment ref object of peak).
 * @class
 * @augments Kekule.ChemObject
 *
 * @param {Hash} params
 *
 * @property {Kekule.ChemObject} assignments The assignment targets array of peak, usually containing only an atom or a bond.
 * @property {Kekule.ChemObject} assignment The assignment target of peak, first item of {@link Kekule.Spectroscopy.SpectrumDataDetails.assignments}, usually an atom or a bond.
 */
Kekule.Spectroscopy.SpectrumDataDetails = Class.create(Kekule.ChemObject,
/** @lends Kekule.Spectroscopy.SpectrumDataDetails# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Spectroscopy.SpectrumDataDetails',
	/** @private */
	initialize: function(params)
	{
		this.setPropStoreFieldValue('assignments', []);
		this.tryApplySuper('initialize', []);
		this.setPropValues(params);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('assignments', {'dataType': DataType.ARRAY, 'objRef': true, 'autoUpdate': true,
			'setter': function(value)
			{
				var a = value? (DataType.isArrayValue(value)? AU.clone(value): AU.toArray(value)): [];
				this.setPropStoreFieldValue('assignments', a);
			}
		});
		this.defineProp('assignment', {'dataType': 'Kekule.ChemObject', 'serializable': false, 'scope': Class.PropertyScope.PUBLIC,
			'getter': function() { return (this.getAssignments() || [])[0]; },
			'setter': function(value) { this.setAssignments(value); }
		});
	},
	/** @ignore */
	doFinalize: function()
	{
		this.setPropStoreFieldValue('assignments', null);
		this.tryApplySuper('doFinalize');
	},
	/** @ignore */
	getAutoIdPrefix: function()
	{
		return 'd';
	},
	/** @ignore */
	isEmpty: function()
	{
		var assignments = this.getAssignments();
		return (!assignments || !assignments.length);
	},
	/**
	 * Returns whether this peak detail has assignments info.
	 * @returns {Bool}
	 */
	hasAssignments: function()
	{
		var a = this.getAssignments();
		return !!(a && a.length);
	},
	/** @ignore */
	doGetComparisonPropNames: function(options)
	{
		var result = this.tryApplySuper('doGetComparisonPropNames', [options]);
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			result = result || [];
			result.unshift('assignments');
		}
		return result;
	}
});

/**
 * A special class to store the additional peak information (e.g. the assignment ref object of peak, the peak shape, multiplicity).
 * @class
 * @augments Kekule.Spectroscopy.SpectrumDataDetails
 *
 * @property {String} shape Shape of peak, usually be set with value from {@link Kekule.Spectroscopy.PeakShape}.
 * @property {VARIANT} multiplicity Multiplicity of peak.
 *   Usually be set with value from {@link Kekule.Spectroscopy.PeakMultiplicity}, but a custom string value (e.g. 'triplet121') is also allowed.
 */
Kekule.Spectroscopy.SpectrumPeakDetails = Class.create(Kekule.Spectroscopy.SpectrumDataDetails,
/** @lends Kekule.Spectroscopy.SpectrumPeakDetails# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Spectroscopy.SpectrumPeakDetails',
	/** @private */
	initProperties: function()
	{
		this.defineProp('shape', {'dataType': DataType.STRING});
		this.defineProp('multiplicity', {'dataType': DataType.VARIANT});
	},
	/** @ignore */
	getAutoIdPrefix: function()
	{
		return 'p';
	},
	/** @ignore */
	isEmpty: function()
	{
		var result = this.tryApplySuper('isEmpty');
		result = result && !this.getShape() && Kekule.ObjUtils.isUnset(this.getMultiplicity());
		return result;
	},
	/** @ignore */
	doGetComparisonPropNames: function(options)
	{
		var result = this.tryApplySuper('doGetComparisonPropNames', [options]);
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			result = result || [];
			result.unshift('shape');
			result.unshift('multiplicity');
		}
		return result;
	}
});

/**
 * Enumeration of spectrum types.
 * @enum
 */
Kekule.Spectroscopy.SpectrumType = {
	NMR: 'NMR',
	IR: 'IR',
	MS: 'MS',
	UV_VIS: 'UV_VIS',
	IMS: 'IMS',   // ION MOBILITY SPECTRUM
	RAMAN: 'Raman',
	CHROMATOGRAPHY: 'chromatography',
	GENERAL: 'general'   // unknown type
};

/**
 * Some constants used by NMR spectrum.
 * @object
 */
Kekule.Spectroscopy.SpectrumNMR = {
	TargetNucleus: {
		C13: 'C13',
		H: 'H'
	}
};

/**
 * The base spectrum class. Concrete spectrum classes should be inherited from this one.
 * @class
 * @augments Kekule.ChemObject
 *
 * @property {String} spectrumType Type of spectrum, value from {@link Kekule.Spectroscopy.SpectrumType}.
 * @property {String} name Name of spectrum.
 * @property {String} title Title of spectrum.
 * @property {Hash} metaData Meta information of spectrum.
 * @property {Hash} conditions Conditions of spectrum.
 * @property {Hash} parameters Important parameters of spectrum.
 * @property {Hash} annotations Additional annotations of spectrum.
 * @property {Kekule.Spectroscopy.SpectrumData} data Spectrum data.
 * @property {Array} refMolecules Related molecules to spectrum.
 */
Kekule.Spectroscopy.Spectrum = Class.create(Kekule.ChemObject,
/** @lends Kekule.Spectroscopy.Spectrum# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Spectroscopy.Spectrum',
	/** @private */
	initialize: function(id)
	{
		this.setPropStoreFieldValue('data', new Kekule.Spectroscopy.SpectrumData(null, null, this));
		this.tryApplySuper('initialize', [id]);
		this._initDelegatedMethods();
	},
	/** @ignore */
	doFinalize: function()
	{
		this.setPropStoreFieldValue('refMolecules', null);
		var d = this.getData();
		if (d)
			d.finalize();
		this.tryApplySuper('doFinalize');
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('spectrumType', {'dataType': DataType.STRING});
		this.defineProp('name', {'dataType': DataType.STRING});
		//this.defineProp('title', {'dataType': DataType.STRING});
		this.defineProp('data', {'dataType': 'Kekule.Spectroscopy.SpectrumData',
			'setter': function(value)
			{
				var old = this.getData();
				if (value !== old)
				{
					if (old)
					{
						old.finalize();
					}
					if (value)
					{
						value.setPropValue('parent', this, true);
					}
					this.setPropStoreFieldValue('data', value);
				}
			}
		});
		this.defineProp('refMolecules', {'dataType': DataType.ARRAY, 'objRef': true, 'autoUpdate': true,
			'setter': function(value)
			{
				var a = value? (DataType.isArrayValue(value)? AU.clone(value): AU.toArray(value)): [];
				this.setPropStoreFieldValue('refMolecules', a);
			}
		});
		this.defineProp('refMolecule', {'dataType': 'Kekule.Molecule', 'serializable': false, 'scope': Class.PropertyScope.PUBLIC,
			'getter': function() { return (this.getRefMolecules() || [])[0]; },
			'setter': function(value) { this.setrRefMolecules(value); }
		});
		/*
		this.defineProp('spectrumParams',
			{
				'dataType': DataType.HASH,
				'getter': function(canCreate)
				{
					var r = this.getPropStoreFieldValue('spectrumParams');
					if ((!r) && canCreate)
					{
						r = {};
						this.setPropStoreFieldValue('spectrumParams', r);
					}
					return r;
				},
				'setter': null
			});
		*/
		this._defineInfoProperty('title');
		this._defineInfoProperty('metaData', null, {'dataType': DataType.HASH});
		this._defineInfoProperty('conditions', null, {'dataType': DataType.HASH});
		this._defineInfoProperty('parameters', null, {'dataType': DataType.HASH});
		this._defineInfoProperty('annotations', null, {'dataType': DataType.HASH});
		this._defineDataDelegatedProperty('variables');
		this._defineDataDelegatedProperty('varSymbols');
		this._defineDataDelegatedProperty('dataSections', 'sections');
		this._defineDataDelegatedProperty('activeDataSectionIndex', 'activeSectionIndex');
		this._defineDataDelegatedProperty('activeDataSection', 'activeSection');
	},
	/** @private */
	_initDelegatedMethods: function()
	{
		this._defineDataDelegatedMethod('isEmpty');
		this._defineDataDelegatedMethod('createDataSection', 'createSection');
		this._defineDataDelegatedMethod('clearDataSection', 'clearSection');
		this._defineDataDelegatedMethod('getDataSectionCount', 'getSectionCount');
		this._defineDataDelegatedMethod('getDataSectionAt', 'getSectionAt');
		this._defineDataDelegatedMethod('indexOfDataSection', 'indexOfSection');
		this._defineDataDelegatedMethod('hasDataSection', 'hasSection');
		this._defineDataDelegatedMethod('removeDataSectionAt', 'removeSectionAt');
		this._defineDataDelegatedMethod('removeDataSection', 'removeSection');
		this._defineDataDelegatedMethod('insertDataSectionAt', 'insertSectionAt');
		this._defineDataDelegatedMethod('insertDataSectionBefore', 'insertSectionBefore');
		this._defineDataDelegatedMethod('appendDataSection', 'appendSection');
		this._defineDataDelegatedMethod('iterateDataSection', 'iterateSection');
		this._defineDataDelegatedMethod('sortData', 'sort');
		this._defineDataDelegatedMethod('clearData');
		this._defineDataDelegatedMethod('getVariableCount');
		this._defineDataDelegatedMethod('getVariable');
		this._defineDataDelegatedMethod('indexOfVariable');
		this._defineDataDelegatedMethod('insertVariableAt');
		this._defineDataDelegatedMethod('insertVariableBefore');
		this._defineDataDelegatedMethod('appendVariable');
		this._defineDataDelegatedMethod('removeVariableAt');
		this._defineDataDelegatedMethod('removeVariable');
		this._defineDataDelegatedMethod('getVariablesOfDependency');
		this._defineDataDelegatedMethod('getContinuousVarRange');
		this._defineDataDelegatedMethod('setContinuousVarRange');
		this._defineDataDelegatedMethod('clearContinuousVarRange');
		this._defineDataDelegatedMethod('getDefaultVarValue');
		this._defineDataDelegatedMethod('setDefaultVarValue');
		this._defineDataDelegatedMethod('clearDefaultVarValue');
	},
	/**
	 * Defines property which storing value in {@link Kekule.ChemObject.info}.
	 * @param {String} propName
	 * @param {String} infoFieldName
	 * @param {Hash} options
	 * @private
	 */
	_defineInfoProperty: function(propName, infoFieldName, options)
	{
		var defs;
		(function() {
			var name = infoFieldName || propName;
			var name2 = name.upperFirst();
			defs = Object.extend({
				'getter': function () {
					return this.getInfoValue(name) || this.getInfoValue(name2);
				},
				'setter': function(value) {
					this.setInfoValue(name, value);
				},
				'serializable': false
			}, options);
		})();
		return this.defineProp(propName, defs);
	},
	/**
	 * Defines property which reflecting the property values in {@link Kekule.Spectroscopy.Spectrum.data}.
	 * @param {String} propName
	 * @param {String} dataPropName
	 * @private
	 */
	_defineDataDelegatedProperty: function(propName, dataPropName)
	{
		if (!dataPropName)
			dataPropName = propName;
		var dataPropInfo = ClassEx.getPropInfo(Kekule.Spectroscopy.SpectrumData, dataPropName);
		var propOptions = Object.create(dataPropInfo);
		propOptions.getter = null;
		propOptions.setter = null;
		propOptions.serializable = false;
		if (dataPropInfo.getter)
		{
			propOptions.getter = function()
			{
				return this.getData().getPropValue(dataPropName);
			};
		}
		if (dataPropInfo.setter)
		{
			propOptions.setter = function(value)
			{
				this.getData().setPropValue(dataPropName, value);
			}
		}
		return this.defineProp(propName, propOptions);
	},
	/**
	 * Defines method which directly calling the corresponding one in {@link Kekule.Spectroscopy.Spectrum.data}.
	 * @param {String} methodName
	 * @param {String} dataMethodName
	 * @private
	 */
	_defineDataDelegatedMethod: function(methodName, dataMethodName)
	{
		if (!dataMethodName)
			dataMethodName = methodName;
		var proto = ClassEx.getPrototype(this.getClass());
		proto[methodName] = function()
		{
			//console.log('call', methodName, arguments);
			var target = this.getData();
			var result = target[dataMethodName].apply(target, arguments);
			if (result === target)  // fix for chaining calling of method
				result = this;
			return result;
		}
	},

	/** @ignore */
	getAutoIdPrefix: function()
	{
		return 's';
	},

	/** @ignore */
	ownerChanged: function(/*$super, */newOwner, oldOwner)
	{
		// change the owner of child data and sections
		var data = this.getData();
		if (data)
			data.setOwner(newOwner);
		this.tryApplySuper('ownerChanged', [newOwner, oldOwner]);
	},
	/** @ignore */
	isEmpty: function()
	{
		if (this.getDataSectionCount() <= 0)
			return true;
		else
		{
			for (var i = 0, l = this.getDataSectionCount(); i < l; ++i)
			{
				if (!this.getDataSectionAt(i).isEmpty())
					return false;
			}
			return true;
		}
	},

	/** @ignore */
	getChildSubgroupNames: function()
	{
		return ['dataSection'].concat(this.tryApplySuper('getChildSubgroupNames'));
	},
	/** @ignore */
	getBelongChildSubGroupName: function(obj)
	{
		if (obj instanceof Kekule.Spectroscopy.SpectrumDataSection)
			return 'dataSection';
		else
			return this.tryApplySuper('getBelongChildSubGroupName', [obj]);
	},
	/** @ignore */
	doGetChildCount: function()
	{
		return this.getDataSectionCount();
	},
	/** @ignore */
	doGetChildAt: function(index)
	{
		return this.getDataSectionAt(index);
	},
	/** @ignore */
	doIndexOfChild: function(obj)
	{
		return this.indexOfDataSection(obj);
	},

	/** @private */
	_getCandidateInfoPropNames: function(baseName)
	{
		if (baseName.indexOf(MetaPropNamespace.DELIMITER) >= 0)  // already in namespace form
			return [baseName];
		var prefixes = MetaPropNamespace.getNamespaces();
		var spectrumType = this.getSpectrumType();
		if (spectrumType)
		{
			prefixes = [spectrumType].concat(prefixes);
		}
		var result = [baseName];  // the core name will always be in candicates
		for (var i = 0, l = prefixes.length; i < l; ++i)
		{
			result.push(prefixes[i] + MetaPropNamespace.DELIMITER + baseName);
		}
		return result;
	},
	/** @private */
	_getSpectrumInfoValueOfCategory: function(category, key)
	{
		var hash = category? this.getInfoValue(category): this.getInfo();
		if (!hash)
			return undefined;
		if (Kekule.globalOptions.spectrum.spectrumInfo.enablePrefixOmissionInGetter)
		{
			var candicateNames = this._getCandidateInfoPropNames(key);
			for (var i = 0, l = candicateNames.length; i < l; ++i)
			{
				var name = candicateNames[i];
				if (hash[name] !== undefined)
					return hash[name];
			}
			return undefined;
		}
		else
			return hash[key];
	},
	/**
	 * Set the spectrum info value of a category.
	 * @param {String} category
	 * @param {String} key
	 * @param {Variant} value
	 */
	setSpectrumInfoValue: function(category, key, value)
	{
		if (category)
		{
			var hash = this.getInfoValue(category);
			if (!hash)
			{
				hash = {};
				this.setInfoValue(category, hash);
			}
			hash[key] = value;
		}
		else
			this.setInfoValue(key, value);
		return this;
	},
	/**
	 * Returns all keys of a spectrum info category.
	 * @param {String} spectrumInfoCategory
	 * @returns {Array}
	 */
	getSpectrumInfoKeysOfCategory: function(spectrumInfoCategory)
	{
		if (spectrumInfoCategory)
		{
			var hash = this.getInfoValue(spectrumInfoCategory);
			return hash ? Kekule.ObjUtils.getOwnedFieldNames(hash, false) : [];
		}
		else if (spectrumInfoCategory === '')  // retrieve the keys at the top level of info property
		{
			var result = this.getInfoKeys();
			result = AU.exclude(result, this.getSpectrumInfoCategories());
			return result;
		}
		else
			return [];
	},
	/**
	 * Returns the spectrum info category names existed in current spectrum.
	 * @returns {Array}
	 */
	getSpectrumInfoCategories: function()
	{
		var candicateCategories = ['metaData', 'conditions', 'parameters', 'annotations'];
		var result = [];
		for (var i = 0, l = candicateCategories.length; i < l; ++i)
		{
			var c = candicateCategories[i];
			var keys = this.getSpectrumInfoKeysOfCategory(c);
			if (keys && keys.length)
				result.push(c);
		}
		return result;
	},
	/**
	 * Returns value of spectrum meta/condition/parameter/annotation.
	 * @param {String} key
	 * @param {Variant} candicateCategories A single category or an array of categories.
	 * @returns {Variant}
	 */
	getSpectrumInfoValue: function(key, candicateCategories)
	{
		var categories;
		if (!candicateCategories)
			categories = ['conditions', 'parameters', 'metaData', 'annotations', ''];
		else
			categories = AU.toArray(candicateCategories);
		for (var i = 0, l = categories.length; i < l; ++i)
		{
			var c = categories[i];
			var v = this._getSpectrumInfoValueOfCategory(c, key);
			if (Kekule.ObjUtils.notUnset(v))
				return v;
		}
		return undefined;
	},
	/**
	 * Returns the value of a spectrum meta data.
	 * @param {String} key
	 * @returns {Variant}
	 */
	getMeta: function(key)
	{
		return this._getSpectrumInfoValueOfCategory('metaData', key);
	},
	/**
	 * Set the value of a spectrum meta data.
	 * @param {String} key
	 * @param {Variant} value
	 */
	setMeta: function(key, value)
	{
		this.setSpectrumInfoValue('metaData', key, value);
		return this;
	},
	/**
	 * Returns all the keys of spectrum parameter list.
	 * @returns {Array}
	 */
	getMetaKeys: function()
	{
		return this.getSpectrumInfoKeysOfCategory('metaData');
	},
	/**
	 * Returns the value of a spectrum condition.
	 * @param {String} key
	 * @returns {Variant}
	 */
	getCondition: function(key)
	{
		return this._getSpectrumInfoValueOfCategory('conditions', key);
	},
	/**
	 * Set the value of a spectrum condition.
	 * @param {String} key
	 * @param {Variant} value
	 */
	setCondition: function(key, value)
	{
		this.setSpectrumInfoValue('conditions', key, value);
		return this;
	},
	/**
	 * Returns all the keys of spectrum condition list.
	 * @returns {Array}
	 */
	getConditionKeys: function()
	{
		return this.getSpectrumInfoKeysOfCategory('conditions');
	},
	/**
	 * Returns the value of a spectrum parameter.
	 * @param {String} key
	 * @returns {Variant}
	 */
	getParameter: function(key)
	{
		return this._getSpectrumInfoValueOfCategory('parameters', key);
	},
	/**
	 * Set the value of a spectrum parameter.
	 * @param {String} key
	 * @param {Variant} value
	 */
	setParameter: function(key, value)
	{
		this.setSpectrumInfoValue('parameters', key, value);
		return this;
	},
	/**
	 * Returns all the keys of spectrum parameter list.
	 * @returns {Array}
	 */
	getParameterKeys: function()
	{
		return this.getSpectrumInfoKeysOfCategory('parameters');
	},
	/**
	 * Returns the value of a spectrum annotation.
	 * @param {String} key
	 * @returns {Variant}
	 */
	getAnnotation: function(key)
	{
		return this._getSpectrumInfoValueOfCategory('annotations', key);
	},
	/**
	 * Set the value of a spectrum annotation.
	 * @param {String} key
	 * @param {Variant} value
	 */
	setAnnotation: function(key, value)
	{
		this.setSpectrumInfoValue('annotations', key, value);
		return this;
	},
	/**
	 * Returns all the keys of spectrum annotation list.
	 * @returns {Array}
	 */
	getAnnotationKeys: function()
	{
		return this.getSpectrumInfoKeysOfCategory('annotations');
	},

	/*
	 * Create the data object.
	 * @param variables
	 * @returns {Kekule.Spectroscopy.SpectrumData}
	 */
	/*
	createData: function(variables)
	{
		var result = new Kekule.Spectroscopy.SpectrumData(null, variables);
		this.setPropStoreFieldValue('data', result);
		return result;
	}
	*/

	/**
	 * Returns the recommended external units that can be converted from internal unit for this variable.
	 * @param {Kekule.Spectroscopy.SpectrumVarDefinition} varDef
	 * @returns {Array} Array of unit objects.
	 */
	getVarAvailableExternalUnitObjs: function(varDef)
	{
		return Kekule.Spectroscopy.DataValueConverterManager.getAltUnits(varDef, varDef.getInternalUnit? varDef.getInternalUnit(): varDef.getUnit(), null, this);
	},
	/**
	 * Returns the recommended external units that can be converted from internal unit for this variable.
	 * @param {Kekule.Spectroscopy.SpectrumVarDefinition} varDef
	 * @returns {Array} Array of unit symbols (string).
	 */
	getVarAvailableExternalUnitSymbols: function(varDef)
	{
		var unitObjs = Kekule.Spectroscopy.DataValueConverterManager.getAltUnits(varDef, varDef.getInternalUnit? varDef.getInternalUnit(): varDef.getUnit(), null, this);
		var result = [];
		for (var i = 0, l = unitObjs.length; i < l; ++i)
		{
			result.push(unitObjs[i].symbol);
		}
		return result;
	},

	// methods about object comparison
	/** @ignore*/
	getComparisonPropNames: function(options)
	{
		var result = this.tryApplySuper('getComparisonPropNames', [options]);
		// remove data property from comparison, since dataSections property are included
		var index = result.indexOf('data');
		if (index >= 0)
			result.splice(index, 1);
		//console.log('props', result);
		return result;
	},
	/** @ignore*/
	doGetComparisonPropNames: function(options)
	{
		var result = this.tryApplySuper('doGetComparisonPropNames', [options]);
		if (options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			// variables will be compared at the dataSections level, so it should not be included here
			result = (result || []).concat([
				'refMolecules', 'metaData', 'conditions', 'parameters', /*'variables',*/ 'dataSections',
				//'_spectrumInfos_'   // a fake property, to manually comparing on metaData/conditions/parameters
			]);
		}
		return result;
	},
	/** @ignore */
	doCompareProperty: function(targetObj, propName, options)
	{
		if (propName === 'dataSections' && options.method === Kekule.ComparisonMethod.CHEM_STRUCTURE)
		{
			// dataSections is a ChemObjList, when comparing, we extract the section array directly
			var sections1 = this.getDataSections().getItems();
			var sections2 = targetObj.getDataSections().getItems();
			//console.log('compare', propName, this.doCompareOnValue(sections1, sections2, options));
			return this.doCompareOnValue(sections1, sections2, options);
		}
		else if (['metaData', 'conditions', 'parameters'].indexOf(propName) >= 0)
		{
			//var candicateCategories = ['metaData', 'conditions', 'parameters'];
			var widerCandicateCategories = ['metaData', 'conditions', 'parameters', 'annotations'];
			var srcKeys = this.getSpectrumInfoKeysOfCategory(propName); //(this.getMetaKeys() || []).concat(this.getParameterKeys() || []).concat(this.getConditionKeys() || []);
			for (var i = 0, l = srcKeys.length; i < l; ++i)
			{
				var key = srcKeys[i];
				var v1 = this.getSpectrumInfoValue(key, [propName]);
				var v2 = targetObj.getSpectrumInfoValue(key, widerCandicateCategories);
				if (Kekule.ObjUtils.isUnset(v2))
				{
					var coreKey = MetaPropNamespace.getPropertyCoreName(key);
					v2 = targetObj.getSpectrumInfoValue(coreKey, widerCandicateCategories);
				}
				var result = this.doCompareOnValue(v1, v2, options);
				if (result)
				{
					//console.log('diff', key, v1, v2, result);
					return result;
				}
				else
				{
					//console.log('same', key, v1, v2, result);
				}
			}
			return 0;
		}
		else
		{
			//var result = this.tryApplySuper('doCompareProperty', [targetObj, propName, options]);
			var v1 = this.getPropValue(propName);
			var v2 = targetObj.getPropValue(propName);
			var result = this.doCompareOnValue(v1, v2, options);
			//console.log('compare', propName, v1, v2, result);
			return result;
		}
	}

	/*
	 * Returns all keys in {@link Kekule.Spectroscopy.Spectrum#spectrumParams} property.
	 * @returns {Array}
	 */
	/*
	getSpectrumParamKeys: function()
	{
		return this.getSpectrumParams()? Kekule.ObjUtils.getOwnedFieldNames(this.getSpectrumParams()): [];
	},
	*/
	/*
	 * Get param value from {@link Kekule.Spectroscopy.Spectrum#spectrumParams}.
	 * @param {String} key
	 * @returns {Variant}
	 */
	/*
	getSpectrumParam: function(key)
	{
		return this.getSpectrumParams()? this.getSpectrumParams()[key]: null;
	},
	*/
	/*
	 * Set value of a spectrum param. If key already exists, its value will be overwritten.
	 * @param {String} key
	 * @param {Variant} value
	 */
	/*
	setSpectrumParam: function(key, value)
	{
		this.doGetSpectrumParams(true)[key] = value;
		this.notifyPropSet('spectrumParams', this.getPropStoreFieldValue('spectrumParams'));
	}
	*/
});

Kekule.ClassDefineUtils.addStandardCoordSupport(Kekule.Spectroscopy.Spectrum);
Kekule.ClassDefineUtils.addStandardSizeSupport(Kekule.Spectroscopy.Spectrum);


// register spectrum related units
(function(){
	var register = Kekule.Unit.register;
	// IR
	register('transmittance', 'transmittance', 'OpticalTransmittance', 1);  // IT/I0
	register('transmittance%', 'transmittance_percent', 'OpticalTransmittance', 1e-2);  // IT/I0
	register('reflectance', 'reflectance', 'OpticalReflectance', 1);  // IR/I0
	register('absorbance', 'absorbance', 'OpticalAbsorbance', 1);  // log10(IR/I0)
	register('Kubelka Munk', 'Kubelka_Munk', 'OpticalKubelkaMunk', 1);  // (1-R^2)/(2R)

	// MS
	register('counts', 'ms_count', 'Misc', null);
	register('relative abundance', 'ms_relative_abundance', 'SpectrumMS', null);
	register('m/z', 'ms_mass_charge_ratio', 'SpectrumMS', null);

})();

})();