1 /***************************************************************************
2  *   Copyright (C) 2007-2008 by Harm van Eersel                            *
3  *   Copyright (C) 2009 Tim Vandermeersch                                  *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) any later version.                                   *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20 
21 #include <QtGlobal>
22 #include <QPainter>
23 #include <QMenu>
24 #include <QAction>
25 #include <QGraphicsSceneMouseEvent>
26 
27 #include <math.h>
28 
29 #include "qtdeprecations.h"
30 #include "bond.h"
31 
32 #include "atom.h"
33 #include "element.h"
34 #include "molscene.h"
35 #include "math2d.h"
36 #include "molecule.h"
37 #include <actions/bondtypeaction.h>
38 #include <actions/flipbondaction.h>
39 #include <actions/flipstereobondsaction.h>
40 #include "scenesettings.h"
41 #include "settingsitem.h"
42 #include <QDebug>
43 
44 #define CHECKFORATOMS if (!m_beginAtom || !m_endAtom || !molecule())
45 
46 namespace Molsketch {
47 
orderFromType(const Bond::BondType & type)48   int Bond::orderFromType(const Bond::BondType &type) {
49     return type / 10;
50   }
51 
simpleTypeFromOrder(const int & order)52   Bond::BondType Bond::simpleTypeFromOrder(const int &order)
53   {
54     switch (order)
55     {
56       case 1: return Single;
57       case 2: return DoubleLegacy;
58       case 3: return Triple;
59       default: return Invalid;
60     }
61   }
62 
Bond(Atom * atomA,Atom * atomB,Bond::BondType type,QGraphicsItem * parent)63   Bond::Bond(Atom* atomA, Atom* atomB, Bond::BondType type, QGraphicsItem* parent)
64     : graphicsItem (parent),
65       m_bondType(type),
66       m_beginAtom(0),
67       m_endAtom(0)
68   {
69     setAtoms(atomA, atomB);
70 
71     MolScene* molScene = dynamic_cast<MolScene*>(scene());
72     if (molScene)
73       setColor(molScene->settings()->defaultColor()->get());
74     else
75       setColor(QColor(0, 0, 0));
76 
77     setZValue(2);
78   }
79 
Bond(const Bond & other,Atom * atomA,Atom * atomB)80   Bond::Bond(const Bond &other, Atom* atomA, Atom* atomB)
81     : graphicsItem(other),
82       m_bondType(other.m_bondType),
83       m_beginAtom(0),
84       m_endAtom(0)
85   {
86     setAtoms(atomA, atomB);
87   }
88 
~Bond()89   Bond::~Bond() {}
90 
boundingRect() const91   QRectF Bond::boundingRect() const
92   {
93     CHECKFORATOMS return QRect() ;
94     qreal w = m_endAtom->x() - m_beginAtom->x();
95     qreal h = m_endAtom->y() - m_beginAtom->y();
96     return QRectF(mapFromParent(m_beginAtom->pos()) - QPointF(5,5), QSizeF(w+10,h+10)).normalized();
97   }
98 
bondAngle(const Atom * origin) const99   qreal Bond::bondAngle(const Atom *origin) const
100   {
101     CHECKFORATOMS return 0 ;
102     return Molecule::toDegrees(bondAxis().angle() + (origin == endAtom()) * 180.) ;
103   }
104 
bondAxis() const105   QLineF Bond::bondAxis() const
106   {
107     CHECKFORATOMS return QLineF();
108     return QLineF(mapFromParent(m_beginAtom->pos()),
109                   mapFromParent(m_endAtom->pos())) ;
110   }
111 
limitLineToExtents(const QLineF & line,qreal beginExtent,qreal endExtent)112   QLineF limitLineToExtents(const QLineF& line, qreal beginExtent, qreal endExtent) {
113     return QLineF{line.pointAt(beginExtent), line.pointAt(endExtent)};
114   }
115 
limitLinesToExtents(const QPair<QLineF,QLineF> & lines,qreal beginExtent,qreal endExtent)116   QPair<QLineF, QLineF> limitLinesToExtents(const QPair<QLineF, QLineF> &lines, qreal beginExtent, qreal endExtent) {
117     return qMakePair(limitLineToExtents(lines.first, beginExtent, endExtent),
118                      limitLineToExtents(lines.second, beginExtent, endExtent));
119   }
120 
getWedgeBondPath(const QPair<QLineF,QLineF> & outerLines)121   QPainterPath getWedgeBondPath(const QPair<QLineF, QLineF> &outerLines) {
122     QPainterPath path(outerLines.first.p1());
123     path.lineTo(outerLines.first.p2());
124     path.lineTo(outerLines.second.p2());
125     path.lineTo(outerLines.second.p1());
126     path.closeSubpath();
127     return path;
128   }
129 
drawHashBond() const130   QPainterPath Bond::drawHashBond() const
131   {
132     auto outerLines = getOuterLimitsOfStereoBond();
133     auto beginExtent = getExtentForStereoBond(beginAtom(), outerLines, false),
134         endExtent = getExtentForStereoBond(endAtom(), outerLines, true);
135 
136     auto outerLinesToDraw = limitLinesToExtents(outerLines, beginExtent, endExtent);
137 
138     QVector<QPair<qreal, qreal> > sections {
139       {0., .08},
140       {.23, .31},
141       {.46, .54},
142       {.69, .77},
143       {.92, 1.}
144     };
145 
146     QPainterPath path;
147     for (auto range : sections)
148       path.addPath(getWedgeBondPath(limitLinesToExtents(outerLinesToDraw, range.first, range.second)));
149 
150     return path;
151   }
152 
getOuterLimitsOfStereoBond() const153   QPair<QLineF, QLineF> Bond::getOuterLimitsOfStereoBond() const {
154     auto axis = bondAxis();
155     auto normalVector = axis.normalVector().unitVector();
156     if (MolScene* s = qobject_cast<MolScene*>(scene()))
157       normalVector.setLength(s->settings()->bondWedgeWidth()->get()/2.);
158     normalVector.translate(axis.dx(), axis.dy());
159 
160     return qMakePair(QLineF{axis.p1(), normalVector.p2()},
161                      QLineF{axis.p1(), normalVector.pointAt(-1)});
162   }
163 
mapOuterLineToAtom(const Atom * atom,const QLineF & line,bool reverse) const164   QLineF Bond::mapOuterLineToAtom(const Atom *atom, const QLineF& line, bool reverse) const {
165    return QLineF(mapToItem(atom, reverse ? line.p2() : line.p1()),
166                  mapToItem(atom, reverse ? line.p1() : line.p2()));
167   }
168 
getExtentForStereoBond(const Atom * atom,const QPair<QLineF,QLineF> & outerLines,bool reverse) const169   qreal Bond::getExtentForStereoBond(const Atom *atom, const QPair<QLineF, QLineF> &outerLines, bool reverse) const {
170     if (reverse) return 1 - atom->getBondExtent(mapOuterLineToAtom(atom, outerLines.first, reverse),
171                                                 mapOuterLineToAtom(atom, outerLines.second, reverse),
172                                                 lineWidth());
173     return atom->getBondExtent(mapOuterLineToAtom(atom, outerLines.first, reverse),
174                                mapOuterLineToAtom(atom, outerLines.second, reverse),
175                                lineWidth());
176   }
177 
drawWedgeBond() const178   QPainterPath Bond::drawWedgeBond() const
179   {
180     auto outerLines = getOuterLimitsOfStereoBond();
181     auto beginExtent = getExtentForStereoBond(beginAtom(), outerLines, false),
182         endExtent = getExtentForStereoBond(endAtom(), outerLines, true);
183     auto outerLinesToDraw = limitLinesToExtents(outerLines, beginExtent, endExtent);
184     return getWedgeBondPath(outerLinesToDraw);
185   }
186 
shiftAndElongate(const QLineF & line,const QPointF & shift,const QPointF & elongation)187   QLineF shiftAndElongate(const QLineF &line, const QPointF &shift, const QPointF &elongation) {
188     auto newLine = line.translated(shift);
189     newLine.setPoints(newLine.p1() - elongation, newLine.p2() + elongation);
190     return newLine;
191   }
192 
getWedgeBondShape() const193   QPainterPath Bond::getWedgeBondShape() const {
194     auto outerLines = getOuterLimitsOfStereoBond();
195     auto beginExtent = getExtentForStereoBond(beginAtom(), outerLines, false),
196         endExtent = getExtentForStereoBond(endAtom(), outerLines, true);
197 
198     auto outerLinesToDraw = limitLinesToExtents(outerLines, beginExtent, endExtent);
199 
200     auto axis = bondAxis();
201     axis.setLength(bondShapeGap());
202     QPointF bondShift{axis.dx(), axis.dy()};
203     QPointF normalShift{axis.dy(), -axis.dx()};
204 
205     auto shiftedOuterLines = qMakePair(shiftAndElongate(outerLinesToDraw.first, normalShift, bondShift),
206               shiftAndElongate(outerLinesToDraw.second, -normalShift, bondShift));
207 
208     return getWedgeBondPath(shiftedOuterLines);
209   }
210 
minimumAngle(const Bond * reference,const QSet<Bond * > & others,const Atom * origin,bool clockwise)211   double minimumAngle(const Bond* reference, const QSet<Bond*>& others, const Atom* origin, bool clockwise)
212   {
213     double minAngle = 361;
214     for (auto b : others)
215     {
216       double angle = b->bondAngle(origin) - reference->bondAngle(origin);
217       if (clockwise) angle = 360 - angle;
218       angle = Molecule::toDegrees(angle);
219       minAngle = qMin(minAngle, angle);
220     }
221     return minAngle;
222   }
223 
determineDoubleBondOrientation()224   void Bond::determineDoubleBondOrientation()
225   {
226     if (m_bondType != DoubleLegacy) return;
227     m_bondType = DoubleSymmetric;
228     auto beginBondList = m_beginAtom->bonds();
229     auto beginBonds = toSet(beginBondList);
230     beginBonds -= this;
231     auto endBondList = m_endAtom->bonds();
232     auto endBonds = toSet(endBondList);
233     endBonds -= this;
234     // no other bonds: symmetric
235     if (beginBonds.empty() && endBonds.empty()) return;
236     // other bonds: add up angles upper and lower
237     double sumUpperAngles = minimumAngle(this, beginBonds, m_beginAtom, false)
238         + minimumAngle(this, endBonds, m_endAtom, true);
239     double sumLowerAngles = minimumAngle(this, beginBonds, m_beginAtom, true)
240         + minimumAngle(this, endBonds, m_endAtom, false);
241     // equal (float tolerance): symmetric
242     if (qAbs(sumUpperAngles - sumLowerAngles) < 1e-7) return;
243     // else: unsymmetric
244     m_bondType = DoubleAsymmetric;
245     // consider swapping atoms
246     if (sumUpperAngles > sumLowerAngles) qSwap(m_beginAtom, m_endAtom);
247   }
248 
effectiveBondLine(const Bond * b,const Atom * a)249   QLineF effectiveBondLine(const Bond* b, const Atom* a)
250   {
251     QLineF bl(b->bondAxis());
252     if (b->beginAtom() != a)
253       return QLineF(bl.p2(), bl.p1());
254     return bl;
255   }
256 
findIdealAngle(const Atom * atom,const Bond * bond,bool inverted)257   qreal findIdealAngle(const Atom* atom, const Bond* bond, bool inverted)
258   {
259     qreal angle = 120; // normal angle (e.g. in benzene)
260     QLineF bondLine(effectiveBondLine(bond, atom));
261     // TODO skip if we have a displayed label for the beginAtom
262     foreach (const Bond* otherBond, atom->bonds())
263     {
264       if (otherBond == bond) continue;
265       QLineF otherBondAxis(effectiveBondLine(otherBond, atom));
266       angle = qMin(angle, (inverted
267                            ? otherBondAxis.angleTo(bondLine)
268                            : bondLine.angleTo(otherBondAxis)));
269     }
270     return angle*M_PI/360.; // includes division by 2
271   }
272 
bondPath() const273   QPainterPath Bond::bondPath() const {
274     // Get beginning and end
275     QPointF begin = determineBondDrawingStart(m_beginAtom, m_endAtom);
276     QPointF end = determineBondDrawingStart(m_endAtom, m_beginAtom);
277     QPointF vb = end - begin;
278     QPointF uvb = vb / sqrt(vb.x()*vb.x() + vb.y()*vb.y());
279     if (MolScene* s = qobject_cast<MolScene*>(scene()))
280       uvb *= s->settings()->bondSeparation()->get();
281     QPointF normalVector(uvb.y(), -uvb.x());
282 
283     QPainterPath result;
284     switch ( m_bondType ) // TODO beautify
285     {
286       case Bond::Single:
287       case Bond::DativeDot:
288       case Bond::DativeDash:
289       case Bond::WedgeOrHash:
290         result.moveTo(begin);
291         result.lineTo(end);
292         break;
293       case Bond::Wedge:
294         return drawWedgeBond();
295       case Bond::Hash:
296         return drawHashBond();
297       case Bond::DoubleSymmetric:
298         {
299           QPointF offset = .5*normalVector;
300           result.moveTo(begin + offset);
301           result.lineTo(end + offset);
302           result.moveTo(begin - offset);
303           result.lineTo(end - offset);
304           break;
305         }
306       case Bond::CisOrTrans:
307         {
308           QPointF offset = .5*normalVector;
309           result.moveTo(begin + offset);
310           result.lineTo(end - offset);
311           result.moveTo(begin - offset);
312           result.lineTo(end + offset);
313           break;
314         }
315       case Bond::DoubleAsymmetric:
316         {
317           result.moveTo(begin);
318           result.lineTo(end);
319           // now the double part
320           QPointF offset = normalVector;
321           qreal beginAngle = findIdealAngle(beginAtom(), this, false),
322               endAngle = findIdealAngle(endAtom(), this, true),
323               limitAngle = atan(2*QLineF(QPointF(0,0),uvb).length()/QLineF(begin, end).length());
324           beginAngle = qMax(beginAngle, limitAngle);
325           endAngle = qMax(endAngle, limitAngle);
326           result.moveTo(begin + uvb/tan(beginAngle) + offset);
327           result.lineTo(end - uvb/tan(endAngle) + offset);
328           break;
329         }
330       case Bond::Triple:
331         {
332           result.moveTo(begin);
333           result.lineTo(end);
334           QPointF offset = normalVector;
335           result.moveTo(begin + offset);
336           result.lineTo(end + offset);
337           result.moveTo(begin - offset);
338           result.lineTo(end - offset);
339           break;
340         }
341       case TripleAsymmetric:
342         {
343           result.moveTo(begin);
344           result.lineTo(end);
345           // now the double part
346           QPointF offset = normalVector;
347           qreal beginAngle = findIdealAngle(beginAtom(), this, false),
348               endAngle = findIdealAngle(endAtom(), this, true),
349               limitAngle = atan(2*QLineF(QPointF(0,0),uvb).length()/QLineF(begin, end).length());
350           beginAngle = qMax(beginAngle, limitAngle);
351           endAngle = qMax(endAngle, limitAngle);
352           result.moveTo(begin + uvb/tan(beginAngle) + offset);
353           result.lineTo(end - uvb/tan(endAngle) + offset);
354           beginAngle = findIdealAngle(beginAtom(), this, true);
355           endAngle = findIdealAngle(endAtom(), this, false);
356           beginAngle = qMax(beginAngle, limitAngle);
357           endAngle = qMax(endAngle, limitAngle);
358           result.moveTo(begin + uvb/tan(beginAngle) - offset);
359           result.lineTo(end - uvb/tan(endAngle) - offset);
360           break;
361         }
362       default: ;
363     }
364     return result;
365   }
366 
determineBondDrawingStart(Atom * start,Atom * end) const367   QPointF Bond::determineBondDrawingStart(Atom *start, Atom *end) const {
368     return mapFromScene(start->bondDrawingStart(end, lineWidth()));
369   }
370 
coveringBonds() const371   QList<Bond *> Bond::coveringBonds() const
372   {
373     QList<Bond*> result;
374     auto sc = scene();
375     if (!sc) return result;
376     for (auto item : sc->items())
377       if (auto bond = dynamic_cast<Bond*>(item))
378         if (bond->zValue() > zValue() && collidesWithItem(bond))
379           result << bond;
380     return result;
381   }
382 
brokenBondIndicator(const QPointF & point,const QPointF & bondVector,const QPointF & normalVector)383   QPainterPath brokenBondIndicator(const QPointF &point, const QPointF &bondVector, const QPointF &normalVector) {
384     QPointF bondUnitVector(bondVector/QLineF(QPointF(0,0), bondVector).length());
385     const qreal decorationScale = 0.2; // TODO modifiable - from wedge bond width!
386     QPointF
387         x = normalVector*decorationScale,
388         y = -8*bondUnitVector*decorationScale;
389     QPainterPath path;
390     path.moveTo(-7*x);
391     path.quadTo(-7*x+y, -6*x+y);
392     path.cubicTo(-5*x+y, -5*x, -4*x);
393     path.cubicTo(-3*x, -3*x+y, -2*x+y);
394     path.cubicTo(-x+y, -x, 0*x);
395     path.cubicTo(x, x+y, 2*x+y);
396     path.cubicTo(3*x+y, 3*x, 4*x);
397     path.cubicTo(5*x, 5*x+y, 6*x+y);
398     path.quadTo(7*x+y, 7*x);
399     path.translate(point + 0.3 * bondVector);
400 
401     return path;
402   }
403 
getBrokenBondIndicatorsPath(const QPointF & begin,const QPointF & end,const QPointF & normalVector) const404   QPainterPath Bond::getBrokenBondIndicatorsPath(const QPointF &begin, const QPointF &end, const QPointF &normalVector) const {
405     QPointF bondUnitVector((end-begin)/QLineF(end, begin).length());
406     const qreal decorationScale = 0.2; // TODO modifiable - from wedge bond width!
407     QPointF
408         x = normalVector*decorationScale,
409         y = 8*bondUnitVector*decorationScale,
410         xl = normalVector/QLineF(QPointF(), normalVector).length() * bondShapeGap() / 1.5,
411         yl = bondUnitVector * bondShapeGap() / 1.5;
412     QPainterPath path;
413     path.moveTo(-7*x - xl - y - yl);
414     path.lineTo(+7*x + xl - y - yl);
415     path.lineTo(+7*x + xl + yl);
416     path.lineTo(-7*x - xl + yl);
417     path.closeSubpath();
418     return path.translated(begin + 0.3 * (end - begin));
419   }
420 
421 
clipBrokenBondIndicator(const QPointF & point,const QPointF & otherAtom,const QPointF & normalVector) const422   QPainterPath Bond::clipBrokenBondIndicator(const QPointF& point,
423                      const QPointF& otherAtom,
424                      const QPointF& normalVector) const
425   {
426     QPointF bondVector = otherAtom - point;
427     QPointF bondVectorOffset = bondVector / QLineF(QPointF(), bondVector).length() * 2 * lineWidth();
428     QPainterPath path(brokenBondIndicator(point, bondVector, normalVector));
429     path.lineTo(point + 1.4 * normalVector - bondVectorOffset); // TODO get from wedge bond width + linewidth
430     path.lineTo(point - 1.4 * normalVector - bondVectorOffset); // TODO get from wedge bond width + linewidth
431     path.closeSubpath();
432     return path;
433   }
434 
drawBrokenIndicator(QPainter * painter,const QPainterPath & path)435   void drawBrokenIndicator(QPainter* painter,
436                            const QPainterPath& path)
437   {
438     QPen subPen(painter->pen());
439     subPen.setWidthF(subPen.widthF()* 0.75); // TODO fix shape box accordingly
440     painter->save();
441     painter->setPen(subPen);
442     painter->drawPath(path);
443     painter->restore();
444   }
445 
paintBrokenBondIndicators(QPainter * painter,const QPointF & begin,const QPointF & end,const QPointF & vb,const QPointF & normalVector)446   void Bond::paintBrokenBondIndicators(QPainter *painter, const QPointF &begin, const QPointF &end, const QPointF &vb, const QPointF &normalVector) {
447     if (m_beginAtom->element().isEmpty()) drawBrokenIndicator(painter, brokenBondIndicator(begin, vb, normalVector));
448     if (m_endAtom->element().isEmpty()) drawBrokenIndicator(painter, brokenBondIndicator(end, -vb, normalVector));
449   }
450 
paint(QPainter * painter,const QStyleOptionGraphicsItem * option,QWidget * widget)451   void Bond::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
452   {
453     Q_UNUSED(option);
454     Q_UNUSED(widget);
455     CHECKFORATOMS return ;
456 
457     if (m_bondType == DoubleLegacy) determineDoubleBondOrientation();
458 
459     QRectF startRect = m_beginAtom->mapRectToItem(this, m_beginAtom->boundingRect()),
460         endRect = m_endAtom->mapRectToItem(this, m_endAtom->boundingRect());
461     if (startRect.intersects(endRect) && m_beginAtom->isDrawn() && m_endAtom->isDrawn()) return; // TODO should not be necessary anymore
462 
463 
464     // Get beginning and end
465     QPointF begin = mapFromParent(m_beginAtom->pos());
466     QPointF end = mapFromParent(m_endAtom->pos());
467     QPointF vb = end - begin;
468     QPointF uvb = vb / sqrt(vb.x()*vb.x() + vb.y()*vb.y());
469     if (MolScene* s = qobject_cast<MolScene*>(scene()))
470       uvb *= s->settings()->bondSeparation()->get();
471     QPointF normalVector(uvb.y(), -uvb.x());
472 
473     begin = determineBondDrawingStart(m_beginAtom, m_endAtom);
474     end = determineBondDrawingStart(m_endAtom, m_beginAtom);
475     // TODO this collision detection rests on the factor used in Atom for the determination of the starting point. Improve!
476     if (m_beginAtom->contains(mapToItem(m_beginAtom, end
477                                         + 0.75 * lineWidth() * QLineF(QPointF(), m_beginAtom->pos() - m_endAtom->pos()).unitVector().p2()))
478         || m_endAtom->contains(mapToItem(m_endAtom, begin
479                                          + 0.75* lineWidth() * QLineF(QPointF(), m_endAtom->pos() - m_beginAtom->pos()).unitVector().p2())))
480       return;
481 
482     QPen pen;
483 
484     painter->save();
485     pen.setWidthF(lineWidth());
486     pen.setCapStyle(Qt::RoundCap);
487     pen.setJoinStyle(Qt::RoundJoin);
488     pen.setColor(getColor());
489     painter->setPen(pen);
490 
491     QPainterPath coveringShapes;
492     for (auto coveringBond : coveringBonds())
493       coveringShapes += mapFromItem(coveringBond, coveringBond->shape());
494 
495     begin = mapFromParent(m_beginAtom->pos());
496     end = mapFromParent(m_endAtom->pos());
497     painter->setClipPath(shape().subtracted(coveringShapes));
498     paintBrokenBondIndicators(painter, begin, end, vb, normalVector);
499     if (m_beginAtom->element().isEmpty())
500       painter->setClipPath(painter->clipPath().subtracted(clipBrokenBondIndicator(begin, end, normalVector)));
501     if (m_endAtom->element().isEmpty())
502       painter->setClipPath(painter->clipPath().subtracted(clipBrokenBondIndicator(end, begin, normalVector)));
503 
504     switch ( m_bondType ) // TODO beautify
505     {
506       case Bond::DativeDot:
507         pen.setStyle(Qt::DotLine);
508         break;
509       case Bond::DativeDash:
510         pen.setStyle(Qt::DashLine);
511         break;
512       case Bond::WedgeOrHash:
513         pen.setDashPattern(QVector<qreal>() << 2 << 5);
514         break;
515       case Bond::Wedge:
516       case Bond::Hash:
517           painter->setBrush( QBrush(getColor()) );
518       default: ;
519     }
520     painter->setPen(pen);
521     QPainterPath path = bondPath();
522     painter->drawPath(path);
523 
524     painter->setClipping(false);
525     painter->setBrush(Qt::NoBrush);
526 
527     if (isSelected()) {
528       painter->setPen(Qt::blue);
529       painter->drawPath(shape());
530     }
531     painter->restore();
532 
533     graphicsItem::paint(painter, option, widget);
534   }
535 
itemChange(GraphicsItemChange change,const QVariant & value)536   QVariant Bond::itemChange(GraphicsItemChange change, const QVariant &value)
537   {
538     if (change == ItemPositionChange && parentItem()) parentItem()->update();
539     return graphicsItem::itemChange(change, value);
540   }
541 
shape() const542   QPainterPath Bond::shape() const
543   {
544     CHECKFORATOMS return QPainterPath() ;
545     return outline();
546   }
547 
setType(const BondType & t)548   void Bond::setType(const BondType &t)
549   {
550     m_bondType = t;
551     if (Molecule *m = molecule()) {
552       m->updateElectronSystems();
553       m->updateTooltip();
554     }
555     update();
556   }
557 
bondOrder() const558   int Bond::bondOrder() const
559   {
560     return orderFromType(bondType());
561   }
562 
bondType() const563   Bond::BondType Bond::bondType() const
564   {
565     return m_bondType;
566   }
567 
beginAtom() const568   Atom* Bond::beginAtom() const
569   {
570     return m_beginAtom;
571   }
572 
endAtom() const573   Atom* Bond::endAtom() const
574   {
575     return m_endAtom;
576   }
577 
hasAtom(const Atom * atom) const578   bool Bond::hasAtom(const Atom* atom) const
579   {
580     return m_beginAtom == atom || m_endAtom == atom;
581   }
582 
otherAtom(const Atom * atom) const583   Atom* Bond::otherAtom(const Atom *atom) const
584   {
585     return (atom == m_beginAtom) ? m_endAtom : m_beginAtom;
586   }
587 
setAtoms(Atom * A,Atom * B)588   void Bond::setAtoms(Atom *A, Atom *B)
589   {
590     m_beginAtom = A ;
591     m_endAtom = B ;
592     if (m_beginAtom) {
593       m_beginAtom->updateShape();
594       setPos(m_beginAtom->scenePos()) ;
595     }
596     if (m_endAtom) m_endAtom->updateShape();
597   }
598 
setAtoms(const QPair<Atom *,Atom * > & atoms)599   void Bond::setAtoms(const QPair<Atom *, Atom *> &atoms) {
600     setAtoms(atoms.first, atoms.second);
601   }
602 
atoms() const603   QPair<Atom *, Atom *> Bond::atoms() const {
604     return qMakePair(beginAtom(), endAtom());
605   }
606 
molecule() const607   Molecule* Bond::molecule() const
608   {
609     return dynamic_cast<Molecule*>(this->parentItem());
610   }
611 
shiftVector(const QLineF & vector,qreal shift)612   QLineF Bond::shiftVector(const QLineF &vector, qreal shift) // Shifts a vector on the perpendicular axis
613   {
614     qreal rx1 = vector.x1() + shift*(vector.unitVector().y2()-vector.unitVector().y1());
615     qreal ry1 = vector.y1() + shift*-(vector.unitVector().x2()-vector.unitVector().x1());
616     qreal rx2 = vector.x2() + shift*(vector.unitVector().y2()-vector.unitVector().y1());
617     qreal ry2 = vector.y2() + shift*-(vector.unitVector().x2()-vector.unitVector().x1());
618     return QLineF(rx1,ry1,rx2,ry2);
619   }
620 
xmlName() const621   QString Bond::xmlName() const { return xmlClassName(); }
622 
xmlClassName()623   QString Bond::xmlClassName() { return "bond" ; }
624 
setCoordinates(const QVector<QPointF> & c)625   void Bond::setCoordinates(const QVector<QPointF> &c)
626   {
627     if (c.size() != 2) return ;
628     CHECKFORATOMS return;
629     m_beginAtom->setCoordinates(c.mid(0,1)) ;
630     m_endAtom->setCoordinates(c.mid(1,1));
631   }
632 
coordinates() const633   QPolygonF Bond::coordinates() const
634   {
635    CHECKFORATOMS return QVector<QPointF>();
636     return QVector<QPointF>()
637         << m_beginAtom->coordinates()
638         << m_endAtom->coordinates() ;
639   }
640 
graphicAttributes() const641   QXmlStreamAttributes Bond::graphicAttributes() const
642   {
643     QXmlStreamAttributes attributes ;
644     attributes.append("atomRefs2", m_beginAtom->index() + " " + m_endAtom->index()) ;
645     attributes.append("type", QString::number(m_bondType));
646     return attributes ;
647   }
648 
readGraphicAttributes(const QXmlStreamAttributes & attributes)649   void Bond::readGraphicAttributes(const QXmlStreamAttributes &attributes)
650   {
651     QStringList atomIndexes = attributes.value("atomRefs2").toString().split(" ") ;
652     if (atomIndexes.size() != 2) return ;
653 
654     if (auto mol = molecule())
655       setAtoms(mol->atom(atomIndexes.first()),
656                mol->atom(atomIndexes.last())) ;
657     m_bondType = (BondType) (attributes.value("type").toString().toInt());
658     if (attributes.hasAttribute("order"))
659       m_bondType = (BondType) (10 *attributes.value("order").toInt());
660   }
661 
prepareContextMenu(QMenu * contextMenu)662   void Bond::prepareContextMenu(QMenu *contextMenu) // TODO simply use a function that returns the scene's actions pertaining to the item
663   {
664     // Prepare bond menu
665     MolScene *sc = qobject_cast<MolScene*>(scene());
666     if (sc)
667     {
668       QList<QAction*> actions;
669       actions << sc->findChild<bondTypeAction*>()
670               << sc->findChild<flipBondAction*>()
671               << sc->findChild<flipStereoBondsAction*>();
672       foreach(QAction* action, actions)
673       {
674         if (!action) continue;
675         QObject::connect(action, SIGNAL(triggered()), contextMenu, SLOT(close())); // TODO check if "changed()" event can accomplish this. Only required for bondType action
676         contextMenu->addAction(action);
677       }
678     }
679     graphicsItem::prepareContextMenu(contextMenu);
680   }
681 
682   class LegacyBondStereo : public XmlObjectInterface {
683     Bond *bond;
684   public:
LegacyBondStereo(Bond * bond)685     LegacyBondStereo(Bond *bond) : bond(bond) {}
readXml(QXmlStreamReader & in)686     QXmlStreamReader& readXml(QXmlStreamReader &in) override {
687       auto text = in.readElementText();
688       if ("H" == text) bond->setType(Bond::Hash);
689       if ("W" == text) bond->setType(Bond::Wedge);
690       return in;
691     }
692 
writeXml(QXmlStreamWriter & out) const693     QXmlStreamWriter& writeXml(QXmlStreamWriter &out) const override {
694       return out;
695     }
696   };
697 
produceChild(const QString & name,const QXmlStreamAttributes & attributes)698   XmlObjectInterface *Bond::produceChild(const QString &name, const QXmlStreamAttributes &attributes) {
699     if (name != "bondStereo" || !attributes.isEmpty()) return nullptr;
700     XmlObjectInterface *helper = new LegacyBondStereo(this);
701     helpers << helper;
702     return helper;
703   }
704 
afterReadFinalization()705   void Bond::afterReadFinalization()
706   {
707     for (auto helper : helpers) delete helper;
708     helpers.clear();
709   }
710 
bondShapeGap() const711   qreal Bond::bondShapeGap() const {
712     return lineWidth(); // TODO make this configurable!
713   }
714 
outline() const715   QPainterPath Bond::outline() const {
716     // Get beginning and end (taken from bondPath()
717     QPointF begin = determineBondDrawingStart(m_beginAtom, m_endAtom);
718     QPointF end = determineBondDrawingStart(m_endAtom, m_beginAtom);
719     QPointF vb = end - begin;
720     QPointF uvb = vb / sqrt(vb.x()*vb.x() + vb.y()*vb.y());
721     QPointF bondUnitVector(uvb);
722     QPointF bondNormalVector(uvb.y(), -uvb.x());
723     if (MolScene* s = qobject_cast<MolScene*>(scene()))
724       uvb *= s->settings()->bondSeparation()->get();
725     QPointF normalVector(uvb.y(), -uvb.x());
726 
727     QPainterPath result;
728 
729     qreal gap = bondShapeGap();
730 
731     switch (m_bondType) {
732       case Bond::Single:
733       case Bond::DativeDot:
734       case Bond::DativeDash:
735       case WedgeOrHash:
736         result.moveTo(begin + gap * (-bondUnitVector + bondNormalVector));
737         result.lineTo(begin + gap * (-bondUnitVector - bondNormalVector));
738         result.lineTo(end + gap * (bondUnitVector - bondNormalVector));
739         result.lineTo(end + gap * (bondUnitVector + bondNormalVector));
740         result.closeSubpath();
741         break;
742       case Bond::Wedge:
743       case Bond::Hash:
744         result = getWedgeBondShape();
745         break;
746       case DoubleSymmetric:
747         {
748           QPointF offset = .5*normalVector;
749           result.moveTo(begin + offset + gap * (-bondUnitVector + bondNormalVector));
750           result.lineTo(end + offset + gap * (bondUnitVector + bondNormalVector));
751           result.lineTo(end - offset + gap * (bondUnitVector - bondNormalVector));
752           result.lineTo(begin - offset + gap * (-bondUnitVector - bondNormalVector));
753           result.closeSubpath();
754           break;
755         }
756       case DoubleAsymmetric:
757         { // TODO add more gap
758           result.moveTo(begin + gap * (-bondUnitVector - bondNormalVector));
759           result.lineTo(end + gap * (bondUnitVector - bondNormalVector));
760           // now the double part
761           QPointF offset = normalVector;
762           qreal beginAngle = findIdealAngle(beginAtom(), this, false),
763               endAngle = findIdealAngle(endAtom(), this, true),
764               limitAngle = atan(2*QLineF(QPointF(0,0),uvb).length()/QLineF(begin, end).length());
765           beginAngle = qMax(beginAngle, limitAngle);
766           endAngle = qMax(endAngle, limitAngle);
767           result.lineTo(end - uvb/tan(endAngle) + offset + gap * (bondUnitVector + bondNormalVector));
768           result.lineTo(begin + uvb/tan(beginAngle) + offset + gap * (-bondUnitVector + bondNormalVector));
769           result.closeSubpath();
770           break;
771         }
772       case CisOrTrans:
773         {
774           QPointF offset = .5*normalVector;
775           result.moveTo(begin + offset + gap * (-bondUnitVector + bondNormalVector));
776           result.lineTo((begin + end)/2 + gap * bondNormalVector);
777           result.lineTo(end + offset + gap * (bondUnitVector + bondNormalVector));
778           result.lineTo(end - offset + gap * (bondUnitVector - bondNormalVector));
779           result.lineTo((begin + end)/2 + gap * -bondNormalVector);
780           result.lineTo(begin - offset + gap * (-bondUnitVector - bondNormalVector));
781           result.closeSubpath();
782           break;
783         }
784       case Bond::Triple:
785         {
786           QPointF offset = normalVector;
787           result.moveTo(begin + offset + gap * (-bondUnitVector + bondNormalVector));
788           result.lineTo(end + offset + gap * (bondUnitVector + bondNormalVector));
789           result.lineTo(end - offset + gap * (bondUnitVector - bondNormalVector));
790           result.lineTo(begin - offset + gap * (-bondUnitVector - bondNormalVector));
791           result.closeSubpath();
792           break;
793         }
794       case Bond::TripleAsymmetric:
795         { // TODO add more gap to central line
796           result.moveTo(begin + gap * -bondUnitVector);
797           // now the double part
798           QPointF offset = normalVector;
799           qreal beginAngle = findIdealAngle(beginAtom(), this, false),
800               endAngle = findIdealAngle(endAtom(), this, true),
801               limitAngle = atan(2*QLineF(QPointF(0,0),uvb).length()/QLineF(begin, end).length());
802           beginAngle = qMax(beginAngle, limitAngle);
803           endAngle = qMax(endAngle, limitAngle);
804           result.lineTo(begin + uvb/tan(beginAngle) + offset + gap * (-bondUnitVector + bondNormalVector));
805           result.lineTo(end - uvb/tan(endAngle) + offset + gap * (bondUnitVector + bondNormalVector));
806           result.lineTo(end + gap * bondUnitVector);
807           beginAngle = findIdealAngle(beginAtom(), this, true);
808           endAngle = findIdealAngle(endAtom(), this, false);
809           beginAngle = qMax(beginAngle, limitAngle);
810           endAngle = qMax(endAngle, limitAngle);
811           result.lineTo(end - uvb/tan(endAngle) - offset + gap * (bondUnitVector - bondNormalVector));
812           result.lineTo(begin + uvb/tan(beginAngle) - offset + gap * (-bondUnitVector - bondNormalVector));
813           result.closeSubpath();
814           break;
815         }
816       default: ;
817     }
818     if (m_beginAtom->element().isEmpty())
819       (result -= clipBrokenBondIndicator(mapFromParent(m_beginAtom->pos()), mapFromParent(m_endAtom->pos()), normalVector))
820         += getBrokenBondIndicatorsPath(mapFromParent(m_beginAtom->pos()), mapFromParent(m_endAtom->pos()), normalVector);
821     if (m_endAtom->element().isEmpty())
822       (result -= clipBrokenBondIndicator(mapFromParent(m_endAtom->pos()), mapFromParent(m_beginAtom->pos()), normalVector))
823         += getBrokenBondIndicatorsPath(mapFromParent(m_endAtom->pos()), mapFromParent(m_beginAtom->pos()), normalVector);
824     return result;
825   }
826 } // namespace
827