Added support for line "dy" parameter
[GPXSee.git] / src / map / mapsforge / style.cpp
blob49df83c39813bccb3ef4c4b8acd97415d2d87ccf
1 #include <QFile>
2 #include <QXmlStreamReader>
3 #include <QUrl>
4 #include <QFileInfo>
5 #include <QImageReader>
6 #include "common/programpaths.h"
7 #include "style.h"
9 using namespace Mapsforge;
11 static QString resourcePath(const QString &src, const QString &dir)
13 QUrl url(src);
14 if (url.scheme().isEmpty())
15 return src;
16 else
17 return dir + "/" + url.toLocalFile();
20 static QImage image(const QString &path, int width, int height, qreal ratio)
22 QImageReader ir(path, "svg");
24 if (ir.canRead()) {
25 if (!height && !width) {
26 height = 20;
27 width = 20;
28 } else if (!width) {
29 width = ir.size().height() / (ir.size().height() / (double)height);
30 } else if (!height)
31 height = ir.size().width() / (ir.size().width() / (double)width);
33 ir.setScaledSize(QSize(width * ratio, height * ratio));
34 QImage img(ir.read());
35 img.setDevicePixelRatio(ratio);
36 return img;
37 } else
38 return QImage(path);
41 static QList<unsigned> keyList(const MapData &data, const QList<QByteArray> &in)
43 QList<unsigned> out;
45 for (int i = 0; i < in.size(); i++) {
46 if (in.at(i) == "*")
47 out.append(0);
48 else {
49 unsigned key = data.tagId(in.at(i));
50 if (key)
51 out.append(key);
55 return out;
58 static QList<QByteArray> valList(const QList<QByteArray> &in)
60 QList<QByteArray> out;
62 for (int i = 0; i < in.size(); i++) {
63 if (in.at(i) == "*")
64 out.append(QByteArray());
65 else
66 out.append(in.at(i));
69 return out;
72 Style::Rule::Filter::Filter(const MapData &data, const QList<QByteArray> &keys,
73 const QList<QByteArray> &vals) : _neg(false)
75 _keys = keyList(data, keys);
77 QList<QByteArray> vc(vals);
78 if (vc.removeAll("~"))
79 _neg = true;
80 _vals = valList(vc);
83 bool Style::Rule::match(const QVector<MapData::Tag> &tags) const
85 for (int i = 0; i < _filters.size(); i++)
86 if (!_filters.at(i).match(tags))
87 return false;
89 return true;
92 bool Style::Rule::match(bool closed, const QVector<MapData::Tag> &tags) const
94 Closed cl = closed ? YesClosed : NoClosed;
96 if (_closed && cl != _closed)
97 return false;
99 for (int i = 0; i < _filters.size(); i++)
100 if (!_filters.at(i).match(tags))
101 return false;
103 return true;
106 bool Style::Rule::match(int zoom, bool closed,
107 const QVector<MapData::Tag> &tags) const
109 Closed cl = closed ? YesClosed : NoClosed;
111 if (!_zooms.contains(zoom))
112 return false;
113 if (_closed && cl != _closed)
114 return false;
116 for (int i = 0; i < _filters.size(); i++)
117 if (!_filters.at(i).match(tags))
118 return false;
120 return true;
123 bool Style::Rule::match(int zoom, const QVector<MapData::Tag> &tags) const
125 if (!_zooms.contains(zoom))
126 return false;
128 for (int i = 0; i < _filters.size(); i++)
129 if (!_filters.at(i).match(tags))
130 return false;
132 return true;
135 void Style::area(QXmlStreamReader &reader, const QString &dir, qreal ratio,
136 const Rule &rule)
138 PathRender ri(rule, _paths.size() + _circles.size());
139 const QXmlStreamAttributes &attr = reader.attributes();
140 QString file;
141 QColor fillColor;
142 int height = 0, width = 0;
143 bool ok;
145 ri._area = true;
146 if (attr.hasAttribute("fill"))
147 fillColor = QColor(attr.value("fill").toString());
148 if (attr.hasAttribute("stroke"))
149 ri._strokeColor = QColor(attr.value("stroke").toString());
150 if (attr.hasAttribute("stroke-width")) {
151 ri._strokeWidth = attr.value("stroke-width").toFloat(&ok);
152 if (!ok || ri._strokeWidth < 0) {
153 reader.raiseError("invalid stroke-width value");
154 return;
157 if (attr.hasAttribute("scale")) {
158 QString scale(attr.value("scale").toString());
159 if (scale == "all")
160 ri._scale = PathRender::Scale::All;
161 else if (scale == "none")
162 ri._scale = PathRender::Scale::None;
164 if (attr.hasAttribute("src"))
165 file = resourcePath(attr.value("src").toString(), dir);
166 if (attr.hasAttribute("symbol-height")) {
167 height = attr.value("symbol-height").toInt(&ok);
168 if (!ok || height < 0) {
169 reader.raiseError("invalid symbol-height value");
170 return;
173 if (attr.hasAttribute("symbol-width")) {
174 width = attr.value("symbol-width").toInt(&ok);
175 if (!ok || width < 0) {
176 reader.raiseError("invalid symbol-width value");
177 return;
181 if (!file.isNull())
182 ri._brush = QBrush(image(file, width, height, ratio));
183 else if (fillColor.isValid())
184 ri._brush = QBrush(fillColor);
186 if (ri.rule()._type == Rule::AnyType || ri.rule()._type == Rule::WayType)
187 _paths.append(ri);
189 reader.skipCurrentElement();
192 void Style::line(QXmlStreamReader &reader, const Rule &rule)
194 PathRender ri(rule, _paths.size() + _circles.size());
195 const QXmlStreamAttributes &attr = reader.attributes();
196 bool ok;
198 if (attr.hasAttribute("stroke"))
199 ri._strokeColor = QColor(attr.value("stroke").toString());
200 if (attr.hasAttribute("stroke-width")) {
201 ri._strokeWidth = attr.value("stroke-width").toFloat(&ok);
202 if (!ok || ri._strokeWidth < 0) {
203 reader.raiseError("invalid stroke-width value");
204 return;
207 if (attr.hasAttribute("stroke-dasharray")) {
208 QStringList l(attr.value("stroke-dasharray").toString().split(','));
209 ri._strokeDasharray.resize(l.size());
210 for (int i = 0; i < l.size(); i++) {
211 ri._strokeDasharray[i] = l.at(i).toDouble(&ok);
212 if (!ok || ri._strokeDasharray[i] < 0) {
213 reader.raiseError("invalid stroke-dasharray value");
214 return;
218 if (attr.hasAttribute("stroke-linecap")) {
219 QString cap(attr.value("stroke-linecap").toString());
220 if (cap == "butt")
221 ri._strokeCap = Qt::FlatCap;
222 else if (cap == "round")
223 ri._strokeCap = Qt::RoundCap;
224 else if (cap == "square")
225 ri._strokeCap = Qt::SquareCap;
227 if (attr.hasAttribute("stroke-linejoin")) {
228 QString join(attr.value("stroke-linejoin").toString());
229 if (join == "miter")
230 ri._strokeJoin = Qt::MiterJoin;
231 else if (join == "round")
232 ri._strokeJoin = Qt::RoundJoin;
233 else if (join == "bevel")
234 ri._strokeJoin = Qt::BevelJoin;
236 if (attr.hasAttribute("scale")) {
237 QString scale(attr.value("scale").toString());
238 if (scale == "all")
239 ri._scale = PathRender::Scale::All;
240 else if (scale == "none")
241 ri._scale = PathRender::Scale::None;
243 if (attr.hasAttribute("curve")) {
244 QString curve(attr.value("curve").toString());
245 if (curve == "cubic")
246 ri._curve = true;
248 if (attr.hasAttribute("dy")) {
249 ri._dy = attr.value("dy").toDouble(&ok);
250 if (!ok) {
251 reader.raiseError("invalid dy value");
252 return;
256 if (ri.rule()._type == Rule::AnyType || ri.rule()._type == Rule::WayType)
257 _paths.append(ri);
259 reader.skipCurrentElement();
262 void Style::circle(QXmlStreamReader &reader, const Rule &rule)
264 CircleRender ri(rule, _paths.size() + _circles.size());
265 const QXmlStreamAttributes &attr = reader.attributes();
266 bool ok;
267 QColor fillColor, strokeColor;
268 qreal strokeWidth = 0;
270 if (attr.hasAttribute("fill"))
271 fillColor = QColor(attr.value("fill").toString());
272 if (attr.hasAttribute("stroke"))
273 strokeColor = QColor(attr.value("stroke").toString());
274 if (attr.hasAttribute("stroke-width")) {
275 strokeWidth = attr.value("stroke-width").toFloat(&ok);
276 if (!ok || strokeWidth < 0) {
277 reader.raiseError("invalid stroke-width value");
278 return;
281 if (attr.hasAttribute("radius")) {
282 ri._radius = attr.value("radius").toDouble(&ok);
283 if (!ok || ri._radius <= 0) {
284 reader.raiseError("invalid radius value");
285 return;
287 } else {
288 reader.raiseError("missing radius");
289 return;
291 if (attr.hasAttribute("scale-radius")) {
292 if (attr.value("scale-radius").toString() == "true")
293 ri._scale = true;
296 ri._pen = (strokeColor.isValid() && strokeWidth > 0)
297 ? QPen(QBrush(strokeColor), strokeWidth) : Qt::NoPen;
298 ri._brush = fillColor.isValid() ? QBrush(fillColor) : Qt::NoBrush;
300 if (ri.rule()._type == Rule::AnyType || ri.rule()._type == Rule::NodeType)
301 _circles.append(ri);
303 reader.skipCurrentElement();
306 void Style::text(QXmlStreamReader &reader, const MapData &data, const Rule &rule,
307 QList<QList<TextRender>*> &lists)
309 TextRender ri(rule);
310 const QXmlStreamAttributes &attr = reader.attributes();
311 int fontSize = 9;
312 bool bold = false, italic = false;
313 QString fontFamily("Helvetica");
314 QFont::Capitalization capitalization = QFont::MixedCase;
315 bool ok;
317 if (attr.hasAttribute("k"))
318 ri._key = data.tagId(attr.value("k").toLatin1());
319 if (attr.hasAttribute("fill"))
320 ri._fillColor = QColor(attr.value("fill").toString());
321 if (attr.hasAttribute("stroke"))
322 ri._strokeColor = QColor(attr.value("stroke").toString());
323 if (attr.hasAttribute("stroke-width")) {
324 ri._strokeWidth = attr.value("stroke-width").toFloat(&ok);
325 if (!ok || ri._strokeWidth < 0) {
326 reader.raiseError("invalid stroke-width value");
327 return;
330 if (attr.hasAttribute("font-size")) {
331 fontSize = attr.value("font-size").toFloat(&ok);
332 if (!ok || fontSize < 0) {
333 reader.raiseError("invalid font-size value");
334 return;
337 if (attr.hasAttribute("font-style")) {
338 QString style(attr.value("font-style").toString());
339 if (style == "bold")
340 bold = true;
341 else if (style == "italic")
342 italic = true;
343 else if (style == "bold_italic") {
344 bold = true;
345 italic = true;
348 if (attr.hasAttribute("font-family")) {
349 QString family(attr.value("font-family").toString());
350 if (family == "monospace")
351 fontFamily = "Courier New";
352 else if (family == "serif")
353 fontFamily = "Times New Roman";
355 if (attr.hasAttribute("text-transform")) {
356 QString transform(attr.value("text-transform").toString());
357 if (transform == "uppercase")
358 capitalization = QFont::AllUppercase;
359 else if (transform == "lowercase")
360 capitalization = QFont::AllLowercase;
361 else if (transform == "capitalize")
362 capitalization = QFont::Capitalize;
364 if (attr.hasAttribute("priority")) {
365 ri._priority = attr.value("priority").toInt(&ok);
366 if (!ok) {
367 reader.raiseError("invalid priority value");
368 return;
372 ri._font.setFamily(fontFamily);
373 ri._font.setPixelSize(fontSize);
374 ri._font.setBold(bold);
375 ri._font.setItalic(italic);
376 ri._font.setCapitalization(capitalization);
378 if (fontSize)
379 for (int i = 0; i < lists.size(); i++)
380 lists[i]->append(ri);
382 reader.skipCurrentElement();
385 void Style::symbol(QXmlStreamReader &reader, const QString &dir, qreal ratio,
386 const Rule &rule)
388 Symbol ri(rule);
389 const QXmlStreamAttributes &attr = reader.attributes();
390 QString file;
391 int height = 0, width = 0;
392 bool ok;
394 if (attr.hasAttribute("src"))
395 file = resourcePath(attr.value("src").toString(), dir);
396 else {
397 reader.raiseError("missing src value");
398 return;
400 if (attr.hasAttribute("symbol-height")) {
401 height = attr.value("symbol-height").toInt(&ok);
402 if (!ok || height < 0) {
403 reader.raiseError("invalid symbol-height value");
404 return;
407 if (attr.hasAttribute("symbol-width")) {
408 width = attr.value("symbol-width").toInt(&ok);
409 if (!ok || width < 0) {
410 reader.raiseError("invalid symbol-width value");
411 return;
414 if (attr.hasAttribute("priority")) {
415 ri._priority = attr.value("priority").toInt(&ok);
416 if (!ok) {
417 reader.raiseError("invalid priority value");
418 return;
422 ri._img = image(file, width, height, ratio);
424 _symbols.append(ri);
426 reader.skipCurrentElement();
429 void Style::rule(QXmlStreamReader &reader, const QString &dir,
430 const MapData &data, qreal ratio, const QSet<QString> &cats,
431 const Rule &parent)
433 Rule r(parent);
434 const QXmlStreamAttributes &attr = reader.attributes();
435 bool ok;
437 if (attr.hasAttribute("cat")
438 && !cats.contains(attr.value("cat").toString())) {
439 reader.skipCurrentElement();
440 return;
443 if (attr.value("e").toString() == "way")
444 r.setType(Rule::WayType);
445 else if (attr.value("e").toString() == "node")
446 r.setType(Rule::NodeType);
448 if (attr.hasAttribute("zoom-min")) {
449 r.setMinZoom(attr.value("zoom-min").toInt(&ok));
450 if (!ok || r._zooms.min() < 0) {
451 reader.raiseError("invalid zoom-min value");
452 return;
455 if (attr.hasAttribute("zoom-max")) {
456 r.setMaxZoom(attr.value("zoom-max").toInt(&ok));
457 if (!ok || r._zooms.max() < 0) {
458 reader.raiseError("invalid zoom-max value");
459 return;
463 if (attr.hasAttribute("closed")) {
464 if (attr.value("closed").toString() == "yes")
465 r.setClosed(Rule::YesClosed);
466 else if (attr.value("closed").toString() == "no")
467 r.setClosed(Rule::NoClosed);
470 QList<QByteArray> keys(attr.value("k").toLatin1().split('|'));
471 QList<QByteArray> vals(attr.value("v").toLatin1().split('|'));
472 r.addFilter(Rule::Filter(data, keys, vals));
474 while (reader.readNextStartElement()) {
475 if (reader.name() == QLatin1String("rule"))
476 rule(reader, dir, data, ratio, cats, r);
477 else if (reader.name() == QLatin1String("area"))
478 area(reader, dir, ratio, r);
479 else if (reader.name() == QLatin1String("line"))
480 line(reader, r);
481 else if (reader.name() == QLatin1String("circle"))
482 circle(reader, r);
483 else if (reader.name() == QLatin1String("pathText")) {
484 QList<QList<TextRender>*> list;
485 list.append(&_pathLabels);
486 text(reader, data, r, list);
487 } else if (reader.name() == QLatin1String("caption")) {
488 QList<QList<TextRender>*> list;
489 if (r._type == Rule::WayType || r._type == Rule::AnyType)
490 list.append(&_areaLabels);
491 if (r._type == Rule::NodeType || r._type == Rule::AnyType)
492 list.append(&_pointLabels);
493 text(reader, data, r, list);
495 else if (reader.name() == QLatin1String("symbol"))
496 symbol(reader, dir, ratio, r);
497 else
498 reader.skipCurrentElement();
502 void Style::cat(QXmlStreamReader &reader, QSet<QString> &cats)
504 const QXmlStreamAttributes &attr = reader.attributes();
506 cats.insert(attr.value("id").toString());
508 reader.skipCurrentElement();
511 void Style::layer(QXmlStreamReader &reader, QSet<QString> &cats)
513 const QXmlStreamAttributes &attr = reader.attributes();
514 bool enabled = (attr.value("enabled").toString() == "true");
516 while (reader.readNextStartElement()) {
517 if (enabled && reader.name() == QLatin1String("cat"))
518 cat(reader, cats);
519 else
520 reader.skipCurrentElement();
524 void Style::stylemenu(QXmlStreamReader &reader, QSet<QString> &cats)
526 while (reader.readNextStartElement()) {
527 if (reader.name() == QLatin1String("layer"))
528 layer(reader, cats);
529 else
530 reader.skipCurrentElement();
534 void Style::rendertheme(QXmlStreamReader &reader, const QString &dir,
535 const MapData &data, qreal ratio)
537 Rule r;
538 QSet<QString> cats;
540 while (reader.readNextStartElement()) {
541 if (reader.name() == QLatin1String("rule"))
542 rule(reader, dir, data, ratio, cats, r);
543 else if (reader.name() == QLatin1String("stylemenu"))
544 stylemenu(reader, cats);
545 else
546 reader.skipCurrentElement();
550 bool Style::loadXml(const QString &path, const MapData &data, qreal ratio)
552 QFile file(path);
553 if (!file.open(QFile::ReadOnly))
554 return false;
555 QXmlStreamReader reader(&file);
556 QFileInfo fi(path);
558 if (reader.readNextStartElement()) {
559 if (reader.name() == QLatin1String("rendertheme"))
560 rendertheme(reader, fi.absolutePath(), data, ratio);
561 else
562 reader.raiseError("Not a Mapsforge style file");
565 if (reader.error())
566 qWarning("%s:%lld %s", qPrintable(path), reader.lineNumber(),
567 qPrintable(reader.errorString()));
569 return !reader.error();
572 void Style::load(const MapData &data, qreal ratio)
574 QString path(ProgramPaths::renderthemeFile());
576 if (!QFileInfo::exists(path) || !loadXml(path, data, ratio))
577 loadXml(":/mapsforge/default.xml", data, ratio);
580 void Style::clear()
582 _paths.clear();
583 _circles.clear();
584 _pathLabels.clear();
585 _pointLabels.clear();
586 _areaLabels.clear();
587 _symbols.clear();
590 QList<const Style::PathRender *> Style::paths(int zoom, bool closed,
591 const QVector<MapData::Tag> &tags) const
593 QList<const PathRender*> ri;
595 for (int i = 0; i < _paths.size(); i++)
596 if (_paths.at(i).rule().match(zoom, closed, tags))
597 ri.append(&_paths.at(i));
599 return ri;
602 QList<const Style::CircleRender *> Style::circles(int zoom,
603 const QVector<MapData::Tag> &tags) const
605 QList<const CircleRender*> ri;
607 for (int i = 0; i < _circles.size(); i++)
608 if (_circles.at(i).rule().match(zoom, tags))
609 ri.append(&_circles.at(i));
611 return ri;
614 QList<const Style::TextRender*> Style::pathLabels(int zoom) const
616 QList<const TextRender*> list;
618 for (int i = 0; i < _pathLabels.size(); i++)
619 if (_pathLabels.at(i).rule()._zooms.contains(zoom))
620 list.append(&_pathLabels.at(i));
622 return list;
625 QList<const Style::TextRender*> Style::pointLabels(int zoom) const
627 QList<const TextRender*> list;
629 for (int i = 0; i < _pointLabels.size(); i++)
630 if (_pointLabels.at(i).rule()._zooms.contains(zoom))
631 list.append(&_pointLabels.at(i));
633 return list;
636 QList<const Style::TextRender*> Style::areaLabels(int zoom) const
638 QList<const TextRender*> list;
640 for (int i = 0; i < _areaLabels.size(); i++)
641 if (_areaLabels.at(i).rule()._zooms.contains(zoom))
642 list.append(&_areaLabels.at(i));
644 return list;
647 QList<const Style::Symbol*> Style::pointSymbols(int zoom) const
649 QList<const Symbol*> list;
651 for (int i = 0; i < _symbols.size(); i++) {
652 const Symbol &symbol = _symbols.at(i);
653 const Rule & rule = symbol.rule();
654 if (rule._zooms.contains(zoom) && (rule._type == Rule::AnyType
655 || rule._type == Rule::NodeType))
656 list.append(&symbol);
659 return list;
662 QList<const Style::Symbol*> Style::areaSymbols(int zoom) const
664 QList<const Symbol*> list;
666 for (int i = 0; i < _symbols.size(); i++) {
667 const Symbol &symbol = _symbols.at(i);
668 const Rule &rule = symbol.rule();
669 if (rule._zooms.contains(zoom) && (rule._type == Rule::AnyType
670 || rule._type == Rule::WayType))
671 list.append(&symbol);
674 return list;
677 QPen Style::PathRender::pen(int zoom) const
679 if (_strokeColor.isValid()) {
680 qreal width = (_scale > None && zoom >= 12)
681 ? pow(1.5, zoom - 12) * _strokeWidth : _strokeWidth;
682 QPen p(QBrush(_strokeColor), width, Qt::SolidLine, _strokeCap,
683 _strokeJoin);
684 if (!_strokeDasharray.isEmpty()) {
685 QVector<qreal>pattern(_strokeDasharray);
686 for (int i = 0; i < _strokeDasharray.size(); i++) {
687 if (_scale > Stroke && zoom >= 12)
688 pattern[i] = (pow(1.5, zoom - 12) * pattern[i]);
689 // QPainter pattern is specified in units of the pens width!
690 pattern[i] /= width;
692 p.setDashPattern(pattern);
694 return p;
695 } else
696 return Qt::NoPen;
699 qreal Style::PathRender::dy(int zoom) const
701 return (_scale && zoom >= 12) ? pow(1.5, zoom - 12) * _dy : _dy;
704 qreal Style::CircleRender::radius(int zoom) const
706 return (_scale && zoom >= 12) ? pow(1.5, zoom - 12) * _radius : _radius;