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