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