1 /*
2 * Copyright 2012, 2013 Thomas Schöps
3 * Copyright 2012-2019 Kai Pastor
4 *
5 * This file is part of OpenOrienteering.
6 *
7 * OpenOrienteering is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * OpenOrienteering is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with OpenOrienteering. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21
22 #include "symbol.h"
23
24 #include <algorithm>
25 #include <cmath>
26 #include <cstddef>
27 #include <iterator>
28 #include <memory>
29
30 #include <QtGlobal>
31 #include <QBuffer>
32 #include <QByteArray>
33 #include <QImageReader>
34 #include <QImageWriter>
35 #include <QLatin1Char>
36 #include <QLatin1String>
37 #include <QPainter>
38 #include <QPoint>
39 #include <QPointF>
40 #include <QRectF>
41 #include <QStringRef>
42 #include <QVariant>
43 #include <QXmlStreamReader>
44 #include <QXmlStreamWriter>
45
46 #include "settings.h"
47 #include "core/map.h"
48 #include "core/map_color.h"
49 #include "core/map_coord.h"
50 #include "core/objects/object.h"
51 #include "core/objects/text_object.h"
52 #include "core/renderables/renderable.h"
53 #include "core/renderables/renderable_implementation.h"
54 #include "core/symbols/area_symbol.h"
55 #include "core/symbols/combined_symbol.h"
56 #include "core/symbols/line_symbol.h"
57 #include "core/symbols/point_symbol.h"
58 #include "core/symbols/text_symbol.h"
59 #include "fileformats/file_format.h"
60 #include "fileformats/file_import_export.h"
61 #include "util/xml_stream_util.h"
62 #include "gui/util_gui.h"
63
64 // IWYU pragma: no_include <QObject>
65
66
67 namespace OpenOrienteering {
68
Symbol(Type type)69 Symbol::Symbol(Type type) noexcept
70 : number { { -1, -1, -1 } }
71 , type { type }
72 , is_helper_symbol { false }
73 , is_hidden { false }
74 , is_protected { false }
75 {
76 // nothing else
77 }
78
79
Symbol(const Symbol & proto)80 Symbol::Symbol(const Symbol& proto)
81 : icon { proto.icon }
82 , custom_icon { proto.custom_icon }
83 , name { proto.name }
84 , description { proto.description }
85 , number ( proto.number ) // Cannot use {} with Android gcc 4.9
86 , type { proto.type }
87 , is_helper_symbol { proto.is_helper_symbol }
88 , is_hidden { proto.is_hidden }
89 , is_protected { proto.is_protected }
90 , is_rotatable { proto.is_rotatable }
91 {
92 // nothing else
93 }
94
95
96 Symbol::~Symbol() = default;
97
98
99
validate() const100 bool Symbol::validate() const
101 {
102 return true;
103 }
104
105
106
equals(const Symbol * other,Qt::CaseSensitivity case_sensitivity) const107 bool Symbol::equals(const Symbol* other, Qt::CaseSensitivity case_sensitivity) const
108 {
109 return type == other->type
110 && numberEquals(other)
111 && is_helper_symbol == other->is_helper_symbol
112 && is_rotatable == other->is_rotatable
113 && name.compare(other->name, case_sensitivity) == 0
114 && description.compare(other->description, case_sensitivity) == 0
115 && equalsImpl(other, case_sensitivity);
116 }
117
118
stateEquals(const Symbol * other) const119 bool Symbol::stateEquals(const Symbol* other) const
120 {
121 return is_hidden == other->is_hidden
122 && is_protected == other->is_protected;
123 }
124
125
126
asPoint() const127 const PointSymbol* Symbol::asPoint() const
128 {
129 Q_ASSERT(type == Point);
130 return static_cast<const PointSymbol*>(this);
131 }
asPoint()132 PointSymbol* Symbol::asPoint()
133 {
134 Q_ASSERT(type == Point);
135 return static_cast<PointSymbol*>(this);
136 }
asLine() const137 const LineSymbol* Symbol::asLine() const
138 {
139 Q_ASSERT(type == Line);
140 return static_cast<const LineSymbol*>(this);
141 }
asLine()142 LineSymbol* Symbol::asLine()
143 {
144 Q_ASSERT(type == Line);
145 return static_cast<LineSymbol*>(this);
146 }
asArea() const147 const AreaSymbol* Symbol::asArea() const
148 {
149 Q_ASSERT(type == Area);
150 return static_cast<const AreaSymbol*>(this);
151 }
asArea()152 AreaSymbol* Symbol::asArea()
153 {
154 Q_ASSERT(type == Area);
155 return static_cast<AreaSymbol*>(this);
156 }
asText() const157 const TextSymbol* Symbol::asText() const
158 {
159 Q_ASSERT(type == Text);
160 return static_cast<const TextSymbol*>(this);
161 }
asText()162 TextSymbol* Symbol::asText()
163 {
164 Q_ASSERT(type == Text);
165 return static_cast<TextSymbol*>(this);
166 }
asCombined() const167 const CombinedSymbol* Symbol::asCombined() const
168 {
169 Q_ASSERT(type == Combined);
170 return static_cast<const CombinedSymbol*>(this);
171 }
asCombined()172 CombinedSymbol* Symbol::asCombined()
173 {
174 Q_ASSERT(type == Combined);
175 return static_cast<CombinedSymbol*>(this);
176 }
177
178
179
getContainedTypes() const180 Symbol::TypeCombination Symbol::getContainedTypes() const
181 {
182 return getType();
183 }
184
185
isTypeCompatibleTo(const Object * object) const186 bool Symbol::isTypeCompatibleTo(const Object* object) const
187 {
188 switch (object->getType())
189 {
190 case Object::Point:
191 return type == Point;
192 case Object::Path:
193 return type == Line || type == Area || type == Combined;
194 case Object::Text:
195 return type == Text;
196 }
197 return false;
198 }
199
200
201
numberEquals(const Symbol * other) const202 bool Symbol::numberEquals(const Symbol* other) const
203 {
204 for (auto lhs = begin(number), rhs = begin(other->number); lhs != end(number); ++lhs, ++rhs)
205 {
206 if (*lhs != *rhs)
207 return false;
208 if (*lhs == -1 && *rhs == -1)
209 break;
210 }
211 return true;
212 }
213
214
numberEqualsRelaxed(const Symbol * other) const215 bool Symbol::numberEqualsRelaxed(const Symbol* other) const
216 {
217 for (auto lhs = begin(number), rhs = begin(other->number); lhs != end(number) && rhs != end(other->number); ++lhs, ++rhs)
218 {
219 // When encountering -1 on one side and 0 on the other side,
220 // move forward over all zeros on the other side.
221 if (Q_UNLIKELY(*lhs == -1 && *rhs == 0))
222 {
223 do
224 {
225 ++rhs;
226 if (rhs == end(other->number))
227 return true;
228 }
229 while (*rhs == 0);
230 }
231 else if (Q_UNLIKELY(*lhs == 0 && *rhs == -1))
232 {
233 do
234 {
235 ++lhs;
236 if (lhs == end(number))
237 return true;
238 }
239 while (*lhs == 0);
240 }
241
242 if (*lhs != *rhs)
243 return false;
244 if (*lhs == -1 && *rhs == -1)
245 break;
246 }
247 return true;
248 }
249
250
251
save(QXmlStreamWriter & xml,const Map & map) const252 void Symbol::save(QXmlStreamWriter& xml, const Map& map) const
253 {
254 XmlElementWriter symbol_element(xml, QLatin1String("symbol"));
255 symbol_element.writeAttribute(QLatin1String("type"), int(type));
256 auto id = map.findSymbolIndex(this);
257 if (id >= 0)
258 symbol_element.writeAttribute(QLatin1String("id"), id); // unique if given
259 symbol_element.writeAttribute(QLatin1String("code"), getNumberAsString()); // not always unique
260 if (!name.isEmpty())
261 symbol_element.writeAttribute(QLatin1String("name"), name);
262 symbol_element.writeAttribute(QLatin1String("is_helper_symbol"), is_helper_symbol);
263 symbol_element.writeAttribute(QLatin1String("is_hidden"), is_hidden);
264 symbol_element.writeAttribute(QLatin1String("is_protected"), is_protected);
265 if (!description.isEmpty())
266 xml.writeTextElement(QLatin1String("description"), description);
267 saveImpl(xml, map);
268 if (!custom_icon.isNull())
269 {
270 QBuffer buffer;
271 QImageWriter writer{&buffer, QByteArrayLiteral("PNG")};
272 if (writer.write(custom_icon))
273 {
274 auto data = buffer.data().toBase64();
275 // The "data" URL scheme, RFC2397 (https://tools.ietf.org/html/rfc2397)
276 data.insert(0, "data:image/png;base64,");
277 xml.writeCharacters(QLatin1String("\n"));
278 XmlElementWriter icon_element(xml, QLatin1String("icon"));
279 icon_element.writeAttribute(QLatin1String("src"), QString::fromLatin1(data));
280 }
281 else
282 {
283 qDebug("Couldn't save symbol icon '%s': %s",
284 qPrintable(getPlainTextName()),
285 qPrintable(writer.errorString()) );
286 }
287 }
288 }
289
load(QXmlStreamReader & xml,const Map & map,SymbolDictionary & symbol_dict,int version)290 std::unique_ptr<Symbol> Symbol::load(QXmlStreamReader& xml, const Map& map, SymbolDictionary& symbol_dict, int version)
291 {
292 Q_ASSERT(xml.name() == QLatin1String("symbol"));
293 XmlElementReader symbol_element(xml);
294 auto symbol_type = symbol_element.attribute<int>(QLatin1String("type"));
295 auto symbol = Symbol::makeSymbolForType(static_cast<Symbol::Type>(symbol_type));
296 if (!symbol)
297 throw FileFormatException(::OpenOrienteering::ImportExport::tr("Error while loading a symbol of type %1 at line %2 column %3.").arg(symbol_type).arg(xml.lineNumber()).arg(xml.columnNumber()));
298
299 auto code = symbol_element.attribute<QString>(QLatin1String("code"));
300 if (symbol_element.hasAttribute(QLatin1String("id")))
301 {
302 const auto id = symbol_element.attribute<QString>(QLatin1String("id"));
303
304 bool conversion_ok;
305 auto const converted_id = id.toInt(&conversion_ok);
306
307 if (!id.isEmpty())
308 {
309 if (conversion_ok)
310 {
311 if (symbol_dict.contains(converted_id))
312 throw FileFormatException(::OpenOrienteering::ImportExport::tr("Symbol ID '%1' not unique at line %2 column %3.").arg(id).arg(xml.lineNumber()).arg(xml.columnNumber()));
313
314 symbol_dict[converted_id] = symbol.get(); // Will be dangling pointer when we throw an exception later
315 }
316 else
317 {
318 throw FileFormatException(::OpenOrienteering::ImportExport::tr("Malformed symbol ID '%1' at line %2 column %3.")
319 .arg(id).arg(xml.lineNumber())
320 .arg(xml.columnNumber()));
321 }
322 }
323
324 if (code.isEmpty())
325 code = id;
326 }
327
328 std::fill(begin(symbol->number), end(symbol->number), -1);
329 if (!code.isEmpty())
330 {
331 int pos = 0;
332 for (auto i = 0u; i < number_components; ++i)
333 {
334 int dot = code.indexOf(QLatin1Char('.'), pos+1);
335 symbol->number[i] = code.midRef(pos, (dot == -1) ? -1 : (dot - pos)).toInt();
336 pos = ++dot;
337 if (pos < 1)
338 break;
339 }
340 }
341
342 symbol->name = symbol_element.attribute<QString>(QLatin1String("name"));
343 symbol->is_helper_symbol = symbol_element.attribute<bool>(QLatin1String("is_helper_symbol"));
344 symbol->is_hidden = symbol_element.attribute<bool>(QLatin1String("is_hidden"));
345 symbol->is_protected = symbol_element.attribute<bool>(QLatin1String("is_protected"));
346
347 while (xml.readNextStartElement())
348 {
349 if (xml.name() == QLatin1String("description"))
350 {
351 symbol->description = xml.readElementText();
352 }
353 else if (xml.name() == QLatin1String("icon"))
354 {
355 XmlElementReader icon_element(xml);
356 auto data = icon_element.attribute<QStringRef>(QLatin1String("src")).toLatin1();
357
358 // The "data" URL scheme, RFC2397 (https://tools.ietf.org/html/rfc2397)
359 auto start = data.indexOf(',') + 1;
360 if (start > 0 && data.startsWith("data:image/"))
361 {
362 auto base64 = data.indexOf(";base64");
363 data = data.remove(0, start);
364 if (base64 + 8 == start)
365 data = QByteArray::fromBase64(data);
366 }
367 else
368 {
369 data.clear(); // Ignore unknown data, warning is generated later.
370 }
371
372 QBuffer buffer{&data};
373 QImageReader reader{&buffer, QByteArrayLiteral("PNG")};
374 auto icon = reader.read();
375 if (!icon.isNull())
376 symbol->setCustomIcon(icon);
377 else
378 qDebug("Couldn't load symbol icon '%s': %s",
379 qPrintable(symbol->getPlainTextName()),
380 qPrintable(reader.errorString()) );
381 }
382 else
383 {
384 if (!symbol->loadImpl(xml, map, symbol_dict, version))
385 xml.skipCurrentElement();
386 }
387 }
388
389 if (xml.error())
390 {
391 throw FileFormatException(
392 ::OpenOrienteering::ImportExport::tr("Error while loading a symbol of type %1 at line %2 column %3: %4")
393 .arg(symbol_type)
394 .arg(xml.lineNumber())
395 .arg(xml.columnNumber())
396 .arg(xml.errorString()) );
397 }
398
399 return symbol;
400 }
401
402
403
loadingFinishedEvent(Map *)404 bool Symbol::loadingFinishedEvent(Map* /*map*/)
405 {
406 return true;
407 }
408
409
410
createRenderables(const PathObject *,const PathPartVector &,ObjectRenderables &,Symbol::RenderableOptions) const411 void Symbol::createRenderables(
412 const PathObject* /*object*/,
413 const PathPartVector& /*path_parts*/,
414 ObjectRenderables& /*output*/,
415 Symbol::RenderableOptions /*options*/) const
416 {
417 qWarning("Missing implementation of Symbol::createRenderables(const PathObject*, const PathPartVector&, ObjectRenderables&, Symbol::RenderableOptions)");
418 }
419
420
createBaselineRenderables(const PathObject *,const PathPartVector & path_parts,ObjectRenderables & output,const MapColor * color) const421 void Symbol::createBaselineRenderables(
422 const PathObject* /*object*/,
423 const PathPartVector& path_parts,
424 ObjectRenderables& output,
425 const MapColor* color) const
426 {
427 Q_ASSERT((getContainedTypes() & ~(Symbol::Line | Symbol::Area | Symbol::Combined)) == 0);
428
429 if (color)
430 {
431 // Insert line renderable
432 LineSymbol line_symbol;
433 line_symbol.setColor(color);
434 line_symbol.setLineWidth(0);
435 for (const auto& part : path_parts)
436 {
437 output.insertRenderable(new LineRenderable(&line_symbol, part, false));
438 }
439 }
440 }
441
442
443
symbolChangedEvent(const Symbol *,const Symbol *)444 bool Symbol::symbolChangedEvent(const Symbol* /*old_symbol*/, const Symbol* /*new_symbol*/)
445 {
446 return false;
447 }
448
449
containsSymbol(const Symbol *) const450 bool Symbol::containsSymbol(const Symbol* /*symbol*/) const
451 {
452 return false;
453 }
454
455
456
setCustomIcon(const QImage & image)457 void Symbol::setCustomIcon(const QImage& image)
458 {
459 resetIcon(); // Cache must become scaled version of custom icon.
460 custom_icon = image;
461 }
462
463
getIcon(const Map * map) const464 QImage Symbol::getIcon(const Map* map) const
465 {
466 if (icon.isNull())
467 {
468 auto size = Settings::getInstance().getSymbolWidgetIconSizePx();
469 if (Settings::getInstance().getSetting(Settings::SymbolWidget_ShowCustomIcons).toBool()
470 && !custom_icon.isNull())
471 icon = custom_icon.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
472 else if (map)
473 icon = createIcon(*map, size);
474 }
475 return icon;
476 }
477
478
createIcon(const Map & map,int side_length,bool antialiasing,qreal zoom) const479 QImage Symbol::createIcon(const Map& map, int side_length, bool antialiasing, qreal zoom) const
480 {
481 // Desktop default used to be 2x zoom at 8 mm side length, plus/minus
482 // a border of one white pixel around some objects.
483 // If the icon is bigger than the rectangle with this zoom factor, the zoom
484 // is reduced later to make the icon fit into the rectangle.
485 if (zoom <= 0)
486 zoom = map.symbolIconZoom();
487 auto max_icon_mm_half = 0.5 / zoom;
488
489 // Create image
490 QImage image(side_length, side_length, QImage::Format_ARGB32_Premultiplied);
491 static auto const background = qPremultiply(qRgba(254, 254, 254, 255));
492 image.fill(background);
493
494 QPainter painter(&image);
495 if (antialiasing)
496 painter.setRenderHint(QPainter::Antialiasing);
497
498 // Make background transparent
499 auto mode = painter.compositionMode();
500 painter.setCompositionMode(QPainter::CompositionMode_Clear);
501 painter.setCompositionMode(mode);
502 painter.translate(0.5 * side_length, 0.5 * side_length);
503
504 // Create geometry
505 Object* object = nullptr;
506 std::unique_ptr<Symbol> symbol_copy;
507 auto offset = MapCoord{};
508 auto contained_types = getContainedTypes();
509 if (type == Point)
510 {
511 max_icon_mm_half *= 0.90; // white border
512 object = new PointObject(static_cast<const PointSymbol*>(this));
513 }
514 else if (type == Text)
515 {
516 max_icon_mm_half *= 0.95; // white border
517 auto text = new TextObject(this);
518 text->setText(static_cast<const TextSymbol*>(this)->getIconText());
519 object = text;
520 }
521 else if (type == Area || (type == Combined && contained_types & Area))
522 {
523 auto path = new PathObject(this);
524 path->addCoordinate(0, MapCoord(-max_icon_mm_half, -max_icon_mm_half));
525 path->addCoordinate(1, MapCoord(max_icon_mm_half, -max_icon_mm_half));
526 path->addCoordinate(2, MapCoord(max_icon_mm_half, max_icon_mm_half));
527 path->addCoordinate(3, MapCoord(-max_icon_mm_half, max_icon_mm_half));
528 path->parts().front().setClosed(true, true);
529 object = path;
530 }
531 else if (type == Line || type == Combined)
532 {
533 bool show_dash_symbol = false;
534 auto line_length_half = max_icon_mm_half;
535 if (type == Line)
536 {
537 auto line = static_cast<const LineSymbol*>(this);
538 if (!line->isDashed() || line->getBreakLength() <= 0)
539 {
540 if (line->getCapStyle() == LineSymbol::RoundCap)
541 {
542 offset.setNativeX(-line->getLineWidth()/3);
543 }
544 else if (line->getCapStyle() == LineSymbol::PointedCap)
545 {
546 line_length_half = std::max(line_length_half, 0.0006 * (line->startOffset() + line->endOffset()));
547 }
548 }
549
550 if (line->getDashSymbol() && !line->getDashSymbol()->isEmpty())
551 {
552 line_length_half = std::max(line_length_half, line->getDashSymbol()->dimensionForIcon());
553 show_dash_symbol = true;
554 }
555
556 if (line->getMidSymbol() && !line->getMidSymbol()->isEmpty())
557 {
558 auto icon_size = line->getMidSymbol()->dimensionForIcon();
559 icon_size += line->getMidSymbolDistance() * (line->getMidSymbolsPerSpot() - 1) / 2000;
560 line_length_half = std::max(line_length_half, icon_size);
561
562 if (!line->getShowAtLeastOneSymbol())
563 {
564 if (!symbol_copy)
565 symbol_copy = duplicate(*line);
566 auto icon_line = static_cast<LineSymbol*>(symbol_copy.get());
567 icon_line->setShowAtLeastOneSymbol(true);
568 }
569 }
570
571 if (line->getStartSymbol() && !line->getStartSymbol()->isEmpty())
572 {
573 line_length_half = std::max(line_length_half, line->getStartSymbol()->dimensionForIcon() / 2);
574 }
575
576 if (line->getEndSymbol() && !line->getEndSymbol()->isEmpty())
577 {
578 line_length_half = std::max(line_length_half, line->getEndSymbol()->dimensionForIcon() / 2);
579 }
580
581 // If there are breaks in the line, scale them down so they fit into the icon exactly.
582 auto max_ideal_length = 0;
583 if (line->isDashed() && line->getBreakLength() > 0)
584 {
585 auto ideal_length = 2 * line->getDashesInGroup() * line->getDashLength()
586 + 2 * (line->getDashesInGroup() - 1) * line->getInGroupBreakLength()
587 + 3 * line->getBreakLength() / 2;
588 if (max_ideal_length < ideal_length)
589 max_ideal_length = ideal_length;
590 }
591 if (line->hasBorder())
592 {
593 auto& border = line->getBorder();
594 if (border.dashed && border.break_length > 0)
595 {
596 auto ideal_length = 2 * border.dash_length + border.break_length;
597 if (max_ideal_length < ideal_length)
598 max_ideal_length = ideal_length;
599 }
600 auto& right_border = line->getRightBorder();
601 if (line->areBordersDifferent() && right_border.dashed && right_border.break_length > 0)
602 {
603 auto ideal_length = 2 * right_border.dash_length + right_border.break_length;
604 if (max_ideal_length < ideal_length)
605 max_ideal_length = ideal_length;
606 }
607 }
608 if (max_ideal_length > 0)
609 {
610 auto cap_length = 0;
611 auto offset_factor = qreal(0);
612 auto ideal_length_half = qreal(max_ideal_length) / 2000;
613
614 if (line->getCapStyle() == LineSymbol::PointedCap)
615 {
616 offset_factor = 1;
617 ideal_length_half += qreal(line->startOffset() + line->endOffset()) / 2000;
618 }
619 else if (line->getCapStyle() != LineSymbol::FlatCap)
620 {
621 cap_length = line->getLineWidth();
622 ideal_length_half += qreal(cap_length) / 1000;
623 }
624
625 auto factor = qMin(qreal(0.5), line_length_half / qMax(qreal(0.001), ideal_length_half));
626 offset_factor *= factor;
627
628 if (!symbol_copy)
629 symbol_copy = duplicate(*line);
630
631 auto icon_line = static_cast<LineSymbol*>(symbol_copy.get());
632 icon_line->setDashLength(qRound(factor * icon_line->getDashLength()));
633 icon_line->setBreakLength(cap_length + qRound(factor * (icon_line->getBreakLength() - cap_length)));
634 icon_line->setInGroupBreakLength(qRound(factor * icon_line->getInGroupBreakLength()));
635 icon_line->setShowAtLeastOneSymbol(true);
636 icon_line->getBorder().dash_length *= factor;
637 icon_line->getBorder().break_length *= factor;
638 icon_line->getRightBorder().dash_length *= factor;
639 icon_line->getRightBorder().break_length *= factor;
640 icon_line->setStartOffset(qRound(offset_factor * icon_line->startOffset()));
641 icon_line->setEndOffset(qRound(offset_factor * icon_line->endOffset()));
642 }
643 }
644 else if (type == Combined)
645 {
646 auto max_ideal_length = 0;
647 auto combined = static_cast<const CombinedSymbol*>(this);
648 for (int i = 0; i < combined->getNumParts(); ++i)
649 {
650 auto part = combined->getPart(i);
651 if (part && part->getType() == Line)
652 {
653 auto line = static_cast<const LineSymbol*>(part);
654 auto dash_symbol = line->getDashSymbol();
655 if (dash_symbol && !dash_symbol->isEmpty())
656 show_dash_symbol = true;
657
658 if (line->isDashed() && line->getBreakLength() > 0)
659 {
660 auto ideal_length = 2 * line->getDashesInGroup() * line->getDashLength()
661 + 2 * (line->getDashesInGroup() - 1) * line->getInGroupBreakLength()
662 + line->getBreakLength();
663 if (max_ideal_length < ideal_length)
664 max_ideal_length = ideal_length;
665 }
666 if (line->hasBorder())
667 {
668 auto& border = line->getBorder();
669 if (border.dashed && border.break_length > 0)
670 {
671 auto ideal_length = 2 * border.dash_length + border.break_length;
672 if (max_ideal_length < ideal_length)
673 max_ideal_length = ideal_length;
674 }
675 auto& right_border = line->getRightBorder();
676 if (line->areBordersDifferent() && right_border.dashed && right_border.break_length > 0)
677 {
678 auto ideal_length = 2 * right_border.dash_length + right_border.break_length;
679 if (max_ideal_length < ideal_length)
680 max_ideal_length = ideal_length;
681 }
682 }
683 }
684 }
685 // If there are breaks in the line, scale them down so they fit into the icon exactly.
686 if (max_ideal_length > 0)
687 {
688 auto ideal_length_half = qreal(max_ideal_length) / 2000;
689 auto factor = qMin(qreal(0.5), line_length_half / qMax(qreal(0.001), ideal_length_half));
690
691 symbol_copy.reset(new CombinedSymbol());
692 static_cast<CombinedSymbol*>(symbol_copy.get())->setNumParts(combined->getNumParts());
693
694 for (int i = 0; i < combined->getNumParts(); ++i)
695 {
696 auto proto = static_cast<const CombinedSymbol*>(combined)->getPart(i);
697 if (!proto)
698 continue;
699
700 auto copy = duplicate(*proto);
701 if (copy->getType() == Line)
702 {
703 auto icon_line = static_cast<LineSymbol*>(copy.get());
704 icon_line->setDashLength(qRound(factor * icon_line->getDashLength()));
705 icon_line->setBreakLength(qRound(factor * icon_line->getBreakLength()));
706 icon_line->setInGroupBreakLength(qRound(factor * icon_line->getInGroupBreakLength()));
707 icon_line->setShowAtLeastOneSymbol(true);
708 icon_line->getBorder().dash_length *= factor;
709 icon_line->getBorder().break_length *= factor;
710 icon_line->getRightBorder().dash_length *= factor;
711 icon_line->getRightBorder().break_length *= factor;
712 }
713 static_cast<CombinedSymbol*>(symbol_copy.get())->setPart(i, copy.release(), true);
714 }
715 }
716 }
717
718 auto path = new PathObject(symbol_copy ? symbol_copy.get() : this);
719 path->addCoordinate(0, MapCoord(-line_length_half, 0.0));
720 path->addCoordinate(1, MapCoord(line_length_half, 0.0));
721 if (show_dash_symbol)
722 {
723 MapCoord dash_coord(0, 0);
724 dash_coord.setDashPoint(true);
725 path->addCoordinate(1, dash_coord);
726 }
727 object = path;
728 }
729 else
730 {
731 qWarning("Unhandled symbol: %s", qPrintable(getDescription()));
732 return image;
733 }
734
735 // Create icon map and view
736 Map icon_map;
737 // const_cast promise: We won't change the colors, thus we won't change map.
738 icon_map.useColorsFrom(const_cast<Map*>(&map));
739 icon_map.setScaleDenominator(map.getScaleDenominator());
740 icon_map.addObject(object);
741
742 const auto& extent = object->getExtent();
743 auto w = std::max(std::abs(extent.left()), std::abs(extent.right()));
744 auto h = std::max(std::abs(extent.top()), std::abs(extent.bottom()));
745 auto real_icon_mm_half = std::max(w, h);
746 if (real_icon_mm_half <= 0)
747 return image;
748
749 auto final_zoom = side_length * zoom * std::min(qreal(1), max_icon_mm_half / real_icon_mm_half);
750 painter.scale(final_zoom, final_zoom);
751
752 if (type == Text)
753 {
754 // Center text
755 painter.translate(-extent.center());
756 }
757 else if (type == Point)
758 {
759 // Do not completely offset the symbols relative position
760 painter.translate(-extent.center() / 2);
761 }
762 else if (contained_types & Line && !(contained_types & Area))
763 {
764 painter.translate(MapCoordF(-offset));
765 }
766
767 // Ensure that an icon is created for hidden symbols.
768 if (is_hidden && !symbol_copy)
769 {
770 symbol_copy = duplicate(*this);
771 object->setSymbol(symbol_copy.get(), true);
772 }
773 if (symbol_copy)
774 {
775 symbol_copy->setHidden(false);
776 }
777
778 auto config = RenderConfig { map, QRectF(-10000, -10000, 20000, 20000), final_zoom, RenderConfig::HelperSymbols, 1.0 };
779 icon_map.draw(&painter, config);
780 painter.end();
781
782 // Add shadow to dominant white on transparent
783 auto color = guessDominantColor();
784 if (color && color->isWhite())
785 {
786 for (int y = image.height() - 1; y >= 0; --y)
787 {
788 for (int x = image.width() - 1; x >= 0; --x)
789 {
790 if (image.pixel(x, y) != background)
791 continue;
792
793 auto is_white = [](const QRgb& rgb) {
794 return rgb > 0
795 && qAlpha(rgb) == qRed(rgb)
796 && qRed(rgb) == qGreen(rgb)
797 && qGreen(rgb) == qBlue(rgb);
798 };
799 if (x > 0)
800 {
801 auto preceding = image.pixel(x-1, y);
802 if (is_white(preceding))
803 {
804 image.setPixel(x, y, qPremultiply(qRgba(192, 192, 192, 255)));
805 continue;
806 }
807 }
808 if (y > 0)
809 {
810 auto preceding = image.pixel(x, y-1);
811 if (is_white(preceding))
812 {
813 image.setPixel(x, y, qPremultiply(qRgba(192, 192, 192, 255)));
814 }
815 }
816 }
817 }
818 }
819
820 return image;
821 }
822
823
resetIcon()824 void Symbol::resetIcon()
825 {
826 icon = {};
827 }
828
829
dimensionForIcon() const830 qreal Symbol::dimensionForIcon() const
831 {
832 return 0;
833 }
834
835
836
calculateLargestLineExtent() const837 qreal Symbol::calculateLargestLineExtent() const
838 {
839 return 0;
840 }
841
842
843
getPlainTextName() const844 QString Symbol::getPlainTextName() const
845 {
846 return Util::plainText(name);
847 }
848
849
850
getNumberAsString() const851 QString Symbol::getNumberAsString() const
852 {
853 QString string;
854 string.reserve(4 * number_components - 1);
855 for (auto n : number)
856 {
857 if (n < 0)
858 break;
859 string.append(QString::number(n));
860 string.append(QLatin1Char('.'));
861 }
862 string.chop(1);
863 return string;
864 }
865
866
867
setRotatable(bool value)868 void Symbol::setRotatable(bool value)
869 {
870 is_rotatable = value;
871 }
872
873
874
makeSymbolForType(Symbol::Type type)875 std::unique_ptr<Symbol> Symbol::makeSymbolForType(Symbol::Type type)
876 {
877 switch (type)
878 {
879 case Area:
880 return std::make_unique<AreaSymbol>();
881 case Combined:
882 return std::make_unique<CombinedSymbol>();
883 case Line:
884 return std::make_unique<LineSymbol>();
885 case Point:
886 return std::make_unique<PointSymbol>();
887 case Text:
888 return std::make_unique<TextSymbol>();
889 default:
890 return std::unique_ptr<Symbol>();
891 }
892 }
893
894
895
areTypesCompatible(Symbol::Type a,Symbol::Type b)896 bool Symbol::areTypesCompatible(Symbol::Type a, Symbol::Type b)
897 {
898 return (getCompatibleTypes(a) & b) != 0;
899 }
900
901
getCompatibleTypes(Symbol::Type type)902 Symbol::TypeCombination Symbol::getCompatibleTypes(Symbol::Type type)
903 {
904 switch (type)
905 {
906 case Area:
907 case Combined:
908 case Line:
909 return Line | Area | Combined;
910 case Point:
911 case Text:
912 default:
913 return type;
914 }
915 }
916
917
918
lessByNumber(const Symbol * s1,const Symbol * s2)919 bool Symbol::lessByNumber(const Symbol* s1, const Symbol* s2)
920 {
921 for (auto i = 0u; i < number_components; i++)
922 {
923 if (s1->number[i] < s2->number[i]) return true; // s1 < s2
924 if (s1->number[i] > s2->number[i]) return false; // s1 > s2
925 }
926 return false; // s1 == s2
927 }
928
lessByColorPriority(const Symbol * s1,const Symbol * s2)929 bool Symbol::lessByColorPriority(const Symbol* s1, const Symbol* s2)
930 {
931 const MapColor* c1 = s1->guessDominantColor();
932 const MapColor* c2 = s2->guessDominantColor();
933
934 if (c1 && c2)
935 return c1->comparePriority(*c2);
936 else if (c2)
937 return true;
938 else
939 return false;
940 }
941
942
943
lessByColor(const Map * map)944 Symbol::lessByColor::lessByColor(const Map* map)
945 {
946 colors.reserve(std::size_t(map->getNumColors()));
947 for (int i = 0; i < map->getNumColors(); ++i)
948 colors.push_back(QRgb(*map->getColor(i)));
949 }
950
951
operator ()(const Symbol * s1,const Symbol * s2) const952 bool Symbol::lessByColor::operator() (const Symbol* s1, const Symbol* s2) const
953 {
954 auto c2 = s2->guessDominantColor();
955 if (!c2)
956 return false;
957
958 auto c1 = s1->guessDominantColor();
959 if (!c1)
960 return true;
961
962 const auto rgb_c1 = QRgb(*c1);
963 const auto rgb_c2 = QRgb(*c2);
964 if (rgb_c1 == rgb_c2)
965 return false;
966
967 const auto last = colors.rend();
968 auto first = std::find_if(colors.rbegin(), last, [rgb_c2](const auto rgb) {
969 return rgb == rgb_c2;
970 });
971 auto second = std::find_if(first, last, [rgb_c1](const auto rgb) {
972 return rgb == rgb_c1;
973 });
974 return second != last;
975 }
976
977
978 } // namespace OpenOrienteering
979