/*    Sketch Elements: Chemistry molecular diagram drawing tool.
    
    (c) 2008 Dr. Alex M. Clark
    
    Released as GNUware, under the Gnu Public License (GPL)
    
    See www.gnu.org for details.
*/

package WIMSchem;

import java.lang.*;
import java.text.*;
import java.util.*;
import java.awt.geom.*;

/*
    Given an input molecule, and pseudo-pixel resolution, and a way to measure text, this class provides the necessary
    arrangements which precede the actual drawing of the molecule. 
*/

public class ArrangeMolecule
{
    private Molecule mol;
    private double scale; // pixels-per-Angstrom, or similar
    private ArrangeMeasurement measure;

    public static final int SHOW_ELEMENTS=0;
    public static final int SHOW_ALL_ELEMENTS=1;
    public static final int SHOW_INDEXES=2;
    public static final int SHOW_RINGID=3;
    public static final int SHOW_PRIORITY=4;
    public static final int SHOW_MAPNUM=5;
    public int unselected_color = 0x000000; 
    private int elementMode; // one of SHOW_* above
    private boolean showHydrogens; // if false, H's will never be shown
    private double fontSize; // font size for main features, e.g. elements
    private double lineSize; // width of most lines, such as bonds
    private double bondSep; // size of bond separation between non-single bonds
    private boolean devRounding; // if true, sometimes rounds to "pixel" boundaries to improve rendering
    private boolean annotRS=false,annotEZ=false; // optional extra annotations
    class APoint
    {
    	int anum; // corresponds to molecule atom index
	String text; // the primary label, or null if invisible
	double fsz; // font size for label
	boolean bold;
	int col;
	double cx,cy,rw,rh; // centre and ovaloid radius for label shape; rw,rh==0 if no label
    }
    
    public static final int BLINE_NORMAL=1; // a line segment; may be single bond, part of a multiple bond, or dissected bond
    public static final int BLINE_INCLINED=2; // an up-wedge bond
    public static final int BLINE_DECLINED=3; // a down-wedge bond
    public static final int BLINE_UNKNOWN=4; // a squiggly bond
    public static final int BLINE_DOTTED=5; // dotted line
    
    class BLine
    {
    	int bnum; // corresponds to molecule bond index
	int type; // one of BLINE_*
	double x1,y1,x2,y2;
	double size; 
	int col;
	
	BLine() {}
	BLine(int bnum,int type,double x1,double y1,double x2,double y2,double size,int col)
	{
	    this.bnum=bnum;
	    this.type=type;
	    this.x1=x1;
	    this.y1=y1;
	    this.x2=x2;
	    this.y2=y2;
	    this.size=size;
	    this.col=col;
    	}
    }
    
    private ArrayList<APoint> points=new ArrayList<APoint>();
    private ArrayList<BLine> lines=new ArrayList<BLine>();
    
    // constructor: parameters are options which have no reasonable defaults
    
    public ArrangeMolecule(Molecule mol,ArrangeMeasurement measure)
    {
    	this.mol=mol;
	this.measure=measure;
	
	scale=measure.scale();
	elementMode=SHOW_ELEMENTS;
	showHydrogens=true;
	fontSize=0.6*scale;
    	lineSize=0.075*scale;
	bondSep=0.20*scale;
	devRounding=true;
    }
    
    // methods for using non-default display settings
    
    public void setShowHydrogens(boolean v) {showHydrogens=v;}
    public void setElementMode(int v) {elementMode=v;}
    public void setFontSizeAng(double v) {fontSize=v*scale;} 
    public void setFontSizeDev(double v) {fontSize=v;}
    public void setLineSizeAng(double v) {lineSize=v*scale;}
    public void setDevRounding(boolean v) {devRounding=v;}
    public void setAnnotRS(boolean v) {annotRS=v;}
    public void setAnnotEZ(boolean v) {annotEZ=v;}
    
    public boolean getShowHydrogens() {return showHydrogens;}
    public int getElementMode() {return elementMode;}
    public double getFontSizeDev() {return fontSize;}
    public double getLineSizeDev() {return lineSize;}
    public boolean getDevRounding() {return devRounding;}
    public boolean getAnnotRS() {return annotRS;}
    public boolean getAnnotEZ() {return annotEZ;}

    // the action method: call this before accessing any of the resultant data
    public void arrange()
    {
    	// fill in each of the atom centres
    	if( MainApplet.viewC ){elementMode = 1;}
    	if( MainApplet.viewH ){ showHydrogens = true;}
    	for (int n=1;n<=mol.numAtoms();n++)
	{
	    APoint a=new APoint();
	    a.anum=n;
	    a.fsz=fontSize;
	    a.bold=mol.atomMapNum(n)>0;
	    
	    a.col=unselected_color;
	    if(MainApplet.USER_SELECTION){/*give the molecule the user selected colours */
		if(EditorPane.atomselection[n]){a.col = MainApplet.ATOM_SELECT_HTML_COLOR;}
	    }
	    else
	    {
		if(MainApplet.SUPERUSER_SELECTION){/*give the molecule the param selected colours */
		    if( MainApplet.ExternalAtomSelection != null ){
			for(int p=0 ; p < (MainApplet.ExternalAtomSelection).length; p++ ){
			    if( n == MainApplet.ExternalAtomSelection[p] ){
				a.col = MainApplet.SelectedAtomColorInt[p] ;
			    }
			}
		    }
		}
	    }
	    a.cx=measure.angToX(mol.atomX(n));
	    a.cy=measure.angToY(mol.atomY(n));
	    a.rw=a.rh=0;
	    
	    if (devRounding)
	    {
	    	a.cx=Math.round(a.cx);
		a.cy=Math.round(a.cy);
	    }
    	    // decide whether this atom is to have a label
    	    switch(elementMode){
    	     case SHOW_ELEMENTS: a.text=mol.atomExplicit(n) ? mol.atomElement(n) : null; break;
	     case SHOW_ALL_ELEMENTS: a.text=mol.atomElement(n); break;
	     case SHOW_INDEXES: a.text=String.valueOf(n);break;
	     case SHOW_RINGID: a.text=String.valueOf(mol.atomRingBlock(n));break;
	     case SHOW_PRIORITY: a.text=String.valueOf(mol.atomPriority(n));break;
	     case SHOW_MAPNUM: a.text=mol.atomMapNum(n)>0 ? String.valueOf(mol.atomMapNum(n)) : "";break;
    	     default: a.text="?";break;
    	    }
    	    // if it has a label, then how big
	    if (a.text!=null)
	    {
	    	double[] wad=measure.measureText(a.text,a.fsz);
		a.rw=0.5*wad[0];
		a.rh=0.5*wad[1];
    	    }

	    points.add(a);
	}
	
	// resolve the bonds which can be analyzed immediately
	boolean[] bdbl=new boolean[mol.numBonds()]; // set to true if bond is awaiting a double bond assignment
	int scol; /* select color */
	for (int n=1;n<=mol.numBonds();n++)
	{
	    bdbl[n-1]=mol.bondOrder(n)==2;
	    double x1=pointCX(mol.bondFrom(n)-1),y1=pointCY(mol.bondFrom(n)-1);
	    double x2=pointCX(mol.bondTo(n)-1),y2=pointCY(mol.bondTo(n)-1);
	    scol = unselected_color;	    
	    if(MainApplet.USER_SELECTION){
		if(EditorPane.bondselection[n]){
		    scol = MainApplet.BOND_SELECT_HTML_COLOR;
		}
	    }
	    else
	    {
		if(MainApplet.SUPERUSER_SELECTION){/*give the molecule the param selected colours */
		    if( MainApplet.ExternalBondSelection != null ){
			for(int p = 0 ; p < (MainApplet.ExternalBondSelection).length; p++ ){
			    if( n == MainApplet.ExternalBondSelection[p] ){
				scol = MainApplet.SelectedBondColorInt[p] ;
			    }
			}
		    }
		}
	    }
	    // for non-double bonds, can add the constituents right away
	    if (mol.bondOrder(n)!=2)
	    {
	    	double[] xy1=backOffAtom(mol.bondFrom(n)-1,x1,y1,x2,y2);
		double[] xy2=backOffAtom(mol.bondTo(n)-1,x2,y2,x1,y1);
		double sz=lineSize;
		if (mol.atomMapNum(mol.bondFrom(n))>0 && mol.atomMapNum(mol.bondTo(n))>0) sz*=5.0/3;

		int ltype=BLINE_NORMAL;
		if (mol.bondType(n)==Molecule.BONDTYPE_INCLINED) ltype=BLINE_INCLINED;
		else if (mol.bondType(n)==Molecule.BONDTYPE_DECLINED) ltype=BLINE_DECLINED;
		else if (mol.bondType(n)==Molecule.BONDTYPE_UNKNOWN) ltype=BLINE_UNKNOWN;
		else if (mol.bondOrder(n)<=0) ltype=BLINE_DOTTED;

    	    	int bo=mol.bondOrder(n);

    	    	// for dotted lines, back off intersections if non-terminal
    	    	if (bo==0)
		{
		    double dx=xy2[0]-xy1[0],dy=xy2[1]-xy1[1],d=Math.sqrt(dx*dx+dy*dy);
		    double ox=dx/d*bondSep,oy=dy/d*bondSep;
		    if (mol.atomAdjCount(mol.bondFrom(n))>1) {xy1[0]+=ox; xy1[1]+=oy;}
		    if (mol.atomAdjCount(mol.bondTo(n))>1) {xy2[0]-=ox; xy2[1]-=oy;}
		}

    	    	// add one, or several lines
		if (bo<=1 || mol.bondType(n)!=Molecule.BONDTYPE_NORMAL)
		{
		    // just one line
    	    	    lines.add(new BLine(n,ltype,xy1[0],xy1[1],xy2[0],xy2[1],sz,scol));
		}
		else
		{
		    double[] oxy=orthogonalDelta(xy1[0],xy1[1],xy2[0],xy2[1],bondSep);
		    double v=-0.5*(bo-1);
		    for (int i=0;i<bo;i++,v++)
		    {
		    	lines.add(new BLine(n,BLINE_NORMAL,xy1[0]+v*oxy[0],xy1[1]+v*oxy[1],xy2[0]+v*oxy[0],xy2[1]+v*oxy[1],sz,scol));
		    }
		}
	    }
	}
	
	// process double bonds in rings
	int[][] rings=orderedRingList();
	for (int i=0;i<rings.length;i++)
	{
	    for (int j=0;j<rings[i].length;j++)
	    {
		int k=mol.findBond(rings[i][j],rings[i][j<rings[i].length-1 ? j+1 : 0]);
		if (bdbl[k-1]) 
		{
		    processDoubleBond(k,rings[i]);
		    bdbl[k-1]=false;
		}
	    }
	}
	
	// process all remaining double bonds
	for (int i=1;i<=mol.numBonds();i++) if (bdbl[i-1]) processDoubleBond(i,priorityDoubleSubstit(i));
	
	// place hydrogen labels as explicit "atom centres"
	int[] hcount=new int[mol.numAtoms()];
	for (int n=1;n<=mol.numAtoms();n++) hcount[n-1]=pointText(n-1)==null || !showHydrogens ? 0 : mol.atomHydrogens(n);
	for (int n=0;n<mol.numAtoms();n++) if (hcount[n]>0)
	{
	    if (quadrantOpen(n,1,0) && placeHydrogen(n,hcount[n],1,0)) {hcount[n]=0; continue;}
	    if (quadrantOpen(n,-1,0) && placeHydrogen(n,hcount[n],-1,0)) {hcount[n]=0; continue;}
	    if (quadrantOpen(n,0,1) && placeHydrogen(n,hcount[n],0,1)) {hcount[n]=0; continue;}
	    if (quadrantOpen(n,0,-1) && placeHydrogen(n,hcount[n],0,-1)) {hcount[n]=0; continue;}
	}
	for (int n=0;n<mol.numAtoms();n++) if (hcount[n]>0 && placeHydrogen(n,hcount[n],1,0)) hcount[n]=0;
	for (int n=0;n<mol.numAtoms();n++) if (hcount[n]>0 && placeHydrogen(n,hcount[n],-1,0)) hcount[n]=0;
	for (int n=0;n<mol.numAtoms();n++) if (hcount[n]>0 && placeHydrogen(n,hcount[n],0,1)) hcount[n]=0;
	for (int n=0;n<mol.numAtoms();n++) if (hcount[n]>0 && placeHydrogen(n,hcount[n],0,-1)) hcount[n]=0;
	for (int n=0;n<mol.numAtoms();n++) if (hcount[n]>0) placeHydrogen(n,hcount[n],0,0);
	
	// now do atomic charges/radical notation
	for (int n=1;n<=mol.numAtoms();n++){
	    scol = unselected_color;
	    if(MainApplet.USER_SELECTION){
		if(EditorPane.atomselection[n]){
		    scol = MainApplet.ATOM_SELECT_HTML_COLOR;
		}
	    }
	    else
	    {
		if(MainApplet.SUPERUSER_SELECTION){/*give the molecule the param selected colours */
		    if( MainApplet.ExternalAtomSelection != null ){
			for(int p = 0 ; p < (MainApplet.ExternalAtomSelection).length; p++ ){
			    if( n == MainApplet.ExternalAtomSelection[p] ){
				scol = MainApplet.SelectedAtomColorInt[p] ;
			    }
			}
		    }
		}
	    }
	    String str="";
	    int chg=mol.atomCharge(n);
	    if (chg==-1) str="-";
	    else if (chg==1) str="+";
	    else if (chg<-1) str=String.valueOf(chg);
	    else if (chg>1) str="+"+String.valueOf(chg);
	    for (int i=mol.atomUnpaired(n);i>0;i--) str+=".";
	    if (str.length()==0) continue;
	    annotateAtom(n,str,str.length()==1 ? fontSize : 0.75*fontSize,scol);
	}
	
    	// add stereo annotations, if requested
    	if (annotRS) for (int n=1,chi;n<=mol.numAtoms();n++) 
	    if ((chi=mol.atomChirality(n))!=Molecule.STEREO_NONE)
	{
	    String label=chi==Molecule.STEREO_POS ? "R" : chi==Molecule.STEREO_NEG ? "S" : "R/S";
	    annotateAtom(n,label,0,0x0000FF);
	}
	if (annotEZ) for (int n=1,chi;n<=mol.numBonds();n++) 
	    if ((chi=mol.bondStereo(n))!=Molecule.STEREO_NONE)
	{
	    String label=chi==Molecule.STEREO_POS ? "Z" : chi==Molecule.STEREO_NEG ? "E" : "E/Z";
	    annotateBond(n,label,0,0x0000FF);
	}
    }

    // adds a label to the given atom index, which is drawn near to, but not on top of, the atom itself; font defaults to atom size
    public void annotateAtom(int anum,String label,double fsz,int col)
    {
    	if (fsz==0) fsz=fontSize;
    	double[] wad=measure.measureText(label,fsz);
	double rw=0.5*wad[0],rh=0.5*wad[1];
	double[] pxy=anchorAnnotation(pointCX(anum-1),pointCY(anum-1),rw,rh);

	APoint a=new APoint();
	a.anum=0;
	a.text=label;
	a.fsz=fsz;
	a.bold=false;
	a.col=col;
	a.cx=pxy[0];
	a.cy=pxy[1];
	a.rw=rw;
	a.rh=rh;
	points.add(a);
    }
    
    // adds a label close to the centre of a bond
    public void annotateBond(int bnum,String label,double fsz,int col)
    {
    	if (fsz==0) fsz=fontSize;
    	double[] wad=measure.measureText(label,fsz);
	double rw=0.5*wad[0],rh=0.5*wad[1];
	int bfr=mol.bondFrom(bnum),bto=mol.bondTo(bnum);
	double[] pxy=anchorAnnotation(0.5*(pointCX(bfr-1)+pointCX(bto-1)),0.5*(pointCY(bfr-1)+pointCY(bto-1)),rw,rh);

	APoint a=new APoint();
	a.anum=0;
	a.text=label;
	a.fsz=fsz;
	a.bold=false;
	a.col=col;
	a.cx=pxy[0];
	a.cy=pxy[1];
	a.rw=rw;
	a.rh=rh;
	points.add(a);
    }
    
    // access to atom information; it is valid to assume that {atomcentre}[N-1] matches {moleculeatom}[N], if N<=mol.numAtoms()
    public int numPoints() {return points.size();}
    public int pointANum(int N) {return points.get(N).anum;}
    public String pointText(int N) {return points.get(N).text;}
    public double pointFontSize(int N) {return points.get(N).fsz;}
    public boolean pointBold(int N) {return points.get(N).bold;}
    public int pointCol(int N) {return points.get(N).col;}
    public double pointCX(int N) {return points.get(N).cx;}
    public double pointCY(int N) {return points.get(N).cy;}
    public double pointRW(int N) {return points.get(N).rw;}
    public double pointRH(int N) {return points.get(N).rh;}
    
    // access to bond information; it is NOT valid to read anything into the indices
    public int numLines() {return lines.size();}
    public int lineBNum(int N) {return lines.get(N).bnum;}
    public int lineType(int N) {return lines.get(N).type;}
    public double lineX1(int N) {return lines.get(N).x1;}
    public double lineY1(int N) {return lines.get(N).y1;}
    public double lineX2(int N) {return lines.get(N).x2;}
    public double lineY2(int N) {return lines.get(N).y2;}
    public double lineSize(int N) {return lines.get(N).size;}
    public int lineCol(int N) {return lines.get(N).col;}
    
    // private methods
    
    // given that the position (X,Y) connects with atom N, and is part of a bond that connects at the other end
    // with (FX,FY), considers the possibility that a new (X,Y) may need to be calculated by backing up along the line
    private double[] backOffAtom(int N,double X,double Y,double FX,double FY)
    {
    	final double EXT_RAD=1.4;
    	double cx=pointCX(N),cy=pointCY(N),rw=pointRW(N)*EXT_RAD,rh=pointRH(N)*EXT_RAD;
	if (pointText(N)==null || rw<=0 || rh<=0) return new double[]{X,Y};
	
	double dx=X-cx,dy=Y-cy;
	double sqX=Util.sqr(dx)/Util.sqr(rw),sqY=Util.sqr(dy)/Util.sqr(rh);
	if (sqX+sqY>=1) return new double[]{X,Y}; // does not intrude on the ellipse
	
	// NOTE: this is slightly wrong because it assumes that lines point toward the centre, which is almost true...
	
	dx=FX-cx; dy=FY-cy;
	sqX=Util.sqr(dx)/Util.sqr(rw); sqY=Util.sqr(dy)/Util.sqr(rh);
	double d=Math.sqrt(1/(sqX+sqY));
	return new double[]{X+d*(FX-X),Y+d*(FY-Y)};
    }

    // produces a list of small rings, ordered in a terminal-first manner, which is to be used as the sequence for assigning sides of
    // bond orders
    private int[][] orderedRingList()
    {
    	ArrayList<int[]> rings=new ArrayList<int[]>();
	final int[] SIZE_ORDER={6,5,7,4,3};
	for (int i=0;i<SIZE_ORDER.length;i++)
	{
	    int[][] nring=mol.findRingSize(SIZE_ORDER[i]);
	    for (int j=0;j<nring.length;j++) rings.add(nring[j]);
	}
	
	// keep adding terminal rings to the result set - or, the first ring if none
	ArrayList<int[]> result=new ArrayList<int[]>();
	while (rings.size()>0)
	{
	    int which=0; // (default to first)
	    
	    for (int i=0;i<rings.size();i++)
	    {
	    	int ncon=0;
		for (int j=0;j<rings.size();j++) if (i!=j)
		{
		    boolean shared=false;
		    int[] r1=rings.get(i),r2=rings.get(j);
		    for (int k1=0;k1<r1.length && !shared;k1++) for (int k2=0;k2<r2.length;k2++) 
		    	if (r1[k1]==r2[k2]) {shared=true; break;}
		    if (shared) ncon++;
		}
		
		if (ncon<=1) {which=i; break;}
	    }
	    
	    result.add(rings.get(which));
	    rings.remove(which);
	}
	
	//for (int n=0;n<result.size();n++) Util.writeln(Util.arrayStr(result.get(n)));
	
    	return result.toArray(new int[result.size()][]);
    }

    // convenience function which returns {ox,oy} which is orthogonal to the direction of the input vector, of magnitude D; the direction
    // of {ox,oy} is to the "left" of the input vector
    private double[] orthogonalDelta(double X1,double Y1,double X2,double Y2,double D)
    {
    	double ox=Y1-Y2,oy=X2-X1;
	double dsq=ox*ox+oy*oy;
	double sc=dsq>0 ? D/Math.sqrt(dsq) : 1;
	return new double[]{ox*sc,oy*sc};
    }

    // given the guideline index of a double bond, and some information about the atoms which are on the "important side", creates the
    // appropriate line segments
    private void processDoubleBond(int N,int[] Priority)
    {
    	int bf=mol.bondFrom(N),bt=mol.bondTo(N);
	int[] nf=mol.atomAdjList(bf),nt=mol.atomAdjList(bt);

    	double x1=pointCX(bf-1),y1=pointCY(bf-1),x2=pointCX(bt-1),y2=pointCY(bt-1);
	double dx=x2-x1,dy=y2-y1,btheta=Math.atan2(dy,dx);
	
	// count number of priority atoms on either side of the bond vector
	int countFLeft=0,countFRight=0,countTLeft=0,countTRight=0;
	int idxFLeft=0,idxFRight=0,idxTLeft=0,idxTRight=0;
	boolean noshift=false; // true if definitely not alkene-ish
    	for (int n=0;n<nf.length;n++) if (nf[n]!=bt)
	{
	    if (mol.bondOrder(mol.findBond(bf,nf[n]))>1) {noshift=true; break;}
	    boolean ispri=false;
	    for (int i=0;i<(Priority==null ? 0 : Priority.length);i++) if (Priority[i]==nf[n]) ispri=true;
    	    double theta=Util.angleDiff(Math.atan2(pointCY(nf[n]-1)-y1,pointCX(nf[n]-1)-x1),btheta);
	    if (theta>0) {if (ispri) countFLeft++; idxFLeft=nf[n];} 
	    else {if (ispri) countFRight++; idxFRight=nf[n];}
	}
    	for (int n=0;n<nt.length;n++) if (nt[n]!=bf)
	{
	    if (mol.bondOrder(mol.findBond(bt,nt[n]))>1) {noshift=true; break;}
	    boolean ispri=false;
	    for (int i=0;i<(Priority==null ? 0 : Priority.length);i++) if (Priority[i]==nt[n]) ispri=true;
    	    double theta=Util.angleDiff(Math.atan2(pointCY(nt[n]-1)-y2,pointCX(nt[n]-1)-x2),btheta);
	    if (theta>0) {if (ispri) countTLeft++; idxTLeft=nt[n];} 
	    else {if (ispri) countTRight++; idxTRight=nt[n];}
	}
	
	// decide which side the bond should be shifted to, if either
	int side=0;
	if (noshift || countFLeft>1 || countFRight>1 || countTLeft>1 || countTRight>1) {} // inappropriate
	else if (countFLeft>0 && countFRight>0) {} // ambiguous
	else if (countTLeft>0 && countTRight>0) {} // ambiguous
	else if (countFLeft>0 || countTLeft>0) side=1; // left
	else if (countFRight>0 || countTRight>0) side=-1; // right

    	// create the bond lines

    	double sz=lineSize;
	if (mol.atomMapNum(bf)>0 && mol.atomMapNum(bt)>0) sz*=5.0/3;
    	double[] oxy=orthogonalDelta(x1,y1,x2,y2,bondSep);
	

    	double ax1=x1,ay1=y1,ax2=x2,ay2=y2;
	double bx1=0,by1=0,bx2=0,by2=0;

	if (side==0)
	{
	    ax1=x1+0.5*oxy[0]; ay1=y1+0.5*oxy[1]; ax2=x2+0.5*oxy[0]; ay2=y2+0.5*oxy[1];
	    bx1=x1-0.5*oxy[0]; by1=y1-0.5*oxy[1]; bx2=x2-0.5*oxy[0]; by2=y2-0.5*oxy[1];
	}
	else if (side>0)
	{
	    bx1=x1+oxy[0]; by1=y1+oxy[1]; bx2=x2+oxy[0]; by2=y2+oxy[1];
	    if (nf.length>1 && pointText(bf-1)==null) {bx1+=oxy[1]; by1-=oxy[0];}
	    if (nt.length>1 && pointText(bt-1)==null) {bx2-=oxy[1]; by2+=oxy[0];}
	}
	else if (side<0)
	{
	    bx1=x1-oxy[0]; by1=y1-oxy[1]; bx2=x2-oxy[0]; by2=y2-oxy[1];
	    if (nf.length>1 && pointText(bf-1)==null) {bx1+=oxy[1]; by1-=oxy[0];}
	    if (nt.length>1 && pointText(bt-1)==null) {bx2-=oxy[1]; by2+=oxy[0];}
	}
	
    	double[] xy1=backOffAtom(bf-1,ax1,ay1,ax2,ay2);
	double[] xy2=backOffAtom(bt-1,ax2,ay2,ax1,ay1);
	ax1=xy1[0]; ay1=xy1[1]; ax2=xy2[0]; ay2=xy2[1];

    	xy1=backOffAtom(bf-1,bx1,by1,bx2,by2);
	xy2=backOffAtom(bt-1,bx2,by2,bx1,by1);
	bx1=xy1[0]; by1=xy1[1]; bx2=xy2[0]; by2=xy2[1];

	if (devRounding)
	{
	    // round the positions, then look for pixel-sized adjustments to B to find the best offset (if any) which honours the two lines
	    // being parallel with distance of 'bondSep'
	    
	    ax1=Math.round(ax1); ay1=Math.round(ay1); ax2=Math.round(ax2); ay2=Math.round(ay2);
	    bx1=Math.round(bx1); by1=Math.round(by1); bx2=Math.round(bx2); by2=Math.round(by2);
	    
	    int dx1=0,dy1=0,dx2=0,dy2=0;
	    double best=1E10,bondSepSq=Util.sqr(bondSep);
	    final int[] RDX1={0,-1,1,0,0,0,0,0,0},RDY1={0,0,0,-1,1,0,0,0,0},RDX2={0,0,0,0,0,-1,1,0,0},RDY2={0,0,0,0,0,0,0,-1,1};
	    boolean orthog=(Util.dblEqual(ax1,ax2) && Util.dblEqual(bx1,bx2)) ||
	    	    	   (Util.dblEqual(ay1,ay2) && Util.dblEqual(by1,by2));
	    if (!orthog) for (int n=0;n<9;n++)
	    {
	    	double ux1=bx1+RDX1[n],uy1=by1+RDY1[n],ux2=bx2+RDX2[n],uy2=by2+RDY2[n];
    	    	double ox=uy1-uy2,oy=ux2-ux1;
		double[] ixy1=lineIntersection(ux1,uy1,ux1+ox,uy1+oy,ax1,ay1,ax2,ay2);
		double[] ixy2=lineIntersection(ux2,uy2,ux2+ox,uy2+oy,ax1,ay1,ax2,ay2);
		double dsq1=Util.sqr(ixy1[0]-ux1)+Util.sqr(ixy1[1]-uy1);
		double dsq2=Util.sqr(ixy2[0]-ux2)+Util.sqr(ixy2[1]-uy2);
		double score=Math.abs(dsq1-bondSepSq)+Math.abs(dsq2-bondSepSq);
		if (score<best) 
		{
		    best=score;
		    dx1=RDX1[n];
		    dy1=RDY1[n];
		    dx2=RDX2[n];
		    dy2=RDY2[n];
		}
	    }
	    bx1+=dx1;
	    by1+=dy1;
	    bx2+=dx2;
	    by2+=dy2;
	}
	int scol = unselected_color;
	if(MainApplet.USER_SELECTION){
	    if(EditorPane.bondselection[N]){
		scol = MainApplet.BOND_SELECT_HTML_COLOR;
	    }
	}
	else
	{
	    if(MainApplet.SUPERUSER_SELECTION){/*give the molecule the param selected colours */
		if( MainApplet.ExternalBondSelection != null ){
		    for(int p = 0 ; p < (MainApplet.ExternalBondSelection).length; p++ ){
			if( N == MainApplet.ExternalBondSelection[p] ){
			    scol = MainApplet.SelectedBondColorInt[p] ;
			}
		    }
		}
	    }
	}
    	lines.add(new BLine(N,BLINE_NORMAL,ax1,ay1,ax2,ay2,sz,scol));
    	lines.add(new BLine(N,BLINE_NORMAL,bx1,by1,bx2,by2,sz,scol));

    	if (side==0 && !noshift && mol.atomRingBlock(bf)==0 && mol.atomRingBlock(bt)==0)
	{
	    if (pointText(bf-1)==null)
	    {
	    	adjustBondPosition(idxFLeft,bf,ax1,ay1);
	    	adjustBondPosition(idxFRight,bf,bx1,by1);
	    }
	    if (pointText(bt-1)==null)
	    {
	    	adjustBondPosition(idxTLeft,bt,ax2,ay2);
	    	adjustBondPosition(idxTRight,bt,bx2,by2);
	    }
	}
    }

    // returns true if, for an atom, all of its neighbours are not oriented in the given directory (180deg)
    private boolean quadrantOpen(int N,int DX,int DY)
    {
	int[] nbr=mol.atomAdjList(N+1);
	for (int n=0;n<nbr.length;n++)
	{
	    double ox=pointCX(nbr[n]-1)-pointCX(N),oy=pointCY(nbr[n]-1)-pointCY(N);
	    if (DX==1 && DY==0 && ox>0) return false;
	    if (DX==-1 && DY==0 && ox<0) return false;
	    if (DX==0 && DY==1 && oy>0) return false;
	    if (DX==0 && DY==-1 && oy<0) return false;
	}
	return true;
    }

    // for an atom centre, with a non-zero number of hydrogen labels required, in the direction indicated by (DX,DY), see if there is room
    // to fit the label, and if there is, stick it there and return true; special case: if (DX,DY) are both zero, then search around the
    // central position looking for the least bad position, and place it there; returns true if the label was placed
    private boolean placeHydrogen(int N,int HCount,int DX,int DY)
    {
    	final double KERN_CONST=1.1;
    
    	APoint a=points.get(N);
    
    	double[] wad=measure.measureText("H",a.fsz);
	
	double cx=a.cx+DX*(a.rw+0.5*wad[0])*KERN_CONST;
	double cy=a.cy+DY*(a.rh+0.5*(wad[1]+wad[2]))*KERN_CONST;
	double rw=0.5*wad[0],rh=0.5*wad[1];
	
	String nstr=HCount>1 ? String.valueOf(HCount) : null;
	double nsz=0.5*a.fsz;
	double[] nwad=nstr==null ? null : measure.measureText(nstr,nsz);
    	double nx=0,ny=0,nw=0,nh=0;
	if (nstr!=null)
	{
	    nw=0.5*nwad[0];
	    nh=0.5*nwad[1];
	}

    	// if this isn't fallback mode, stop if the position isn't free
	if (DX!=0 || DY!=0)
	{
	    if (boxOverlaps(cx-rw,cy-rh,2*rw,2*rh)) return false;
	    if (nstr!=null && DX==-1 && DY==0) cx-=nwad[0]*KERN_CONST;
	}
    	else
	{
	    double bestX=0,bestY=0,bestScore=0;
	    for (double th=0;th<2*Math.PI;th+=Math.PI/36) // 5 degree increments
	    {
	    	double tx=a.cx+a.rw*Math.cos(th),ty=a.cy+a.rh*Math.sin(th);
		if (tx>a.cx && tx-rw<a.cx+a.rw) tx=a.cx+a.rw+rw;
		if (tx<a.cx && tx+rw+nw*3>a.cx-a.rw) tx=a.cx-a.rw-rw-nw*2*KERN_CONST;
		if (ty>a.cy && ty-rh<a.cy+a.rh) ty=a.cy+a.rw+rw;
		if (ty<a.cy && ty+rh>a.cy-a.rh) ty=a.cy-a.rh-rh;
		double score=spatialCongestion(tx,ty)+(boxOverlaps(tx-rw,ty-rh,2*rw,2*rh) ? 1000 : 0);
		if (th==0 || score<bestScore) {bestScore=score; bestX=tx; bestY=ty;}
	    }
	    cx=bestX;
	    cy=bestY;
	}

	if (nstr!=null)
	{
	    nx=cx+rw+0.75*nwad[0];
	    ny=cy+0.75*rh;
	}
	
	APoint ah=new APoint();
	ah.anum=0;
	ah.text="H";
	ah.fsz=a.fsz;
	ah.bold=a.bold;
	ah.col=a.col;
	ah.cx=cx;
	ah.cy=cy;
	ah.rw=rw;
	ah.rh=rh;
	points.add(ah);
	
	if (nstr!=null)
	{
	    APoint an=new APoint();
	    an.anum=0;
	    an.text=nstr;
	    an.fsz=0.5*a.fsz;
	    an.bold=a.bold;
	    an.col=a.col;
	    an.cx=nx;
	    an.cy=ny;
	    an.rw=nw;
	    an.rh=nh;
	    points.add(an);
	}
	
	return true;
    }

    // considering a single-bond line from (bf,bt), make sure the endpoint (indicated by 'bt') is moved to the position (x,y)
    private void adjustBondPosition(int bf,int bt,double x,double y)
    {
    	for (int n=0;n<numLines();n++)
	{
	    BLine b=lines.get(n);
	    if (mol.bondOrder(b.bnum)!=1 && mol.bondType(b.bnum)!=Molecule.BONDTYPE_NORMAL) continue;

	    if (mol.bondFrom(b.bnum)==bf && mol.bondTo(b.bnum)==bt) {b.x2=x; b.y2=y;}
	    if (mol.bondFrom(b.bnum)==bt && mol.bondTo(b.bnum)==bf) {b.x1=x; b.y1=y;}
	}
    }

    // for the guideline index of a double bond, determines which side has weighting priority for the drawing of the bond; assumes a chain-
    // like bond (though it could still be in a large ring); a null/empty/ambiguous set implies that there is no priority, and that the bond
    // should not be drawn in a side-shifted manner...
    private int[] priorityDoubleSubstit(int N)
    {
    	int bf=mol.bondFrom(N),bt=mol.bondTo(N);
	int[] nf=mol.atomAdjList(bf),nt=mol.atomAdjList(bt);
    	double x1=pointCX(bf-1),y1=pointCY(bf-1),x2=pointCX(bt-1),y2=pointCY(bt-1);
	double dx=x2-x1,dy=y2-y1,btheta=Math.atan2(dy,dx);

	int idxFLeft=0,idxFRight=0,idxTLeft=0,idxTRight=0;

    	for (int n=0;n<nf.length;n++) if (nf[n]!=bt)
	{
    	    double theta=Util.angleDiff(Math.atan2(pointCY(nf[n]-1)-y1,pointCX(nf[n]-1)-x1),btheta);
	    if (theta>0) {if (idxFLeft!=0) return null; idxFLeft=nf[n];}
	    else {if (idxFRight!=0) return null; idxFRight=nf[n];}
	}
    	for (int n=0;n<nt.length;n++) if (nt[n]!=bf)
	{
    	    double theta=Util.angleDiff(Math.atan2(pointCY(nt[n]-1)-y2,pointCX(nt[n]-1)-x2),btheta);
	    if (theta>0) {if (idxTLeft!=0) return null; idxTLeft=nt[n];}
	    else {if (idxTRight!=0) return null; idxTRight=nt[n];}
	}
	
	int sumFrom=(idxFLeft>0 ? 1 : 0)+(idxFRight>0 ? 1 : 0),sumTo=(idxTLeft>0 ? 1 : 0)+(idxTRight>0 ? 1 : 0);
	
	if (sumFrom==1 && sumTo==0) return new int[]{idxFLeft>0 ? idxFLeft : idxFRight};
	if (sumFrom==0 && sumTo==1) return new int[]{idxTLeft>0 ? idxTLeft : idxTRight};
	if (sumFrom==1 && sumTo==1)
	{
	    // cis? if so, then side is obvious
	    if (idxFLeft>0 && idxTLeft>0) return new int[]{idxFLeft,idxTLeft};
	    if (idxFRight>0 && idxTRight>0) return new int[]{idxFRight,idxTRight};
	    
	    // trans? either is fine, so go with congestion
	    double[] oxy=orthogonalDelta(x1,y1,x2,y2,bondSep);
	    double congestLeft=spatialCongestion(0.5*(x1+x2)+oxy[0],0.5*(y1+y2)+oxy[1]);
	    double congestRight=spatialCongestion(0.5*(x1+x2)-oxy[0],0.5*(y1+y2)-oxy[1]);
	    if (congestLeft<congestRight) return new int[]{idxFLeft>0 ? idxFLeft : idxTLeft};
	    else return new int[]{idxFRight>0 ? idxFRight : idxTRight};
	}
	if (sumFrom==2 && sumTo==1)
	{
	    // side with the majority
	    if (idxTLeft==0) return new int[]{idxFRight,idxTRight};
	    else return new int[]{idxFLeft,idxTLeft};
	}
	if (sumFrom==1 && sumTo==2)
	{
	    // side with the majority
	    if (idxFLeft==0) return new int[]{idxFRight,idxTRight};
	    else return new int[]{idxFLeft,idxTLeft};
	}

    	return null;
    }
    
    // for the lines L1-->L2 and L3-->L4, calculate and return the intersection; (note lines, not line segments)
    private double[] lineIntersection(double x1,double y1,double x2,double y2,double x3,double y3,double x4,double y4)
    {
    	double u=((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3))
    	    	/((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
    	return new double[]{x1+u*(x2-x1),y1+u*(y2-y1)};
    }
    
    // for a specific location, returns a measure of how "congested" it is; lower values mean that the point is generally far away
    // from things
    private double spatialCongestion(double x,double y)
    {
    	double congest=0;
	for (int n=0;n<numPoints();n++) congest+=1/(Util.sqr(pointCX(n)-x)+Util.sqr(pointCY(n)-y));
	return congest;
    }

    // returns true if the indicated box intersects with any of the currently defined atom centres or bond lines
    private boolean boxOverlaps(double x,double y,double w,double h)
    {
    	Rectangle2D.Double box=new Rectangle2D.Double(x,y,w,h);
	for (int n=0;n<numPoints();n++)
	{
	    APoint a=points.get(n);
	    double aw=Math.max(a.rw,1),ah=Math.max(a.rh,1);
	    if (box.intersects(a.cx-aw,a.cy-ah,2*aw,2*ah)) return true;
	}
	for (int n=0;n<numLines();n++)
	{
	    BLine b=lines.get(n);
	    double dx=b.x2-b.x1,dy=b.y2-b.y1;
	    if (!box.intersects(Math.min(b.x1,b.x2)-1,Math.min(b.y1,b.y2)-1,Math.abs(dx)+2,Math.abs(dy)+2)) continue;
	    double stepsz=0.1*0.25*(w+h)/Math.sqrt(dx*dx+dy*dy);
	    for (double v=0.0;v<=1.0;v+=stepsz) if (box.contains(b.x1+v*dx,b.y1+v*dy)) return true;
	}
	return false;
    }
    
    // for a given piece of text, tries to find a position close to that which is given, which does not overlap with anything
    private double[] anchorAnnotation(double px,double py,double rw,double rh)
    {
	if (!boxOverlaps(px,py,rw,rh)) return new double[]{px,py}; // just in case it's blank...
	
	// do radial sweeps: take the best angle at the current extension, or if none, loop around with a bigger sweep
	double stepext=0.25*scale,ext=stepext;
	boolean lastChance=false;
	while (true)
	{
	    double bestX=0,bestY=0,bestScore=-1;
	    for (double th=0;th<2*Math.PI;th+=Math.PI/36) // 5 degree increments
	    {
	    	double tx=px+ext*Math.cos(th),ty=py+ext*Math.sin(th);
	    	if (!lastChance) if (boxOverlaps(tx-rw,ty-rh,2*rw,2*rh)) continue;
	    	double score=spatialCongestion(tx,ty);
		if (bestScore<0 || score<bestScore) {bestScore=score; bestX=tx; bestY=ty;}
	    }
	    if (bestScore>=0) return new double[]{bestX,bestY};
	    
	    // nothing flies, so increase the extent; if gone too far, wind it back in, and pick the best of the bad
	    ext+=stepext;
	    if (ext>3*scale) {ext=rw+rh; lastChance=true;}
	}
    }
}

