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