1 // Copyright (C) 2010 Jeremy Sanders
2 
3 // This program is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU General Public License
5 // as published by the Free Software Foundation; either version 2
6 // of the License, or (at your option) any later version.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with this program; if not, write to the Free Software
15 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
16 // 02110-1301, USA.
17 ////////////////////////////////////////////////////////////////////
18 
19 #include <cmath>
20 #include <limits>
21 #include <algorithm>
22 
23 #include <QPointF>
24 #include <QPen>
25 
26 #include <polylineclip.h>
27 
28 using std::fabs;
29 
30 // Cohen-Sutherland line clipping algorithm
31 
32 // codes used classify endpoints are combinations of these
33 #define LEFT 1
34 #define RIGHT 2
35 #define TOP 4
36 #define BOTTOM 8
37 
38 namespace
39 {
40   // compute intersection with horizontal line
horzIntersection(qreal yval,const QPointF & pt1,const QPointF & pt2)41   inline QPointF horzIntersection(qreal yval, const QPointF& pt1,
42                                   const QPointF& pt2)
43   {
44     if( pt1.y() == pt2.y() )
45       // line is vertical
46       return QPointF( pt1.x(), yval );
47     else
48       return QPointF( pt1.x() + (yval-pt1.y()) *
49                       (pt2.x()-pt1.x()) / (pt2.y()-pt1.y()),
50                       yval );
51   }
52 
53   // compute intersection with vertical line
vertIntersection(qreal xval,const QPointF & pt1,const QPointF & pt2)54   inline QPointF vertIntersection(qreal xval, const QPointF& pt1,
55                                   const QPointF& pt2)
56   {
57     if( pt1.x() == pt2.x() )
58       // line is horizontal
59       return QPointF( xval, pt1.y() );
60     else
61       return QPointF( xval,
62                       pt1.y() + (xval-pt1.x()) *
63                       (pt2.y()-pt1.y()) / (pt2.x()-pt1.x()) );
64   }
65 
66   // is difference between points very small?
smallDelta(const QPointF & pt1,const QPointF & pt2)67   inline bool smallDelta(const QPointF& pt1, const QPointF& pt2)
68   {
69     return fabs(pt1.x() - pt2.x()) < 0.01 &&
70       fabs(pt1.y()- pt2.y()) < 0.01;
71   }
72 
sqr(T v)73   template<class T> T sqr(T v)
74   {
75     return v*v;
76   }
77 
78   // private class for helping clip
79   class _Clipper
80   {
81   public:
82     _Clipper(const QRectF& cliprect);
83     unsigned computeCode(const QPointF& pt) const;
84     void fixPt(QPointF& pt) const;
85     bool clipLine(QPointF& pt1, QPointF& pt2) const;
86 
87   private:
88     QRectF clip;
89   };
90 
91 
92   // This class is use to separate out the clipping of polylines
93   // overrite emitPolyline to handle the clipped part of the original
94   // polyline
95   class _PolyClipper
96   {
97   public:
_PolyClipper(QRectF clip)98     _PolyClipper(QRectF clip)
99       : _clipper(clip)
100     {}
~_PolyClipper()101     virtual ~_PolyClipper() {};
102 
103     // override this to draw the output polylines
104     virtual void emitPolyline(const QPolygonF& poly) = 0;
105 
106     // do clipping on the polyline
107     void clipPolyline(const QPolygonF& poly);
108 
109   private:
110     _Clipper _clipper;
111   };
112 
113 }
114 
115 ////////////////////////////////////////////////////////////////////////
116 
_Clipper(const QRectF & cliprect)117 _Clipper::_Clipper(const QRectF& cliprect)
118   : clip(cliprect)
119 {
120 }
121 
122 // compute the Cohen-Sutherland code
computeCode(const QPointF & pt) const123 unsigned _Clipper::computeCode(const QPointF& pt) const
124 {
125   unsigned code = 0;
126   if (pt.x() < clip.left())
127     code |= LEFT;
128   else if (pt.x() > clip.right())
129     code |= RIGHT;
130   if (pt.y() < clip.top())
131     code |= TOP;
132   else if (pt.y() > clip.bottom())
133     code |= BOTTOM;
134   return code;
135 }
136 
137 // get consistent clipping on different platforms by making line
138 // edges meet clipping box if close
fixPt(QPointF & pt) const139 void _Clipper::fixPt(QPointF& pt) const
140 {
141   if( fabs(pt.x() - clip.left()) < 1e-4 )
142     pt.setX(clip.left());
143   if( fabs(pt.x() - clip.right()) < 1e-4 )
144     pt.setX(clip.right());
145   if( fabs(pt.y() - clip.top()) < 1e-4 )
146     pt.setY(clip.top());
147   if( fabs(pt.y() - clip.bottom()) < 1e-4 )
148     pt.setY(clip.bottom());
149 }
150 
151 // modifies points, returning true if okay to accept
clipLine(QPointF & pt1,QPointF & pt2) const152 bool _Clipper::clipLine(QPointF& pt1, QPointF& pt2) const
153 {
154   // fixup ends to meet clip box if close
155   fixPt(pt1);
156   fixPt(pt2);
157 
158   unsigned code1 = computeCode(pt1);
159   unsigned code2 = computeCode(pt2);
160 
161   // repeat until points are at edge of box
162   // bail out if we repeat too many times (avoid numerical issues)
163   for(unsigned count = 0 ; count < 16 ; count++ )
164     {
165       if( (code1 | code2) == 0 )
166         {
167           // no more clipping required - inside
168           return true;
169         }
170       else if( (code1 & code2) != 0 )
171         {
172           // line should not be drawn - outside
173           return false;
174         }
175       else
176         {
177           // compute intersection
178 
179           // which point to compute for?
180           const unsigned code = (code1 != 0) ? code1 : code2;
181 
182           // get intersection new point and new code for line
183           QPointF pt;
184           if( code & LEFT )
185             pt = vertIntersection(clip.left(), pt1, pt2);
186           else if( code & RIGHT )
187             pt = vertIntersection(clip.right(), pt1, pt2);
188           else if( code & TOP )
189             pt = horzIntersection(clip.top(), pt1, pt2);
190           else if ( code & BOTTOM )
191             pt = horzIntersection(clip.bottom(), pt1, pt2);
192 
193           // update point as intersection
194           if( code == code1 )
195             {
196               // changed first point
197               pt1 = pt;
198               code1 = computeCode(pt1);
199             }
200           else
201             {
202               // changed second point
203               pt2 = pt;
204               code2 = computeCode(pt2);
205             }
206         }
207     } // loop
208   return false;
209 }
210 
211 
clipLine(const QRectF & clip,QPointF & pt1,QPointF & pt2)212 bool clipLine(const QRectF& clip, QPointF& pt1, QPointF& pt2)
213 {
214   _Clipper clipper(clip);
215   return clipper.clipLine(pt1, pt2);
216 }
217 
218 //////////////////////////////////////////////////////////////////////
219 
clipPolyline(const QPolygonF & poly)220 void _PolyClipper::clipPolyline(const QPolygonF& poly)
221 {
222   // exit if fewer than 2 points in polygon
223   if ( poly.size() < 2 )
224     return;
225 
226   // output goes here
227   QPolygonF pout;
228 
229   QPolygonF::const_iterator polyiter = poly.begin();
230   QPointF lastpt = *polyiter;
231   polyiter++;
232 
233   for( ; polyiter != poly.end(); ++polyiter )
234     {
235       QPointF p1 = lastpt;
236       QPointF p2 = *polyiter;
237 
238       bool plotline = _clipper.clipLine(p1, p2);
239       if( plotline )
240         {
241           if( pout.isEmpty() )
242             {
243               // add first line
244               pout << p1;
245               if( ! smallDelta(p1, p2) )
246                 pout << p2;
247             }
248           else
249             {
250               if( p1 == pout.last() )
251                 {
252                   if( ! smallDelta(p1, p2) )
253                     // extend polyline
254                     pout << p2;
255                 }
256               else
257                 {
258                   // paint existing line
259                   if( pout.size() >= 2 )
260                     emitPolyline(pout);
261 
262                   // start new line
263                   pout.clear();
264                   pout << p1;
265                   if( ! smallDelta(p1, p2) )
266                     pout << p2;
267                 }
268             }
269         }
270       else
271         {
272           // line isn't in region, so ignore results from clip function
273 
274           // paint existing line
275           if( pout.size() >= 2 )
276             emitPolyline(pout);
277 
278           // cleanup
279           pout.clear();
280         }
281 
282 
283       lastpt = *polyiter;
284     }
285 
286   if( pout.size() >= 2 )
287     emitPolyline(pout);
288 }
289 
290 // class used for drawing clipped polylines
291 
292 class PlotDrawCallback : public _PolyClipper
293 {
294  public:
PlotDrawCallback(QRectF clip,QPainter & painter)295   PlotDrawCallback(QRectF clip, QPainter& painter)
296     : _PolyClipper(clip),
297       _painter(painter)
298   {}
299 
emitPolyline(const QPolygonF & poly)300   void emitPolyline(const QPolygonF& poly)
301   {
302     _painter.drawPolyline(poly);
303   }
304 
305  private:
306   QPainter& _painter;
307 };
308 
309 // take polyline and paint to painter, clipping
plotClippedPolyline(QPainter & painter,QRectF clip,const QPolygonF & poly,bool autoexpand)310 void plotClippedPolyline(QPainter& painter,
311                          QRectF clip,
312                          const QPolygonF& poly,
313                          bool autoexpand)
314 {
315   // if autoexpand, expand rectangle by line width
316   if ( autoexpand )
317     {
318       const qreal lw = painter.pen().widthF();
319       clip.adjust(-lw, -lw, lw, lw);
320     }
321 
322   PlotDrawCallback pcb(clip, painter);
323   pcb.clipPolyline(poly);
324 }
325 
326 //////////////////////////////////////////////////////
327 // clip polyline and add polyines clipped to a vector
328 
329 class PolyAddCallback : public _PolyClipper
330 {
331  public:
PolyAddCallback(QRectF clip)332   PolyAddCallback(QRectF clip)
333     : _PolyClipper(clip) {}
334 
emitPolyline(const QPolygonF & poly)335   void emitPolyline(const QPolygonF& poly)
336   {
337     polys.push_back(poly);
338   }
339 
340 public:
341   QVector<QPolygonF> polys;
342 };
343 
clipPolyline(QRectF clip,const QPolygonF & poly)344 QVector<QPolygonF> clipPolyline(QRectF clip, const QPolygonF& poly)
345 {
346   PolyAddCallback pcb(clip);
347   pcb.clipPolyline(poly);
348   return pcb.polys;
349 }
350 
351 //////////////////////////////////////////////////////
352 
353 typedef QVector<QPolygonF> PolyVector;
354 
355 // clip polygon, adding clipped parts to output vector of polygons
356 class _LineLabClipper : public _PolyClipper
357 {
358 public:
_LineLabClipper(QRectF cliprect,PolyVector & polyvec)359   _LineLabClipper(QRectF cliprect, PolyVector& polyvec)
360     : _PolyClipper(cliprect),
361       _polyvec(polyvec)
362   {
363   }
364 
emitPolyline(const QPolygonF & poly)365   void emitPolyline(const QPolygonF& poly)
366   {
367     _polyvec.append(poly);
368   }
369 
370 private:
371   PolyVector& _polyvec;
372 };
373 
374 ///////////////////////////////////////////////////////
375 
376 // Check whether polygons intersect
377 // http://stackoverflow.com/questions/10962379/how-to-check-intersection-between-2-rotated-rectangles
378 
379 // note: requires clockwise polygons
380 
doPolygonsIntersect(const QPolygonF & a,const QPolygonF & b)381 bool doPolygonsIntersect(const QPolygonF& a, const QPolygonF& b)
382 {
383   for(auto const& poly : {a, b})
384     {
385       QPointF prevpt(poly.last());
386 
387       for(auto const& currpt : poly)
388         {
389           // normal to line segment
390           const double normx = currpt.y()-prevpt.y();
391           const double normy = prevpt.x()-currpt.x();
392 
393           double minA = std::numeric_limits<double>::max();
394           double maxA = std::numeric_limits<double>::lowest();
395           for(auto const& pt : a)
396             {
397               const double proj = normx*pt.x() + normy*pt.y();
398               minA = std::min(minA, proj);
399               maxA = std::max(maxA, proj);
400             }
401 
402           double minB = std::numeric_limits<double>::max();
403           double maxB = std::numeric_limits<double>::lowest();
404           for(auto const& pt : b)
405             {
406               const double proj = normx*pt.x() + normy*pt.y();
407               minB = std::min(minB, proj);
408               maxB = std::max(maxB, proj);
409             }
410 
411           if(maxA<minB || maxB<minA)
412             return false;
413 
414           prevpt = currpt;
415         }
416     }
417 
418   return true;
419 }
420 
421 ///////////////////////////////////////////////////////
422 
rotateAboutOrigin(double dtheta)423 void RotatedRectangle::rotateAboutOrigin(double dtheta)
424 {
425   // rotate centre as well as increasing angle to rotate about origin
426   const double c = std::cos(dtheta);
427   const double s = std::sin(dtheta);
428   const double tcx = cx;
429   const double tcy = cy;
430 
431   cx = c*tcx - s*tcy;
432   cy = c*tcy + s*tcx;
433 
434   angle += dtheta;
435 }
436 
437 
438 // note: output polygon is clockwise
makePolygon() const439 QPolygonF RotatedRectangle::makePolygon() const
440 {
441   const double c = std::cos(angle);
442   const double s = std::sin(angle);
443   const double xh = 0.5*xw;
444   const double yh = 0.5*yw;
445 
446   QPolygonF poly;
447   poly << QPointF(-xh*c + yh*s + cx, -xh*s - yh*c + cy);
448   poly << QPointF(-xh*c - yh*s + cx, -xh*s + yh*c + cy);
449   poly << QPointF( xh*c - yh*s + cx,  xh*s + yh*c + cy);
450   poly << QPointF( xh*c + yh*s + cx,  xh*s - yh*c + cy);
451   return poly;
452 }
453 
RectangleOverlapTester()454 RectangleOverlapTester::RectangleOverlapTester()
455 {
456 }
457 
willOverlap(const RotatedRectangle & rect) const458 bool RectangleOverlapTester::willOverlap(const RotatedRectangle& rect) const
459 {
460   const QPolygonF thispoly(rect.makePolygon());
461 
462   for(auto const &ir : _rects)
463     {
464       if( doPolygonsIntersect(thispoly, ir.makePolygon()) )
465         return true;
466     }
467 
468   return false;
469 }
470 
debug(QPainter & painter) const471 void RectangleOverlapTester::debug(QPainter& painter) const
472 {
473   for(auto const &rect : _rects)
474     {
475       QPolygonF poly = rect.makePolygon();
476       painter.drawPolygon(poly);
477     }
478 }
479 
480 ///////////////////////////////////////////////////////
481 
LineLabeller(QRectF cliprect,bool rotatelabels)482 LineLabeller::LineLabeller(QRectF cliprect, bool rotatelabels)
483   : _cliprect(cliprect),
484     _rotatelabels(rotatelabels)
485 {
486 }
487 
~LineLabeller()488 LineLabeller::~LineLabeller()
489 {
490 }
491 
drawAt(int idx,RotatedRectangle r)492 void LineLabeller::drawAt(int idx, RotatedRectangle r)
493 {
494 }
495 
addLine(const QPolygonF & poly,QSizeF textsize)496 void LineLabeller::addLine(const QPolygonF& poly, QSizeF textsize)
497 {
498   _polys.append( PolyVector() );
499   _textsizes.append(textsize);
500   _LineLabClipper clipper(_cliprect, _polys.last());
501   clipper.clipPolyline(poly);
502 }
503 
504 // returns RotatedRectangle with zero size if error
findLinePosition(const QPolygonF & poly,double frac,QSizeF size)505 RotatedRectangle LineLabeller::findLinePosition(const QPolygonF& poly,
506                                                 double frac, QSizeF size)
507 {
508   double totlength = 0;
509   for(int i = 1; i < poly.size(); ++i)
510     {
511       totlength += std::sqrt( sqr(poly[i-1].x()-poly[i].x()) +
512                               sqr(poly[i-1].y()-poly[i].y()) );
513     }
514 
515   // don't label lines which are too short
516   if( totlength/2 < std::max(size.width(), size.height()) )
517     return RotatedRectangle();
518 
519   // now repeat and stop when we've reached the right length
520   double length = 0;
521   for(int i = 1; i < poly.size(); ++i)
522     {
523       const double seglength = std::sqrt( sqr(poly[i-1].x()-poly[i].x()) +
524                                           sqr(poly[i-1].y()-poly[i].y()) );
525       if(length + seglength >= totlength*frac)
526         {
527           // interpolate along edge
528           const double fseg = (totlength*frac - length) / seglength;
529           const double xp = poly[i-1].x()*(1-fseg) + poly[i].x()*fseg;
530           const double yp = poly[i-1].y()*(1-fseg) + poly[i].y()*fseg;
531 
532           const double angle = _rotatelabels ?
533             std::atan2( poly[i].y() - poly[i-1].y(),
534                         poly[i].x() - poly[i-1].x() )
535             : 0.;
536           return RotatedRectangle(xp, yp, size.width(),
537                                   size.height(), angle);
538         }
539 
540       length += seglength;
541     }
542 
543   // shouldn't get here
544   return RotatedRectangle();
545 }
546 
547 // these are the positions where labels might be placed
548 namespace
549 {
550 #define NUM_LABEL_POSITIONS 7
551   const double label_positions[NUM_LABEL_POSITIONS] = {
552     0.5, 1/3., 2/3., 0.4, 0.6, 0.25, 0.75};
553 }
554 
process()555 void LineLabeller::process()
556 {
557   RectangleOverlapTester rtest;
558 
559   // iterate over each set of polylines
560   for(int polyseti = 0; polyseti < _polys.size(); ++polyseti)
561     {
562       const PolyVector& pv = _polys[polyseti];
563       QSizeF size = _textsizes[polyseti];
564 
565       for(int polyi = 0; polyi < pv.size(); ++polyi)
566         {
567           for(int posi = 0; posi < NUM_LABEL_POSITIONS; ++posi)
568             {
569               const RotatedRectangle r =
570                 findLinePosition(pv[polyi], label_positions[posi], size);
571               if( ! r.isValid() )
572                 break;
573 
574               if( ! rtest.willOverlap(r) )
575                 {
576                   drawAt(polyseti, r);
577                   rtest.addRect(r);
578                   break; // only add label once
579                 }
580             } // positions
581 
582         } // polylines in set of polylines
583 
584     } // sets of polylines
585 }
586 
getPolySet(int i) const587 QVector<QPolygonF> LineLabeller::getPolySet(int i) const
588 {
589   if( i >= 0 && i < _polys.size() )
590     return _polys[i];
591   return QVector<QPolygonF>();
592 }
593