From 63f777125b15fb6962ffef4350c851b925a7ebd5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= <targos@protonmail.com>
Date: Tue, 29 Oct 2024 08:52:55 +0100
Subject: [PATCH] feat: update OCL to v2024.10.2 (#237)

---
 openchemlib                                   |   2 +-
 .../com/actelion/research/chem/Canonizer.java |  33 ++--
 .../research/chem/ExtendedMolecule.java       |  28 ++-
 .../research/chem/IsomericSmilesCreator.java  |   2 +-
 .../actelion/research/chem/SSSearcher.java    |   8 +
 .../descriptor/DescriptorWeightsHelper.java   |   2 +-
 .../chem/shredder/FragmentGeometry3D.java     |  27 ++-
 .../research/chem/shredder/Fragmenter3D.java  |   2 +-
 .../editor/BondQueryFeatureDialogBuilder.java |   9 +-
 .../gui/editor/GenericEditorArea.java         | 159 ++++++++++--------
 10 files changed, 164 insertions(+), 108 deletions(-)

diff --git a/openchemlib b/openchemlib
index e30c9f88..6cb49c86 160000
--- a/openchemlib
+++ b/openchemlib
@@ -1 +1 @@
-Subproject commit e30c9f88bb90c9b1ee688c121ee8ecfb2cc043c4
+Subproject commit 6cb49c868a3e03c6b91343be10cb80279112077c
diff --git a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/Canonizer.java b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/Canonizer.java
index 56f5d038..6beda750 100644
--- a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/Canonizer.java
+++ b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/Canonizer.java
@@ -170,12 +170,12 @@ public class Canonizer {
 	private boolean[] mNitrogenQualifiesForParity;
 	private ArrayList<CanonizerFragment> mFragmentList;
 	private ArrayList<int[]> mTHParityNormalizationGroupList;
-	private int mMode,mNoOfRanks,mNoOfPseudoGroups;
+	private final int mMode;
+	private int mNoOfRanks,mNoOfPseudoGroups;
 	private boolean mIsOddParityRound;
-	private boolean mZCoordinatesAvailable,mAllHydrogensAreExplicit;
+	private final boolean mZCoordinatesAvailable,mAllHydrogensAreExplicit;
 	private boolean mCIPParityNoDistinctionProblem;
 	private boolean mEncodeAvoid127;
-
 	private boolean mGraphGenerated;
 	private int mGraphRings,mFeatureBlock;
 	private int[] mGraphAtom;
@@ -184,9 +184,10 @@ public class Canonizer {
 	private int[] mGraphFrom;
 	private int[] mGraphClosure;
 
-	private String		    mIDCode, mEncodedCoords,mMapping;
-	private StringBuilder	mEncodingBuffer;
-	private	int				mEncodingBitsAvail,mEncodingTempData,mAtomBits,mMaxConnAtoms;
+	private String mIDCode, mEncodedCoords,mMapping;
+	private StringBuilder mEncodingBuffer;
+	private	int mEncodingBitsAvail,mEncodingTempData,mMaxConnAtoms;
+	private final int mAtomBits;
 
 	/**
 	 * Runs a canonicalization procedure for the given molecule that creates unique atom ranks,
@@ -204,7 +205,7 @@ public Canonizer(StereoMolecule mol) {
 	 * If mode includes ENCODE_ATOM_CUSTOM_LABELS, than custom atom labels are
 	 * considered for the atom ranking and are encoded into the idcode.<br>
 	 * If mode includes COORDS_ARE_3D, then getEncodedCoordinates() always returns
-	 * a 3D-encoding even if all z-coordinates are 0.0. Otherwise coordinates are
+	 * a 3D-encoding even if all z-coordinates are 0.0. Otherwise, coordinates are
 	 * encoded in 3D only, if at least one of the z-coords is not 0.0.
 	 * @param mol
 	 * @param mode 0 or one or more of CONSIDER...TOPICITY, CREATE..., ENCODE_ATOM_CUSTOM_LABELS, ASSIGN_PARITIES_TO_TETRAHEDRAL_N, COORDS_ARE_3D
@@ -226,17 +227,7 @@ public Canonizer(StereoMolecule mol, int mode) {
 
 		mZCoordinatesAvailable = ((mode & COORDS_ARE_3D) != 0) || mMol.is3D();
 
-		mAllHydrogensAreExplicit = false;
-		if (mMol.getAllAtoms() > mMol.getAtoms()
-		 && !mMol.isFragment()) {
-			mAllHydrogensAreExplicit = true;
-			for (int i=0; i<mMol.getAtoms(); i++) {
-				if (mMol.getImplicitHydrogens(i) != 0) {
-					mAllHydrogensAreExplicit = false;
-					break;
-					}
-				}
-			}
+		mAllHydrogensAreExplicit = (mMol.getImplicitHydrogens() == 0);
 
 		if ((mMode & NEGLECT_ANY_STEREO_INFORMATION) == 0) {
 			mTHParity = new byte[mMol.getAtoms()];
@@ -639,7 +630,7 @@ private boolean canInnerBreakTiesByHeteroTopicity() {
 	private void canBreakTiesRandomly() {
 		for (int atom=0; atom<mMol.getAtoms(); atom++) {
 			mCanBase[atom].init(atom);
-			mCanBase[atom].add(mAtomBits+1, 2*mCanRank[atom]);
+			mCanBase[atom].add(mAtomBits+1, (long)2*mCanRank[atom]);
 			}
 
 		// promote randomly one atom of lowest shared rank.
@@ -895,7 +886,7 @@ private void canRecursivelyFindAllParities() {
 					thParityInfo |= mTHESRGroup[atom];
 					}
 
-				mCanBase[atom].add(2 * parityInfoBits, thParityInfo << parityInfoBits); // generate space for bond parity
+				mCanBase[atom].add(2 * parityInfoBits, (long)thParityInfo << parityInfoBits); // generate space for bond parity
 				}
 
 			for (int bond=0; bond<mMol.getBonds(); bond++) {
@@ -3371,7 +3362,7 @@ public String getEncodedCoordinates() {
 	 * original molecule including coordinates.<br>
 	 * If keepPositionAndScale==false, then coordinate encoding will be relative,
 	 * i.e. scale and absolute positions get lost during the encoding.
-	 * Otherwise the encoding retains scale and absolute positions.<br>
+	 * Otherwise, the encoding retains scale and absolute positions.<br>
 	 * If the molecule has 3D-coordinates and if there are no implicit hydrogen atoms,
 	 * i.e. all hydrogen atoms are explicitly available with their coordinates, then
 	 * hydrogen 3D-coordinates are also encoded despite the fact that the idcode itself does
diff --git a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/ExtendedMolecule.java b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/ExtendedMolecule.java
index db7c7ae1..b901fdba 100644
--- a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/ExtendedMolecule.java
+++ b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/ExtendedMolecule.java
@@ -1445,7 +1445,28 @@ public boolean supportsImplicitHydrogen(int atom) {
 		}
 
 	/**
-	 * Calculates and return the number of implicit hydrogens at atom.
+	 * Calculates and returns the number of implicit hydrogens of the molecule.
+	 * For hydrogens atoms, metals except Al, or a noble gases, 0 is assumed.
+	 * For all other atom kinds the number of implicit hydrogens is basically
+	 * the lowest typical valence that is compatible with the occupied valence,
+	 * minus the occupied valence corrected by atom charge and radical state.
+	 * If this molecule is a fragment, then 0 is returned.
+	 * @return number of implicit hydrogens of the molecule
+	 */
+	public int getImplicitHydrogens() {
+		if (mIsFragment)
+			return 0;
+
+		ensureHelperArrays(cHelperNeighbours);
+		int implicitHydrogens = 0;
+		for (int atom=0; atom<mAtoms; atom++)
+			implicitHydrogens += getImplicitHydrogens(atom);
+
+		return implicitHydrogens;
+		}
+
+	/**
+	 * Calculates and returns the number of implicit hydrogens at atom.
 	 * If atom is itself a hydrogen atom, a metal except Al, or a noble gas,
 	 * then 0 is returned. For all other atom kinds the number of
 	 * implicit hydrogens is basically the lowest typical valence that is compatible
@@ -1463,8 +1484,9 @@ public int getImplicitHydrogens(int atom) {
 		if (!supportsImplicitHydrogen(atom))
 			return 0;
 
-		if ("*".equals(getAtomCustomLabel(atom)))
-			return 0;
+		// attachment points have at least a valence of 1, i.e. they must be connected to something
+		if (mAtomicNo[atom] == 0 || "*".equals(getAtomCustomLabel(atom)))
+			return mAllConnAtoms[atom] == 0 ? 1 : 0;
 
 		ensureHelperArrays(cHelperNeighbours);
 
diff --git a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/IsomericSmilesCreator.java b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/IsomericSmilesCreator.java
index f282b1d0..d6e9d2bc 100644
--- a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/IsomericSmilesCreator.java
+++ b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/IsomericSmilesCreator.java
@@ -820,7 +820,7 @@ private void appendBondOrderSymbol(int bond, int parentAtom, StringBuilder build
 		if (mEZHalfParity[bond] != 0)
 			builder.append(mEZHalfParity[bond] == 1 ? '/' : '\\');
 		if (mMode == MODE_CREATE_SMARTS) {
-			int bondTypes = mMol.getBondQueryFeatures(Molecule.cBondQFBondTypes | Molecule.cBondQFRareBondTypes);
+			int bondTypes = mMol.getBondQueryFeatures(bond) & (Molecule.cBondQFBondTypes | Molecule.cBondQFRareBondTypes);
 			if (bondTypes != 0) {
 				if ((bondTypes & Molecule.cBondTypeSingle) != 0 && mEZHalfParity[bond] == 0) {
 					builder.append('-');
diff --git a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/SSSearcher.java b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/SSSearcher.java
index ed8af926..c7fcc73a 100644
--- a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/SSSearcher.java
+++ b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/SSSearcher.java
@@ -1322,6 +1322,14 @@ public boolean areBondsSimilar(int moleculeBond, int fragmentBond) {
 			 && ringSize == (mMolecule.getBondQueryFeatures(fragmentBond) & Molecule.cBondQFRingSize) >> Molecule.cBondQFRingSizeShift)
 				return true;
 
+			if (ringSize <= 2) {    // ring size 8-11 is encoded as 1; ring size >=12 is encoded as 2
+				int moleculeRingSize = mMolecule.getBondRingSize(moleculeBond);
+				if (ringSize == 1)
+					return (moleculeRingSize >= 8) && (moleculeRingSize <= 12);
+				else
+					return moleculeRingSize >= 12;
+				}
+
 			boolean found = false;
 			RingCollection ringSet = mMolecule.getRingSet();
 			for (int i=0; i<ringSet.getSize(); i++) {
diff --git a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/descriptor/DescriptorWeightsHelper.java b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/descriptor/DescriptorWeightsHelper.java
index 2d8148fb..b9e6dd68 100644
--- a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/descriptor/DescriptorWeightsHelper.java
+++ b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/descriptor/DescriptorWeightsHelper.java
@@ -71,7 +71,7 @@ public DescriptorWeightsHelper() {
      * - Charged pp points are set mandatory.
      * @param liSubGraphIndices
      * @param molecule3D
-     * @return
+     * @return array with dimension molecule3D.getAtoms().
      */
     public static int [] calcWeightLabels(List<SubGraphIndices> liSubGraphIndices, Molecule3D molecule3D){
 
diff --git a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/shredder/FragmentGeometry3D.java b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/shredder/FragmentGeometry3D.java
index 0a7eb563..986419c8 100644
--- a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/shredder/FragmentGeometry3D.java
+++ b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/shredder/FragmentGeometry3D.java
@@ -74,7 +74,7 @@ private Coordinates[] determineAlignmentCoords() {
 			coords[i] = mMol.getCoordinates(mExitVector[i].rootAtom);
 			coords[mExitVector.length + i] = mMol.getCoordinates(mExitVector[i].exitAtom);
 
-			// for lonely hydrogens (selected H connects to other exit atom)
+			// for lonely hydrogens (single selected H connecting to non-selected exit atom)
 			// we need to place the root coord (hydrogen) further away from the exit atom,
 			// to reflect the longer bond length of a C-C compared to H-C
 			if (mMol.getAtomicNo(mExitVector[i].rootAtom) == 1)
@@ -123,9 +123,10 @@ public boolean equals(FragmentGeometry3D geometry) {
 	 * is returned.
 	 * @param geometry the geometry to be aligned with this
 	 * @param permutation permutation index for equivalent root atoms of the passed geometry
+	 * @param rmsdHolder null or double[1] to receive RMSD value
 	 * @return rotation matrix or null, depending on whether alignment is acceptable
 	 */
-	public double[][] alignRootAndExitAtoms(FragmentGeometry3D geometry, int permutation, double maxRMSD) {
+	public double[][] alignRootAndExitAtoms(FragmentGeometry3D geometry, int permutation, double[] rmsdHolder, double maxRMSD) {
 		ExitVector[] geomEV = geometry.mExitVector;
 		Coordinates[] coords = new Coordinates[2*geomEV.length];
 		for (int i=0; i<geomEV.length; i++)
@@ -141,19 +142,27 @@ public double[][] alignRootAndExitAtoms(FragmentGeometry3D geometry, int permuta
 			c.add(mAlignmentCOG);
 		}
 
-		return Coordinates.getRmsd(mAlignmentCoords, coords) > maxRMSD ? null : matrix;
+		double rmsd = Coordinates.getRmsd(mAlignmentCoords, coords);
+		if (rmsdHolder != null)
+			rmsdHolder[0] = rmsd;
+
+		return rmsd > maxRMSD ? null : matrix;
 	}
 
-	public boolean hasMatchingExitVectors(FragmentGeometry3D geometry, Coordinates[] coords, int permutation, double maxAngleDivergence) {
-		maxAngleDivergence *= Math.PI / 180;
-		for (int i = 0; i<mExitVector.length; i++) {
+	public boolean hasMatchingExitVectors(FragmentGeometry3D geometry, Coordinates[] coords, int permutation, double[][] angleHolder, double maxAngleDivergence) {
+		double[] angleDif = new double[mExitVector.length];
+		boolean qualifies = true;
+		for (int i=0; i<mExitVector.length; i++) {
 			Coordinates v1 = mAlignmentCoords[mExitVector.length+i].subC(mAlignmentCoords[i]);
 			ExitVector ev2 = geometry.mExitVector[mPermutation[permutation][i]];
 			Coordinates v2 = coords[ev2.exitAtom].subC(coords[ev2.rootAtom]);
-			if (v1.getAngle(v2) > maxAngleDivergence)
-				return false;
+			angleDif[i] = v1.getAngle(v2) * 180 / Math.PI;
+			if (angleDif[i] > maxAngleDivergence)
+				qualifies = false;
 		}
-		return true;
+		if (angleHolder != null)
+			angleHolder[0] = angleDif;
+		return qualifies;
 	}
 
 	public int getPermutationCount() {
diff --git a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/shredder/Fragmenter3D.java b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/shredder/Fragmenter3D.java
index 89d76417..409450d0 100644
--- a/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/shredder/Fragmenter3D.java
+++ b/src/com/actelion/research/gwt/chemlib/com/actelion/research/chem/shredder/Fragmenter3D.java
@@ -55,7 +55,7 @@ public ArrayList<Fragment3D> buildFragments(StereoMolecule mol, boolean withHydr
 		int fragmentCount = mol.getFragmentNumbers(fragmentNo, isRotatableBond, true);
 
 		int[] atomCount = new int[fragmentCount];
-		for (int atom=0; atom<mol.getAllAtoms(); atom++)
+		for (int atom=0; atom<mol.getAtoms(); atom++)
 			atomCount[fragmentNo[atom]]++;
 
 		BaseFragmentInfo[] fragmentData = new BaseFragmentInfo[fragmentCount];
diff --git a/src/com/actelion/research/gwt/chemlib/com/actelion/research/gui/editor/BondQueryFeatureDialogBuilder.java b/src/com/actelion/research/gwt/chemlib/com/actelion/research/gui/editor/BondQueryFeatureDialogBuilder.java
index 27dd4560..57731b63 100644
--- a/src/com/actelion/research/gwt/chemlib/com/actelion/research/gui/editor/BondQueryFeatureDialogBuilder.java
+++ b/src/com/actelion/research/gwt/chemlib/com/actelion/research/gui/editor/BondQueryFeatureDialogBuilder.java
@@ -116,6 +116,8 @@ private void build(ExtendedMolecule mol, int bond) {
 		mComboBoxRingSize.addItem("is in 5-membered ring");
 		mComboBoxRingSize.addItem("is in 6-membered ring");
 		mComboBoxRingSize.addItem("is in 7-membered ring");
+		mComboBoxRingSize.addItem("smallest ring 8 to 11");
+		mComboBoxRingSize.addItem("smallest ring >= 12");
 		mDialog.add(mComboBoxRingSize, 1,13,3,13);
 
 		mCBMatchFormalOrder = mDialog.createCheckBox("Match formal bond order");
@@ -223,7 +225,7 @@ else if (aromState == Molecule.cBondQFNotAromatic)
 			mComboBoxRing.setSelectedIndex(0);
 
 		int ringSize = (queryFeatures & Molecule.cBondQFRingSize) >> Molecule.cBondQFRingSizeShift;
-		mComboBoxRingSize.setSelectedIndex((ringSize == 0) ? 0 : ringSize-2);
+		mComboBoxRingSize.setSelectedIndex((ringSize == 0) ? 0 : (ringSize <= 2) ? ringSize+5 : ringSize-2);
 
         if ((queryFeatures & Molecule.cBondQFBridge) != 0) {
             mCBIsBridge.setSelected(true);
@@ -361,9 +363,12 @@ else if (mComboBoxRing.getSelectedIndex() == 4) {
 
 			if (mComboBoxRingSize.getSelectedIndex() != 0) {
 				int ringSize = mComboBoxRingSize.getSelectedIndex() + 2;
+				if (ringSize > 7)	// ringsize 8-11 is encoded as 1; ringsize >12 is encoded as 2
+					ringSize -= 7;
 				int implicitSize = mMol.getBondRingSize(bond);
-				if (ringSize != implicitSize)
+				if (ringSize <= 2 || ringSize != implicitSize) {	// options 1 and 2 cover spans and cannot be implicit
 					queryFeatures |= (ringSize << Molecule.cBondQFRingSizeShift);
+					}
 				}
             }
 
diff --git a/src/com/actelion/research/gwt/chemlib/com/actelion/research/gui/editor/GenericEditorArea.java b/src/com/actelion/research/gwt/chemlib/com/actelion/research/gui/editor/GenericEditorArea.java
index 3e81c7d8..6299313b 100644
--- a/src/com/actelion/research/gwt/chemlib/com/actelion/research/gui/editor/GenericEditorArea.java
+++ b/src/com/actelion/research/gwt/chemlib/com/actelion/research/gui/editor/GenericEditorArea.java
@@ -985,75 +985,8 @@ private void eventHappened(GenericMouseEvent e) {
 
 			switch (mPendingRequest) {
 				case cRequestNewChain:
-					double lastX, lastY;
-					if (mChainAtoms>0) {
-						lastX = mChainAtomX[mChainAtoms - 1];
-						lastY = mChainAtomY[mChainAtoms - 1];
-					} else {
-						lastX = 0;
-						lastY = 0;
-					}
-					double avbl = getScaledAVBL();
-					double s0 = (int)avbl;
-					double s1 = (int)(0.866 * avbl);
-					double s2 = (int)(0.5 * avbl);
-					double dx = mX2 - mX1;
-					double dy = mY2 - mY1;
-					if (Math.abs(dy)>Math.abs(dx)) {
-						mChainAtoms = (int)(2 * Math.abs(dy) / (s0 + s2));
-						if (Math.abs(dy) % (s0 + s2)>s0) {
-							mChainAtoms++;
-						}
-						mChainAtomX = new double[mChainAtoms];
-						mChainAtomY = new double[mChainAtoms];
-						if (mX2<mX1) {
-							s1 = -s1;
-						}
-						if (mY2<mY1) {
-							s0 = -s0;
-							s2 = -s2;
-						}
-						for (int i = 0; i<mChainAtoms; i++) {
-							mChainAtomX[i] = mX1 + 0.5 * (i + 1) * s1;
-							mChainAtomY[i] = mY1 + 0.5 * (i + 1) * (s0 + s2);
-							if ((i & 1) == 0) {
-								mChainAtomY[i] += s0;
-							}
-						}
-					} else {
-						mChainAtoms = (int)(Math.abs(dx) / s1);
-						mChainAtomX = new double[mChainAtoms];
-						mChainAtomY = new double[mChainAtoms];
-						if (mX2<mX1) {
-							s1 = -s1;
-						}
-						if (mY2<mY1) {
-							s2 = -s2;
-						}
-						for (int i = 0; i<mChainAtoms; i++) {
-							mChainAtomX[i] = mX1 + (i + 1) * s1;
-							mChainAtomY[i] = mY1;
-							if ((i & 1) == 0) {
-								mChainAtomY[i] += s2;
-							}
-						}
-					}
-					if (mChainAtoms>0) {
-						mChainAtom = new int[mChainAtoms];
-						for (int i = 0; i<mChainAtoms; i++) {
-							mChainAtom[i] = mMol.findAtom(mChainAtomX[i], mChainAtomY[i]);
-							if (mChainAtom[i] != -1) {
-								mChainAtomX[i] = mMol.getAtomX(mChainAtom[i]);
-								mChainAtomY[i] = mMol.getAtomY(mChainAtom[i]);
-							}
-						}
-						if (mChainAtomX[mChainAtoms - 1] != lastX
-								|| mChainAtomY[mChainAtoms - 1] != lastY) {
-							repaintNeeded = true;
-						}
-					} else if (lastX != 0 || lastY != 0) {
+					if (suggestNewChain())
 						repaintNeeded = true;
-					}
 					break;
 				case cRequestNewBond:
 					if ((mX2 - mX1) * (mX2 - mX1) + (mY2 - mY1) * (mY2 - mY1)<MIN_BOND_LENGTH_SQUARE) {
@@ -1173,6 +1106,94 @@ private void eventHappened(GenericMouseEvent e) {
 		}
 	}
 
+	private boolean suggestNewChain() {
+		double mouseAngle = Molecule.getAngle(mX1, mY1, mX2, mY2);
+
+		double mdx = mX2 - mX1;
+		double mdy = mY2 - mY1;
+
+		int lastChainAtoms = mChainAtoms;
+		int lastX1 = 0;
+		int lastY1 = 0;
+		int lastX2 = 0;
+		int lastY2 = 0;
+		if (lastChainAtoms > 0) {
+			lastX1 = (int)Math.round(mChainAtomX[0]);
+			lastY1 = (int)Math.round(mChainAtomY[0]);
+		}
+		if (lastChainAtoms > 1) {
+			lastX2 = (int)Math.round(mChainAtomX[1]);
+			lastY2 = (int)Math.round(mChainAtomY[1]);
+		}
+
+		double exitAngle = 0;
+		if (mAtom1 == -1 || mMol.getAllConnAtomsPlusMetalBonds(mAtom1) == 0) {
+			exitAngle = Math.PI / 3 * Math.round(mouseAngle * 3 / Math.PI);
+		}
+		else if (mMol.getAllConnAtomsPlusMetalBonds(mAtom1) == 1) {
+			double bondAngle = mMol.getBondAngle(mMol.getConnAtom(mAtom1, 0), mAtom1);
+			double candidate1 = bondAngle - Math.PI / 3;
+			double candidate2 = bondAngle + Math.PI / 3;
+			exitAngle = Math.abs(Molecule.getAngleDif(mouseAngle, candidate1))
+					  < Math.abs(Molecule.getAngleDif(mouseAngle, candidate2)) ? candidate1 : candidate2;
+		}
+		else {
+			double[] connAngle = new double[mMol.getAllConnAtomsPlusMetalBonds(mAtom1)];
+			for (int i=0; i<mMol.getAllConnAtomsPlusMetalBonds(mAtom1); i++)
+				connAngle[i] = mMol.getBondAngle(mAtom1, mMol.getConnAtom(mAtom1, i));
+
+			Arrays.sort(connAngle);
+			for (int i=0; i<connAngle.length; i++) {
+				double leftAngle = (i == 0) ? connAngle[connAngle.length-1] - 2.0*Math.PI : connAngle[i-1];
+				if (leftAngle < mouseAngle && mouseAngle < connAngle[i]) {
+					exitAngle = (connAngle[i] + leftAngle) / 2.0;
+					break;
+				}
+				if (leftAngle < mouseAngle - 2.0 * Math.PI && mouseAngle - 2.0 * Math.PI < connAngle[i]) {
+					exitAngle = (connAngle[i] + leftAngle) / 2.0;
+					break;
+				}
+			}
+		}
+
+		double avbl = getScaledAVBL();
+		mChainAtoms = Math.abs(Molecule.getAngleDif(mouseAngle, exitAngle)) > Math.PI / 3 ? 0 : (int)(Math.sqrt(mdx*mdx + mdy*mdy) / avbl);
+		if (mChainAtoms > 0) {
+			if (mChainAtomX == null || mChainAtomX.length < mChainAtoms) {
+				mChainAtomX = new double[mChainAtoms];
+				mChainAtomY = new double[mChainAtoms];
+			}
+			double[] dx = new double[2];
+			double[] dy = new double[2];
+			double nextAngle = Molecule.getAngleDif(mouseAngle, exitAngle) < 0 ? exitAngle - Math.PI / 3 : exitAngle + Math.PI / 3;
+			dx[0] = avbl * Math.sin(exitAngle);
+			dy[0] = avbl * Math.cos(exitAngle);
+			dx[1] = avbl * Math.sin(nextAngle);
+			dy[1] = avbl * Math.cos(nextAngle);
+			for (int i=0; i<mChainAtoms; i++) {
+				mChainAtomX[i] = (i == 0 ? mX1 : mChainAtomX[i-1]) + dx[i & 1];
+				mChainAtomY[i] = (i == 0 ? mY1 : mChainAtomY[i-1]) + dy[i & 1];
+			}
+
+			mChainAtom = new int[mChainAtoms];
+			for (int i = 0; i<mChainAtoms; i++) {
+				mChainAtom[i] = mMol.findAtom(mChainAtomX[i], mChainAtomY[i]);
+				if (mChainAtom[i] != -1) {
+					mChainAtomX[i] = mMol.getAtomX(mChainAtom[i]);
+					mChainAtomY[i] = mMol.getAtomY(mChainAtom[i]);
+				}
+			}
+		}
+
+		return lastChainAtoms != mChainAtoms
+			|| ((mChainAtoms != 0)
+			 && (lastX1 != (int)Math.round(mChainAtomX[0])
+			  || lastY1 != (int)Math.round(mChainAtomY[0])))
+			|| ((mChainAtoms > 1)
+			 && (lastX2 != (int)Math.round(mChainAtomX[1])
+			  || lastY2 != (int)Math.round(mChainAtomY[1])));
+	}
+
 	public void showHelpDialog() {
 		mUIHelper.showHelpDialog("/html/editor/editor.html", "Structure Editor Help");
 	}
@@ -2369,7 +2390,7 @@ private void suggestNewX2AndY2(int atom)
 	{
 		double newAngle = Math.PI * 2 / 3;
 		if (atom != -1) {
-			double angle[] = new double[MAX_CONNATOMS + 1];
+			double[] angle = new double[MAX_CONNATOMS + 1];
 			for (int i = 0; i<mMol.getAllConnAtomsPlusMetalBonds(atom); i++) {
 				angle[i] = mMol.getBondAngle(atom, mMol.getConnAtom(atom, i));
 			}