/
StandardBondGenerator.java
1798 lines (1507 loc) · 75.3 KB
/
StandardBondGenerator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (c) 2014 European Bioinformatics Institute (EMBL-EBI)
* John May <jwmay@users.sf.net>
* 2014 Mark B Vine (orcid:0000-0002-7794-0426)
*
* Contact: cdk-devel@lists.sourceforge.net
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or (at
* your option) any later version. All we ask is that proper credit is given
* for our work, which includes - but is not limited to - adding the above
* copyright notice to the beginning of your source code files, and to any
* copyright notice that you may distribute with programs based on this work.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.openscience.cdk.renderer.generators.standard;
import org.openscience.cdk.config.Elements;
import org.openscience.cdk.graph.Cycles;
import org.openscience.cdk.interfaces.IAtom;
import org.openscience.cdk.interfaces.IAtomContainer;
import org.openscience.cdk.interfaces.IBond;
import org.openscience.cdk.interfaces.IPseudoAtom;
import org.openscience.cdk.interfaces.IRingSet;
import org.openscience.cdk.renderer.RendererModel;
import org.openscience.cdk.renderer.elements.ElementGroup;
import org.openscience.cdk.renderer.elements.GeneralPath;
import org.openscience.cdk.renderer.elements.IRenderingElement;
import org.openscience.cdk.renderer.elements.LineElement;
import org.openscience.cdk.renderer.elements.MarkedElement;
import org.openscience.cdk.renderer.elements.OvalElement;
import org.openscience.cdk.renderer.elements.path.Close;
import org.openscience.cdk.renderer.elements.path.CubicTo;
import org.openscience.cdk.renderer.elements.path.LineTo;
import org.openscience.cdk.renderer.elements.path.MoveTo;
import org.openscience.cdk.renderer.elements.path.PathElement;
import org.openscience.cdk.renderer.generators.BasicSceneGenerator;
import org.openscience.cdk.tools.ILoggingTool;
import org.openscience.cdk.tools.LoggingToolFactory;
import org.openscience.cdk.tools.manipulator.AtomContainerSetManipulator;
import javax.vecmath.Point2d;
import javax.vecmath.Tuple2d;
import javax.vecmath.Vector2d;
import java.awt.Color;
import java.awt.Font;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.openscience.cdk.interfaces.IBond.Order.SINGLE;
import static org.openscience.cdk.interfaces.IBond.Order.UNSET;
import static org.openscience.cdk.interfaces.IBond.Stereo.NONE;
import static org.openscience.cdk.renderer.generators.BasicSceneGenerator.BondLength;
import static org.openscience.cdk.renderer.generators.standard.StandardGenerator.*;
import static org.openscience.cdk.renderer.generators.standard.VecmathUtil.*;
/**
* Generates {@link IRenderingElement}s for bonds. The generator is internal and called by the
* {@link org.openscience.cdk.renderer.generators.standard.StandardGenerator}. A new bond generator
* is required for each container instance.
*
* The bonds generated are: <ul> <li> {@link #generateSingleBond} - delegates to one of the
* following types: <ul> <li>{@link #generatePlainSingleBond} - single line between two atoms</li>
* <li>{@link #generateBoldWedgeBond} - wedged up stereo </li> <li>{@link #generateHashedWedgeBond}
* - wedged down stereo bond </li> <li>{@link #generateWavyBond} - up or down bond </li> </ul> </li>
* <li> {@link #generateDoubleBond} - delegates to one of the following types: <ul> <li>{@link
* #generateOffsetDoubleBond} - one line rests on the center between the atoms</li> <li>{@link
* #generateCenteredDoubleBond} - both lines rest equidistant from the center between the atoms</li>
* <li>{@link #generateCrossedDoubleBond} - unknown double stereochemistry </li> </ul> </li>
* <li>{@link #generateTripleBond} - composes a single and double bond</li> <li>{@link
* #generateDashedBond} - the unknown bond type</li> </ul>
*
* @author John May
*/
final class StandardBondGenerator {
private final IAtomContainer container;
private final AtomSymbol[] symbols;
private final RendererModel parameters;
private final StandardDonutGenerator donutGenerator;
// logging
private final ILoggingTool logger = LoggingToolFactory.createLoggingTool(getClass());
// indexes of atoms and rings
private final Map<IAtom, Integer> atomIndexMap = new HashMap<>();
private final Map<IBond, IAtomContainer> ringMap;
// parameters
private final double scale;
private final double stroke;
private final double separation;
private final double backOff;
private final double wedgeWidth;
private final double hashSpacing;
private final double waveSpacing;
private final Color foreground, annotationColor;
private final boolean fancyBoldWedges, fancyHashedWedges;
private final double annotationDistance, annotationScale;
private final Font font;
private final ElementGroup annotations;
private final boolean forceDelocalised;
/**
* Create a new standard bond generator for the provided structure (container) with the laid out
* atom symbols. The parameters of the bond generation are also provided and the scaled 'stroke'
* width which is used to scale all other parameters.
*
* @param container structure representation
* @param symbols generated atom symbols
* @param parameters rendering options
* @param stroke scaled stroke width
*/
private StandardBondGenerator(IAtomContainer container,
AtomSymbol[] symbols,
RendererModel parameters,
ElementGroup annotations,
Font font,
double stroke,
StandardDonutGenerator donutGen) {
this.container = container;
this.symbols = symbols;
this.parameters = parameters;
this.annotations = annotations;
this.donutGenerator = donutGen;
// index atoms and rings
for (int i = 0; i < container.getAtomCount(); i++)
atomIndexMap.put(container.getAtom(i), i);
ringMap = ringPreferenceMap(container, donutGenerator.smallest);
// set parameters
this.scale = parameters.get(BasicSceneGenerator.Scale.class);
this.stroke = stroke;
double length = parameters.get(BondLength.class) / scale;
this.separation = (parameters.get(BondSeparation.class) * parameters.get(BondLength.class)) / scale;
this.backOff = parameters.get(StandardGenerator.SymbolMarginRatio.class) * stroke;
this.wedgeWidth = parameters.get(StandardGenerator.WedgeRatio.class) * stroke;
this.hashSpacing = parameters.get(HashSpacing.class) / scale;
this.waveSpacing = parameters.get(WaveSpacing.class) / scale;
this.fancyBoldWedges = parameters.get(StandardGenerator.FancyBoldWedges.class);
this.fancyHashedWedges = parameters.get(StandardGenerator.FancyHashedWedges.class);
this.annotationDistance = parameters.get(StandardGenerator.AnnotationDistance.class)
* (parameters.get(BondLength.class) / scale);
this.annotationScale = (1 / scale) * parameters.get(StandardGenerator.AnnotationFontScale.class);
this.annotationColor = parameters.get(StandardGenerator.AnnotationColor.class);
this.forceDelocalised = parameters.get(StandardGenerator.ForceDelocalisedBondDisplay.class);
this.font = font;
// foreground is based on the carbon color
this.foreground = parameters.get(StandardGenerator.AtomColor.class).getAtomColor(
container.getBuilder().newInstance(IAtom.class, "C"));
}
/**
* Generates bond elements for the provided structure (container) with the laid out atom
* symbols. The parameters of the bond generation are also provided and the scaled 'stroke'
* width which is used to scale all other parameters.
*
* @param container structure representation
* @param symbols generated atom symbols
* @param parameters rendering options
* @param stroke scaled stroke width
*/
static IRenderingElement[] generateBonds(IAtomContainer container,
AtomSymbol[] symbols,
RendererModel parameters,
double stroke,
Font font,
ElementGroup annotations,
StandardDonutGenerator donutGen) {
StandardBondGenerator bondGenerator;
bondGenerator = new StandardBondGenerator(container, symbols,
parameters, annotations,
font, stroke, donutGen);
IRenderingElement[] elements = new IRenderingElement[container.getBondCount()];
for (int i = 0; i < container.getBondCount(); i++) {
final IBond bond = container.getBond(i);
if (!StandardGenerator.isHidden(bond)) {
elements[i] = bondGenerator.generate(bond);
}
}
return elements;
}
/**
* Generate a rendering element for a given bond.
*
* @param bond a bond
* @return rendering element
*/
IRenderingElement generate(IBond bond) {
final IAtom atom1 = bond.getBegin();
final IAtom atom2 = bond.getEnd();
IBond.Order order = bond.getOrder();
if (order == null) order = IBond.Order.UNSET;
IRenderingElement elem;
switch (order) {
case SINGLE:
// TODO check small ring!
if (bond.isAromatic()) {
if (donutGenerator.isDelocalised(bond))
elem = generateSingleBond(bond, atom1, atom2);
else if (forceDelocalised && bond.isInRing())
elem = generateDoubleBond(bond, forceDelocalised);
else
elem = generateSingleBond(bond, atom1, atom2);
} else
elem = generateSingleBond(bond, atom1, atom2);
break;
case DOUBLE:
if (bond.isAromatic()) {
if (donutGenerator.isDelocalised(bond))
elem = generateSingleBond(bond, atom1, atom2);
else
elem = generateDoubleBond(bond, forceDelocalised);
} else
elem = generateDoubleBond(bond, false);
break;
case TRIPLE:
elem = generateTripleBond(bond, atom1, atom2);
break;
default:
if (bond.isAromatic() && order == UNSET) {
if (donutGenerator.isDelocalised(bond))
elem = generateSingleBond(bond, atom1, atom2);
else
elem = generateDoubleBond(bond, true);
} else {
// bond orders > 3 not supported
elem = generateDashedBond(atom1, atom2);
}
break;
}
// attachment point drawing, in future we could also draw the attach point
// number, typically within a circle
if (isAttachPoint(atom1)) {
ElementGroup elemGrp = new ElementGroup();
elemGrp.add(elem);
elemGrp.add(generateAttachPoint(atom1, bond));
elem = elemGrp;
}
if (isAttachPoint(atom2)) {
ElementGroup elemGrp = new ElementGroup();
elemGrp.add(elem);
elemGrp.add(generateAttachPoint(atom2, bond));
elem = elemGrp;
}
return elem;
}
/**
* Generate a rendering element for single bond with the provided stereo type.
*
* @param bond the bond to render
* @param from an atom
* @param to another atom
* @return bond rendering element
*/
private IRenderingElement generateSingleBond(IBond bond, IAtom from, IAtom to) {
// add annotation label
String label = StandardGenerator.getAnnotationLabel(bond);
if (label != null) addAnnotation(from, to, label);
IBond.Display display = bond.getDisplay();
if (display == null || display == IBond.Display.Solid)
return generatePlainSingleBond(from, to);
List<IBond> fromBonds = container.getConnectedBondsList(from);
List<IBond> toBonds = container.getConnectedBondsList(to);
fromBonds.remove(bond);
toBonds.remove(bond);
switch (display) {
case WedgedHashBegin:
return generateHashedWedgeBond(from, to, toBonds);
case WedgedHashEnd:
return generateHashedWedgeBond(to, from, fromBonds);
case WedgeBegin:
return generateBoldWedgeBond(from, to, toBonds);
case WedgeEnd:
return generateBoldWedgeBond(to, from, fromBonds);
case Wavy:
return generateWavyBond(from, to);
case Dash:
return generateDashedBond(from, to);
case ArrowEnd:
return generateArrowBond(from, to);
case ArrowBeg:
return generateArrowBond(to, from);
case Bold:
return generateBoldBond(from, to, fromBonds, toBonds);
case Hash:
return generateHashBond(from, to, fromBonds, toBonds);
case Dot:
return generateDotBond(from, to);
default:
logger.warn("Unknown single bond display=", display, " is not displayed");
return generatePlainSingleBond(from, to);
}
}
/**
* Generate a plain single bond between two atoms accounting for displayed symbols.
*
* @param from one atom
* @param to the other atom
* @return rendering element
*/
IRenderingElement generatePlainSingleBond(final IAtom from, final IAtom to) {
return newLineElement(backOffPoint(from, to), backOffPoint(to, from));
}
/**
* Generates a rendering element for a bold wedge bond (i.e. up) from one atom to another.
*
* @param from narrow end of the wedge
* @param to bold end of the wedge
* @param toBonds bonds connected to the 'to atom'
* @return the rendering element
*/
IRenderingElement generateBoldWedgeBond(IAtom from, IAtom to, List<IBond> toBonds) {
final Point2d fromPoint = from.getPoint2d();
final Point2d toPoint = to.getPoint2d();
final Point2d fromBackOffPoint = backOffPoint(from, to);
final Point2d toBackOffPoint = backOffPoint(to, from);
final Vector2d unit = newUnitVector(fromPoint, toPoint);
final Vector2d perpendicular = newPerpendicularVector(unit);
final double halfNarrowEnd = stroke / 2;
final double halfWideEnd = wedgeWidth / 2;
final double opposite = halfWideEnd - halfNarrowEnd;
final double adjacent = fromPoint.distance(toPoint);
final double fromOffset = halfNarrowEnd + opposite / adjacent * fromBackOffPoint.distance(fromPoint);
final double toOffset = halfNarrowEnd + opposite / adjacent * toBackOffPoint.distance(fromPoint);
// four points of the trapezoid
Tuple2d a = sum(fromBackOffPoint, scale(perpendicular, fromOffset));
Tuple2d b = sum(fromBackOffPoint, scale(perpendicular, -fromOffset));
Tuple2d c = sum(toBackOffPoint, scale(perpendicular, -toOffset));
Tuple2d e = toBackOffPoint;
Tuple2d d = sum(toBackOffPoint, scale(perpendicular, toOffset));
// don't adjust wedge if the angle is shallow than this amount
final double threshold = Math.toRadians(15);
// if the symbol at the wide end of the wedge is not displayed, we can improve
// the aesthetics by adjusting the endpoints based on connected bond angles.
if (fancyBoldWedges && !hasDisplayedSymbol(to)) {
// slanted wedge
if (toBonds.size() == 1) {
final IBond toBondNeighbor = toBonds.get(0);
final IAtom toNeighbor = toBondNeighbor.getOther(to);
Vector2d refVector = newUnitVector(toPoint, toNeighbor.getPoint2d());
boolean wideToWide = false;
// special case when wedge bonds are in a bridged ring, wide-to-wide end we
// don't want to slant as normal but rather butt up against each wind end
if (atWideEndOfWedge(to, toBondNeighbor)) {
refVector = sum(refVector, negate(unit));
wideToWide = true;
}
final double theta = refVector.angle(unit);
if (theta > threshold && theta + threshold + threshold < Math.PI) {
c = intersection(b, newUnitVector(b, c), toPoint, refVector);
d = intersection(a, newUnitVector(a, d), toPoint, refVector);
// the points c, d, and e lie on the center point of the line between
// the 'to' and 'toNeighbor'. Since the bond is drawn with a stroke and
// has a thickness we need to move these points slightly to be flush
// with the bond depiction, we only do this if the bond is not
// wide-on-wide with another bold wedge
if (!wideToWide) {
final double nudge = (stroke / 2) / Math.sin(theta);
c = sum(c, scale(unit, nudge));
d = sum(d, scale(unit, nudge));
e = sum(e, scale(unit, nudge));
}
}
}
// bifurcated (forked) wedge
else if (toBonds.size() > 1) {
Vector2d refVectorA = getNearestVector(perpendicular, to, toBonds);
Vector2d refVectorB = getNearestVector(negate(perpendicular), to, toBonds);
if (refVectorB.angle(unit) > threshold) c = intersection(b, newUnitVector(b, c), toPoint, refVectorB);
if (refVectorA.angle(unit) > threshold) d = intersection(a, newUnitVector(a, d), toPoint, refVectorA);
}
}
return new GeneralPath(Arrays.asList(new MoveTo(new Point2d(a)), new LineTo(new Point2d(b)), new LineTo(
new Point2d(c)), new LineTo(new Point2d(e)), new LineTo(new Point2d(d)), new Close()), foreground);
}
/**
* Generates a rendering element for a hashed wedge bond (i.e. down) from one atom to another.
*
* @param from narrow end of the wedge
* @param to bold end of the wedge
* @param toBonds bonds connected to
* @return the rendering element
*/
IRenderingElement generateHashedWedgeBond(IAtom from, IAtom to, List<IBond> toBonds) {
final Point2d fromPoint = from.getPoint2d();
final Point2d toPoint = to.getPoint2d();
final Point2d fromBackOffPoint = backOffPoint(from, to);
final Point2d toBackOffPoint = backOffPoint(to, from);
final Vector2d unit = newUnitVector(fromPoint, toPoint);
final Vector2d perpendicular = newPerpendicularVector(unit);
final double halfNarrowEnd = stroke / 2;
final double halfWideEnd = wedgeWidth / 2;
final double opposite = halfWideEnd - halfNarrowEnd;
double adjacent = fromPoint.distance(toPoint);
final int nSections = (int) (adjacent / hashSpacing);
final double step = adjacent / (nSections - 1);
final ElementGroup group = new ElementGroup();
final double start = hasDisplayedSymbol(from) ? fromPoint.distance(fromBackOffPoint) : Double.NEGATIVE_INFINITY;
final double end = hasDisplayedSymbol(to) ? fromPoint.distance(toBackOffPoint) : Double.POSITIVE_INFINITY;
// don't adjust wedge if the angle is shallow than this amount
final double threshold = Math.toRadians(35);
Vector2d hatchAngle = perpendicular;
// fancy hashed wedges with slanted hatch sections aligned with neighboring bonds
if (canDrawFancyHashedWedge(to, toBonds, adjacent)) {
final IBond toBondNeighbor = toBonds.get(0);
final IAtom toNeighbor = toBondNeighbor.getOther(to);
Vector2d refVector = newUnitVector(toPoint, toNeighbor.getPoint2d());
// special case when wedge bonds are in a bridged ring, wide-to-wide end we
// don't want to slant as normal but rather butt up against each wind end
if (atWideEndOfWedge(to, toBondNeighbor)) {
refVector = sum(refVector, negate(unit));
refVector.normalize();
}
// only slant if the angle isn't shallow
double theta = refVector.angle(unit);
if (theta > threshold && theta + threshold + threshold < Math.PI) {
hatchAngle = refVector;
}
}
for (int i = 0; i < nSections; i++) {
final double distance = i * step;
// don't draw if we're within an atom symbol
if (distance < start || distance > end) continue;
final double offset = halfNarrowEnd + opposite / adjacent * distance;
Tuple2d interval = sum(fromPoint, scale(unit, distance));
group.add(newLineElement(sum(interval, scale(hatchAngle, offset)),
sum(interval, scale(hatchAngle, -offset))));
}
return group;
}
/**
* A fancy hashed wedge can be drawn if the following conditions are met:
* (1) {@link StandardGenerator.FancyHashedWedges} is enabled
* (2) Bond is of 'normal' length
* (3) The atom at the wide has one other neighbor and no symbol displayed
*
* @param to the target atom
* @param toBonds bonds to the target atom (excluding the hashed wedge)
* @param length the length of the bond (unscaled)
* @return a fancy hashed wedge can be rendered
*/
private boolean canDrawFancyHashedWedge(IAtom to, List<IBond> toBonds, double length) {
// a bond is long if is more than 4 units larger that the desired 'BondLength'
final boolean longBond = (length * scale) - parameters.get(BondLength.class) > 4;
return fancyHashedWedges && !longBond && !hasDisplayedSymbol(to) && toBonds.size() == 1;
}
/**
* Generates a wavy bond (up or down stereo) between two atoms.
*
* @param from drawn from this atom
* @param to drawn to this atom
* @return generated rendering element
*/
IRenderingElement generateWavyBond(final IAtom from, final IAtom to) {
final Point2d fromPoint = from.getPoint2d();
final Point2d toPoint = to.getPoint2d();
final Point2d fromBackOffPoint = backOffPoint(from, to);
final Point2d toBackOffPoint = backOffPoint(to, from);
final Vector2d unit = newUnitVector(fromPoint, toPoint);
final Vector2d perpendicular = newPerpendicularVector(unit);
final double length = fromPoint.distance(toPoint);
// 2 times the number of wave sections because each semi circle is drawn with two parts
final int nCurves = 2 * (int) (length / waveSpacing);
final double step = length / nCurves;
Vector2d peak = scale(perpendicular, step);
boolean started = false;
final double start = fromPoint.equals(fromBackOffPoint) ? Double.MIN_VALUE : fromPoint
.distance(fromBackOffPoint);
final double end = toPoint.equals(toBackOffPoint) ? Double.MAX_VALUE : fromPoint.distance(toBackOffPoint);
List<PathElement> path = new ArrayList<>();
if (start == Double.MIN_VALUE) {
path.add(new MoveTo(fromPoint.x, fromPoint.y));
started = true;
}
// the wavy bond is drawn using Bezier curves, removing the control points each
// first 'endPoint' of the iteration forms a zig-zag pattern. The second 'endPoint'
// lies on the central line between the atoms.
// the following may help to visualise what we're doing,
// s = start (could be any end point)
// e = end point
// cp = control points 1 and 2
//
// cp2 e cp1 cp2 e cp1
// cp1 cp2 cp1 cp2
// s ---------- e ----------- e ----------- e ------------ center line
// cp1 cp2 cp1
// cp2 e cp1 cp2 e
// | |
// --------------
// one iteration
//
// | |
// -------
// one curveTo / 'step' distance
// for the back off on atom symbols, the start position is the first end point after
// the backed off point. Similarly, the curve is only drawn if the end point is
// before the 'toBackOffPoint'
for (int i = 1; i < nCurves; i += 2) {
peak = negate(peak); // alternate wave side
// curving away from the center line
{
double dist = i * step;
if (dist >= start && dist <= end) {
// first end point
final Tuple2d endPoint = sum(sum(fromPoint, scale(unit, dist)), peak);
if (started) {
final Tuple2d controlPoint1 = sum(sum(fromPoint, scale(unit, (i - 1) * step)), scale(peak, 0.5));
final Tuple2d controlPoint2 = sum(sum(fromPoint, scale(unit, (i - 0.5) * step)), peak);
path.add(new CubicTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y,
endPoint.x, endPoint.y));
} else {
path.add(new MoveTo(endPoint.x, endPoint.y));
started = true;
}
}
}
// curving towards the center line
{
double dist = (i + 1) * step;
if (dist >= start && dist <= end) {
// second end point
final Tuple2d endPoint = sum(fromPoint, scale(unit, dist));
if (started) {
final Tuple2d controlPoint1 = sum(sum(fromPoint, scale(unit, (i + 0.5) * step)), peak);
final Tuple2d controlPoint2 = sum(sum(fromPoint, scale(unit, dist)), scale(peak, 0.5));
path.add(new CubicTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y,
endPoint.x, endPoint.y));
} else {
path.add(new MoveTo(endPoint.x, endPoint.y));
started = true;
}
}
}
}
return new GeneralPath(path, foreground).outline(stroke);
}
/**
* Generates a double bond rendering element by deciding how best to display it.
*
* @param bond the bond to render
* @param arom the second line should be dashed
* @return rendering element
*/
private IRenderingElement generateDoubleBond(IBond bond, boolean arom) {
final boolean cyclic = ringMap.containsKey(bond);
// select offset bonds from either the preferred ring or the whole structure
final IAtomContainer refContainer = cyclic ? ringMap.get(bond) : container;
final int length = refContainer.getAtomCount();
final int index1 = refContainer.indexOf(bond.getBegin());
final int index2 = refContainer.indexOf(bond.getEnd());
// if the bond is in a cycle we are using ring bonds to determine offset, since rings
// have been normalised and ordered to wind anti-clockwise we want to get the atoms
// in the order they are in the ring.
final boolean outOfOrder = cyclic && index1 == (index2 + 1) % length;
final IAtom atom1 = outOfOrder ? bond.getEnd() : bond.getBegin();
final IAtom atom2 = outOfOrder ? bond.getBegin() : bond.getEnd();
if (IBond.Stereo.E_OR_Z.equals(bond.getStereo())) return generateCrossedDoubleBond(atom1, atom2);
final List<IBond> atom1Bonds = refContainer.getConnectedBondsList(atom1);
final List<IBond> atom2Bonds = refContainer.getConnectedBondsList(atom2);
atom1Bonds.remove(bond);
atom2Bonds.remove(bond);
if (cyclic) {
// get the winding relative to the ring
final int wind1 = winding(atom1Bonds.get(0), bond);
final int wind2 = winding(bond, atom2Bonds.get(0));
if (wind1 > 0) {
return generateOffsetDoubleBond(bond, atom1, atom2, atom1Bonds.get(0), atom2Bonds, arom);
} else if (wind2 > 0) {
return generateOffsetDoubleBond(bond, atom2, atom1, atom2Bonds.get(0), atom1Bonds, arom);
} else {
// special case, offset line is drawn on the opposite side for
// when concave in macro cycle
//
// ---
// a --- b
// / \
// -- x x --
return generateOffsetDoubleBond(bond, atom1, atom2, atom1Bonds.get(0), atom2Bonds, true, arom);
}
} else if (!(hasDisplayedSymbol(atom1) && hasDisplayedSymbol(atom2))) {
if (atom1Bonds.size() == 1 && atom2Bonds.isEmpty())
return generateOffsetDoubleBond(bond, atom1, atom2, atom1Bonds.get(0), atom2Bonds, arom);
else if (atom2Bonds.size() == 1 && atom1Bonds.isEmpty())
return generateOffsetDoubleBond(bond, atom2, atom1, atom2Bonds.get(0), atom1Bonds, arom);
else if (specialOffsetBondNextToWedge(atom1, atom1Bonds))
return generateOffsetDoubleBond(bond, atom1, atom2, selectPlainSingleBond(atom1Bonds), atom2Bonds, arom);
else if (specialOffsetBondNextToWedge(atom2, atom2Bonds))
return generateOffsetDoubleBond(bond, atom2, atom1, selectPlainSingleBond(atom2Bonds), atom1Bonds, arom);
else if (atom1Bonds.size() == 1)
return generateOffsetDoubleBond(bond, atom1, atom2, atom1Bonds.get(0), atom2Bonds, arom);
else if (atom2Bonds.size() == 1)
return generateOffsetDoubleBond(bond, atom2, atom1, atom2Bonds.get(0), atom1Bonds, arom);
else if (selectUnsetAromBond(atom1Bonds) != null)
return generateOffsetDoubleBond(bond, atom1, atom2, selectUnsetAromBond(atom1Bonds), atom2Bonds, arom);
else if (selectUnsetAromBond(atom2Bonds) != null)
return generateOffsetDoubleBond(bond, atom2, atom1, selectUnsetAromBond(atom2Bonds), atom1Bonds, arom);
else
return generateCenteredDoubleBond(bond, atom1, atom2, atom1Bonds, atom2Bonds);
} else {
if (arom) {
return generateDashedBond(atom1, atom2);
} else {
return generateCenteredDoubleBond(bond, atom1, atom2, atom1Bonds, atom2Bonds);
}
}
}
/**
* Special condition for drawing offset bonds. If the double bond is adjacent to two bonds
* and one of those bonds is wedge (with this atom at the wide end) and the other is plain
* single bond, we can improve aesthetics by offsetting the double bond.
*
* @param atom an atom
* @param bonds bonds connected to 'atom'
* @return special case
*/
private boolean specialOffsetBondNextToWedge(IAtom atom, List<IBond> bonds) {
if (bonds.size() != 2) return false;
if (atWideEndOfWedge(atom, bonds.get(0)) && isPlainBond(bonds.get(1))) return true;
if (atWideEndOfWedge(atom, bonds.get(1)) && isPlainBond(bonds.get(0))) return true;
return false;
}
/**
* Select a plain bond from a list of bonds. If no bond was found, the first
* is returned.
*
* @param bonds list of bonds
* @return a plain bond
* @see #isPlainBond(org.openscience.cdk.interfaces.IBond)
*/
private IBond selectPlainSingleBond(List<IBond> bonds) {
for (IBond bond : bonds) {
if (isPlainBond(bond)) return bond;
}
return bonds.get(0);
}
private IBond selectUnsetAromBond(List<IBond> bonds) {
for (IBond bond : bonds) {
if (bond.isAromatic() && bond.getOrder() == UNSET) return bond;
}
return null;
}
/**
* A plain bond is a single bond with no stereochemistry type.
*
* @param bond the bond to check
* @return the bond is plain
*/
private static boolean isPlainBond(IBond bond) {
return SINGLE.equals(bond.getOrder()) && (bond.getStereo() == null || bond.getStereo() == NONE);
}
/**
* Check if the provided bond is a wedge (bold or hashed) and whether the atom is at the wide
* end.
*
* @param atom atom to check
* @param bond bond to check
* @return the atom is at the wide end of the wedge in the provided bond
*/
private boolean atWideEndOfWedge(final IAtom atom, final IBond bond) {
if (bond.getStereo() == null) return false;
switch (bond.getDisplay()) {
case Bold:
case Hash:
return true;
case WedgeBegin:
case WedgedHashBegin:
return bond.getEnd().equals(atom);
case WedgeEnd:
case WedgedHashEnd:
return bond.getBegin().equals(atom);
default:
return false;
}
}
/**
* Displays an offset double bond as per the IUPAC recomendation (GR-1.10) {@cdk.cite
* Brecher08}. An offset bond has one line drawn between the two atoms and other draw to one
* side. The side is determined by the 'atom1Bond' parameter. The first atom should not have a
* displayed symbol.
*
* @param atom1 first atom
* @param atom2 second atom
* @param atom1Bond the reference bond used to decide which side the bond is offset
* @param atom2Bonds the bonds connected to atom 2
* @return the rendered bond element
*/
private IRenderingElement generateOffsetDoubleBond(IBond bond, IAtom atom1, IAtom atom2, IBond atom1Bond,
List<IBond> atom2Bonds, boolean dashed) {
return generateOffsetDoubleBond(bond, atom1, atom2, atom1Bond, atom2Bonds, false, dashed);
}
/**
* Displays an offset double bond as per the IUPAC recomendation (GR-1.10) {@cdk.cite
* Brecher08}. An offset bond has one line drawn between the two atoms and other draw to one
* side. The side is determined by the 'atom1Bond' parameter. The first atom should not have a
* displayed symbol.
*
* @param atom1 first atom
* @param atom2 second atom
* @param atom1Bond the reference bond used to decide which side the bond is offset
* @param atom2Bonds the bonds connected to atom 2
* @param invert invert the offset (i.e. opposite to reference bond)
* @return the rendered bond element
*/
private IRenderingElement generateOffsetDoubleBond(IBond bond, IAtom atom1, IAtom atom2, IBond atom1Bond,
List<IBond> atom2Bonds, boolean invert, boolean dashed) {
assert atom1Bond != null;
final Point2d atom1Point = atom1.getPoint2d();
final Point2d atom2Point = atom2.getPoint2d();
final Point2d atom1BackOffPoint = backOffPoint(atom1, atom2);
final Point2d atom2BackOffPoint = backOffPoint(atom2, atom1);
final Vector2d unit = newUnitVector(atom1Point, atom2Point);
Vector2d perpendicular = newPerpendicularVector(unit);
final Vector2d reference = newUnitVector(atom1.getPoint2d(), atom1Bond.getOther(atom1).getPoint2d());
// there are two perpendicular vectors, this check ensures we have one on the same side as
// the reference
if (reference.dot(perpendicular) < 0) perpendicular = negate(perpendicular);
// caller requested inverted drawing
if (invert) perpendicular = negate(perpendicular);
// when the symbol is terminal, we move it such that it is between the two lines
if (atom2Bonds.isEmpty() && hasDisplayedSymbol(atom2)) {
final int atom2index = atomIndexMap.get(atom2);
final Tuple2d nudge = scale(perpendicular, separation / 2);
symbols[atom2index] = symbols[atom2index].translate(nudge.x, nudge.y);
}
// the offset line isn't drawn the full length and is backed off more depending on the
// angle of adjacent bonds, see GR-1.10 in the IUPAC recommendations
double atom1Offset = 0;
double atom2Offset = 0;
if (dashed || !hasDisplayedSymbol(atom1)) {
atom1Offset = adjacentLength(sum(reference, unit), perpendicular, separation);
}
// reference bond may be on the other side (invert specified) - the offset needs negating
if (reference.dot(perpendicular) < 0) atom1Offset = -atom1Offset;
// the second atom may have zero or more bonds which we can use to get the offset
// we find the one which is closest to the perpendicular vector
if (!atom2Bonds.isEmpty() && (dashed || !hasDisplayedSymbol(atom2))) {
Vector2d closest = getNearestVector(perpendicular, atom2, atom2Bonds);
atom2Offset = adjacentLength(sum(closest, negate(unit)), perpendicular, separation);
// closest bond may still be on the other side, if so the offset needs
// negating
if (closest.dot(perpendicular) < 0) atom2Offset = -atom2Offset;
}
final double halfBondLength = atom1Point.distance(atom2BackOffPoint) / 2;
if (atom1Offset > halfBondLength || atom1Offset < 0) atom1Offset = 0;
if (atom2Offset > halfBondLength || atom2Offset < 0) atom2Offset = 0;
final ElementGroup group = new ElementGroup();
// first of offset double bond may have some style
switch (bond.getDisplay()) {
case Bold:
group.add(generateBoldBond(atom1, atom2,
Collections.singletonList(atom1Bond), atom2Bonds));
break;
case Hash:
group.add(generateHashBond(atom1, atom2,
Collections.singletonList(atom1Bond), atom2Bonds));
break;
case Dash:
group.add(generateDashedBond(atom1, atom2));
break;
case Dot:
group.add(generateDashedBond(atom1, atom2));
break;
default: // solid
group.add(newLineElement(atom1BackOffPoint, atom2BackOffPoint));
break;
}
if (dashed) {
Point2d beg = new Point2d(sum(atom1Point, scale(perpendicular, separation)));
Point2d end = new Point2d(sum(atom2Point, scale(perpendicular, separation)));
group.add(generateDashedBond(beg, end,
atom1Offset,
beg.distance(end) - atom2Offset));
} else {
Point2d beg = new Point2d(sum(atom1BackOffPoint, scale(perpendicular, separation)));
Point2d end = new Point2d(sum(atom2BackOffPoint, scale(perpendicular, separation)));
group.add(newLineElement(sum(beg, scale(unit, atom1Offset)),
sum(end, scale(unit, -atom2Offset))));
}
// add annotation label on the opposite side
String label = StandardGenerator.getAnnotationLabel(bond);
if (label != null) addAnnotation(atom1, atom2, label, VecmathUtil.negate(perpendicular));
return group;
}
/**
* Generates a centered double bond. Here the lines are depicted each side and equidistant from
* the line that travel through the two atoms.
*
* @param atom1 an atom
* @param atom2 the other atom
* @param atom1Bonds bonds to the first atom (excluding that being rendered)
* @param atom2Bonds bonds to the second atom (excluding that being rendered)
* @return the rendering element
*/
private IRenderingElement generateCenteredDoubleBond(IBond bond, IAtom atom1, IAtom atom2, List<IBond> atom1Bonds,
List<IBond> atom2Bonds) {
final Point2d atom1BackOffPoint = backOffPoint(atom1, atom2);
final Point2d atom2BackOffPoint = backOffPoint(atom2, atom1);
final Vector2d unit = newUnitVector(atom1BackOffPoint, atom2BackOffPoint);
final Vector2d perpendicular1 = newPerpendicularVector(unit);
final Vector2d perpendicular2 = negate(perpendicular1);
final double halfBondLength = atom1BackOffPoint.distance(atom2BackOffPoint) / 2;
final double halfSeparation = separation / 2;
ElementGroup group = new ElementGroup();
Tuple2d line1Atom1Point = sum(atom1BackOffPoint, scale(perpendicular1, halfSeparation));
Tuple2d line1Atom2Point = sum(atom2BackOffPoint, scale(perpendicular1, halfSeparation));
Tuple2d line2Atom1Point = sum(atom1BackOffPoint, scale(perpendicular2, halfSeparation));
Tuple2d line2Atom2Point = sum(atom2BackOffPoint, scale(perpendicular2, halfSeparation));
// adjust atom 1 lines to be flush with adjacent bonds
if (!hasDisplayedSymbol(atom1) && atom1Bonds.size() > 1) {
Vector2d nearest1 = getNearestVector(perpendicular1, atom1, atom1Bonds);
Vector2d nearest2 = getNearestVector(perpendicular2, atom1, atom1Bonds);
double line1Adjust = adjacentLength(nearest1, perpendicular1, halfSeparation);
double line2Adjust = adjacentLength(nearest2, perpendicular2, halfSeparation);
// don't adjust beyond half the bond length
if (line1Adjust > halfBondLength || line1Adjust < 0) line1Adjust = 0;
if (line2Adjust > halfBondLength || line2Adjust < 0) line2Adjust = 0;
// corner case when the adjacent bonds are acute to the double bond,
if (nearest1.dot(unit) > 0) line1Adjust = -line1Adjust;
if (nearest2.dot(unit) > 0) line2Adjust = -line2Adjust;
line1Atom1Point = sum(line1Atom1Point, scale(unit, -line1Adjust));
line2Atom1Point = sum(line2Atom1Point, scale(unit, -line2Adjust));
}
// adjust atom 2 lines to be flush with adjacent bonds
if (!hasDisplayedSymbol(atom2) && atom2Bonds.size() > 1) {
Vector2d nearest1 = getNearestVector(perpendicular1, atom2, atom2Bonds);
Vector2d nearest2 = getNearestVector(perpendicular2, atom2, atom2Bonds);
double line1Adjust = adjacentLength(nearest1, perpendicular1, halfSeparation);
double line2Adjust = adjacentLength(nearest2, perpendicular2, halfSeparation);
// don't adjust beyond half the bond length
if (line1Adjust > halfBondLength || line1Adjust < 0) line1Adjust = 0;
if (line2Adjust > halfBondLength || line2Adjust < 0) line2Adjust = 0;
// corner case when the adjacent bonds are acute to the double bond
if (nearest1.dot(unit) < 0) line1Adjust = -line1Adjust;
if (nearest2.dot(unit) < 0) line2Adjust = -line2Adjust;
line1Atom2Point = sum(line1Atom2Point, scale(unit, line1Adjust));
line2Atom2Point = sum(line2Atom2Point, scale(unit, line2Adjust));
}
group.add(newLineElement(line1Atom1Point, line1Atom2Point));
group.add(newLineElement(line2Atom1Point, line2Atom2Point));
// add annotation label
String label = StandardGenerator.getAnnotationLabel(bond);
if (label != null) addAnnotation(atom1, atom2, label);
return group;
}
/**
* The crossed bond defines unknown geometric isomerism on a double bond. The cross is
* displayed for {@link IBond.Stereo#E_OR_Z}.
*