/**
* @fileoverview
* Input chem structure by touch or mouse track.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /utils/kekule.utils.js
* requires /algorithm/kekule.graph.js
* requires /widgets/chem/editor/kekule.chemEditor.baseEditors.js
* requires /widgets/chem/editor/kekule.chemEditor.configs.js
*/
(function(){
"use strict";
var AU = Kekule.ArrayUtils;
var CU = Kekule.CoordUtils;
/**
* An enumeration of constraint when refining a track.
* Indicating whether the end of track points is fixed to an existing node.
* @type {enum}
*/
Kekule.Editor.PointerTrackConstraint = {
/** The track is totally free, not linked to any existing nodes. **/
NONE: 0,
/** The leading point is binding to a node. **/
LEADING: 1,
/** The leading and ending points are binding to two different nodes. **/
BOTH: 2,
/** The leading and ending points are binding to one nodes, forming a circle. **/
CIRCLE: 5
};
/**
* A class to parse the touch/mouse track and generate corresponding chem structure.
* @class
* @augments ObjectEx
*/
Kekule.Editor.TrackParser = Class.create(ObjectEx,
/** @lends Kekule.Editor.TrackParser# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.TrackParser',
/**
* Simplify curve to line segments using Ramer–Douglas–Peucker algorithm.
* @param {Array} curvePoints Array of {x, y} coords to define a curve.
* @param {Float} distanceThreshold
* @returns {Array} Array of {x, y} coords, every two points define a line segment.
* @private
*/
simplifyCurveToLineSegments: function(curvePoints, distanceThreshold)
{
return Kekule.GeometryUtils.simplifyCurveToLineSegments(curvePoints, distanceThreshold);
/*
//distanceThreshold = distanceThreshold || this.getLineSimplificationDistanceThreshold();
if (!distanceThreshold)
return curvePoints;
if (curvePoints.length <= 2)
return AU.clone(curvePoints);
return this._simplifyCurvePartToLineSegments(curvePoints, 0, curvePoints.length - 1, distanceThreshold);
*/
},
/* @private */
/*
_simplifyCurvePartToLineSegments: function(curvePoints, startIndex, endIndex, distanceThreshold)
{
var startCoord = curvePoints[startIndex];
var endCoord = curvePoints[endIndex];
if (endIndex - startIndex <= 1)
{
return [startCoord, endCoord];
}
var maxDistance = distanceThreshold, maxIndex = null;
for (var i = startIndex + 1; i < endIndex; ++i)
{
var d = Kekule.GeometryUtils.getDistanceFromPointToLine(curvePoints[i], startCoord, endCoord);
if (d > maxDistance)
{
maxDistance = d;
maxIndex = i;
}
}
if (maxIndex === null) // no max distance point
{
return [startCoord, endCoord];
}
else // split track to two
{
var lines1 = this._simplifyCurvePartToLineSegments(curvePoints, startIndex, maxIndex, distanceThreshold);
var lines2 = this._simplifyCurvePartToLineSegments(curvePoints, maxIndex, endIndex, distanceThreshold);
lines2.shift(); // remove the common point between lines1 and lines2
var result = [].concat(lines1).concat(lines2);
return result;
}
},
*/
/**
* Add cross points of line segments. Those points should be regarded as node in structure.
* @param {Array} linePoints
* @returns {Array} New line points with cross points.
*/
addLineCrossPoints: function(linePoints)
{
var crossPoints = [];
var length = linePoints.length;
for (var i = 0; i < length - 1; ++i)
{
var currLine = {'coord1': linePoints[i], 'coord2': linePoints[i + 1]};
for (var j = i + 2; j < length - 1; ++j)
{
var testLine = {'coord1': linePoints[j], 'coord2': linePoints[j + 1]};
// test cross point
var crossPoint = Kekule.GeometryUtils.getCrossPointOfLines(currLine.coord1, currLine.coord2, testLine.coord1, testLine.coord2);
if (crossPoint)
{
//console.log('cross at', i, j);
this._addInsertedPoint(i, crossPoint, crossPoints);
this._addInsertedPoint(j, crossPoint, crossPoints);
}
}
}
//console.log('crossPoints', crossPoints);
// sort cross points
for (var i = 0; i < length - 1; ++i)
{
if (crossPoints[i])
{
this._sortInsertedPointsInLine(crossPoints[i], linePoints[i], linePoints[i + 1]);
/*
var currLine = {'coord1': linePoints[i], 'coord2': linePoints[i + 1]};
crossPoints[i].sort(function(p1, p2){
var deltaX = currLine.coord2.x - currLine.coord1.x;
var deltaY = currLine.coord2.y - currLine.coord1.y;
if (Math.abs(deltaY) > Math.abs(deltaX))
{
return -(p2.y - p1.y) * deltaY;
}
else
{
return -(p2.x - p1.x) * deltaX;
}
});
*/
}
}
// add those cross points
var result = [];
for (var i = 0; i < length - 1; ++i)
{
result.push(linePoints[i]);
if (crossPoints[i])
{
for (var j = 0, k = crossPoints[i].length; j < k; ++j)
result.push(crossPoints[i][j]);
}
}
result.push(linePoints[length - 1]);
return result;
},
/**
* If a point is very close to line (distance less than threshold), this point should be considered
* on the line.
* @param {Array} linePoints
* @param {Float} distanceThreshold
* @returns {Array}
*/
addVertexToLineMergePoint: function(linePoints, distanceThreshold)
{
//distanceThreshold = distanceThreshold || this.getVertexMergeRadius();
if (distanceThreshold <= 0)
return linePoints;
var length = linePoints.length;
var insertedPoints = [];
for (var i = 0; i < length - 1; ++i)
{
var currPoint = linePoints[i];
for (var j = 0; j < length - 1; ++j)
{
if (j < i - 1 || j > i + 1) // point i is not in line j
{
var perpendicularPoint = Kekule.GeometryUtils.getPerpendicularCrossPointFromCoordToLine(currPoint, linePoints[j], linePoints[j + 1], false);
if (perpendicularPoint)
{
var d = Kekule.CoordUtils.getDistance(currPoint, perpendicularPoint);
if (d <= distanceThreshold) // linePoints[i] very close to line j - j+1, add merge point
{
currPoint.x = perpendicularPoint.x;
currPoint.y = perpendicularPoint.y;
this._addInsertedPoint(j, currPoint, insertedPoints);
}
}
}
}
}
// at last add all insertedPoints to result
var result = [];
for (var i = 0; i < length - 1; ++i)
{
result.push(linePoints[i]);
if (insertedPoints[i])
{
this._sortInsertedPointsInLine(insertedPoints[i], linePoints[i], linePoints[i + 1]);
for (var j = 0, k = insertedPoints[i].length; j < k; ++j)
result.push(insertedPoints[i][j]);
}
}
result.push(linePoints[length - 1]);
return result;
},
/** @private */
_addInsertedPoint: function(index, point, points)
{
if (!points[index])
points[index] = [];
points[index].push(point);
},
/** @private */
_sortInsertedPointsInLine: function(insertedPoints, lineCoord1, lineCoord2)
{
if (!insertedPoints)
return;
var deltaX = lineCoord2.x - lineCoord1.x;
var deltaY = lineCoord2.y - lineCoord1.y;
insertedPoints.sort(function(p1, p2){
if (Math.abs(deltaY) > Math.abs(deltaX))
{
return -(p2.y - p1.y) * deltaY;
}
else
{
return -(p2.x - p1.x) * deltaX;
}
});
},
/**
* Merge some near points in line segments.
* @param {Kekule.Graph} graph
* @param {Float} distanceThreshold
*/
mergeNearbyTrackPoints: function(trackPoints, distanceThreshold)
{
//console.log('merge point', distanceThreshold);
if (distanceThreshold <= 0)
return trackPoints;
var HANDLED_FLAG_FIELD = '__$merge_handled__';
var WEIGHT_FIELD = '__$merge_weight__';
var length = trackPoints.length;
var result = trackPoints.concat([]);
for (var i = 0; i < length; ++i)
{
var basePoint = result[i];
if (!basePoint[HANDLED_FLAG_FIELD])
{
//basePoint.__index__ = i; // debug flag
for (var j = i + 1; j < length; ++j)
{
var currPoint = result[j];
if (!currPoint[HANDLED_FLAG_FIELD])
{
var baseWeight = basePoint[WEIGHT_FIELD] || 1;
var currWeight = currPoint[WEIGHT_FIELD] || 1;
var d = Kekule.CoordUtils.getDistance(basePoint, currPoint);
if (d <= distanceThreshold) // need merge
{
//console.log('distance of ', i, j, 'is', d);
var newCoord;
var newWeight = baseWeight + currWeight;
basePoint[WEIGHT_FIELD] = newWeight;
if (basePoint.boundObj) // basePoint bounded to an existing node, should not change its coord
newCoord = basePoint;
else if (currPoint.boundObj) // should use currPoint coord
{
newCoord = currPoint;
basePoint.boundObj = currPoint.boundObj; // important, do not lose bound obj info
}
else // no bound node, calculate new coord
{
newCoord = (baseWeight === 1) ? basePoint : CU.multiply(basePoint, baseWeight);
newCoord = CU.add(newCoord, (currWeight === 1) ? currPoint : CU.multiply(currPoint, currWeight));
newCoord = CU.divide(newCoord, newWeight);
}
// assign new coord value to old point
basePoint.x = newCoord.x;
basePoint.y = newCoord.y;
//basePoint.boundObj = basePoint.boundObj || currPoint.boundObj; // important, do not lose bound obj info
// replace currPoint with basePoint
result.splice(j, 1, basePoint);
}
}
}
}
basePoint[HANDLED_FLAG_FIELD] = true;
}
//console.log('track after merge', result);
return result;
},
/**
* Merge some near vertexes in graph.
* @param {Kekule.Graph} graph
* @param {Float} distanceThreshold
*/
mergeCloseVertexes: function(graph, distanceThreshold)
{
distanceThreshold = distanceThreshold || this.getVertexMergeRadius();
if (distanceThreshold <= 0)
return graph;
return this._doMergeCloseVertexes(graph, distanceThreshold);
},
/** @private */
_doMergeCloseVertexes: function(graph, distanceThreshold)
{
var vertexes = graph.getVertexes();
var length = vertexes.length;
var hasNewMerge = false;
for (var i = 0; i < length; ++i)
{
var baseVertex = vertexes[i];
var baseCoord = vertexes[i].getData('coord');
for (var j = i + 1; j < length; ++j)
{
var currVertex = vertexes[j];
var currCoord = currVertex.getData('coord');
var d = Kekule.CoordUtils.getDistance(baseCoord, currCoord);
if (d <= distanceThreshold) // need merge
{
this._doMergeVertex(graph, baseVertex, currVertex);
hasNewMerge = true;
break;
}
}
if (hasNewMerge)
break;
}
if (hasNewMerge) // need to check if further merge is needed
return this._doMergeCloseVertexes(graph, distanceThreshold);
else // no new merge, return the old graph
return graph;
},
/** @private */
_doMergeVertex: function(graph, baseVertex, mergedVertex)
{
var baseVertexCoord = baseVertex.getData('coord');
var baseVertexWeight = baseVertex.getData('weight') || 1;
var mergedVertexCoord = mergedVertex.getData('coord');
var mergedVertexWeight = mergedVertex.getData('weight') || 1;
var mergingEdges = AU.clone(mergedVertex.getEdges());
for (var i = 0, l = mergingEdges.length; i < l; ++i)
{
var edge = mergingEdges[i];
edge.replaceVertex(mergedVertex, baseVertex);
}
graph.removeVertex(mergedVertex);
/*
console.log('merged edges', mergingEdges.length);
var neighborVertexes = [];
for (i = 0, l = mergingEdges.length; i < l; ++i)
{
var vertex = mergedVertex.getNeighborOnEdge(mergingEdges[i]);
neighborVertexes.push(vertex);
}
graph.removeVertex(mergedVertex);
for (var i = 0, l = mergingEdges.length; i < l; ++i)
{
var edge = mergingEdges[i];
var neighborVertex = neighborVertexes[i];
if (neighborVertex !== baseVertex)
graph.addEdge(edge, baseVertex, neighborVertex);
}
*/
// change coord
var newCoord = (baseVertexWeight === 1)? baseVertexCoord: CU.multiply(baseVertexCoord, baseVertexWeight);
newCoord += (mergedVertexWeight === 1)? mergedVertexCoord: CU.multiply(mergedVertexCoord, mergedVertexWeight);
newCoord = CU.divide(newCoord, baseVertexWeight + mergedVertexWeight);
baseVertex.setData('coord', newCoord);
},
/**
* Remove continuous coord sequence caused by nearby point merging.
* @param {Array} trackCoords
* @returns {Array}
*/
removeRepetitivePoints: function(trackCoords)
{
var result = [];
var lastCoord = null;
for (var i = 0, l = trackCoords.length; i < l; ++i)
{
if (trackCoords[i] !== lastCoord)
{
result.push(trackCoords[i]);
lastCoord = trackCoords[i];
}
}
return result;
},
/**
* Simplify and standardize a track
* @param {Hash} trackCoords
* @returns {Hash}
*/
refineTrack: function(trackCoords, simplifyDistanceThreshold, mergeDistanceThreshold)
{
var result = this.simplifyCurveToLineSegments(trackCoords, simplifyDistanceThreshold);
result = this.addLineCrossPoints(result);
//console.log('1', result);
result = this.addVertexToLineMergePoint(result, mergeDistanceThreshold);
//console.log('2', result);
result = this.mergeNearbyTrackPoints(result, mergeDistanceThreshold);
//console.log('3', result);
result = this.removeRepetitivePoints(result);
//console.log('4', result);
return result;
},
/**
* Convert track coord sequence to a chem structure fragment.
* @param {Kekule.Editor.BaseEditor} editor The target editor widget.
* @param {Array} trackCoords Screen coords of track.
* @param {String} defIsotopeId
* @param {Kekule.StructureFragment} structFragment Optional. If not set, a new structure fragment will be created and returned.
* @returns {Kekule.StructureFragment}
*/
convertTrackToStructure: function(editor, trackCoords, defIsotopeId, structFragment)
{
if (!trackCoords || !trackCoords.length)
return null;
var length = trackCoords.length;
if (length < 2) // at least we need two coords to form a bond
return null;
var result = structFragment || new Kekule.StructureFragment();
var NODE_FIELD = '__$structure_node__';
var lastNode;
for (var i = 0; i < length; ++i)
{
var coord = trackCoords[i];
var node = coord[NODE_FIELD];
if (!node) // haven't added to structure yet
{
// add new node
node = new Kekule.Atom(); //null, defIsotopeId); // add C atom
node.setIsotopeId(defIsotopeId);
editor.setObjectScreenCoord(node, coord);
coord[NODE_FIELD] = node;
result.appendNode(node);
}
if (lastNode && lastNode !== node) // add edge from the second line point
{
if (!node.getConnectorTo(lastNode))
{
var connector = new Kekule.Bond(null, [lastNode, node]);
connector.setBondOrder(1);
result.appendConnector(connector);
}
}
lastNode = node;
}
return result;
},
/**
* Convert line point sequence to a graph.
* @param {Array} linePoints
* @returns {Kekule.Graph}
*/
convertLinesToGraph: function(linePoints)
{
if (!linePoints || !linePoints.length)
return null;
else
{
var VERTEX_FIELD = '__$graph_vertex__';
var result = new Kekule.Graph();
var length = linePoints.length;
var vertex, lastVertex;
// add vertexes first
for (var i = 0; i < length; ++i)
{
var point = linePoints[i];
var vertex = point[VERTEX_FIELD];
if (!vertex) // haven't added to graph yet
{
// add new vertex
vertex = new Kekule.GraphVertex();
vertex.setData('coord', point);
point[VERTEX_FIELD] = vertex;
result.addVertex(vertex);
}
if (lastVertex) // add edge from the second line point
{
if (!vertex.getEdgeTo(lastVertex))
{
var edge = new Kekule.GraphEdge();
result.addEdge(edge, lastVertex, vertex);
}
}
lastVertex = vertex;
}
return result;
}
}
});
/**
* A class to parse the touch/mouse track and generate corresponding chem structure.
* @class
* @augments ObjectEx
*/
Kekule.Editor.TrackLayoutOptimizer = Class.create(ObjectEx,
/** @lends Kekule.Editor.TrackLayoutOptimizer# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.TrackLayoutOptimizer',
/** @private */
OPTIMIZED_COORD_FIELD: '__$optimizedCoord__',
/** @private */
COORD_INDEX_FIELD: '__$coordIndex__',
/** @private */
RING_END_INDEX_FIELD: '__$ringEndIndex__',
/** @constructs */
initialize: function($super)
{
$super();
},
/** @ignore */
initProperties: function()
{
},
/** @ignore */
initPropValues: function($super)
{
$super();
},
/**
* Split track to chain and ring parts.
* @param {Array} track A series of track coor
* ds.
* @returns {Array} splitted tracks
*/
splitTrackByRings: function(track)
{
var ringRanges = [];
var allNodesNotOccupied = function(startIndex, endIndex)
{
for (var i = 0, l = ringRanges.length; i < l; ++i)
{
var range = ringRanges[i];
if (range.startIndex > endIndex || range.endIndex < startIndex) // not interseted, bypass
continue;
else // has intersection
return false;
}
return true;
};
// assign index to all coords in track and check for rings
for (var i = 0, l = track.length; i < l; ++i)
{
var coord = track[i];
if (coord[this.COORD_INDEX_FIELD] === undefined)
coord[this.COORD_INDEX_FIELD] = i;
else // this coord may forms a ring
{
var startIndex = coord[this.COORD_INDEX_FIELD];
var endIndex = i;
coord[this.RING_END_INDEX_FIELD] = endIndex;
// check if all coord from startIndex and endIndex are not be occupied by other rings
if (allNodesNotOccupied(startIndex, endIndex)) // add to ring record
{
ringRanges.push({'startIndex': startIndex, 'endIndex': endIndex});
}
}
}
//console.log('found ring ranges', ringRanges, track);
// split ring and chains
var result = [];
var ringCount = ringRanges.length;
if (ringCount > 0)
{
var lastRingEndIndex = 0;
// the nodes brfore the first ring
for (var i = 0; i < ringCount; ++i)
{
var currRingRange = ringRanges[i];
// nodes before curr ring, as chain, not, the intersection point is included
if (currRingRange.startIndex > lastRingEndIndex)
{
result.push({
'track': track.slice(lastRingEndIndex, currRingRange.startIndex + 1),
'isRing': false
});
}
// curr ring
result.push({
'track': track.slice(currRingRange.startIndex, currRingRange.endIndex + 1),
'isRing': true
});
lastRingEndIndex = currRingRange.endIndex;
}
// at last, the remaining points
if (lastRingEndIndex < track.length - 1)
result.push({
'track': track.slice(lastRingEndIndex, track.length),
'isRing': false
});
return result;
}
else
return [{
'track': track,
'isRing': false
}];
},
/**
* Optimize the layout of a hand-input track.
* @param {Array} track A series of track coords.
* @param {Hash} options Optimization params.
*/
optimize: function(track, options)
{
var currTrack = track;
var C = Kekule.Editor.PointerTrackConstraint;
var constraint;
// determinate the constraint first
var pointLeading = track[0];
var boundLeading = pointLeading.boundObj;
var pointTailing = track[track.length - 1];
var boundTailing = pointTailing.boundObj;
if (boundLeading && boundTailing)
{
constraint = (boundLeading === boundTailing)? C.CIRCLE: C.BOTH;
}
else if (boundLeading || boundTailing)
{
constraint = C.LEADING;
if (boundTailing) // if bound on tailing, just reverse the track to put it on head
{
currTrack = AU.reverse(track);
//console.log('REVERSE TRACK', currTrack);
}
}
else // !boundLeading && !boundTailing, no bound, but keep the leading coord unchanged
{
if (pointLeading === pointTailing) // a closed track, both end should be unmodified
constraint = C.CIRCLE;
else
constraint = C.LEADING;
}
// calculate some common data
var op = Object.extend({}, options);
op.preferredStartingAngles = options.preferredStartingAngles || [0]; // preferred direction angle of first line segment
op.segmentsInfo = this._calcTrackSegmentsInfo(currTrack);
op.distanceMedian = AU.getMedian(op.segmentsInfo.distances);
//console.log('constraint', constraint, pointLeading, pointTailing, track);
// debug
/*
if (constraint === C.LEADING)
constraint = C.BOTH;
*/
// do actual optimization
var newTrack = (constraint === C.CIRCLE)? this.doOptimizeOnCircleConstraint(currTrack, op):
(constraint === C.BOTH)? this.doOptimizeOnBothConstraint(currTrack, op):
this.doOptimizeOnLeadingConstraint(currTrack, op);
//console.log('optimized track', newTrack);
return newTrack;
},
/**
* Calculate the detailed data of each segment in a track.
* @param {Array} track
* @returns {Array}
* @private
*/
_calcTrackSegmentsInfo: function(track)
{
var distances = [], angles = [];
for (var i = 0, l = track.length; i < l - 1; ++i)
{
var coord1 = track[i];
var coord2 = track[i + 1];
var distance = CU.getDistance(coord1, coord2);
var delta = CU.substract(coord2, coord1);
var angle = Math.atan2(delta.y, delta.x);
distances.push(distance);
angles.push(angle);
}
return {
'distances': distances,
'angles': angles
}
},
// actual optimization method on different constraint
/** @private */
doOptimizeOnLeadingConstraint: function(track, options, isBasicPart)
{
var GU = Kekule.GeometryUtils;
var result = [];
if (isBasicPart) // track is not splitted into chain and ring yet, do a split first
{
var C = Kekule.Editor.PointerTrackConstraint;
var trackParts = this.splitTrackByRings(track);
if (trackParts.length === 1) // only one part
return trackParts[i].isRing?
this.doOptimizeOnCircleConstraint(childTrack, options, true):
this.doOptimizeOnLeadingConstraint(childTrack, options, true);
var lastEndCoord;
for (var i = 0, l = trackParts.length; i < l; ++i)
{
//console.log(i, trackParts[i]);
var childTrack = trackParts[i].track;
//var constraint = trackParts[i].isRing? C.CIRCLE: C.LEADING;
var optimizedChildTrack = trackParts[i].isRing?
this.doOptimizeOnCircleConstraint(childTrack, options, true):
this.doOptimizeOnLeadingConstraint(childTrack, options, true);
// align to lastEndCoord
if (!lastEndCoord) // first part
{
lastEndCoord = optimizedChildTrack[optimizedChildTrack.length - 1];
result = result.concat(optimizedChildTrack);
}
else // the following part
{
var currFirstCoord = optimizedChildTrack[0];
var delta = CU.substract(lastEndCoord, currFirstCoord);
//var transformMatrix = CU.calcTransform2DMatrix({'translateX': delta.x, 'translateY': delta.y});
for (var j = 0, k = optimizedChildTrack.length; j < k; ++j)
{
optimizedChildTrack[j] = CU.add(optimizedChildTrack[j], delta);
}
lastEndCoord = optimizedChildTrack[optimizedChildTrack.length - 1];
optimizedChildTrack.shift(); // the first coord is the same to last one of prev track part, erase it
result = result.concat(optimizedChildTrack);
}
}
return result;
}
var baseAngles = options.preferredStartingAngles;
var lastAngle;
// put first fixed coord
result.push(track[0]);
var preferredIncludeAngles = [options.preferredIncludeAngle, -options.preferredIncludeAngle];
for (var i = 1, l = track.length; i < l; ++i)
{
if (track[i][this.OPTIMIZED_COORD_FIELD]) // already optimized
{
result.push(track[i][this.OPTIMIZED_COORD_FIELD]);
}
else
{
//var coord1 = result[i - 1]; // new baseCoord
var coord1 = track[i - 1];
var newCoord1 = result[i - 1];
var coord2 = track[i];
var delta = CU.substract(coord2, coord1);
var oldDistance = CU.getDistance(coord1, coord2);
var newDistance = this._optimizeLineSegmentDistance(oldDistance, options); // temp
var oldAngle = Math.atan2(delta.y, delta.x);
var newAngle;
/*
if (i > 2) // calc include angle
{
var coord0 = track[i - 2];
var vector1 = CU.substract(coord0, coord1);
var vector2 = CU.substract(coord2, coord1);
var oldIncludeAngle = GU.getVectorIncludedAngle2(vector1, vector2);
var newIncludeAngle = this._optimizeIncludeAngle(oldIncludeAngle, preferredIncludeAngles, options);
//newAngle = -(Math.PI - (oldAngle + newIncludeAngle));
newAngle = - (Math.PI - GU.standardizeAngle(newIncludeAngle - oldAngle));
console.log('angle calc', oldIncludeAngle * 180 / Math.PI, newIncludeAngle * 180 / Math.PI, oldAngle * 180 / Math.PI, newAngle * 180 / Math.PI);
}
else
*/
{
newAngle = this._optimizeDirectionAngle(oldAngle, baseAngles, options, lastAngle);
}
lastAngle = newAngle;
//baseAngle = newAngle + Math.PI / 3 * 2; // temp, fixed next baseAngle
var a = Math.PI - options.preferredIncludeAngle;
//var a = options.preferredIncludeAngle;
var baseAngles = [GU.standardizeAngle(newAngle - a), GU.standardizeAngle(newAngle + a)];
var newCoordDelta = {x: newDistance * Math.cos(newAngle), 'y': newDistance * Math.sin(newAngle)};
var newCoord2 = CU.add(newCoord1, newCoordDelta);
result.push(newCoord2);
track[i][this.OPTIMIZED_COORD_FIELD] = newCoord2;
}
}
return result;
},
/** @private */
doOptimizeOnBothConstraint: function(track, options, isBasicPart)
{
var trackLength = track.length;
// do a leading optimization first
var track1 = this.doOptimizeOnLeadingConstraint(track, options, isBasicPart);
// then compare the end poisition of original constraint and newly optimized track1
var endCoord1 = track1[trackLength - 1];
var endCoordOld = track[trackLength - 1];
/*
// compare two end coords, try to transform them to one (rotate and scale)
var startCoord = track[0];
var deltaOld = CU.substract(endCoordOld, startCoord);
var delta1 = CU.substract(endCoord1, startCoord);
var rotateAngle = Math.atan2(deltaOld.y, deltaOld.x) - Math.atan2(delta1.y, delta1.x);
var scale = CU.getDistance(deltaOld) / CU.getDistance(delta1);
// at last, transform each coord in track1
var result = [track1[0]]; // track1[0] need not to be changed
var transformOptions = {'scale': scale, 'rotateAngle': rotateAngle, 'center': startCoord};
var transformMatrix = CU.calcTransform2DMatrix(transformOptions);
for (var i = 1; i < trackLength; ++i)
{
result.push(CU.transform2DByMatrix(track1[i], transformMatrix));
}
*/
var startCoord = track[0];
var deltaOld = CU.substract(endCoordOld, startCoord);
var angleOld = Math.atan2(deltaOld.y, deltaOld.x);
var delta1 = CU.substract(endCoord1, startCoord);
var angle1 = Math.atan2(delta1.y, delta1.x);
var scale = CU.getDistance(deltaOld) / CU.getDistance(delta1);
if (scale <= 2) // Too large scale will cause layout error, todo: this threshold is currently fixed
{
// rotate track1 to x axis, then scale, then rotate to angleOld
var transformMatrix1 = CU.calcTransform2DMatrix({'rotateAngle': -angle1, 'center': startCoord});
var transformMatrix2 = CU.calcTransform2DMatrix({'scale': scale, 'center': startCoord});
var transformMatrix3 = CU.calcTransform2DMatrix({'rotateAngle': angleOld, 'center': startCoord});
var transformMatrix = Kekule.MatrixUtils.multiply(transformMatrix1, transformMatrix2);
transformMatrix = Kekule.MatrixUtils.multiply(transformMatrix, transformMatrix3);
var result = [track1[0]]; // track1[0] need not to be changed
for (var i = 1; i < trackLength - 1; ++i)
{
result.push(CU.transform2DByMatrix(track1[i], transformMatrix));
}
result.push(track[trackLength - 1]);
}
else
{
var deltaStep = CU.divide(CU.substract(endCoordOld, endCoord1), trackLength - 1);
var result = [track1[0]]; // track1[0] need not to be changed
//var result = [];
var currDelta = {'x': 0, 'y': 0};
for (var i = 0; i < trackLength - 1; ++i)
{
currDelta = CU.add(currDelta, deltaStep);
result.push(CU.add(track1[i], currDelta));
}
result.push(track[trackLength - 1]);
}
return result; //.concat(track1);
},
/** @private */
doOptimizeOnCircleConstraint: function(track, options, isBasicPart)
{
var edgeCount = track.length - 1;
//console.log('edge', edgeCount);
var op = Object.create(options);
if (edgeCount <= 8) // to small rings, bond angle is (1 - 2/n) * pi
{
var preferredIncludeAngle = (1 - 2 / edgeCount) * Math.PI;
var op = Object.create(options);
op.preferredIncludeAngle = preferredIncludeAngle;
}
var trackLength = track.length;
// do a leading optimization first
var track1 = this.doOptimizeOnLeadingConstraint(track, op, isBasicPart);
// then compare the end poisition of original constraint and newly optimized track1
var endCoord1 = track1[trackLength - 1];
var endCoordOld = track[trackLength - 1];
var deltaStep = CU.divide(CU.substract(endCoordOld, endCoord1), trackLength - 1);
var result = [track1[0]]; // track1[0] need not to be changed
//var result = [];
var currDelta = {'x': 0, 'y': 0};
for (var i = 1; i < trackLength - 1; ++i)
{
currDelta = CU.add(currDelta, deltaStep);
result.push(CU.add(track1[i], currDelta));
}
result.push(track[trackLength - 1]);
return result; //.concat(track1);
},
/** @private */
_optimizeIncludeAngle: function(oldAngle, preferredIncludeAngles, options)
{
return this._optimizeDirectionAngle(oldAngle, preferredIncludeAngles, options);
},
/** @private */
_optimizeDirectionAngle: function(oldAngle, baseAngles, options, prevAngle)
{
var GU = Kekule.GeometryUtils;
var result = null, closetBaseAngle = null;
if (baseAngles && baseAngles.length)
{
// find closet base angle
var minDelta = null;
for (var i = 0, l = baseAngles.length; i < l; ++i)
{
var deltaAngle = Math.abs(GU.standardizeAngle(oldAngle) - baseAngles[i]);
if (minDelta === null || deltaAngle < minDelta)
{
closetBaseAngle = baseAngles[i];
minDelta = deltaAngle;
}
}
//return closetBaseAngle;
if (minDelta <= Math.PI / 6) // TODO: temp fixed, +-30 degree all aligns to base angle
result = closetBaseAngle;
}
if (result === null) // not close to base angle, need further calculation
{
var angleConstraint = options.angleConstraint;
if (!angleConstraint)
return oldAngle;
if (closetBaseAngle === null)
closetBaseAngle = 0;
var deltaAngle = oldAngle - closetBaseAngle;
var times = Math.round(deltaAngle / angleConstraint);
result = closetBaseAngle + times * angleConstraint;
if (prevAngle) // if prevAngle is set, we must ensure that result is not at the same line of prev segment
{
var threshold = 5 * Math.PI / 180; // angle diff threshold, currently fixed
var diff = Math.abs(GU.standardizeAngle(result - prevAngle, -Math.PI));
if (Kekule.NumUtils.isFloatEqual(diff, 0, threshold) || Kekule.NumUtils.isFloatEqual(diff, Math.PI, threshold))
{
// adjust
var anglePlus = result + angleConstraint;
var angleMinus = result - angleConstraint;
if (Math.abs(oldAngle - anglePlus) < Math.abs(oldAngle - angleMinus))
result = anglePlus;
else
result = angleMinus;
}
}
}
return result;
/*
var angleConstraint = options.angleConstraint;
if (!angleConstraint)
return oldAngle;
var deltaAngle = oldAngle - (baseAngle || 0);
var times = Math.round(deltaAngle / angleConstraint);
return (baseAngle || 0) + times * angleConstraint;
*/
},
/** @private */
_optimizeLineSegmentDistance: function(oldDistance, options)
{
var defBondScreenLength = options.defBondScreenLength;
var distanceConstraints = options.distanceConstraints || [];
if (!distanceConstraints)
return oldDistance;
var maxConstraintDistance = distanceConstraints[distanceConstraints.length - 1] * defBondScreenLength;
if (oldDistance > maxConstraintDistance) // large than the max constraint, use default method to calculate
{
var times = Math.round(oldDistance / defBondScreenLength);
return times * defBondScreenLength;
}
else
{
/*
var minDiff = null, closetIndex;
for (var i = 0, l = distanceConstraints.length; i < l; ++i)
{
var constraint = distanceConstraints[i];
var diff = Math.abs(oldDistance - constraint * defBondScreenLength);
if (minDiff === null || diff < minDiff)
{
minDiff = diff;
closetIndex = i;
}
}
*/
var primaryConstraintDistance = options.primaryDistanceConstraint;
var constraintCount = distanceConstraints.length;
var primaryIndex = distanceConstraints.indexOf(primaryConstraintDistance);
if (primaryIndex < 0) // no primary
primaryIndex = 0;
var currDistance;
for (var i = constraintCount- 1; i >= primaryIndex; --i)
{
var constraint = distanceConstraints[i];
currDistance = constraint * defBondScreenLength;
if (oldDistance > currDistance)
return currDistance;
}
for (var i = 0; i <= primaryIndex; ++i)
{
currDistance = constraint * defBondScreenLength;
if (oldDistance < currDistance)
return currDistance;
}
//return distanceConstraints[closetIndex] * defBondScreenLength;
return currDistance; // defaultly returns primaryConstraintDistance
}
}
});
/**
* Controller to input chem structure by touch or mouse track.
* @class
* @augments Kekule.Editor.StructureInsertIaController
*
*/
Kekule.Editor.TrackInputIaController = Class.create(Kekule.Editor.StructureInsertIaController,
/** @lends Kekule.Editor.TrackInputIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.TrackInputIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this.setEnableSelect(false);
this._isTracking = false;
this._trackCoordToObjBindings = null;
this._trackParser = new Kekule.Editor.TrackParser();
this._trackLayoutOptimizer = new Kekule.Editor.TrackLayoutOptimizer();
this._addStructureOperation = null;
this._editorScaled = false;
this._editorOriginalZoomLevel = 1;
this._lastRenderedMarkerCoord = null;
// debug
this._debugMarkers = {};
},
/** @ignore */
doFinalize: function($super)
{
this._trackLayoutOptimizer.finalize();
this._trackParser.finalize();
var trackMarker = this.getPropStoreFieldValue('trackMarker');
if (trackMarker)
trackMarker.finalize();
/*
var boundMarkers = this.getBoundChemObjMarkers();
for (var i = boundMarkers.length - 1; i >= 0; --i)
boundMarkers[i].finalize();
*/
$super();
},
/** @private */
initProperties: function()
{
// marker to display the track path
this.defineProp('trackMarker', {'dataType': DataType.OBJECT, 'serializable': false,
'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('trackMarker');
if (!result)
{
result = new Kekule.ChemWidget.MetaShapeUIMarker({'shapeType': Kekule.Render.MetaShapeType.POLYLINE, 'coords': []});
//result.__$$$$$__ = 'trackMarker';
this.setPropStoreFieldValue('trackMarker', result);
}
return result;
}
});
/*
// markers to display the bound chem nodes in track
this.defineProp('boundChemObjMarkers', {'dataType': DataType.ARRAY, 'serializable': false,'setter': null});
*/
},
/** @ignore */
initPropValues: function($super)
{
$super();
//this.setPropStoreFieldValue('boundChemObjMarkers', []);
},
/** @private */
getEditorUiMarkers: function()
{
return this.getEditor().getUiMarkers();
},
/**
* Refine and simplify track, split it to several tracks according to merging dest,
* for further operations.
*/
refineTrackCoords: function(trackCoords)
{
var editor = this.getEditor();
var configs = this.getEditorConfigs();
var optimizationOptions = {
'angleConstraint': configs.getInteractionConfigs().getTrackOptimizationAngleConstraint(),
'distanceConstraints': configs.getInteractionConfigs().getTrackOptimizationDistanceConstraints(),
'primaryDistanceConstraint': configs.getInteractionConfigs().getTrackOptimizationPrimaryDistanceConstraint(),
'preferredIncludeAngle': Math.PI * 2 / 3 // TODO: currently fixed
};
var defBondLength = editor.getDefBondLength? editor.getDefBondLength(): configs.getStructureConfigs().getDefBondLength();
// calc def bond length in screen coord system
var coord1 = editor.objCoordToScreen({'x': 0, 'y': 0});
var coord2 = editor.objCoordToScreen({'x': defBondLength, 'y': 0});
var defBondScreenLength = CU.getDistance(coord1, coord2);
optimizationOptions.defBondScreenLength = defBondScreenLength;
var simpilifyDistanceThreshold = this.getEditorConfigs().getInteractionConfigs().getTrackSimplifierDistanceThreshold();
var mergeDistanceThreshold = this.getEditorConfigs().getInteractionConfigs().getTrackMergeDistanceThreshold();
var splittedTracks = this._mergeCoordsToBoundNodesAndSplitTrack(trackCoords);
var refinedTracks = [];
for (var i = 0, l = splittedTracks.length; i < l; ++i)
{
var track = splittedTracks[i];
track = this._trackParser.refineTrack(track, simpilifyDistanceThreshold, mergeDistanceThreshold);
var startingObj = track[0].obj || track[track.length - 1].obj;
var preferredStartingAngle = startingObj? configs.getStructureConfigs.getNewBondDefAngle(startingObj, 1): -Math.PI / 6; // TODO: currently fix bond order to 1
optimizationOptions.preferredStartingAngles = [preferredStartingAngle];
track = this._trackLayoutOptimizer.optimize(track, optimizationOptions);
refinedTracks.push(track);
}
//console.log('splitted', splittedTracks);
// debug
/*
var totalTracks = [];
for (var i = 0, l = refinedTracks.length; i < l; ++i)
{
totalTracks = totalTracks.concat(refinedTracks[i]);
}
if (this._debugMarkers.refinedTracks)
this.getEditor().getUiMarkers().removeMarker(this._debugMarkers.refinedTracks);
this._debugMarkers.refinedTracks = new Kekule.ChemWidget.MetaShapeUIMarker({'shapeType': Kekule.Render.MetaShapeType.POLYLINE, 'coords': totalTracks});
var drawStyles = {
'strokeColor': 'red',
'strokeWidth': 1,
'strokeDash': false,
'opacity': 1
};
this._debugMarkers.refinedTracks.setDrawStyles(drawStyles);
this._debugMarkers.refinedTracks.setVisible(true);
this.getEditor().getUiMarkers().addMarker(this._debugMarkers.refinedTracks);
*/
return refinedTracks;
},
/** @private */
_mergeCoordsToBoundNodesAndSplitTrack: function(trackCoords)
{
var boundRecs = this._trackCoordToObjBindings;
//console.log('original coords', trackCoords, boundRecs);
//console.log(boundRecs);
var length = boundRecs.length;
if (length)
{
var editor = this.getEditor();
var trackParts = []; // split track to parts seperated by bound items
var currBoundObj = boundRecs[0].obj;
var startIndex = boundRecs[0].coordIndex;
/*
if (startIndex > 0)
trackParts.push(trackCoords.slice(0, startIndex));
*/
var lastEndIndex = 0;
for (var i = 1; i <= length; ++i)
{
var rec = boundRecs[i];
if ((i < length && rec && (currBoundObj !== rec.obj || (rec.coordIndex - startIndex > 10)) || i === length)) // meet a new binding, or the last item
{
// put prev unbounded coords first
if (startIndex > lastEndIndex)
//trackParts.push(trackCoords.slice(lastEndIndex, startIndex + 1));
trackParts.push(trackCoords.slice(lastEndIndex, startIndex));
// then replace trackCoords item startIndex to endIndex with a new coord
var objScreenCoord = editor.getObjectScreenCoord(currBoundObj);
objScreenCoord.boundObj = currBoundObj; // store obj field
var endIndex = boundRecs[i - 1].coordIndex;
lastEndIndex = endIndex + 1;
trackParts.push([objScreenCoord]);
//trackCoords.splice(startIndex, endIndex - startIndex + 1, objScreenCoord);
if (i !== length)
{
currBoundObj = rec.obj;
startIndex = rec.coordIndex;
}
}
}
if (trackCoords.length - 1 > boundRecs[length - 1].coordIndex)
{
trackParts.push(trackCoords.slice(boundRecs[length - 1].coordIndex));
}
//console.log('here', trackParts);
// Put to result
var lastPart = [];
var result = [];
for (var i = 0, l = trackParts.length; i < l; ++i)
{
var currPart = trackParts[i];
lastPart = lastPart.concat(currPart);
if (i === l - 1) // last part
{
result.push(lastPart);
}
else
{
if (currPart[0].boundObj && i > 0) // is bound item
{
result.push(lastPart);
lastPart = currPart;
}
}
}
//console.log('split', result);
return result;
}
else
return [trackCoords];
},
/** @private */
convertTracksToStructure: function(tracks, defIsotopeId)
{
var result = new Kekule.Molecule();
result.setCoord2D({'x': 0, 'y': 0});
var editor = this.getEditor();
for (var i = 0, l = tracks.length; i < l; ++i)
{
this._trackParser.convertTrackToStructure(editor, tracks[i], defIsotopeId, result);
}
if (result.getNodeCount() < 2) // at least we should have two nodes to form a structure
return null;
else
return result;
},
/** @private */
getStructureSelectableChildren: function(structure)
{
return [].concat(structure.getNodes()).concat(structure.getConnectors());
},
/** @private */
addStructureToEditor: function(structure)
{
var editor = this.getEditor();
this._addStructureOperation = null;
editor.beginUpdateObject();
try
{
var oper;
var chemSpace = editor.getChemSpace();
if (this._targetMol)
{
oper = new Kekule.ChemStructOperation.MergeStructFragment(structure, this._targetMol, editor);
this._targetMol = null;
}
else
oper = new Kekule.ChemObjOperation.Add(structure, chemSpace, null, editor);
oper.execute();
this._addStructureOperation = oper;
}
finally
{
editor.endUpdateObject();
}
},
/**
* Returns coords of track ui marker.
* @returns {Array}
* @private
*/
getTrackCoords: function()
{
return this.getTrackMarker().getShapeInfo().coords;
},
/**
* Clear coords of track ui marker.
* @private
*/
clearTrackCoords: function()
{
var marker = this.getTrackMarker();
marker.getShapeInfo().coords = [];
//this.setPropStoreFieldValue('boundChemObjMarkers', []);
},
/** @private */
startTracking: function(event, startScreenCoord)
{
this._targetMol = null;
var editor = this.getEditor();
var startObj = this._getMergeDestObjAtCoord(startScreenCoord);
if (!editor.canCreateNewChild() && !startObj) // can not create a standalone child
{
if (!editor.canAddUnconnectedStructFragment())
return null;
else
{
var blankMol = editor.getOnlyOneBlankStructFragment();
this._targetMol = blankMol;
}
}
this._isTracking = true;
this._trackCoordToObjBindings = [];
/*
var marker = this.getTrackMarker();
marker.getShapeInfo().coords = [];
*/
this.setManuallyHotTrack(true); // manually use hot track to mark mergable nodes
this.clearTrackCoords();
//console.log('before add', this.getEditorUiMarkers().getMarkerCount());
var marker = this.getTrackMarker();
this.getEditorUiMarkers().addMarker(marker);
//console.log('add', this.getEditorUiMarkers().getMarkerCount(), this.getEditorUiMarkers());
var styleConfigs = this.getEditorConfigs().getUiMarkerConfigs();
var drawStyles = {
'strokeColor': styleConfigs.getTrackMarkerStrokeColor(),
'strokeWidth': styleConfigs.getTrackMarkerStrokeWidth(),
'strokeDash': styleConfigs.getTrackMarkerStrokeDash(),
'opacity': styleConfigs.getTrackMarkerOpacity()
};
marker.setDrawStyles(drawStyles);
marker.setVisible(true);
// save the first coord and the possible merge target obj
// If autoscale, the merge obj may be disconnected after enlarge, so save it here first
this.addTrackCoord(startScreenCoord, false); // do not repaint marker
if (this.getEditorConfigs().getInteractionConfigs().getAutoAdjustZoomLevelOnTrackTouching())
{
var editor = this.getEditor();
var refLengthRatio = this.getEditorConfigs().getInteractionConfigs().getTrackTouchRefLength();
if (refLengthRatio)
{
var doc = editor.getDocument();
var bondScreenLength = editor.getDefBondScreenLength();
var minLength = Kekule.DocumentUtils.getDevicePPI(doc) * refLengthRatio / Kekule.DocumentUtils.getClientScaleLevel(doc);
var scaleRatio = minLength / bondScreenLength;
//console.log(bondScreenLength, scaleRatio);
if (scaleRatio > 0.9) // TODO: current fixed
{
var currZoomLevel = editor.getCurrZoom();
editor.setZoomCenter(startScreenCoord);
editor.zoomTo(currZoomLevel * scaleRatio);
// since we change the zoom, coord should be recalculated
var coord = this._getEventMouseCoord(event);
// and update the coord info prev added to coords array
var coords = this.getTrackCoords();
var recCoord = coords[coords.length - 1];
startScreenCoord = coord;
Object.extend(recCoord, coord);
this._editorScaled = true;
this._editorOriginalZoomLevel = currZoomLevel;
this._autoScaleStartCoord = coord;
// repaint hot track marker in editor
// editor.recalcHotTrackMarker();
}
else
{
this._editorScaled = false;
this._editorOriginalZoomLevel = null;
}
}
}
this.repaintMarker();
this._lastRenderedMarkerCoord = startScreenCoord;
},
/** @private */
endTracking: function(endScreenCoord)
{
var defIsotopeId = this.getEditorConfigs().getStructureConfigs().getDefIsotopeId() || 'C';
this._isTracking = false;
this.addTrackCoord(endScreenCoord);
//this.getEditorUiMarkers().removeMarker(this.getTrackMarker());
var tracks = this.refineTrackCoords(this.getTrackCoords());
//console.log('tracks', tracks.length, tracks);
if (tracks.length)
{
var mol = this.convertTracksToStructure(tracks, defIsotopeId);
if (mol)
{
this.addStructureToEditor(mol);
/*
var children = this.getStructureSelectableChildren(mol);
this.getEditor().select(children);
*/
this.startDirectManipulate(null, mol, endScreenCoord);
this.moveManipulatedObjs(endScreenCoord); // force a "move" action, to apply possible merge
this.stopManipulate();
var children = this.getStructureSelectableChildren(mol);
this.doneInsertOrModifyBasicObjects(children);
}
}
this.doneTracking();
},
/** @private */
cancelTracking: function()
{
this._isTracking = false;
this.getEditorUiMarkers().removeMarker(this.getTrackMarker());
this.doneTracking();
},
/** @private */
doneTracking: function()
{
if (this._editorScaled)
{
var coords = this.getTrackCoords();
var lastCoord = coords[coords.length - 1];
this._editorScaled = false;
//this.getEditor().setZoomCenter(lastCoord);
this.getEditor().zoomTo(this._editorOriginalZoomLevel, null, this._autoScaleStartCoord);
this._editorOriginalZoomLevel = null;
this._autoScaleStartCoord = null;
}
this.setManuallyHotTrack(false);
this.getEditor().hideHotTrack();
this.clearTrackCoords();
this.repaintMarker();
},
/** @private */
_getMergeDestObjAtCoord: function(screenCoord)
{
var obj = this.getEditor().getTopmostBasicObjectAtCoord(screenCoord, this.getCurrBoundInflation());
if (this.isValidMergeDestObj(obj))
return obj;
else
return null;
},
/** @private */
addTrackCoord: function(screenCoord, doNotRepaint)
{
//console.log('add track coord', screenCoord);
// var objCoord = this.getEditor().screenCoordToObj(screenCoord);
var coords = this.getTrackCoords();
var lastCoord = coords && coords[coords.length - 1];
if (lastCoord && Kekule.CoordUtils.isEqual(screenCoord, lastCoord)) // avoid adding duplicate coord
{
//console.log('equal', lastCoord);
return;
}
/*
var obj = this.getEditor().getTopmostBasicObjectAtCoord(screenCoord, this.getCurrBoundInflation());
if (this.isValidMergeDestObj(obj))
*/
var obj = this._getMergeDestObjAtCoord(screenCoord);
if (obj)
{
// bind coord to obj
this._trackCoordToObjBindings.push({'coord': screenCoord, 'obj': obj, 'coordIndex': this.getTrackCoords().length});
this.getEditor().addHotTrackedObj(obj);
}
// coords.push(objCoord);
coords.push(screenCoord);
// console.log(this.getTrackMarker(), this.getEditor().getUiMarkers());
if (!doNotRepaint)
{
var distance;
if (this._lastRenderedMarkerCoord)
{
distance = Kekule.CoordUtils.getDistance(this._lastRenderedMarkerCoord, screenCoord);
if (distance > 5) // reduce the repaint count
{
this.repaintMarker();
this._lastRenderedMarkerCoord = screenCoord;
}
/*
else
console.log('bypass');
*/
}
else
{
this.repaintMarker();
this._lastRenderedMarkerCoord = screenCoord;
}
}
},
/** @private */
repaintMarker: function()
{
//console.log(this.getEditor().isUpdatingUiMarkers());
this.getEditor().repaintUiMarker();
},
/**
* Whether obj can be a merging destination node.
* @private
*/
isValidMergeDestObj: function(obj)
{
return obj instanceof Kekule.ChemStructureNode;
},
/** @ignore */
canInteractWithObj: function(obj)
{
return obj instanceof Kekule.ChemStructureNode;
},
/** @ignore */
getAllObjOperations: function($super, isTheFinalOperationToEditor)
{
var result = $super(isTheFinalOperationToEditor) || [];
var op = this._addStructureOperation;
if (op)
result.unshift(op);
//console.log('operation', result);
return result;
},
/** @private */
react_pointerdown: function($super, e)
{
//$super(e);
// important, since we did not call $super, a bound inflation should be done manually here
//this.updateCurrBoundInflation(e);
this.getEditor().setCurrPointerType(e.pointerType);
if (e.getButton() === Kekule.X.Event.MouseButton.LEFT)
{
var coord = this._getEventMouseCoord(e);
this.startTracking(e, coord);
e.preventDefault();
}
else if (e.getButton() === Kekule.X.Event.MouseButton.RIGHT)
{
this.cancelTracking();
e.preventDefault();
}
},
/** @private */
react_pointerup: function($super, e)
{
//$super(e);
if (e.getButton() === Kekule.X.Event.MouseButton.LEFT)
{
if (this._isTracking)
{
var coord = this._getEventMouseCoord(e);
// console.log(this._trackCoordToObjBindings);
this.endTracking(coord);
this.addOperationToEditor();
}
this.setState(Kekule.Editor.BasicManipulationIaController.State.NORMAL);
e.preventDefault();
}
},
/** @private */
react_pointermove: function($super, e)
{
$super(e);
if (this._isTracking)
{
var coord = this._getEventMouseCoord(e);
this.addTrackCoord(coord);
e.preventDefault();
}
}
});
// register
Kekule.Editor.IaControllerManager.register(Kekule.Editor.TrackInputIaController, Kekule.Editor.ChemSpaceEditor);
})();