1 /*
2  *    Copyright 2012, 2013 Thomas Schöps
3  *    Copyright 2012-2020 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 "line_symbol.h"
23 
24 #include <algorithm>
25 #include <cmath>
26 #include <initializer_list>
27 #include <iterator>
28 #include <memory>
29 #include <utility>
30 
31 #include <QtNumeric>
32 #include <QCoreApplication>
33 #include <QLatin1String>
34 #include <QStringRef>
35 #include <QXmlStreamAttributes>
36 #include <QXmlStreamReader>
37 #include <QXmlStreamWriter>
38 
39 #include <private/qbezier_p.h>
40 
41 #include "core/map.h"
42 #include "core/map_color.h"
43 #include "core/map_coord.h"
44 #include "core/path_coord.h"
45 #include "core/objects/object.h"
46 #include "core/renderables/renderable.h"
47 #include "core/renderables/renderable_implementation.h"
48 #include "core/symbols/area_symbol.h"
49 #include "core/symbols/point_symbol.h"
50 #include "core/symbols/symbol.h"
51 #include "core/virtual_coord_vector.h"
52 #include "core/virtual_path.h"
53 #include "util/xml_stream_util.h"
54 
55 namespace OpenOrienteering {
56 
57 using length_type = PathCoord::length_type;
58 
59 
60 // ### LineSymbolBorder ###
61 
save(QXmlStreamWriter & xml,const Map & map) const62 void LineSymbolBorder::save(QXmlStreamWriter& xml, const Map& map) const
63 {
64 	XmlElementWriter element(xml, QLatin1String("border"));
65 	element.writeAttribute(QLatin1String("color"), map.findColorIndex(color));
66 	element.writeAttribute(QLatin1String("width"), width);
67 	element.writeAttribute(QLatin1String("shift"), shift);
68 	if (dashed)
69 	{
70 		element.writeAttribute(QLatin1String("dashed"), true);
71 		element.writeAttribute(QLatin1String("dash_length"), dash_length);
72 		element.writeAttribute(QLatin1String("break_length"), break_length);
73 	}
74 }
75 
load(QXmlStreamReader & xml,const Map & map)76 bool LineSymbolBorder::load(QXmlStreamReader& xml, const Map& map)
77 {
78 	Q_ASSERT(xml.name() == QLatin1String("border"));
79 	XmlElementReader element(xml);
80 	color = map.getColor(element.attribute<int>(QLatin1String("color")));
81 	width = element.attribute<int>(QLatin1String("width"));
82 	shift = element.attribute<int>(QLatin1String("shift"));
83 	dashed = element.attribute<bool>(QLatin1String("dashed"));
84 	if (dashed)
85 	{
86 		dash_length = element.attribute<int>(QLatin1String("dash_length"));
87 		break_length = element.attribute<int>(QLatin1String("break_length"));
88 	}
89 	return !xml.error();
90 }
91 
92 
isVisible() const93 bool LineSymbolBorder::isVisible() const
94 {
95 	return width > 0 && color && !(dashed && dash_length == 0);
96 }
97 
setupSymbol(LineSymbol & out) const98 void LineSymbolBorder::setupSymbol(LineSymbol& out) const
99 {
100 	out.setLineWidth(0.001 * width);
101 	out.setColor(color);
102 
103 	if (dashed)
104 	{
105 		out.setDashed(true);
106 		out.setDashLength(dash_length);
107 		out.setBreakLength(break_length);
108 	}
109 }
110 
scale(double factor)111 void LineSymbolBorder::scale(double factor)
112 {
113 	width = qRound(factor * width);
114 	shift = qRound(factor * shift);
115 	dash_length = qRound(factor * dash_length);
116 	break_length = qRound(factor * break_length);
117 }
118 
119 
120 namespace {
121 
equalDimensions(const LineSymbolBorder & lhs,const LineSymbolBorder & rhs)122 bool equalDimensions(const LineSymbolBorder& lhs, const LineSymbolBorder& rhs)
123 {
124 	return lhs.width == rhs.width
125 	       && lhs.shift == rhs.shift
126 	       && lhs.dashed == rhs.dashed
127 	       && (!lhs.dashed
128 	           || (lhs.dash_length == rhs.dash_length
129 	               && lhs.break_length == rhs.break_length) );
130 }
131 
132 }  // namespace
133 
equals(const LineSymbolBorder & other) const134 bool LineSymbolBorder::equals(const LineSymbolBorder& other) const
135 {
136 	return (!isVisible() && !other.isVisible())
137 	       || (equalDimensions(*this, other)
138 	           && MapColor::equal(color, other.color) );
139 }
140 
operator ==(const LineSymbolBorder & lhs,const LineSymbolBorder & rhs)141 bool operator==(const LineSymbolBorder& lhs, const LineSymbolBorder& rhs) noexcept
142 {
143 	return (!lhs.isVisible() && !rhs.isVisible())
144 	       || (equalDimensions(lhs, rhs)
145 	           && lhs.color && rhs.color && *lhs.color == *rhs.color);
146 }
147 
148 
149 
150 // ### LineSymbol ###
151 
LineSymbol()152 LineSymbol::LineSymbol() noexcept
153 : Symbol ( Symbol::Line )
154 , start_symbol ( nullptr )
155 , mid_symbol ( nullptr )
156 , end_symbol ( nullptr )
157 , dash_symbol ( nullptr )
158 , color ( nullptr )
159 , line_width ( 0 )
160 , minimum_length ( 0 )
161 , start_offset ( 0 )
162 , end_offset ( 0 )
163 , mid_symbols_per_spot ( 1 )
164 , mid_symbol_distance ( 0 )
165 , minimum_mid_symbol_count ( 0 )
166 , minimum_mid_symbol_count_when_closed ( 0 )
167 , segment_length ( 4000 )
168 , end_length ( 0 )
169 , dash_length ( 4000 )
170 , break_length ( 1000 )
171 , dashes_in_group ( 1 )
172 , in_group_break_length ( 500 )
173 , cap_style ( FlatCap )
174 , join_style ( MiterJoin )
175 , mid_symbol_placement ( CenterOfDash )
176 , dashed ( false )
177 , half_outer_dashes ( false )
178 , show_at_least_one_symbol ( true )
179 , suppress_dash_symbol_at_ends ( false )
180 , scale_dash_symbol ( true )
181 , have_border_lines ( false )
182 {
183 	// nothing else
184 }
185 
186 
LineSymbol(const LineSymbol & proto)187 LineSymbol::LineSymbol(const LineSymbol& proto)
188 : Symbol ( proto )
189 , border ( proto.border )
190 , right_border ( proto.right_border )
191 , start_symbol ( proto.start_symbol ? Symbol::duplicate(*proto.start_symbol).release() : nullptr )
192 , mid_symbol ( proto.mid_symbol ? Symbol::duplicate(*proto.mid_symbol).release() : nullptr )
193 , end_symbol ( proto.end_symbol ? Symbol::duplicate(*proto.end_symbol).release() : nullptr )
194 , dash_symbol ( proto.dash_symbol ? Symbol::duplicate(*proto.dash_symbol).release() : nullptr )
195 , color ( proto.color )
196 , line_width ( proto.line_width )
197 , minimum_length ( proto.minimum_length )
198 , start_offset ( proto.start_offset )
199 , end_offset ( proto.end_offset )
200 , mid_symbols_per_spot ( proto.mid_symbols_per_spot )
201 , mid_symbol_distance ( proto.mid_symbol_distance )
202 , minimum_mid_symbol_count ( proto.minimum_mid_symbol_count )
203 , minimum_mid_symbol_count_when_closed ( proto.minimum_mid_symbol_count_when_closed )
204 , segment_length ( proto.segment_length )
205 , end_length ( proto.end_length )
206 , dash_length ( proto.dash_length )
207 , break_length ( proto.break_length )
208 , dashes_in_group ( proto.dashes_in_group )
209 , in_group_break_length ( proto.in_group_break_length )
210 , cap_style ( proto.cap_style )
211 , join_style ( proto.join_style )
212 , mid_symbol_placement ( proto.mid_symbol_placement )
213 , dashed ( proto.dashed )
214 , half_outer_dashes ( proto.half_outer_dashes )
215 , show_at_least_one_symbol ( proto.show_at_least_one_symbol )
216 , suppress_dash_symbol_at_ends ( proto.suppress_dash_symbol_at_ends )
217 , scale_dash_symbol ( proto.scale_dash_symbol )
218 , have_border_lines ( proto.have_border_lines )
219 {
220 	// nothing else
221 }
222 
223 
~LineSymbol()224 LineSymbol::~LineSymbol()
225 {
226 	delete start_symbol;
227 	delete mid_symbol;
228 	delete end_symbol;
229 	delete dash_symbol;
230 }
231 
232 
233 
duplicate() const234 LineSymbol* LineSymbol::duplicate() const
235 {
236 	return new LineSymbol(*this);
237 }
238 
239 
240 
validate() const241 bool LineSymbol::validate() const
242 {
243 	using std::begin;
244 	using std::end;
245 	using MemberSymbol = PointSymbol* LineSymbol::*;
246 	MemberSymbol members[4] = { &LineSymbol::start_symbol, &LineSymbol::mid_symbol, &LineSymbol::end_symbol, &LineSymbol::dash_symbol };
247 	return std::all_of(begin(members), end(members), [this](auto& member) {
248 		auto sub_symbol = this->*member;
249 		return !sub_symbol || !sub_symbol->isEmpty() || sub_symbol->validate();
250 	});
251 }
252 
253 
254 
createRenderables(const Object *,const VirtualCoordVector & coords,ObjectRenderables & output,RenderableOptions) const255 void LineSymbol::createRenderables(
256         const Object* /*object*/,
257         const VirtualCoordVector& coords,
258         ObjectRenderables& output,
259         RenderableOptions /*options*/) const
260 {
261 	const auto path_parts = PathPart::calculatePathParts(coords);
262 	createStartEndSymbolRenderables(path_parts, output);
263 	for (const auto& part : path_parts)
264 	{
265 		createSinglePathRenderables(part, part.isClosed(), output);
266 	}
267 }
268 
createRenderables(const PathObject * object,const PathPartVector & path_parts,ObjectRenderables & output,RenderableOptions options) const269 void LineSymbol::createRenderables(
270         const PathObject* object,
271         const PathPartVector& path_parts,
272         ObjectRenderables& output,
273         RenderableOptions options ) const
274 {
275 	if (options.testFlag(Symbol::RenderBaselines))
276 	{
277 		createBaselineRenderables(object, path_parts, output, guessDominantColor());
278 	}
279 	else
280 	{
281 		createStartEndSymbolRenderables(path_parts, output);
282 		for (const auto& part : path_parts)
283 		{
284 			createSinglePathRenderables(part, part.isClosed(), output);
285 		}
286 	}
287 }
288 
289 
createSinglePathRenderables(const VirtualPath & path,bool path_closed,ObjectRenderables & output) const290 void LineSymbol::createSinglePathRenderables(const VirtualPath& path, bool path_closed, ObjectRenderables& output) const
291 {
292 	if (path.size() < 2)
293 		return;
294 
295 	// Dash symbols?
296 	if (dash_symbol && !dash_symbol->isEmpty())
297 	{
298 		createDashSymbolRenderables(path, path_closed, output);
299 	}
300 
301 	auto create_line = color && line_width > 0;
302 	auto create_border = have_border_lines && (border.isVisible() || right_border.isVisible());
303 	auto use_offset = !path.isClosed() && (start_offset > 0 || end_offset > 0);
304 	if (!use_offset && !dashed)
305 	{
306 		// This is a simple plain line (no pointed line ends, no dashes).
307 		// It may be drawn directly from the given path.
308 		if (create_line)
309 			output.insertRenderable(new LineRenderable(this, path, path_closed));
310 
311 		auto create_mid_symbols = mid_symbol && !mid_symbol->isEmpty() && segment_length > 0;
312 		if (create_mid_symbols || create_border)
313 		{
314 			auto start = SplitPathCoord::begin(path.path_coords);
315 			auto end   = SplitPathCoord::end(path.path_coords);
316 
317 			if (create_mid_symbols)
318 				createMidSymbolRenderables(path, start, end, path_closed, output);
319 
320 			if (create_border)
321 				createBorderLines(path, start, end, output);
322 		}
323 
324 		return;
325 	}
326 
327 	// This is a non-trivial line symbol.
328 	// First we preprocess coordinates and handle mid symbols.
329 	// Later we create the line renderable and border.
330 	auto start = SplitPathCoord::begin(path.path_coords);
331 	auto end   = SplitPathCoord::end(path.path_coords);
332 
333 	if (use_offset)
334 	{
335 		// Create offset at line ends, or pointed line ends
336 		auto effective_start_len = start.clen;
337 		auto effective_end_len = end.clen;
338 		{
339 			auto effective_start_offset = length_type(0.001) * std::max(0, start_offset);
340 			auto effective_end_offset = length_type(0.001) * std::max(0, end_offset);
341 			auto total_offset = effective_start_offset + effective_end_offset;
342 			auto available_length = std::max(length_type(0), end.clen - start.clen);
343 
344 			if (total_offset > available_length)
345 			{
346 				create_line = false;
347 				effective_start_offset *= available_length / total_offset;
348 				effective_end_offset *= available_length / total_offset;
349 			}
350 			effective_start_len += effective_start_offset;
351 			effective_end_len -= effective_end_offset;
352 			// Safeguard against floating point quirks
353 			effective_end_len = std::max(effective_start_len, effective_end_len);
354 		}
355 		if (start_offset > 0)
356 		{
357 			auto split = SplitPathCoord::at(effective_start_len, start);
358 			if (cap_style == PointedCap)
359 			{
360 				createPointedLineCap(path, start, split, false, output);
361 			}
362 			start = split;
363 		}
364 		if (end_offset > 0)
365 		{
366 			auto split = SplitPathCoord::at(effective_end_len, start);
367 			if (cap_style == PointedCap)
368 			{
369 				// Get updated curve end
370 				if (end.is_curve_end)
371 					end = SplitPathCoord::at(end.clen, split);
372 				createPointedLineCap(path, split, end, true, output);
373 			}
374 			end = split;
375 		}
376 		if (!create_line)
377 		{
378 			return;
379 		}
380 	}
381 
382 	MapCoordVector processed_flags;
383 	MapCoordVectorF processed_coords;
384 	if (dashed)
385 	{
386 		// Dashed lines
387 		if (dash_length > 0)
388 			processDashedLine(path, start, end, path_closed, processed_flags, processed_coords, output);
389 	}
390 	else
391 	{
392 		// Symbols?
393 		if (mid_symbol && !mid_symbol->isEmpty() && segment_length > 0)
394 			createMidSymbolRenderables(path, start, end, path_closed, output);
395 
396 		if (line_width > 0)
397 			processContinuousLine(path, start, end, false,
398 			                      processed_flags, processed_coords, output);
399 	}
400 
401 	Q_ASSERT(processed_coords.size() != 1);
402 
403 	if ((create_line || create_border) && processed_coords.size() > 1)
404 	{
405 		VirtualPath processed_path = { processed_flags, processed_coords };
406 		processed_path.path_coords.update(path.first_index);
407 		if (create_line)
408 		{
409 			output.insertRenderable(new LineRenderable(this, processed_path, path_closed));
410 		}
411 		if (create_border)
412 		{
413 			const auto processed_start = SplitPathCoord::begin(processed_path.path_coords);
414 			const auto processed_end = SplitPathCoord::end(processed_path.path_coords);
415 			createBorderLines(processed_path, processed_start, processed_end, output);
416 		}
417 	}
418 }
419 
420 
createBorderLines(const VirtualPath & path,const SplitPathCoord & start,const SplitPathCoord & end,ObjectRenderables & output) const421 void LineSymbol::createBorderLines(
422         const VirtualPath& path,
423         const SplitPathCoord& start,
424         const SplitPathCoord& end,
425         ObjectRenderables& output) const
426 {
427 	const auto main_shift = 0.0005 * line_width;
428 	const auto path_closed = path.isClosed();
429 
430 	MapCoordVector border_flags;
431 	MapCoordVectorF border_coords;
432 
433 	auto border_dashed = false;
434 	// The following variables are needed for dashed borders only, but default
435 	// initialization should be cheap enough so that we can always create them
436 	// on the stack for easy sharing of state between symmetrical dashed borders,
437 	// and even for reusing allocated memory in case of different dash patterns.
438 	MapCoordVector dashed_flags;
439 	MapCoordVectorF dashed_coords;
440 
441 	LineSymbol border_symbol;
442 	border_symbol.setJoinStyle(join_style == RoundJoin ? RoundJoin : MiterJoin);
443 
444 	if (border.isVisible())
445 	{
446 		border.setupSymbol(border_symbol);
447 		border_dashed = border.dashed && border.dash_length > 0 && border.break_length > 0;
448 		if (border_dashed)
449 		{
450 			// Left border is dashed
451 			border_symbol.processDashedLine(path, start, end, path_closed, dashed_flags, dashed_coords, output);
452 			border_symbol.dashed = false;	// important, otherwise more dashes might be added by createRenderables()!
453 			shiftCoordinates({dashed_flags, dashed_coords}, -main_shift, border_flags, border_coords);
454 		}
455 		else
456 		{
457 			// Solid left border
458 			shiftCoordinates(path, -main_shift, border_flags, border_coords);
459 		}
460 		auto border_path = VirtualPath{border_flags, border_coords};
461 		auto last = border_path.path_coords.update(0);
462 		Q_ASSERT(last+1 == border_coords.size()); Q_UNUSED(last);
463 		output.insertRenderable(new LineRenderable(&border_symbol, border_path, path_closed));
464 	}
465 
466 	if (right_border.isVisible())
467 	{
468 		right_border.setupSymbol(border_symbol);
469 		auto right_border_dashed = right_border.dashed && right_border.dash_length > 0 && right_border.break_length > 0;
470 		if (right_border_dashed)
471 		{
472 			if (areBordersDifferent()
473 			    && (!border_dashed
474 			        || (border.dash_length != right_border.dash_length
475 			            || border.break_length != right_border.break_length)))
476 			{
477 				// Right border is dashed, but different from left border
478 				dashed_flags.clear();
479 				dashed_coords.clear();
480 				border_symbol.processDashedLine(path, start, end, path_closed, dashed_flags, dashed_coords, output);
481 			}
482 			border_symbol.dashed = false;	// important, otherwise more dashes might be added by createRenderables()!
483 			shiftCoordinates({dashed_flags, dashed_coords}, main_shift, border_flags, border_coords);
484 		}
485 		else
486 		{
487 			// Solid right border
488 			shiftCoordinates(path, main_shift, border_flags, border_coords);
489 		}
490 		auto border_path = VirtualPath{border_flags, border_coords};
491 		auto last = border_path.path_coords.update(0);
492 		Q_ASSERT(last+1 == border_coords.size()); Q_UNUSED(last);
493 		output.insertRenderable(new LineRenderable(&border_symbol, border_path, path_closed));
494 	}
495 }
496 
497 
shiftCoordinates(const VirtualPath & path,double main_shift,MapCoordVector & out_flags,MapCoordVectorF & out_coords) const498 void LineSymbol::shiftCoordinates(const VirtualPath& path, double main_shift, MapCoordVector& out_flags, MapCoordVectorF& out_coords) const
499 {
500 	const float curve_threshold = 0.03f;	// TODO: decrease for export/print?
501 	const int MAX_OFFSET = 16;
502 	QBezier offsetCurves[MAX_OFFSET];
503 
504 	double miter_limit = 2.0 * miterLimit(); // needed more than once
505 	if (miter_limit <= 0.0)
506 		miter_limit = 1.0e6;                 // Q_ASSERT(miter_limit != 0)
507 	double miter_reference = 0.0;            // reference value:
508 	if (join_style == MiterJoin)             //  when to bevel MiterJoins
509 		miter_reference = cos(atan(4.0 / miter_limit));
510 
511 	// sign of shift and main shift indicates left or right border
512 	// but u_border_shift is unsigned
513 	double u_border_shift = 0.001 * ((main_shift > 0.0 && areBordersDifferent()) ? right_border.shift : border.shift);
514 	double shift = main_shift + ((main_shift > 0.0) ? u_border_shift : -u_border_shift);
515 
516 	auto size = path.size();
517 	out_flags.clear();
518 	out_coords.clear();
519 	out_flags.reserve(size);
520 	out_coords.reserve(size);
521 
522 	const MapCoord no_flags;
523 
524 	bool ok_in, ok_out;
525 	MapCoordF segment_start,
526 	          right_vector, end_right_vector,
527 	          vector_in, vector_out,
528 	          tangent_in, tangent_out,
529 	          middle0, middle1;
530 	auto last_i = path.last_index;
531 	for (auto i = path.first_index; i <= last_i; ++i)
532 	{
533 		auto coords_i = path.coords[i];
534 		const auto& flags_i  = path.coords.flags[i];
535 
536 		vector_in  = path.calculateIncomingTangent(i, ok_in);
537 		vector_out = path.calculateOutgoingTangent(i, ok_out);
538 
539 		if (!ok_in)
540 		{
541 			vector_in = vector_out;
542 			ok_in = ok_out;
543 		}
544 		if (ok_in)
545 		{
546 			tangent_in = vector_in;
547 			tangent_in.normalize();
548 		}
549 
550 		if (!ok_out)
551 		{
552 			vector_out = vector_in;
553 			ok_out = ok_in;
554 		}
555 		if (ok_out)
556 		{
557 			tangent_out = vector_out;
558 			tangent_out.normalize();
559 		}
560 
561 		if (!ok_in && !ok_out)
562 		{
563 			// Rare but existing case.  No valid solution, but
564 			// at least we need to output a point to handle the flags correctly.
565 			//qDebug("No valid tangent");
566 			segment_start = coords_i;
567 		}
568 		else if (i == 0 && !path.isClosed())
569 		{
570 			// Simple start point
571 			right_vector = tangent_out.perpRight();
572 			segment_start = coords_i + shift * right_vector;
573 		}
574 		else if (i == last_i && !path.isClosed())
575 		{
576 			// Simple end point
577 			right_vector = tangent_in.perpRight();
578 			segment_start = coords_i + shift * right_vector;
579 		}
580 		else
581 		{
582 			// Corner point
583 			right_vector = tangent_out.perpRight();
584 
585 			middle0 = tangent_in + tangent_out;
586 			middle0.normalize();
587 			double offset;
588 
589 			// Determine type of corner (inner vs. outer side of corner)
590 			double a = (tangent_out.x() * tangent_in.y() - tangent_in.x() * tangent_out.y()) * main_shift;
591 			if (a > 0.0)
592 			{
593 				// Outer side of corner
594 
595 				if (join_style == BevelJoin || join_style == RoundJoin)
596 				{
597 					middle1 = tangent_in + middle0;
598 					middle1.normalize();
599 					double phi1 = acos(MapCoordF::dotProduct(middle1, tangent_in));
600 					offset = tan(phi1) * u_border_shift;
601 
602 					if (i > 0 && !qIsNaN(offset))
603 					{
604 						// First border corner point
605 						end_right_vector = tangent_in.perpRight();
606 						out_flags.push_back(no_flags);
607 						out_coords.push_back(coords_i + shift * end_right_vector + offset * tangent_in);
608 
609 						if (join_style == RoundJoin)
610 						{
611 							// Extra border corner point
612 							// TODO: better approximation of round corner / use bezier curve
613 							out_flags.push_back(no_flags);
614 							out_coords.push_back(coords_i + shift * middle0.perpRight());
615 						}
616 					}
617 				}
618 				else /* join_style == MiterJoin */
619 				{
620 					// miter_check has no concrete interpretation,
621 					// but was derived by mathematical simplifications.
622 					double miter_check = MapCoordF::dotProduct(middle0, tangent_in);
623 					if (miter_check <= miter_reference)
624 					{
625 						// Two border corner points
626 						middle1 = tangent_in + middle0;
627 						middle1.normalize();
628 						double phi1 = acos(MapCoordF::dotProduct(middle1, tangent_in));
629 						offset = miter_limit * fabs(main_shift) + tan(phi1) * u_border_shift;
630 
631 						if (i > 0 && !qIsNaN(offset))
632 						{
633 							// First border corner point
634 							end_right_vector = tangent_in.perpRight();
635 							out_flags.push_back(no_flags);
636 							out_coords.push_back(coords_i + shift * end_right_vector + offset * tangent_in);
637 						}
638 					}
639 					else
640 					{
641 						double phi = acos(MapCoordF::dotProduct(middle0.perpRight(), tangent_in));
642 						offset = fabs(1.0/tan(phi) * shift);
643 					}
644 				}
645 
646 				if (qIsNaN(offset))
647 				{
648 					offset = 0.0;
649 				}
650 
651 				// single or second border corner point
652 				segment_start = coords_i + shift * right_vector - offset * tangent_out;
653 			}
654 			else if (i > 2 && path.coords.flags[i-3].isCurveStart() && flags_i.isCurveStart())
655 			{
656 				// Inner side of corner (or no corner), and both sides are beziers
657 				// old behaviour
658 				right_vector = middle0.perpRight();
659 				double phi = acos(MapCoordF::dotProduct(right_vector, tangent_in));
660 				double sin_phi = sin(phi);
661 				double inset = (sin_phi > (1.0/miter_limit)) ? (1.0 / sin_phi) : miter_limit;
662 				segment_start = coords_i + (shift * inset) * right_vector;
663 			}
664 			else
665 			{
666 				// Inner side of corner (or no corner), and no more than on bezier involved
667 
668 				// Default solution
669 				double phi = acos(MapCoordF::dotProduct(middle0.perpRight(), tangent_in));
670 				double tan_phi = tan(phi);
671 				offset = -fabs(shift/tan_phi);
672 
673 				if (tan_phi >= 1.0)
674 				{
675 					segment_start = coords_i + shift * right_vector - offset * tangent_out;
676 				}
677 				else
678 				{
679 					// Critical case
680 					double len_in  = vector_in.length();
681 					double len_out = vector_out.length();
682 					a = qIsNaN(offset) ? 0.0 : (fabs(offset) - qMin(len_in, len_out));
683 
684 					if (a < -0.0)
685 					{
686 						// Offset acceptable
687 						segment_start = coords_i + shift * right_vector - offset * tangent_out;
688 
689 #ifdef MAPPER_FILL_MITER_LIMIT_GAP
690 						// Avoid miter limit effects by extra points
691 						// This is be nice for ISOM roads, but not for powerlines...
692 						// TODO: add another flag to the signature?
693 						out_flags.push_back(no_flags);
694 						out_coords.push_back(coords_i + shift * right_vector - offset * tangent_out);
695 
696 						out_flags.push_back(no_flags);
697 						out_coords.push_back(out_coords.back() - (shift - main_shift) * qMax(0.0, 1.0 / sin(phi) - miter_limit) * middle0);
698 
699 						// single or second border corner point
700 						segment_start = coords_i + shift * right_vector - offset * tangent_out;
701 #endif
702 					}
703 					else
704 					{
705 						// Default solution is too far off from main path
706 						if (len_in < len_out)
707 						{
708 							segment_start = coords_i + shift * right_vector + len_in * tangent_out;
709 						}
710 						else
711 						{
712 							right_vector = tangent_in.perpRight();
713 							segment_start = coords_i + shift * right_vector - len_out * tangent_in;
714 						}
715 					}
716 				}
717 			}
718 		}
719 
720 		out_flags.emplace_back(flags_i);
721 		out_flags.back().setCurveStart(false); // Must not be set if bezier.shifted() fails!
722 		out_coords.emplace_back(segment_start);
723 
724 		if (flags_i.isCurveStart())
725 		{
726 			Q_ASSERT(i+2 < path.last_index);
727 
728 			// Use QBezierCopy code to shift the curve, but set start and end point manually to get the correct end points (because of line joins)
729 			// TODO: it may be necessary to remove some of the generated curves in the case an outer point is moved inwards
730 			if (main_shift > 0.0)
731 			{
732 				QBezier bezier = QBezier::fromPoints(path.coords[i+3], path.coords[i+2], path.coords[i+1], coords_i);
733 				auto count = bezier.shifted(offsetCurves, MAX_OFFSET, qAbs(shift), curve_threshold);
734 				for (auto j = count - 1; j >= 0; --j)
735 				{
736 					out_flags.back().setCurveStart(true);
737 
738 					out_flags.emplace_back();
739 					out_coords.emplace_back(offsetCurves[j].pt3());
740 
741 					out_flags.emplace_back();
742 					out_coords.emplace_back(offsetCurves[j].pt2());
743 
744 					if (j > 0)
745 					{
746 						out_flags.emplace_back();
747 						out_coords.emplace_back(offsetCurves[j].pt1());
748 					}
749 				}
750 			}
751 			else
752 			{
753 				QBezier bezier = QBezier::fromPoints(path.coords[i], path.coords[i+1], path.coords[i+2], path.coords[i+3]);
754 				int count = bezier.shifted(offsetCurves, MAX_OFFSET, qAbs(shift), curve_threshold);
755 				for (int j = 0; j < count; ++j)
756 				{
757 					out_flags.back().setCurveStart(true);
758 
759 					out_flags.emplace_back();
760 					out_coords.emplace_back(offsetCurves[j].pt2());
761 
762 					out_flags.emplace_back();
763 					out_coords.emplace_back(offsetCurves[j].pt3());
764 
765 					if (j < count - 1)
766 					{
767 						out_flags.emplace_back();
768 						out_coords.emplace_back(offsetCurves[j].pt4());
769 					}
770 				}
771 			}
772 
773 			i += 2;
774 		}
775 	}
776 }
777 
processContinuousLine(const VirtualPath & path,const SplitPathCoord & start,const SplitPathCoord & end,bool set_mid_symbols,MapCoordVector & processed_flags,MapCoordVectorF & processed_coords,ObjectRenderables & output) const778 void LineSymbol::processContinuousLine(
779         const VirtualPath& path,
780         const SplitPathCoord& start,
781         const SplitPathCoord& end,
782         bool set_mid_symbols,
783         MapCoordVector& processed_flags,
784         MapCoordVectorF& processed_coords,
785         ObjectRenderables& output ) const
786 {
787 	auto mid_symbol_distance_f = length_type(0.001) * mid_symbol_distance;
788 	auto mid_symbols_length   = (qMax(1, mid_symbols_per_spot) - 1) * mid_symbol_distance_f;
789 	auto effective_end_length = end.clen;
790 
791 	auto split = start;
792 
793 	set_mid_symbols = set_mid_symbols && mid_symbol && !mid_symbol->isEmpty() && mid_symbols_per_spot;
794 	if (set_mid_symbols && mid_symbols_length <= effective_end_length - split.clen)
795 	{
796 		auto mid_position = (split.clen + effective_end_length - mid_symbols_length) / 2;
797 		auto next_split = SplitPathCoord::at(mid_position, split);
798 		path.copy(split, next_split, processed_flags, processed_coords);
799 		split = next_split;
800 
801 		auto orientation = qreal(0);
802 		bool mid_symbol_rotatable = bool(mid_symbol) && mid_symbol->isRotatable();
803 		for (auto i = mid_symbols_per_spot; i > 0; --i)
804 		{
805 			if (mid_symbol_rotatable)
806 				orientation = split.tangentVector().angle();
807 			mid_symbol->createRenderablesScaled(split.pos, orientation, output);
808 
809 			if (i > 1)
810 			{
811 				mid_position += mid_symbol_distance_f;
812 				next_split = SplitPathCoord::at(mid_position, split);
813 				path.copy(split, next_split, processed_flags, processed_coords);
814 				split = next_split;
815 			}
816 		}
817 	}
818 
819 	path.copy(split, end, processed_flags, processed_coords);
820 
821 	processed_flags.back().setGapPoint(true);
822 	Q_ASSERT(!processed_flags[processed_flags.size()-2].isCurveStart());
823 }
824 
createPointedLineCap(const VirtualPath & path,const SplitPathCoord & start,const SplitPathCoord & end,bool is_end,ObjectRenderables & output) const825 void LineSymbol::createPointedLineCap(
826         const VirtualPath& path,
827         const SplitPathCoord& start,
828         const SplitPathCoord& end,
829         bool is_end,
830         ObjectRenderables& output ) const
831 {
832 	AreaSymbol area_symbol;
833 	area_symbol.setColor(color);
834 
835 	auto line_half_width = length_type(0.0005) * line_width;
836 	auto cap_length = length_type(0.001) * (is_end ? end_offset : start_offset);
837 	auto tan_angle = qreal(line_half_width / cap_length);
838 
839 	MapCoordVector cap_flags;
840 	MapCoordVectorF cap_middle_coords;
841 	path.copy(start, end, cap_flags, cap_middle_coords);
842 
843 	auto cap_size = VirtualPath::size_type(cap_middle_coords.size());
844 	auto cap_middle_path = VirtualPath { cap_flags, cap_middle_coords };
845 
846 	std::vector<float> cap_lengths;
847 	cap_lengths.reserve(cap_size);
848 	path.copyLengths(start, end, cap_lengths);
849 	Q_ASSERT(cap_middle_coords.size() == cap_lengths.size());
850 
851 	// Calculate coordinates on the left and right side of the line
852 	MapCoordVectorF cap_coords;
853 	MapCoordVectorF cap_coords_left;
854 	auto sign = is_end ? -1 : 1;
855 	for (MapCoordVectorF::size_type i = 0; i < cap_size; ++i)
856 	{
857 		auto dist_from_start = is_end ? (end.clen - cap_lengths[i]) : (cap_lengths[i] - start.clen);
858 		auto factor = dist_from_start / cap_length;
859 		//Q_ASSERT(factor >= -0.01f && factor <= 1.01f); happens when using large break lengths as these are not adjusted
860 		factor = qBound(length_type(0), factor, length_type(1));
861 
862 		auto tangent_info = cap_middle_path.calculateTangentScaling(i);
863 		auto right_vector = tangent_info.first.perpRight();
864 		right_vector.normalize();
865 
866 		auto radius = qBound(qreal(0), qreal(line_half_width * factor) * tangent_info.second, qreal(line_half_width) * 2);
867 
868 		cap_flags[i].setHolePoint(false);
869 		cap_coords.emplace_back(cap_middle_coords[i] + radius * right_vector);
870 		cap_coords_left.emplace_back(cap_middle_coords[i] - radius * right_vector);
871 
872 		// Control points for bezier curves
873 		if (i >= 3 && cap_flags[i-3].isCurveStart())
874 		{
875 			cap_coords.emplace_back(cap_coords.back());
876 			cap_coords_left.emplace_back(cap_coords_left.back());
877 
878 			MapCoordF tangent = cap_middle_coords[i] - cap_middle_coords[i-1];
879 			Q_ASSERT(tangent.lengthSquared() < 999*999);
880 			auto right_scale = tangent.length() * tan_angle * sign;
881 			cap_coords[i-1] = cap_coords[i] - tangent - right_vector * right_scale;
882 			cap_coords_left[i-1] = cap_coords_left[i] - tangent + right_vector * right_scale;
883 		}
884 		if (cap_flags[i].isCurveStart())
885 		{
886 			// TODO: Tangent scaling depending on curvature? Adaptive subdivision of the curves?
887 			MapCoordF tangent = cap_middle_coords[i+1] - cap_middle_coords[i];
888 			Q_ASSERT(tangent.lengthSquared() < 999*999);
889 			auto right_scale = tangent.length() * tan_angle * sign;
890 			cap_coords.emplace_back(cap_coords[i] + tangent + right_vector * right_scale);
891 			cap_coords_left.emplace_back(cap_coords_left[i] + tangent - right_vector * right_scale);
892 			i += 2;
893 		}
894 	}
895 
896 	// Create small overlap to avoid visual glitches with the on-screen display (should not matter for printing, could probably turn it off for this)
897 	constexpr auto overlap_length = length_type(0.05);
898 	if ((end.clen - start.clen) > 4 * overlap_length)
899 	{
900 		bool ok;
901 		auto end_pos = is_end ? 0 : (cap_size - 1);
902 		auto end_cap_pos = is_end ? 0 : (cap_coords.size() - 1);
903 		MapCoordF tangent = cap_middle_path.calculateTangent(end_pos, !is_end, ok);
904 		if (ok)
905 		{
906 			tangent.setLength(qreal(overlap_length * sign));
907 			auto right = MapCoordF{ is_end ? tangent : -tangent }.perpRight();
908 
909 			MapCoordF shifted_coord = cap_coords[end_cap_pos];
910 			shifted_coord += tangent + right;
911 
912 			MapCoordF shifted_coord_left = cap_coords_left[end_cap_pos];
913 			shifted_coord_left += tangent - right;
914 
915 			if (is_end)
916 			{
917 				cap_flags.insert(cap_flags.begin(), MapCoord());
918 				cap_coords.insert(cap_coords.begin(), shifted_coord);
919 				cap_coords_left.insert(cap_coords_left.begin(), shifted_coord_left);
920 			}
921 			else
922 			{
923 				cap_flags.emplace_back();
924 				cap_coords.emplace_back(shifted_coord);
925 				cap_coords_left.emplace_back(shifted_coord_left);
926 			}
927 		}
928 	}
929 
930 	// Concatenate left and right side coordinates
931 	cap_flags.reserve(2 * cap_flags.size());
932 	cap_coords.reserve(2 * cap_coords.size());
933 
934 	MapCoord curve_start;
935 	curve_start.setCurveStart(true);
936 	if (!is_end)
937 	{
938 		for (auto i = cap_coords_left.size(); i > 0; )
939 		{
940 			--i;
941 			if (i >= 3 && cap_flags[i-3].isCurveStart())
942 			{
943 				cap_flags.emplace_back(curve_start);
944 				cap_flags.emplace_back();
945 				cap_flags.emplace_back();
946 				cap_coords.emplace_back(cap_coords_left[i]);
947 				cap_coords.emplace_back(cap_coords_left[i-1]);
948 				cap_coords.emplace_back(cap_coords_left[i-2]);
949 				i -= 2;
950 			}
951 			else
952 			{
953 				cap_flags.emplace_back();
954 				cap_coords.emplace_back(cap_coords_left[i]);
955 			}
956 		}
957 	}
958 	else
959 	{
960 		for (auto i = cap_coords_left.size() - 1; i > 0; )
961 		{
962 			--i;
963 			if (i >= 3 && cap_flags[i - 3].isCurveStart())
964 			{
965 				cap_flags.emplace_back(curve_start);
966 				cap_flags.emplace_back();
967 				cap_flags.emplace_back();
968 				cap_coords.emplace_back(cap_coords_left[i]);
969 				cap_coords.emplace_back(cap_coords_left[i-1]);
970 				cap_coords.emplace_back(cap_coords_left[i-2]);
971 				i -= 2;
972 			}
973 			else if (i >= 2 && i == cap_coords_left.size() - 2 && cap_flags[i - 2].isCurveStart())
974 			{
975 				cap_flags[cap_flags.size() - 1].setCurveStart(true);
976 				cap_flags.emplace_back();
977 				cap_flags.emplace_back();
978 				cap_coords.emplace_back(cap_coords_left[i]);
979 				cap_coords.emplace_back(cap_coords_left[i-1]);
980 				i -= 1;
981 			}
982 			else
983 			{
984 				cap_flags.emplace_back();
985 				cap_coords.emplace_back(cap_coords_left[i]);
986 			}
987 		}
988 	}
989 
990 	// Add renderable
991 	Q_ASSERT(cap_coords.size() >= 3);
992 	Q_ASSERT(cap_coords.size() == cap_flags.size());
993 
994 	VirtualPath cap_path { cap_flags, cap_coords };
995 	cap_path.path_coords.update(0);
996 	output.insertRenderable(new AreaRenderable(&area_symbol, cap_path));
997 }
998 
processDashedLine(const VirtualPath & path,const SplitPathCoord & start,const SplitPathCoord & end,bool path_closed,MapCoordVector & out_flags,MapCoordVectorF & out_coords,ObjectRenderables & output) const999 void LineSymbol::processDashedLine(
1000         const VirtualPath& path,
1001         const SplitPathCoord& start,
1002         const SplitPathCoord& end,
1003         bool path_closed,
1004         MapCoordVector& out_flags,
1005         MapCoordVectorF& out_coords,
1006         ObjectRenderables& output ) const
1007 {
1008 	auto& path_coords = path.path_coords;
1009 	Q_ASSERT(!path_coords.empty());
1010 
1011 	auto out_coords_size = path.size() * 4;
1012 	out_flags.reserve(out_coords_size);
1013 	out_coords.reserve(out_coords_size);
1014 
1015 	auto groups_start = start;
1016 	auto line_start   = groups_start;
1017 	for (auto is_part_start = true, is_part_end = false; !is_part_end; is_part_start = false)
1018 	{
1019 		auto groups_end_path_coord_index = path_coords.findNextDashPoint(groups_start.path_coord_index);
1020 		is_part_end = path.path_coords[groups_end_path_coord_index].clen >= end.clen;
1021 		auto groups_end = is_part_end ? end : SplitPathCoord::at(path_coords, groups_end_path_coord_index);
1022 
1023 		line_start = createDashGroups(path, path_closed,
1024 		                              line_start, groups_start, groups_end,
1025 		                              is_part_start, is_part_end,
1026 		                              out_flags, out_coords, output);
1027 
1028 		groups_start = groups_end; // Search then next split (node) after groups_end (current node).
1029 	}
1030 	Q_ASSERT(line_start.clen == groups_start.clen);
1031 }
1032 
createDashGroups(const VirtualPath & path,bool path_closed,const SplitPathCoord & line_start,const SplitPathCoord & start,const SplitPathCoord & end,bool is_part_start,bool is_part_end,MapCoordVector & out_flags,MapCoordVectorF & out_coords,ObjectRenderables & output) const1033 SplitPathCoord LineSymbol::createDashGroups(
1034         const VirtualPath& path,
1035         bool path_closed,
1036         const SplitPathCoord& line_start,
1037         const SplitPathCoord& start,
1038         const SplitPathCoord& end,
1039         bool is_part_start,
1040         bool is_part_end,
1041         MapCoordVector& out_flags,
1042         MapCoordVectorF& out_coords,
1043         ObjectRenderables& output ) const
1044 {
1045 	auto& flags = path.coords.flags;
1046 	auto& path_coords = path.path_coords;
1047 
1048 	auto orientation = qreal(0);
1049 	bool mid_symbol_rotatable = bool(mid_symbol) && mid_symbol->isRotatable();
1050 
1051 	const auto mid_symbols = (mid_symbols_per_spot > 0 && mid_symbol && !mid_symbol->isEmpty()) ? mid_symbol_placement : LineSymbol::NoMidSymbols;
1052 	auto mid_symbol_distance_f = length_type(0.001) * mid_symbol_distance;
1053 
1054 	bool half_first_group = is_part_start ? (half_outer_dashes || path_closed)
1055 	                                      : (flags[start.index].isDashPoint() && dashes_in_group == 1);
1056 
1057 	bool ends_with_dashpoint  = is_part_end ? path_closed : true;
1058 
1059 	auto dash_length_f           = length_type(0.001) * dash_length;
1060 	auto break_length_f          = length_type(0.001) * break_length;
1061 	auto in_group_break_length_f = length_type(0.001) * in_group_break_length;
1062 
1063 	auto total_in_group_dash_length  = dashes_in_group * dash_length_f;
1064 	auto total_in_group_break_length = (dashes_in_group - 1) * in_group_break_length_f;
1065 	auto total_group_length = total_in_group_dash_length + total_in_group_break_length;
1066 	auto total_group_and_break_length = total_group_length + break_length_f;
1067 
1068 	auto length = end.clen - start.clen;
1069 	auto length_plus_break = length + break_length_f;
1070 
1071 	bool half_last_group  = is_part_end ? (half_outer_dashes || path_closed)
1072 	                                    : (ends_with_dashpoint && dashes_in_group == 1);
1073 	int num_half_groups = (half_first_group ? 1 : 0) + (half_last_group ? 1 : 0);
1074 
1075 	auto num_dashgroups_f = num_half_groups +
1076 	                        (length_plus_break - num_half_groups * dash_length_f / 2) / total_group_and_break_length;
1077 
1078 	int lower_dashgroup_count = int(std::floor(num_dashgroups_f));
1079 
1080 	auto minimum_optimum_num_dashes = dashes_in_group * 2 - num_half_groups / 2;
1081 	auto minimum_optimum_length  = 2 * total_group_and_break_length;
1082 	auto switch_deviation        = length_type(0.2) * total_group_and_break_length / dashes_in_group;
1083 
1084 	bool set_mid_symbols = mid_symbols != LineSymbol::NoMidSymbols
1085 	                       && (length >= dash_length_f - switch_deviation || show_at_least_one_symbol);
1086 	if (mid_symbols == LineSymbol::CenterOfDash)
1087 	{
1088 		if (line_start.clen < start.clen || (!is_part_start && flags[start.index].isDashPoint()))
1089 		{
1090 			set_mid_symbols = false;
1091 
1092 			// Handle mid symbols at begin for explicit dash points when we draw the whole dash here
1093 			auto split = SplitPathCoord::begin(path_coords);
1094 
1095 			auto position = start.clen - (mid_symbols_per_spot - 1) * mid_symbol_distance_f / 2;
1096 			for (int s = 0; s < mid_symbols_per_spot; s+=2)
1097 			{
1098 				if (position >= split.clen)
1099 				{
1100 					auto next_split = SplitPathCoord::at(position, split);
1101 					if (mid_symbol_rotatable)
1102 						orientation = next_split.tangentVector().angle();
1103 					mid_symbol->createRenderablesScaled(next_split.pos, orientation, output);
1104 					split = next_split;
1105 				}
1106 				position  += mid_symbol_distance_f;
1107 			}
1108 		}
1109 		if (half_first_group)
1110 		{
1111 			// Handle mid symbols at start for closing point or explicit dash points.
1112 			auto split = start;
1113 
1114 			auto position = split.clen + (mid_symbols_per_spot % 2 + 1) * mid_symbol_distance_f / 2;
1115 			for (int s = 1; s < mid_symbols_per_spot && position <= path_coords.back().clen; s += 2)
1116 			{
1117 				auto next_split = SplitPathCoord::at(position, split);
1118 				if (mid_symbol_rotatable)
1119 					orientation = next_split.tangentVector().angle();
1120 				mid_symbol->createRenderablesScaled(next_split.pos, orientation, output);
1121 
1122 				position  += mid_symbol_distance_f;
1123 				split = next_split;
1124 			}
1125 		}
1126 	}
1127 
1128 	if (length <= 0 || (lower_dashgroup_count <= 1 && length < minimum_optimum_length - minimum_optimum_num_dashes * switch_deviation))
1129 	{
1130 		// Line part too short for dashes
1131 		if (is_part_end)
1132 		{
1133 			// Can't be handled correctly by the next round of dash groups drawing:
1134 			// Just draw a continuous line.
1135 			processContinuousLine(path, line_start, end, set_mid_symbols,
1136 			                      out_flags, out_coords, output);
1137 		}
1138 		else
1139 		{
1140 			// Give this length to the next round of dash groups drawing.
1141 			return line_start;
1142 		}
1143 	}
1144 	else
1145 	{
1146 		auto higher_dashgroup_count = int(std::ceil(num_dashgroups_f));
1147 		auto lower_dashgroup_deviation  = (length_plus_break - lower_dashgroup_count * total_group_and_break_length)  / lower_dashgroup_count;
1148 		auto higher_dashgroup_deviation = (higher_dashgroup_count * total_group_and_break_length - length_plus_break) / higher_dashgroup_count;
1149 		Q_ASSERT(half_first_group || half_last_group || (lower_dashgroup_deviation >= length_type(-0.001) && higher_dashgroup_deviation >= length_type(-0.001))); // TODO; seems to fail as long as halving first/last dashes affects the outermost dash only
1150 		auto num_dashgroups = (lower_dashgroup_deviation > higher_dashgroup_deviation) ? higher_dashgroup_count : lower_dashgroup_count;
1151 		Q_ASSERT(num_dashgroups >= 2);
1152 
1153 		auto num_half_dashes = 2 * num_dashgroups * dashes_in_group - num_half_groups;
1154 		auto adjusted_dash_length = (length - (num_dashgroups-1) * break_length_f - num_dashgroups * total_in_group_break_length) * 2 / num_half_dashes;
1155 		adjusted_dash_length = qMax(adjusted_dash_length, 0.0f);	// could be negative for large break lengths
1156 
1157 		auto cur_length = start.clen;
1158 		SplitPathCoord dash_start = line_start;
1159 
1160 		for (int dashgroup = 1; dashgroup <= num_dashgroups; ++dashgroup)
1161 		{
1162 			bool is_first_dashgroup = dashgroup == 1;
1163 			bool is_last_dashgroup  = dashgroup == num_dashgroups;
1164 
1165 			if (mid_symbols == CenterOfDashGroup)
1166 			{
1167 				auto position = cur_length;
1168 				if (is_last_dashgroup && half_last_group)
1169 				{
1170 					position = end.clen;
1171 				}
1172 				else if (!(is_first_dashgroup && half_first_group))
1173 				{
1174 					position += (adjusted_dash_length * dashes_in_group
1175 					             + in_group_break_length_f * (dashes_in_group - 1)) / 2;
1176 				}
1177 				position -= (mid_symbol_distance_f * (mid_symbols_per_spot - 1)) / 2;
1178 				auto split = dash_start;
1179 				for (int i = 0; i < mid_symbols_per_spot && position <= end.clen; ++i, position += mid_symbol_distance_f)
1180 				{
1181 					if (position <= start.clen)
1182 						continue;
1183 					split = SplitPathCoord::at(position, split);
1184 					if (mid_symbol_rotatable)
1185 						orientation = split.tangentVector().angle();
1186 					mid_symbol->createRenderablesScaled(split.pos, orientation, output);
1187 				}
1188 			}
1189 
1190 			for (int dash = 1; dash <= dashes_in_group; ++dash)
1191 			{
1192 				// Draw a single dash
1193 				bool is_first_dash = is_first_dashgroup && dash == 1;
1194 				bool is_last_dash  = is_last_dashgroup  && dash == dashes_in_group;
1195 
1196 				// The dash has an start if it is not the first dash in a half first group.
1197 				bool has_start = !(is_first_dash && half_first_group);
1198 				// The dash has an end if it is not the last dash in a half last group.
1199 				bool has_end   = !(is_last_dash && half_last_group);
1200 
1201 				Q_ASSERT(has_start || has_end); // At least one half must be left...
1202 
1203 				bool is_half_dash = has_start != has_end;
1204 				auto cur_dash_length = is_half_dash ? adjusted_dash_length / 2 : adjusted_dash_length;
1205 				const auto set_mid_symbols = mid_symbols == CenterOfDash && !is_half_dash;
1206 
1207 				if (!is_first_dash)
1208 				{
1209 					auto next_dash_start = SplitPathCoord::at(cur_length, dash_start);
1210 					path.copy(dash_start, next_dash_start, out_flags, out_coords);
1211 					out_flags.back().setGapPoint(true);
1212 					dash_start = next_dash_start;
1213 				}
1214 				if (is_last_dash && !is_part_end)
1215 				{
1216 					// Last dash before a dash point (which is not the closing point):
1217 					// Give the remaining length to the next round of dash groups drawing.
1218 					return dash_start;
1219 				}
1220 
1221 				SplitPathCoord dash_end = SplitPathCoord::at(cur_length + cur_dash_length, dash_start);
1222 				processContinuousLine(path, dash_start, dash_end, set_mid_symbols,
1223 				                      out_flags, out_coords, output);
1224 				cur_length += cur_dash_length;
1225 				dash_start = dash_end;
1226 
1227 				if (dash < dashes_in_group)
1228 					cur_length += in_group_break_length_f;
1229 			}
1230 
1231 			if (dashgroup < num_dashgroups)
1232 			{
1233 				if (Q_UNLIKELY(mid_symbols == CenterOfGap))
1234 				{
1235 					auto position = cur_length
1236 					                + (break_length_f
1237 					                   - mid_symbol_distance_f * (mid_symbols_per_spot - 1)) / 2;
1238 					auto split = dash_start;
1239 					for (int i = 0; i < mid_symbols_per_spot; ++i, position += mid_symbol_distance_f)
1240 					{
1241 						split = SplitPathCoord::at(position, split);
1242 						if (mid_symbol_rotatable)
1243 							orientation = split.tangentVector().angle();
1244 						mid_symbol->createRenderablesScaled(split.pos, orientation, output);
1245 					}
1246 				}
1247 
1248 				cur_length += break_length_f;
1249 			}
1250 		}
1251 
1252 		if (half_last_group && mid_symbols == LineSymbol::CenterOfDash)
1253 		{
1254 			// Handle mid symbols at end for closing point or for (some) explicit dash points.
1255 			auto split = start;
1256 
1257 			auto position = end.clen - (mid_symbols_per_spot-1) * mid_symbol_distance_f / 2;
1258 			for (int s = 0; s < mid_symbols_per_spot; s+=2)
1259 			{
1260 				auto next_split = SplitPathCoord::at(position, split);
1261 				if (mid_symbol_rotatable)
1262 					orientation = next_split.tangentVector().angle();
1263 				mid_symbol->createRenderablesScaled(next_split.pos, orientation, output);
1264 
1265 				position  += mid_symbol_distance_f;
1266 				split = next_split;
1267 			}
1268 		}
1269 	}
1270 
1271 	return end;
1272 }
1273 
createDashSymbolRenderables(const VirtualPath & path,bool path_closed,ObjectRenderables & output) const1274 void LineSymbol::createDashSymbolRenderables(
1275         const VirtualPath& path,
1276         bool path_closed ,
1277         ObjectRenderables& output) const
1278 {
1279 	Q_ASSERT(dash_symbol);
1280 
1281 	auto& flags  = path.coords.flags;
1282 	auto& coords = path.coords;
1283 
1284 	auto last = path.last_index;
1285 	auto i = path.first_index;
1286 	if (suppress_dash_symbol_at_ends && path.size() > 0)
1287 	{
1288 		// Suppress dash symbol at line ends
1289 		++i;
1290 		--last;
1291 	}
1292 	else if (path_closed)
1293 	{
1294 		++i;
1295 	}
1296 
1297 	for (; i <= last; ++i)
1298 	{
1299 		if (flags[i].isDashPoint())
1300 		{
1301 			const auto params = path.calculateTangentScaling(i);
1302 			//params.first.perpRight();
1303 			auto rotation = dash_symbol->isRotatable() ? params.first.angle() : 0.0;
1304 			auto scale = scale_dash_symbol ? qMin(params.second, 2.0 * LineSymbol::miterLimit()) : 1.0;
1305 			dash_symbol->createRenderablesScaled(coords[i], rotation, output, scale);
1306 		}
1307 	}
1308 }
1309 
createMidSymbolRenderables(const VirtualPath & path,const SplitPathCoord & start,const SplitPathCoord & end,bool path_closed,ObjectRenderables & output) const1310 void LineSymbol::createMidSymbolRenderables(
1311         const VirtualPath& path,
1312         const SplitPathCoord& start,
1313         const SplitPathCoord& end,
1314         bool path_closed,
1315         ObjectRenderables& output) const
1316 {
1317 	Q_ASSERT(mid_symbol);
1318 	auto orientation = qreal(0);
1319 	bool mid_symbol_rotatable = bool(mid_symbol) && mid_symbol->isRotatable();
1320 
1321 	int mid_symbol_num_gaps       = mid_symbols_per_spot - 1;
1322 
1323 	auto segment_length_f       = length_type(0.001) * segment_length;
1324 	auto end_length_f           = length_type(0.001) * end_length;
1325 	auto end_length_twice_f     = length_type(0.002) * end_length;
1326 	auto mid_symbol_distance_f  = length_type(0.001) * mid_symbol_distance;
1327 	auto mid_symbols_length     = mid_symbol_num_gaps * mid_symbol_distance_f;
1328 
1329 	auto& path_coords = path.path_coords;
1330 	Q_ASSERT(!path_coords.empty());
1331 
1332 	if (end_length == 0 && !path_closed)
1333 	{
1334 		// Insert point at start coordinate
1335 		if (mid_symbol_rotatable)
1336 			orientation = start.tangentVector().angle();
1337 		mid_symbol->createRenderablesScaled(start.pos, orientation, output);
1338 	}
1339 
1340 	auto groups_start = start;
1341 	for (auto is_part_end = false; !is_part_end; )
1342 	{
1343 		auto groups_end_path_coord_index = path_coords.findNextDashPoint(groups_start.path_coord_index);
1344 		is_part_end = path.path_coords[groups_end_path_coord_index].clen >= end.clen;
1345 		auto groups_end = is_part_end ? end : SplitPathCoord::at(path_coords, groups_end_path_coord_index);
1346 
1347 		// The total length of the current continuous part
1348 		auto length = groups_end.clen - groups_start.clen;
1349 		// The length which is available for placing mid symbols
1350 		auto segmented_length = qMax(length_type(0), length - end_length_twice_f) - mid_symbols_length;
1351 		// The number of segments to be created by mid symbols
1352 		auto segment_count_raw = qMax((end_length == 0) ? length_type(1) : length_type(0), (segmented_length / (segment_length_f + mid_symbols_length)));
1353 		auto lower_segment_count = int(std::floor(segment_count_raw));
1354 		auto higher_segment_count = int(std::ceil(segment_count_raw));
1355 
1356 		if (end_length > 0)
1357 		{
1358 			if (length <= mid_symbols_length)
1359 			{
1360 				if (show_at_least_one_symbol)
1361 				{
1362 					// Insert point at start coordinate
1363 					if (mid_symbol_rotatable)
1364 						orientation = groups_start.tangentVector().angle();
1365 					mid_symbol->createRenderablesScaled(groups_start.pos, orientation, output);
1366 
1367 					// Insert point at end coordinate
1368 					if (mid_symbol_rotatable)
1369 						orientation = groups_end.tangentVector().angle();
1370 					mid_symbol->createRenderablesScaled(groups_end.pos, orientation, output);
1371 				}
1372 			}
1373 			else
1374 			{
1375 				auto lower_abs_deviation = qAbs(length - lower_segment_count * segment_length_f - (lower_segment_count+1)*mid_symbols_length - end_length_twice_f);
1376 				auto higher_abs_deviation = qAbs(length - higher_segment_count * segment_length_f - (higher_segment_count+1)*mid_symbols_length - end_length_twice_f);
1377 				auto segment_count = (lower_abs_deviation >= higher_abs_deviation) ? higher_segment_count : lower_segment_count;
1378 
1379 				auto deviation = (lower_abs_deviation >= higher_abs_deviation) ? -higher_abs_deviation : lower_abs_deviation;
1380 				auto ideal_length = segment_count * segment_length_f + end_length_twice_f;
1381 				auto adjusted_end_length = end_length_f + deviation * (end_length_f / ideal_length);
1382 				auto adjusted_segment_length = segment_length_f + deviation * (segment_length_f / ideal_length);
1383 				Q_ASSERT(qAbs(2*adjusted_end_length + segment_count*adjusted_segment_length + (segment_count + 1)*mid_symbols_length - length) < 0.001f);
1384 
1385 				if (adjusted_segment_length >= 0 && (show_at_least_one_symbol || higher_segment_count > 0 || length > end_length_twice_f - (segment_length_f + mid_symbols_length) / 2))
1386 				{
1387 					adjusted_segment_length += mid_symbols_length;
1388 					auto split = groups_start;
1389 					for (int i = 0; i < segment_count + 1; ++i)
1390 					{
1391 						auto position = groups_start.clen + adjusted_end_length + i * adjusted_segment_length - mid_symbol_distance_f;
1392 						for (int s = 0; s < mid_symbols_per_spot; ++s)
1393 						{
1394 							position += mid_symbol_distance_f;
1395 							split = SplitPathCoord::at(position, split);
1396 							if (mid_symbol_rotatable)
1397 								orientation = split.tangentVector().angle();
1398 							mid_symbol->createRenderablesScaled(split.pos, orientation, output);
1399 						}
1400 					}
1401 				}
1402 			}
1403 		}
1404 		else
1405 		{
1406 			// end_length == 0
1407 			if (length > mid_symbols_length)
1408 			{
1409 				auto lower_segment_deviation = qAbs(length - lower_segment_count * segment_length_f - (lower_segment_count+1)*mid_symbols_length) / lower_segment_count;
1410 				auto higher_segment_deviation = qAbs(length - higher_segment_count * segment_length_f - (higher_segment_count+1)*mid_symbols_length) / higher_segment_count;
1411 				int segment_count = (lower_segment_deviation > higher_segment_deviation) ? higher_segment_count : lower_segment_count;
1412 				auto adapted_segment_length = (length - (segment_count+1)*mid_symbols_length) / segment_count + mid_symbols_length;
1413 				Q_ASSERT(qAbs(segment_count * adapted_segment_length + mid_symbols_length) - length < length_type(0.001));
1414 
1415 				if (adapted_segment_length >= mid_symbols_length)
1416 				{
1417 					auto split = groups_start;
1418 					for (int i = 0; i <= segment_count; ++i)
1419 					{
1420 						auto position = groups_start.clen + i * adapted_segment_length - mid_symbol_distance_f;
1421 						for (int s = 0; s < mid_symbols_per_spot; ++s)
1422 						{
1423 							position += mid_symbol_distance_f;
1424 
1425 							// The outermost symbols are handled outside this loop
1426 							if (i == 0 && s == 0)
1427 								continue;
1428 							if (i == segment_count && s == mid_symbol_num_gaps)
1429 								break;
1430 
1431 							split = SplitPathCoord::at(position, split);
1432 							if (mid_symbol_rotatable)
1433 								orientation = split.tangentVector().angle();
1434 							mid_symbol->createRenderablesScaled(split.pos, orientation, output);
1435 						}
1436 					}
1437 				}
1438 			}
1439 
1440 			// Insert point at end coordinate
1441 			if (mid_symbol_rotatable)
1442 				orientation = groups_end.tangentVector().angle();
1443 			mid_symbol->createRenderablesScaled(groups_end.pos, orientation, output);
1444 		}
1445 
1446 		groups_start = groups_end; // Search then next split (node) after groups_end (current node).
1447 	}
1448 }
1449 
1450 
createStartEndSymbolRenderables(const PathPartVector & path_parts,ObjectRenderables & output) const1451 void LineSymbol::createStartEndSymbolRenderables(
1452             const PathPartVector& path_parts,
1453             ObjectRenderables& output) const
1454 {
1455 	if (path_parts.empty())
1456 		return;
1457 
1458 	if (start_symbol && !start_symbol->isEmpty())
1459 	{
1460 		const auto& path = path_parts.front();
1461 		auto orientation = qreal(0);
1462 		if (start_symbol->isRotatable())
1463 		{
1464 			bool ok;
1465 			MapCoordF tangent = path.calculateOutgoingTangent(path.first_index, ok);
1466 			if (ok)
1467 				orientation = tangent.angle();
1468 		}
1469 		start_symbol->createRenderablesScaled(path.coords[path.first_index], orientation, output);
1470 	}
1471 
1472 	if (end_symbol && !end_symbol->isEmpty())
1473 	{
1474 		const auto& path = path_parts.back();
1475 		auto orientation = qreal(0);
1476 		if (end_symbol->isRotatable())
1477 		{
1478 			bool ok;
1479 			MapCoordF tangent = path.calculateIncomingTangent(path.last_index, ok);
1480 			if (ok)
1481 				orientation = tangent.angle();
1482 		}
1483 		end_symbol->createRenderablesScaled(path.coords[path.last_index], orientation, output);
1484 	}
1485 }
1486 
1487 
1488 
colorDeletedEvent(const MapColor * color)1489 void LineSymbol::colorDeletedEvent(const MapColor* color)
1490 {
1491 	bool have_changes = false;
1492 	if (mid_symbol && mid_symbol->containsColor(color))
1493 	{
1494 		mid_symbol->colorDeletedEvent(color);
1495 		have_changes = true;
1496 	}
1497 	if (start_symbol && start_symbol->containsColor(color))
1498 	{
1499 		start_symbol->colorDeletedEvent(color);
1500 		have_changes = true;
1501 	}
1502 	if (end_symbol && end_symbol->containsColor(color))
1503 	{
1504 		end_symbol->colorDeletedEvent(color);
1505 		have_changes = true;
1506 	}
1507 	if (dash_symbol && dash_symbol->containsColor(color))
1508 	{
1509 		dash_symbol->colorDeletedEvent(color);
1510 		have_changes = true;
1511 	}
1512     if (color == this->color)
1513 	{
1514 		this->color = nullptr;
1515 		have_changes = true;
1516 	}
1517 	if (color == border.color)
1518 	{
1519 		this->border.color = nullptr;
1520 		have_changes = true;
1521 	}
1522 	if (color == right_border.color)
1523 	{
1524 		this->right_border.color = nullptr;
1525 		have_changes = true;
1526 	}
1527 	if (have_changes)
1528 		resetIcon();
1529 }
1530 
containsColor(const MapColor * color) const1531 bool LineSymbol::containsColor(const MapColor* color) const
1532 {
1533 	if (color == this->color || color == border.color || color == right_border.color)
1534 		return true;
1535 	if (mid_symbol && mid_symbol->containsColor(color))
1536 		return true;
1537 	if (start_symbol && start_symbol->containsColor(color))
1538 		return true;
1539 	if (end_symbol && end_symbol->containsColor(color))
1540 		return true;
1541     if (dash_symbol && dash_symbol->containsColor(color))
1542 		return true;
1543     return false;
1544 }
1545 
guessDominantColor() const1546 const MapColor* LineSymbol::guessDominantColor() const
1547 {
1548 	bool has_main_line = line_width > 0 && color;
1549 	bool has_border = hasBorder() && border.width > 0 && border.color;
1550 	bool has_right_border = hasBorder() && right_border.width > 0 && right_border.color;
1551 
1552 	// Use the color of the thickest line
1553 	if (has_main_line)
1554 	{
1555 		if (has_border)
1556 		{
1557 			if (has_right_border)
1558 			{
1559 				if (line_width > 2 * border.width)
1560 					return (line_width > 2 * right_border.width) ? color : right_border.color;
1561 				else
1562 					return (border.width > right_border.width) ? border.color : right_border.color;
1563 			}
1564 			else
1565 				return (line_width > 2 * border.width) ? color : border.color;
1566 		}
1567 		else
1568 		{
1569 			if (has_right_border)
1570 				return (line_width > 2 * right_border.width) ? color : right_border.color;
1571 			else
1572 				return color;
1573 		}
1574 	}
1575 	else
1576 	{
1577 		if (has_border)
1578 		{
1579 			if (has_right_border)
1580 				return (border.width > right_border.width) ? border.color : right_border.color;
1581 			else
1582 				return border.color;
1583 		}
1584 		else
1585 		{
1586 			if (has_right_border)
1587 				return right_border.color;
1588 		}
1589 	}
1590 
1591 	const MapColor* dominant_color = mid_symbol ? mid_symbol->guessDominantColor() : nullptr;
1592 	if (dominant_color) return dominant_color;
1593 
1594 	dominant_color = start_symbol ? start_symbol->guessDominantColor() : nullptr;
1595 	if (dominant_color) return dominant_color;
1596 
1597 	dominant_color = end_symbol ? end_symbol->guessDominantColor() : nullptr;
1598 	if (dominant_color) return dominant_color;
1599 
1600 	dominant_color = dash_symbol ? dash_symbol->guessDominantColor() : nullptr;
1601 	if (dominant_color) return dominant_color;
1602 
1603 	return nullptr;
1604 }
1605 
1606 
replaceColors(const MapColorMap & color_map)1607 void LineSymbol::replaceColors(const MapColorMap& color_map)
1608 {
1609 	color = color_map.value(color);
1610 	border.color = color_map.value(border.color);
1611 	right_border.color = color_map.value(right_border.color);
1612 	for (auto member : { &LineSymbol::start_symbol, &LineSymbol::mid_symbol, &LineSymbol::end_symbol, &LineSymbol::dash_symbol })
1613 	{
1614 		if (auto sub_symbol = this->*member)
1615 			sub_symbol->replaceColors(color_map);
1616 	}
1617 }
1618 
1619 
1620 
scale(double factor)1621 void LineSymbol::scale(double factor)
1622 {
1623 	line_width = qRound(factor * line_width);
1624 
1625 	minimum_length = qRound(factor * minimum_length);
1626 	start_offset = qRound(factor * start_offset);
1627 	end_offset = qRound(factor * end_offset);
1628 
1629 	if (start_symbol)
1630 		start_symbol->scale(factor);
1631 	if (mid_symbol)
1632 		mid_symbol->scale(factor);
1633 	if (end_symbol)
1634 		end_symbol->scale(factor);
1635 	if (dash_symbol)
1636 		dash_symbol->scale(factor);
1637 
1638 	segment_length = qRound(factor * segment_length);
1639 	end_length = qRound(factor * end_length);
1640 	minimum_mid_symbol_count = qRound(factor * minimum_mid_symbol_count);
1641 	minimum_mid_symbol_count_when_closed = qRound(factor * minimum_mid_symbol_count_when_closed);
1642 
1643 	dash_length = qRound(factor * dash_length);
1644 	break_length = qRound(factor * break_length);
1645 	in_group_break_length = qRound(factor * in_group_break_length);
1646 	mid_symbol_distance = qRound(factor * mid_symbol_distance);
1647 
1648 	border.scale(factor);
1649 	right_border.scale(factor);
1650 
1651 	resetIcon();
1652 }
1653 
ensurePointSymbols(const QString & start_name,const QString & mid_name,const QString & end_name,const QString & dash_name)1654 void LineSymbol::ensurePointSymbols(const QString& start_name, const QString& mid_name, const QString& end_name, const QString& dash_name)
1655 {
1656 	if (!start_symbol)
1657 	{
1658 		start_symbol = new PointSymbol();
1659 		start_symbol->setRotatable(true);
1660 	}
1661 	start_symbol->setName(start_name);
1662 	if (!mid_symbol)
1663 	{
1664 		mid_symbol = new PointSymbol();
1665 		mid_symbol->setRotatable(true);
1666 	}
1667 	mid_symbol->setName(mid_name);
1668 	if (!end_symbol)
1669 	{
1670 		end_symbol = new PointSymbol();
1671 		end_symbol->setRotatable(true);
1672 	}
1673 	end_symbol->setName(end_name);
1674 	if (!dash_symbol)
1675 	{
1676 		dash_symbol = new PointSymbol();
1677 		dash_symbol->setRotatable(true);
1678 	}
1679 	dash_symbol->setName(dash_name);
1680 }
1681 
cleanupPointSymbols()1682 void LineSymbol::cleanupPointSymbols()
1683 {
1684 	if (start_symbol && start_symbol->isEmpty())
1685 	{
1686 		delete start_symbol;
1687 		start_symbol = nullptr;
1688 	}
1689 	if (mid_symbol && mid_symbol->isEmpty())
1690 	{
1691 		delete mid_symbol;
1692 		mid_symbol = nullptr;
1693 	}
1694 	if (end_symbol && end_symbol->isEmpty())
1695 	{
1696 		delete end_symbol;
1697 		end_symbol = nullptr;
1698 	}
1699 	if (dash_symbol && dash_symbol->isEmpty())
1700 	{
1701 		delete dash_symbol;
1702 		dash_symbol = nullptr;
1703 	}
1704 }
1705 
1706 
1707 
dimensionForIcon() const1708 qreal LineSymbol::dimensionForIcon() const
1709 {
1710 	// calculateLargestLineExtent() gives half line width, and for the icon,
1711 	// we suggest a half line width of white space on each side.
1712 	auto size = 4 * calculateLargestLineExtent();
1713 	if (start_symbol && !start_symbol->isEmpty())
1714 		size = qMax(size, start_symbol->dimensionForIcon());
1715 	if (mid_symbol && !mid_symbol->isEmpty())
1716 		size = qMax(size, 2 * mid_symbol->dimensionForIcon() + getMidSymbolDistance() * (getMidSymbolsPerSpot() - 1) / 1000);
1717 	if (dash_symbol && !dash_symbol->isEmpty())
1718 		size = qMax(size, 2 * dash_symbol->dimensionForIcon());
1719 	if (end_symbol && !end_symbol->isEmpty())
1720 		size = qMax(size, end_symbol->dimensionForIcon());
1721 	return size;
1722 }
1723 
1724 
calculateLargestLineExtent() const1725 qreal LineSymbol::calculateLargestLineExtent() const
1726 {
1727 	auto line_extent_f = 0.001 * 0.5 * getLineWidth();
1728 	auto result = line_extent_f;
1729 	if (hasBorder())
1730 	{
1731 		result = qMax(result, line_extent_f + 0.001 * (getBorder().shift + getBorder().width)/2);
1732 		result = qMax(result, line_extent_f + 0.001 * (getRightBorder().shift + getRightBorder().width)/2);
1733 	}
1734 	return result;
1735 }
1736 
1737 
1738 
setStartSymbol(PointSymbol * symbol)1739 void LineSymbol::setStartSymbol(PointSymbol* symbol)
1740 {
1741 	replaceSymbol(start_symbol, symbol, QCoreApplication::translate("OpenOrienteering::LineSymbolSettings", "Start symbol"));
1742 }
setMidSymbol(PointSymbol * symbol)1743 void LineSymbol::setMidSymbol(PointSymbol* symbol)
1744 {
1745 	replaceSymbol(mid_symbol, symbol, QCoreApplication::translate("OpenOrienteering::LineSymbolSettings", "Mid symbol"));
1746 }
setEndSymbol(PointSymbol * symbol)1747 void LineSymbol::setEndSymbol(PointSymbol* symbol)
1748 {
1749 	replaceSymbol(end_symbol, symbol, QCoreApplication::translate("OpenOrienteering::LineSymbolSettings", "End symbol"));
1750 }
setDashSymbol(PointSymbol * symbol)1751 void LineSymbol::setDashSymbol(PointSymbol* symbol)
1752 {
1753 	replaceSymbol(dash_symbol, symbol, QCoreApplication::translate("OpenOrienteering::LineSymbolSettings", "Dash symbol"));
1754 }
1755 
setMidSymbolPlacement(LineSymbol::MidSymbolPlacement placement)1756 void LineSymbol::setMidSymbolPlacement(LineSymbol::MidSymbolPlacement placement)
1757 {
1758 	mid_symbol_placement = placement;
1759 }
1760 
1761 
replaceSymbol(PointSymbol * & old_symbol,PointSymbol * replace_with,const QString & name)1762 void LineSymbol::replaceSymbol(PointSymbol*& old_symbol, PointSymbol* replace_with, const QString& name)
1763 {
1764 	delete old_symbol;
1765 	old_symbol = replace_with;
1766 	replace_with->setName(name);
1767 }
1768 
1769 
1770 
saveImpl(QXmlStreamWriter & xml,const Map & map) const1771 void LineSymbol::saveImpl(QXmlStreamWriter& xml, const Map& map) const
1772 {
1773 	xml.writeStartElement(QString::fromLatin1("line_symbol"));
1774 	xml.writeAttribute(QString::fromLatin1("color"), QString::number(map.findColorIndex(color)));
1775 	xml.writeAttribute(QString::fromLatin1("line_width"), QString::number(line_width));
1776 	xml.writeAttribute(QString::fromLatin1("minimum_length"), QString::number(minimum_length));
1777 	xml.writeAttribute(QString::fromLatin1("join_style"), QString::number(join_style));
1778 	xml.writeAttribute(QString::fromLatin1("cap_style"), QString::number(cap_style));
1779 	if (cap_style == LineSymbol::PointedCap)
1780 	{
1781 		// Deprecated, only for backwards compatibility.
1782 		/// \todo Remove "pointed_cap_length" in Mapper 1.0
1783 		xml.writeAttribute(QString::fromLatin1("pointed_cap_length"), QString::number(qRound((start_offset+end_offset)/2.0)));
1784 	}
1785 	xml.writeAttribute(QString::fromLatin1("start_offset"), QString::number(start_offset));
1786 	xml.writeAttribute(QString::fromLatin1("end_offset"), QString::number(end_offset));
1787 
1788 	if (dashed)
1789 		xml.writeAttribute(QString::fromLatin1("dashed"), QString::fromLatin1("true"));
1790 	xml.writeAttribute(QString::fromLatin1("segment_length"), QString::number(segment_length));
1791 	xml.writeAttribute(QString::fromLatin1("end_length"), QString::number(end_length));
1792 	if (show_at_least_one_symbol)
1793 		xml.writeAttribute(QString::fromLatin1("show_at_least_one_symbol"), QString::fromLatin1("true"));
1794 	xml.writeAttribute(QString::fromLatin1("minimum_mid_symbol_count"), QString::number(minimum_mid_symbol_count));
1795 	xml.writeAttribute(QString::fromLatin1("minimum_mid_symbol_count_when_closed"), QString::number(minimum_mid_symbol_count_when_closed));
1796 	xml.writeAttribute(QString::fromLatin1("dash_length"), QString::number(dash_length));
1797 	xml.writeAttribute(QString::fromLatin1("break_length"), QString::number(break_length));
1798 	xml.writeAttribute(QString::fromLatin1("dashes_in_group"), QString::number(dashes_in_group));
1799 	xml.writeAttribute(QString::fromLatin1("in_group_break_length"), QString::number(in_group_break_length));
1800 	if (half_outer_dashes)
1801 		xml.writeAttribute(QString::fromLatin1("half_outer_dashes"), QString::fromLatin1("true"));
1802 	xml.writeAttribute(QString::fromLatin1("mid_symbols_per_spot"), QString::number(mid_symbols_per_spot));
1803 	xml.writeAttribute(QString::fromLatin1("mid_symbol_distance"), QString::number(mid_symbol_distance));
1804 	if (mid_symbol_placement != 0)
1805 		xml.writeAttribute(QString::fromLatin1("mid_symbol_placement"), QString::number(mid_symbol_placement));
1806 	if (suppress_dash_symbol_at_ends)
1807 		xml.writeAttribute(QString::fromLatin1("suppress_dash_symbol_at_ends"), QString::fromLatin1("true"));
1808 	if (!scale_dash_symbol)
1809 		xml.writeAttribute(QString::fromLatin1("scale_dash_symbol"), QString::fromLatin1("false"));
1810 
1811 	if (start_symbol)
1812 	{
1813 		xml.writeStartElement(QString::fromLatin1("start_symbol"));
1814 		start_symbol->save(xml, map);
1815 		xml.writeEndElement();
1816 	}
1817 
1818 	if (mid_symbol)
1819 	{
1820 		xml.writeStartElement(QString::fromLatin1("mid_symbol"));
1821 		mid_symbol->save(xml, map);
1822 		xml.writeEndElement();
1823 	}
1824 
1825 	if (end_symbol)
1826 	{
1827 		xml.writeStartElement(QString::fromLatin1("end_symbol"));
1828 		end_symbol->save(xml, map);
1829 		xml.writeEndElement();
1830 	}
1831 
1832 	if (dash_symbol)
1833 	{
1834 		xml.writeStartElement(QString::fromLatin1("dash_symbol"));
1835 		dash_symbol->save(xml, map);
1836 		xml.writeEndElement();
1837 	}
1838 
1839 	if (have_border_lines)
1840 	{
1841 		xml.writeStartElement(QString::fromLatin1("borders"));
1842 		bool are_borders_different = areBordersDifferent();
1843 		if (are_borders_different)
1844 			xml.writeAttribute(QString::fromLatin1("borders_different"), QString::fromLatin1("true"));
1845 		border.save(xml, map);
1846 		if (are_borders_different)
1847 			right_border.save(xml, map);
1848 		xml.writeEndElement(/*borders*/);
1849 	}
1850 
1851 	xml.writeEndElement(/*line_symbol*/);
1852 }
1853 
loadImpl(QXmlStreamReader & xml,const Map & map,SymbolDictionary & symbol_dict,int version)1854 bool LineSymbol::loadImpl(QXmlStreamReader& xml, const Map& map, SymbolDictionary& symbol_dict, int version)
1855 {
1856 	if (xml.name() != QLatin1String("line_symbol"))
1857 		return false;
1858 
1859 	QXmlStreamAttributes attributes = xml.attributes();
1860 	int temp = attributes.value(QLatin1String("color")).toInt();
1861 	color = map.getColor(temp);
1862 	line_width = attributes.value(QLatin1String("line_width")).toInt();
1863 	minimum_length = attributes.value(QLatin1String("minimum_length")).toInt();
1864 	join_style = static_cast<LineSymbol::JoinStyle>(attributes.value(QLatin1String("join_style")).toInt());
1865 	cap_style = static_cast<LineSymbol::CapStyle>(attributes.value(QLatin1String("cap_style")).toInt());
1866 	if (attributes.hasAttribute(QLatin1String("start_offset")) || attributes.hasAttribute(QLatin1String("end_offset")))
1867 	{
1868 		// since version 8
1869 		start_offset = attributes.value(QLatin1String("start_offset")).toInt();
1870 		end_offset = attributes.value(QLatin1String("end_offset")).toInt();
1871 	}
1872 	else if (cap_style == LineSymbol::PointedCap)
1873 	{
1874 		// until version 7
1875 		start_offset = end_offset = attributes.value(QLatin1String("pointed_cap_length")).toInt();
1876 	}
1877 
1878 	dashed = (attributes.value(QLatin1String("dashed")) == QLatin1String("true"));
1879 	segment_length = attributes.value(QLatin1String("segment_length")).toInt();
1880 	end_length = attributes.value(QLatin1String("end_length")).toInt();
1881 	show_at_least_one_symbol = (attributes.value(QLatin1String("show_at_least_one_symbol")) == QLatin1String("true"));
1882 	minimum_mid_symbol_count = attributes.value(QLatin1String("minimum_mid_symbol_count")).toInt();
1883 	minimum_mid_symbol_count_when_closed = attributes.value(QLatin1String("minimum_mid_symbol_count_when_closed")).toInt();
1884 	dash_length = attributes.value(QLatin1String("dash_length")).toInt();
1885 	break_length = attributes.value(QLatin1String("break_length")).toInt();
1886 	dashes_in_group = attributes.value(QLatin1String("dashes_in_group")).toInt();
1887 	in_group_break_length = attributes.value(QLatin1String("in_group_break_length")).toInt();
1888 	half_outer_dashes = (attributes.value(QLatin1String("half_outer_dashes")) == QLatin1String("true"));
1889 	mid_symbols_per_spot = attributes.value(QLatin1String("mid_symbols_per_spot")).toInt();
1890 	mid_symbol_distance = attributes.value(QLatin1String("mid_symbol_distance")).toInt();
1891 	mid_symbol_placement = static_cast<LineSymbol::MidSymbolPlacement>(attributes.value(QLatin1String("mid_symbol_placement")).toInt());
1892 	suppress_dash_symbol_at_ends = (attributes.value(QLatin1String("suppress_dash_symbol_at_ends")) == QLatin1String("true"));
1893 	scale_dash_symbol = (attributes.value(QLatin1String("scale_dash_symbol")) != QLatin1String("false"));
1894 
1895 	have_border_lines = false;
1896 	while (xml.readNextStartElement())
1897 	{
1898 		if (xml.name() == QLatin1String("start_symbol"))
1899 			start_symbol = loadPointSymbol(xml, map, symbol_dict, version);
1900 		else if (xml.name() == QLatin1String("mid_symbol"))
1901 			mid_symbol = loadPointSymbol(xml, map, symbol_dict, version);
1902 		else if (xml.name() == QLatin1String("end_symbol"))
1903 			end_symbol = loadPointSymbol(xml, map, symbol_dict, version);
1904 		else if (xml.name() == QLatin1String("dash_symbol"))
1905 			dash_symbol = loadPointSymbol(xml, map, symbol_dict, version);
1906 		else if (xml.name() == QLatin1String("borders"))
1907 		{
1908 //			bool are_borders_different = (xml.attributes().value("borders_different") == "true");
1909 
1910 			bool right_border_loaded = false;
1911 			while (xml.readNextStartElement())
1912 			{
1913 				if (xml.name() == QLatin1String("border"))
1914 				{
1915 					if (!have_border_lines)
1916 					{
1917 						border.load(xml, map);
1918 						have_border_lines = true;
1919 					}
1920 					else
1921 					{
1922 						right_border.load(xml, map);
1923 						right_border_loaded = true;
1924 						xml.skipCurrentElement();
1925 						break;
1926 					}
1927 				}
1928 				else
1929 					xml.skipCurrentElement();
1930 			}
1931 
1932 			if (have_border_lines && !right_border_loaded)
1933 				right_border = border;
1934 		}
1935 		else
1936 			xml.skipCurrentElement(); // unknown
1937 	}
1938 
1939 	cleanupPointSymbols();
1940 
1941 	return true;
1942 }
1943 
loadPointSymbol(QXmlStreamReader & xml,const Map & map,SymbolDictionary & symbol_dict,int version)1944 PointSymbol* LineSymbol::loadPointSymbol(QXmlStreamReader& xml, const Map& map, SymbolDictionary& symbol_dict, int version)
1945 {
1946 	while (xml.readNextStartElement())
1947 	{
1948 		if (xml.name() == QLatin1String("symbol"))
1949 		{
1950 			auto symbol = static_cast<PointSymbol*>(Symbol::load(xml, map, symbol_dict, version).release());
1951 			xml.skipCurrentElement();
1952 			return symbol;
1953 		}
1954 		else
1955 			xml.skipCurrentElement();
1956 	}
1957 	return nullptr;
1958 }
1959 
equalsImpl(const Symbol * other,Qt::CaseSensitivity case_sensitivity) const1960 bool LineSymbol::equalsImpl(const Symbol* other, Qt::CaseSensitivity case_sensitivity) const
1961 {
1962 	Q_UNUSED(case_sensitivity);
1963 
1964 	const LineSymbol* line = static_cast<const LineSymbol*>(other);
1965 	if (line_width != line->line_width)
1966 		return false;
1967 	if (minimum_length != line->minimum_length)
1968 		return false;
1969 	if (line_width > 0)
1970 	{
1971 		if (!MapColor::equal(color, line->color))
1972 			return false;
1973 
1974 		if ((cap_style != line->cap_style) ||
1975 			(join_style != line->join_style))
1976 			return false;
1977 
1978 		if (start_offset != line->start_offset || end_offset != line->end_offset)
1979 			return false;
1980 
1981 		if (dashed != line->dashed)
1982 			return false;
1983 		if (dashed)
1984 		{
1985 			if (dash_length != line->dash_length ||
1986 				break_length != line->break_length ||
1987 				dashes_in_group != line->dashes_in_group ||
1988 				half_outer_dashes != line->half_outer_dashes ||
1989 				(dashes_in_group > 1 && (in_group_break_length != line->in_group_break_length)))
1990 				return false;
1991 		}
1992 		else
1993 		{
1994 			if (segment_length != line->segment_length ||
1995 				end_length != line->end_length ||
1996 				(mid_symbol && (show_at_least_one_symbol != line->show_at_least_one_symbol ||
1997 								minimum_mid_symbol_count != line->minimum_mid_symbol_count ||
1998 								minimum_mid_symbol_count_when_closed != line->minimum_mid_symbol_count_when_closed)))
1999 				return false;
2000 		}
2001 	}
2002 
2003 	if (bool(start_symbol) != bool(line->start_symbol))
2004 		return false;
2005 	if (start_symbol && !start_symbol->equals(line->start_symbol))
2006 		return false;
2007 
2008 	if (bool(mid_symbol) != bool(line->mid_symbol))
2009 		return false;
2010 	if (mid_symbol && !mid_symbol->equals(line->mid_symbol))
2011 		return false;
2012 
2013 	if (bool(end_symbol) != bool(line->end_symbol))
2014 		return false;
2015 	if (end_symbol && !end_symbol->equals(line->end_symbol))
2016 		return false;
2017 
2018 	if (bool(dash_symbol) != bool(line->dash_symbol))
2019 		return false;
2020 	if (dash_symbol && !dash_symbol->equals(line->dash_symbol))
2021 		return false;
2022 	if (suppress_dash_symbol_at_ends != line->suppress_dash_symbol_at_ends)
2023 		return false;
2024 	if (scale_dash_symbol != line->scale_dash_symbol)
2025 		return false;
2026 
2027 	if (mid_symbol)
2028 	{
2029 		if (mid_symbols_per_spot != line->mid_symbols_per_spot)
2030 			return false;
2031 		if (mid_symbol_distance != line->mid_symbol_distance)
2032 			return false;
2033 		if (mid_symbol_placement != line->mid_symbol_placement)
2034 			return false;
2035 	}
2036 
2037 	if (have_border_lines != line->have_border_lines)
2038 		return false;
2039 	if (have_border_lines)
2040 	{
2041 		if (!border.equals(line->border) || !right_border.equals(line->right_border))
2042 			return false;
2043 	}
2044 
2045 	return true;
2046 }
2047 
2048 
2049 }  // namespace OpenOrienteering
2050