1 /*
2  * LibrePCB - Professional EDA for everyone!
3  * Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
4  * https://librepcb.org/
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 /*******************************************************************************
21  *  Includes
22  ******************************************************************************/
23 #include "toolbox.h"
24 
25 #include <QtCore>
26 
27 /*******************************************************************************
28  *  Namespace
29  ******************************************************************************/
30 namespace librepcb {
31 
32 /*******************************************************************************
33  *  Static Methods
34  ******************************************************************************/
35 
shapeFromPath(const QPainterPath & path,const QPen & pen,const QBrush & brush,const UnsignedLength & minWidth)36 QPainterPath Toolbox::shapeFromPath(const QPainterPath& path, const QPen& pen,
37                                     const QBrush& brush,
38                                     const UnsignedLength& minWidth) noexcept {
39   // http://code.qt.io/cgit/qt/qtbase.git/tree/src/widgets/graphicsview/qgraphicsitem.cpp
40   // Function: qt_graphicsItem_shapeFromPath()
41 
42   if ((path.isEmpty()) || (pen.style() == Qt::NoPen) ||
43       (pen.brush().style() == Qt::NoBrush)) {
44     return path;
45   } else {
46     QPainterPathStroker ps;
47     ps.setCapStyle(pen.capStyle());
48     ps.setWidth(qMax(qMax(pen.widthF(), qreal(0.00000001)), minWidth->toPx()));
49     ps.setJoinStyle(pen.joinStyle());
50     ps.setMiterLimit(pen.miterLimit());
51     QPainterPath p = ps.createStroke(path);
52     if (brush.style() != Qt::NoBrush) {
53       p.addPath(path);
54     }
55     return p;
56   }
57 }
58 
arcRadius(const Point & p1,const Point & p2,const Angle & a)59 Length Toolbox::arcRadius(const Point& p1, const Point& p2,
60                           const Angle& a) noexcept {
61   if (a == 0) {
62     return Length(0);
63   } else {
64     qreal x1 = p1.getX().toMm();
65     qreal y1 = p1.getY().toMm();
66     qreal x2 = p2.getX().toMm();
67     qreal y2 = p2.getY().toMm();
68     qreal angle = a.mappedTo180deg().toRad();
69     qreal d = qSqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
70     qreal r = d / (2 * qSin(angle / 2));
71     return Length::fromMm(r);
72   }
73 }
74 
arcCenter(const Point & p1,const Point & p2,const Angle & a)75 Point Toolbox::arcCenter(const Point& p1, const Point& p2,
76                          const Angle& a) noexcept {
77   if (a == 0) {
78     // there is no arc center...just return the middle of start- and endpoint
79     return (p1 + p2) / 2;
80   } else {
81     // http://math.stackexchange.com/questions/27535/how-to-find-center-of-an-arc-given-start-point-end-point-radius-and-arc-direc
82     qreal x0 = p1.getX().toMm();
83     qreal y0 = p1.getY().toMm();
84     qreal x1 = p2.getX().toMm();
85     qreal y1 = p2.getY().toMm();
86     qreal angle = a.mappedTo180deg().toRad();
87     qreal angleSgn = (angle >= 0) ? 1 : -1;
88     qreal d = qSqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
89     qreal r = d / (2 * qSin(angle / 2));
90     qreal h = qSqrt(r * r - d * d / 4);
91     qreal u = (x1 - x0) / d;
92     qreal v = (y1 - y0) / d;
93     qreal a = ((x0 + x1) / 2) - h * v * angleSgn;
94     qreal b = ((y0 + y1) / 2) + h * u * angleSgn;
95     return Point::fromMm(a, b);
96   }
97 }
98 
nearestPointOnLine(const Point & p,const Point & l1,const Point & l2)99 Point Toolbox::nearestPointOnLine(const Point& p, const Point& l1,
100                                   const Point& l2) noexcept {
101   Point a = l2 - l1;
102   Point b = p - l1;
103   Point c = p - l2;
104   qreal d = ((b.getX().toMm() * a.getX().toMm()) +
105              (b.getY().toMm() * a.getY().toMm()));
106   qreal e = ((a.getX().toMm() * a.getX().toMm()) +
107              (a.getY().toMm() * a.getY().toMm()));
108   if (a.isOrigin() || b.isOrigin() || (d <= 0.0)) {
109     return l1;
110   } else if (c.isOrigin() || (e <= d)) {
111     return l2;
112   } else {
113     Q_ASSERT(e > 0.0);
114     return l1 + Point::fromMm(a.getX().toMm() * d / e, a.getY().toMm() * d / e);
115   }
116 }
117 
shortestDistanceBetweenPointAndLine(const Point & p,const Point & l1,const Point & l2,Point * nearest)118 UnsignedLength Toolbox::shortestDistanceBetweenPointAndLine(
119     const Point& p, const Point& l1, const Point& l2, Point* nearest) noexcept {
120   Point np = nearestPointOnLine(p, l1, l2);
121   if (nearest) {
122     *nearest = np;
123   }
124   return (p - np).getLength();
125 }
126 
incrementNumberInString(QString string)127 QString Toolbox::incrementNumberInString(QString string) noexcept {
128   QRegularExpression regex("([0-9]+)(?!.*[0-9]+)");
129   QRegularExpressionMatch match = regex.match(string);
130   if (match.hasMatch()) {
131     // string contains numbers -> increment last number
132     bool ok = false;
133     uint number = match.captured().toUInt(&ok);
134     if (ok) {
135       string.replace(match.capturedStart(), match.capturedLength(),
136                      QString::number(number + 1U));
137       return string;
138     }
139   }
140 
141   // fallback: just add a "1" at the end
142   return string % "1";
143 }
144 
expandRangesInString(const QString & string)145 QStringList Toolbox::expandRangesInString(const QString& string) noexcept {
146   // Do NOT accept '+' and '-', they are considered as strings, not numbers!
147   // For example in the range connector signals range "X-1..10" you expect
148   // numbers starting from 1, not -1.
149   QString number = "\\d+";
150   QString character = "[a-zA-Z]";
151   QString separator = "\\.\\.";
152   QString numberRange =
153       QString("(?<num_start>%1)%2(?<num_end>%1)").arg(number, separator);
154   QString characterRange =
155       QString("(?<char_start>%1)%2(?<char_end>%1)").arg(character, separator);
156   QString pattern =
157       QString("(?<num>%1)|(?<char>%2)").arg(numberRange, characterRange);
158   QRegularExpression re(pattern);
159   QRegularExpressionMatchIterator it = re.globalMatch(string);
160   QVector<std::tuple<int, int, QStringList>> replacements;
161   while (it.hasNext()) {
162     QRegularExpressionMatch match = it.next();
163     QStringList matchReplacements;
164     if (!match.captured("num").isEmpty()) {
165       bool okStart = false, okEnd = false;
166       int start = match.captured("num_start").toInt(&okStart);
167       int end = match.captured("num_end").toInt(&okEnd);
168       if (okStart && okEnd) {
169         bool invert = start > end;
170         if (invert) std::swap(start, end);
171         for (int i = start; i <= end; ++i) {
172           if (invert) {
173             matchReplacements.prepend(QString::number(i));
174           } else {
175             matchReplacements.append(QString::number(i));
176           }
177         }
178       }
179     } else if (!match.captured("char").isEmpty()) {
180       QString startStr = match.captured("char_start");
181       QString endStr = match.captured("char_end");
182       if ((startStr.length()) == 1 && (endStr.length() == 1)) {
183         int start = startStr[0].unicode();
184         int end = endStr[0].unicode();
185         bool invert = start > end;
186         if (invert) std::swap(start, end);
187         if (((start >= 'a') && (end <= 'z')) ||
188             ((start >= 'A') && (end <= 'Z'))) {
189           for (int i = start; i <= end; ++i) {
190             if (invert) {
191               matchReplacements.prepend(QChar::fromLatin1(i));
192             } else {
193               matchReplacements.append(QChar::fromLatin1(i));
194             }
195           }
196         }
197       }
198     }
199     // allow max. 4 replacements to avoid huge results
200     if (!matchReplacements.isEmpty() && (replacements.count() < 4)) {
201       replacements.append(std::make_tuple(
202           match.capturedStart(), match.capturedLength(), matchReplacements));
203     }
204   }
205   return expandRangesInString(string, replacements);
206 }
207 
cleanUserInputString(const QString & input,const QRegularExpression & removeRegex,bool trim,bool toLower,bool toUpper,const QString & spaceReplacement,int maxLength)208 QString Toolbox::cleanUserInputString(const QString& input,
209                                       const QRegularExpression& removeRegex,
210                                       bool trim, bool toLower, bool toUpper,
211                                       const QString& spaceReplacement,
212                                       int maxLength) noexcept {
213   // perform compatibility decomposition (NFKD)
214   QString ret = input.normalized(QString::NormalizationForm_KD);
215   // change case of all characters
216   if (toLower) ret = ret.toLower();
217   if (toUpper) ret = ret.toUpper();
218   // remove leading and trailing spaces
219   if (trim) ret = ret.trimmed();
220   // replace remaining spaces with replacement
221   ret.replace(" ", spaceReplacement);
222   // remove all invalid characters
223   ret.remove(removeRegex);
224   // truncate to maximum allowed length
225   if (maxLength >= 0) ret.truncate(maxLength);
226   // if there are leading or trailing spaces, remove them again ;)
227   if (trim) ret = ret.trimmed();
228   return ret;
229 }
230 
prettyPrintLocale(const QString & code)231 QString Toolbox::prettyPrintLocale(const QString& code) noexcept {
232   QLocale locale(code);
233   QString str = locale.nativeLanguageName();
234   if (str.isEmpty()) {
235     str = code;  // fallback if language code is not recognized
236   }
237   if (!locale.nativeCountryName().isEmpty()) {
238     str += " (" % locale.nativeCountryName() % ")";
239   }
240   return str;
241 }
242 
243 /*******************************************************************************
244  *  Private Methods
245  ******************************************************************************/
246 
expandRangesInString(const QString & input,const QVector<std::tuple<int,int,QStringList>> & replacements)247 QStringList Toolbox::expandRangesInString(
248     const QString& input,
249     const QVector<std::tuple<int, int, QStringList>>& replacements) noexcept {
250   if (replacements.isEmpty()) {
251     return QStringList{input};
252   } else {
253     QStringList result;
254     int pos = std::get<0>(replacements.first());
255     int len = std::get<1>(replacements.first());
256     foreach (const QString& replacement, std::get<2>(replacements.first())) {
257       foreach (QString str, expandRangesInString(input, replacements.mid(1))) {
258         result.append(str.replace(pos, len, replacement));
259       }
260     }
261     return result;
262   }
263 }
264 
265 /*******************************************************************************
266  *  End of File
267  ******************************************************************************/
268 
269 }  // namespace librepcb
270