source: josm/trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java@ 11671

Last change on this file since 11671 was 11671, checked in by michael2402, 7 years ago

See #10176: Nicer stroke for icons of selected areas.

  • Property svn:eol-style set to native
File size: 82.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm.visitor.paint;
3
4import java.awt.AlphaComposite;
5import java.awt.BasicStroke;
6import java.awt.Color;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.Font;
10import java.awt.FontMetrics;
11import java.awt.Graphics2D;
12import java.awt.Image;
13import java.awt.Point;
14import java.awt.Rectangle;
15import java.awt.RenderingHints;
16import java.awt.Shape;
17import java.awt.TexturePaint;
18import java.awt.font.FontRenderContext;
19import java.awt.font.GlyphVector;
20import java.awt.font.LineMetrics;
21import java.awt.font.TextLayout;
22import java.awt.geom.AffineTransform;
23import java.awt.geom.Path2D;
24import java.awt.geom.Point2D;
25import java.awt.geom.Rectangle2D;
26import java.awt.geom.RoundRectangle2D;
27import java.awt.image.BufferedImage;
28import java.util.ArrayList;
29import java.util.Collection;
30import java.util.Collections;
31import java.util.Comparator;
32import java.util.HashMap;
33import java.util.Iterator;
34import java.util.List;
35import java.util.Map;
36import java.util.NoSuchElementException;
37import java.util.Optional;
38import java.util.concurrent.ForkJoinPool;
39import java.util.concurrent.ForkJoinTask;
40import java.util.concurrent.RecursiveTask;
41import java.util.function.BiConsumer;
42import java.util.function.Supplier;
43import java.util.stream.Collectors;
44
45import javax.swing.AbstractButton;
46import javax.swing.FocusManager;
47
48import org.openstreetmap.josm.Main;
49import org.openstreetmap.josm.data.Bounds;
50import org.openstreetmap.josm.data.coor.EastNorth;
51import org.openstreetmap.josm.data.osm.BBox;
52import org.openstreetmap.josm.data.osm.Changeset;
53import org.openstreetmap.josm.data.osm.DataSet;
54import org.openstreetmap.josm.data.osm.Node;
55import org.openstreetmap.josm.data.osm.OsmPrimitive;
56import org.openstreetmap.josm.data.osm.OsmUtils;
57import org.openstreetmap.josm.data.osm.Relation;
58import org.openstreetmap.josm.data.osm.RelationMember;
59import org.openstreetmap.josm.data.osm.Way;
60import org.openstreetmap.josm.data.osm.WaySegment;
61import org.openstreetmap.josm.data.osm.visitor.Visitor;
62import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
63import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
64import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
65import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
66import org.openstreetmap.josm.gui.NavigatableComponent;
67import org.openstreetmap.josm.gui.draw.MapViewPath;
68import org.openstreetmap.josm.gui.mappaint.ElemStyles;
69import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
70import org.openstreetmap.josm.gui.mappaint.StyleElementList;
71import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
72import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement;
73import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement;
74import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.HorizontalTextAlignment;
75import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.VerticalTextAlignment;
76import org.openstreetmap.josm.gui.mappaint.styleelement.MapImage;
77import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement;
78import org.openstreetmap.josm.gui.mappaint.styleelement.RepeatImageElement.LineImageAlignment;
79import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
80import org.openstreetmap.josm.gui.mappaint.styleelement.Symbol;
81import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel;
82import org.openstreetmap.josm.tools.CompositeList;
83import org.openstreetmap.josm.tools.Geometry;
84import org.openstreetmap.josm.tools.Geometry.AreaAndPerimeter;
85import org.openstreetmap.josm.tools.ImageProvider;
86import org.openstreetmap.josm.tools.Utils;
87import org.openstreetmap.josm.tools.bugreport.BugReport;
88
89/**
90 * A map renderer which renders a map according to style rules in a set of style sheets.
91 * @since 486
92 */
93public class StyledMapRenderer extends AbstractMapRenderer {
94
95 private static final ForkJoinPool THREAD_POOL =
96 Utils.newForkJoinPool("mappaint.StyledMapRenderer.style_creation.numberOfThreads", "styled-map-renderer-%d", Thread.NORM_PRIORITY);
97
98 /**
99 * Iterates over a list of Way Nodes and returns screen coordinates that
100 * represent a line that is shifted by a certain offset perpendicular
101 * to the way direction.
102 *
103 * There is no intention, to handle consecutive duplicate Nodes in a
104 * perfect way, but it should not throw an exception.
105 */
106 private class OffsetIterator implements Iterator<MapViewPoint> {
107
108 private final List<Node> nodes;
109 private final double offset;
110 private int idx;
111
112 private MapViewPoint prev;
113 /* 'prev0' is a point that has distance 'offset' from 'prev' and the
114 * line from 'prev' to 'prev0' is perpendicular to the way segment from
115 * 'prev' to the current point.
116 */
117 private double xPrev0;
118 private double yPrev0;
119
120 OffsetIterator(List<Node> nodes, double offset) {
121 this.nodes = nodes;
122 this.offset = offset;
123 idx = 0;
124 }
125
126 @Override
127 public boolean hasNext() {
128 return idx < nodes.size();
129 }
130
131 @Override
132 public MapViewPoint next() {
133 if (!hasNext())
134 throw new NoSuchElementException();
135
136 MapViewPoint current = getForIndex(idx);
137
138 if (Math.abs(offset) < 0.1d) {
139 idx++;
140 return current;
141 }
142
143 double xCurrent = current.getInViewX();
144 double yCurrent = current.getInViewY();
145 if (idx == nodes.size() - 1) {
146 ++idx;
147 if (prev != null) {
148 return mapState.getForView(xPrev0 + xCurrent - prev.getInViewX(),
149 yPrev0 + yCurrent - prev.getInViewY());
150 } else {
151 return current;
152 }
153 }
154
155 MapViewPoint next = getForIndex(idx + 1);
156 double dxNext = next.getInViewX() - xCurrent;
157 double dyNext = next.getInViewY() - yCurrent;
158 double lenNext = Math.sqrt(dxNext*dxNext + dyNext*dyNext);
159
160 if (lenNext < 1e-11) {
161 lenNext = 1; // value does not matter, because dy_next and dx_next is 0
162 }
163
164 // calculate the position of the translated current point
165 double om = offset / lenNext;
166 double xCurrent0 = xCurrent + om * dyNext;
167 double yCurrent0 = yCurrent - om * dxNext;
168
169 if (idx == 0) {
170 ++idx;
171 prev = current;
172 xPrev0 = xCurrent0;
173 yPrev0 = yCurrent0;
174 return mapState.getForView(xCurrent0, yCurrent0);
175 } else {
176 double dxPrev = xCurrent - prev.getInViewX();
177 double dyPrev = yCurrent - prev.getInViewY();
178 // determine intersection of the lines parallel to the two segments
179 double det = dxNext*dyPrev - dxPrev*dyNext;
180 double m = dxNext*(yCurrent0 - yPrev0) - dyNext*(xCurrent0 - xPrev0);
181
182 if (Utils.equalsEpsilon(det, 0) || Math.signum(det) != Math.signum(m)) {
183 ++idx;
184 prev = current;
185 xPrev0 = xCurrent0;
186 yPrev0 = yCurrent0;
187 return mapState.getForView(xCurrent0, yCurrent0);
188 }
189
190 double f = m / det;
191 if (f < 0) {
192 ++idx;
193 prev = current;
194 xPrev0 = xCurrent0;
195 yPrev0 = yCurrent0;
196 return mapState.getForView(xCurrent0, yCurrent0);
197 }
198 // the position of the intersection or intermittent point
199 double cx = xPrev0 + f * dxPrev;
200 double cy = yPrev0 + f * dyPrev;
201
202 if (f > 1) {
203 // check if the intersection point is too far away, this will happen for sharp angles
204 double dxI = cx - xCurrent;
205 double dyI = cy - yCurrent;
206 double lenISq = dxI * dxI + dyI * dyI;
207
208 if (lenISq > Math.abs(2 * offset * offset)) {
209 // intersection point is too far away, calculate intermittent points for capping
210 double dxPrev0 = xCurrent0 - xPrev0;
211 double dyPrev0 = yCurrent0 - yPrev0;
212 double lenPrev0 = Math.sqrt(dxPrev0 * dxPrev0 + dyPrev0 * dyPrev0);
213 f = 1 + Math.abs(offset / lenPrev0);
214 double cxCap = xPrev0 + f * dxPrev;
215 double cyCap = yPrev0 + f * dyPrev;
216 xPrev0 = cxCap;
217 yPrev0 = cyCap;
218 // calculate a virtual prev point which lies on a line that goes through current and
219 // is perpendicular to the line that goes through current and the intersection
220 // so that the next capping point is calculated with it.
221 double lenI = Math.sqrt(lenISq);
222 double xv = xCurrent + dyI / lenI;
223 double yv = yCurrent - dxI / lenI;
224
225 prev = mapState.getForView(xv, yv);
226 return mapState.getForView(cxCap, cyCap);
227 }
228 }
229 ++idx;
230 prev = current;
231 xPrev0 = xCurrent0;
232 yPrev0 = yCurrent0;
233 return mapState.getForView(cx, cy);
234 }
235 }
236
237 private MapViewPoint getForIndex(int i) {
238 return mapState.getPointFor(nodes.get(i));
239 }
240
241 @Override
242 public void remove() {
243 throw new UnsupportedOperationException();
244 }
245 }
246
247 /**
248 * This stores a style and a primitive that should be painted with that style.
249 */
250 public static class StyleRecord implements Comparable<StyleRecord> {
251 private final StyleElement style;
252 private final OsmPrimitive osm;
253 private final int flags;
254
255 StyleRecord(StyleElement style, OsmPrimitive osm, int flags) {
256 this.style = style;
257 this.osm = osm;
258 this.flags = flags;
259 }
260
261 @Override
262 public int compareTo(StyleRecord other) {
263 if ((this.flags & FLAG_DISABLED) != 0 && (other.flags & FLAG_DISABLED) == 0)
264 return -1;
265 if ((this.flags & FLAG_DISABLED) == 0 && (other.flags & FLAG_DISABLED) != 0)
266 return 1;
267
268 int d0 = Float.compare(this.style.majorZIndex, other.style.majorZIndex);
269 if (d0 != 0)
270 return d0;
271
272 // selected on top of member of selected on top of unselected
273 // FLAG_DISABLED bit is the same at this point
274 if (this.flags > other.flags)
275 return 1;
276 if (this.flags < other.flags)
277 return -1;
278
279 int dz = Float.compare(this.style.zIndex, other.style.zIndex);
280 if (dz != 0)
281 return dz;
282
283 // simple node on top of icons and shapes
284 if (NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(this.style) && !NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(other.style))
285 return 1;
286 if (!NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(this.style) && NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(other.style))
287 return -1;
288
289 // newer primitives to the front
290 long id = this.osm.getUniqueId() - other.osm.getUniqueId();
291 if (id > 0)
292 return 1;
293 if (id < 0)
294 return -1;
295
296 return Float.compare(this.style.objectZIndex, other.style.objectZIndex);
297 }
298
299 /**
300 * Get the style for this style element.
301 * @return The style
302 */
303 public StyleElement getStyle() {
304 return style;
305 }
306
307 /**
308 * Paints the primitive with the style.
309 * @param paintSettings The settings to use.
310 * @param painter The painter to paint the style.
311 */
312 public void paintPrimitive(MapPaintSettings paintSettings, StyledMapRenderer painter) {
313 style.paintPrimitive(
314 osm,
315 paintSettings,
316 painter,
317 (flags & FLAG_SELECTED) != 0,
318 (flags & FLAG_OUTERMEMBER_OF_SELECTED) != 0,
319 (flags & FLAG_MEMBER_OF_SELECTED) != 0
320 );
321 }
322
323 @Override
324 public String toString() {
325 return "StyleRecord [style=" + style + ", osm=" + osm + ", flags=" + flags + "]";
326 }
327 }
328
329 private static Map<Font, Boolean> IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = new HashMap<>();
330
331 /**
332 * Check, if this System has the GlyphVector double translation bug.
333 *
334 * With this bug, <code>gv.setGlyphTransform(i, trfm)</code> has a different
335 * effect than on most other systems, namely the translation components
336 * ("m02" &amp; "m12", {@link AffineTransform}) appear to be twice as large, as
337 * they actually are. The rotation is unaffected (scale &amp; shear not tested
338 * so far).
339 *
340 * This bug has only been observed on Mac OS X, see #7841.
341 *
342 * After switch to Java 7, this test is a false positive on Mac OS X (see #10446),
343 * i.e. it returns true, but the real rendering code does not require any special
344 * handling.
345 * It hasn't been further investigated why the test reports a wrong result in
346 * this case, but the method has been changed to simply return false by default.
347 * (This can be changed with a setting in the advanced preferences.)
348 *
349 * @param font The font to check.
350 * @return false by default, but depends on the value of the advanced
351 * preference glyph-bug=false|true|auto, where auto is the automatic detection
352 * method which apparently no longer gives a useful result for Java 7.
353 */
354 public static boolean isGlyphVectorDoubleTranslationBug(Font font) {
355 Boolean cached = IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.get(font);
356 if (cached != null)
357 return cached;
358 String overridePref = Main.pref.get("glyph-bug", "auto");
359 if ("auto".equals(overridePref)) {
360 FontRenderContext frc = new FontRenderContext(null, false, false);
361 GlyphVector gv = font.createGlyphVector(frc, "x");
362 gv.setGlyphTransform(0, AffineTransform.getTranslateInstance(1000, 1000));
363 Shape shape = gv.getGlyphOutline(0);
364 if (Main.isTraceEnabled()) {
365 Main.trace("#10446: shape: "+shape.getBounds());
366 }
367 // x is about 1000 on normal stystems and about 2000 when the bug occurs
368 int x = shape.getBounds().x;
369 boolean isBug = x > 1500;
370 IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, isBug);
371 return isBug;
372 } else {
373 boolean override = Boolean.parseBoolean(overridePref);
374 IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, override);
375 return override;
376 }
377 }
378
379 private double circum;
380 private double scale;
381
382 private MapPaintSettings paintSettings;
383
384 private Color highlightColorTransparent;
385
386 /**
387 * Flags used to store the primitive state along with the style. This is the normal style.
388 * <p>
389 * Not used in any public interfaces.
390 */
391 private static final int FLAG_NORMAL = 0;
392 /**
393 * A primitive with {@link OsmPrimitive#isDisabled()}
394 */
395 private static final int FLAG_DISABLED = 1;
396 /**
397 * A primitive with {@link OsmPrimitive#isMemberOfSelected()}
398 */
399 private static final int FLAG_MEMBER_OF_SELECTED = 2;
400 /**
401 * A primitive with {@link OsmPrimitive#isSelected()}
402 */
403 private static final int FLAG_SELECTED = 4;
404 /**
405 * A primitive with {@link OsmPrimitive#isOuterMemberOfSelected()}
406 */
407 private static final int FLAG_OUTERMEMBER_OF_SELECTED = 8;
408
409 private static final double PHI = Math.toRadians(20);
410 private static final double cosPHI = Math.cos(PHI);
411 private static final double sinPHI = Math.sin(PHI);
412
413 private Collection<WaySegment> highlightWaySegments;
414
415 // highlight customization fields
416 private int highlightLineWidth;
417 private int highlightPointRadius;
418 private int widerHighlight;
419 private int highlightStep;
420
421 //flag that activate wider highlight mode
422 private boolean useWiderHighlight;
423
424 private boolean useStrokes;
425 private boolean showNames;
426 private boolean showIcons;
427 private boolean isOutlineOnly;
428
429 private Font orderFont;
430
431 private boolean leftHandTraffic;
432 private Object antialiasing;
433
434 private Supplier<RenderBenchmarkCollector> benchmarkFactory = RenderBenchmarkCollector.defaultBenchmarkSupplier();
435
436 /**
437 * Constructs a new {@code StyledMapRenderer}.
438 *
439 * @param g the graphics context. Must not be null.
440 * @param nc the map viewport. Must not be null.
441 * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
442 * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
443 * @throws IllegalArgumentException if {@code g} is null
444 * @throws IllegalArgumentException if {@code nc} is null
445 */
446 public StyledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
447 super(g, nc, isInactiveMode);
448
449 if (nc != null) {
450 Component focusOwner = FocusManager.getCurrentManager().getFocusOwner();
451 useWiderHighlight = !(focusOwner instanceof AbstractButton || focusOwner == nc);
452 }
453 }
454
455 private void displaySegments(MapViewPath path, Path2D orientationArrows, Path2D onewayArrows, Path2D onewayArrowsCasing,
456 Color color, BasicStroke line, BasicStroke dashes, Color dashedColor) {
457 g.setColor(isInactiveMode ? inactiveColor : color);
458 if (useStrokes) {
459 g.setStroke(line);
460 }
461 g.draw(path.computeClippedLine(g.getStroke()));
462
463 if (!isInactiveMode && useStrokes && dashes != null) {
464 g.setColor(dashedColor);
465 g.setStroke(dashes);
466 g.draw(path.computeClippedLine(dashes));
467 }
468
469 if (orientationArrows != null) {
470 g.setColor(isInactiveMode ? inactiveColor : color);
471 g.setStroke(new BasicStroke(line.getLineWidth(), line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
472 g.draw(orientationArrows);
473 }
474
475 if (onewayArrows != null) {
476 g.setStroke(new BasicStroke(1, line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
477 g.fill(onewayArrowsCasing);
478 g.setColor(isInactiveMode ? inactiveColor : backgroundColor);
479 g.fill(onewayArrows);
480 }
481
482 if (useStrokes) {
483 g.setStroke(new BasicStroke());
484 }
485 }
486
487 /**
488 * Displays text at specified position including its halo, if applicable.
489 *
490 * @param gv Text's glyphs to display. If {@code null}, use text from {@code s} instead.
491 * @param s text to display if {@code gv} is {@code null}
492 * @param x X position
493 * @param y Y position
494 * @param disabled {@code true} if element is disabled (filtered out)
495 * @param text text style to use
496 */
497 private void displayText(GlyphVector gv, String s, int x, int y, boolean disabled, TextLabel text) {
498 if (gv == null && s.isEmpty()) return;
499 if (isInactiveMode || disabled) {
500 g.setColor(inactiveColor);
501 if (gv != null) {
502 g.drawGlyphVector(gv, x, y);
503 } else {
504 g.setFont(text.font);
505 g.drawString(s, x, y);
506 }
507 } else if (text.haloRadius != null) {
508 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
509 g.setColor(text.haloColor);
510 Shape textOutline;
511 if (gv == null) {
512 FontRenderContext frc = g.getFontRenderContext();
513 TextLayout tl = new TextLayout(s, text.font, frc);
514 textOutline = tl.getOutline(AffineTransform.getTranslateInstance(x, y));
515 } else {
516 textOutline = gv.getOutline(x, y);
517 }
518 g.draw(textOutline);
519 g.setStroke(new BasicStroke());
520 g.setColor(text.color);
521 g.fill(textOutline);
522 } else {
523 g.setColor(text.color);
524 if (gv != null) {
525 g.drawGlyphVector(gv, x, y);
526 } else {
527 g.setFont(text.font);
528 g.drawString(s, x, y);
529 }
530 }
531 }
532
533 /**
534 * Worker function for drawing areas.
535 *
536 * @param osm the primitive
537 * @param path the path object for the area that should be drawn; in case
538 * of multipolygons, this can path can be a complex shape with one outer
539 * polygon and one or more inner polygons
540 * @param color The color to fill the area with.
541 * @param fillImage The image to fill the area with. Overrides color.
542 * @param extent if not null, area will be filled partially; specifies, how
543 * far to fill from the boundary towards the center of the area;
544 * if null, area will be filled completely
545 * @param pfClip clipping area for partial fill (only needed for unclosed
546 * polygons)
547 * @param disabled If this should be drawn with a special disabled style.
548 * @param text The text to write on the area.
549 */
550 protected void drawArea(OsmPrimitive osm, Path2D.Double path, Color color,
551 MapImage fillImage, Float extent, Path2D.Double pfClip, boolean disabled, TextLabel text) {
552
553 Shape area = path.createTransformedShape(mapState.getAffineTransform());
554
555 if (!isOutlineOnly) {
556 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
557 if (fillImage == null) {
558 if (isInactiveMode) {
559 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.33f));
560 }
561 g.setColor(color);
562 if (extent == null) {
563 g.fill(area);
564 } else {
565 Shape oldClip = g.getClip();
566 Shape clip = area;
567 if (pfClip != null) {
568 clip = pfClip.createTransformedShape(mapState.getAffineTransform());
569 }
570 g.clip(clip);
571 g.setStroke(new BasicStroke(2 * extent, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 4));
572 g.draw(area);
573 g.setClip(oldClip);
574 }
575 } else {
576 TexturePaint texture = new TexturePaint(fillImage.getImage(disabled),
577 new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight()));
578 g.setPaint(texture);
579 Float alpha = fillImage.getAlphaFloat();
580 if (!Utils.equalsEpsilon(alpha, 1f)) {
581 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
582 }
583 if (extent == null) {
584 g.fill(area);
585 } else {
586 Shape oldClip = g.getClip();
587 BasicStroke stroke = new BasicStroke(2 * extent, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
588 g.clip(stroke.createStrokedShape(area));
589 Shape fill = area;
590 if (pfClip != null) {
591 fill = pfClip.createTransformedShape(mapState.getAffineTransform());
592 }
593 g.fill(fill);
594 g.setClip(oldClip);
595 }
596 g.setPaintMode();
597 }
598 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
599 }
600
601 drawAreaText(osm, text, area);
602 }
603
604 private void drawAreaText(OsmPrimitive osm, TextLabel text, Shape area) {
605 if (text != null && isShowNames()) {
606 // abort if we can't compose the label to be rendered
607 if (text.labelCompositionStrategy == null) return;
608 String name = text.labelCompositionStrategy.compose(osm);
609 if (name == null) return;
610
611 Rectangle pb = area.getBounds();
612 FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache
613 Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
614
615 // Using the Centroid is Nicer for buildings like: +--------+
616 // but this needs to be fast. As most houses are | 42 |
617 // boxes anyway, the center of the bounding box +---++---+
618 // will have to do. ++
619 // Centroids are not optimal either, just imagine a U-shaped house.
620
621 // quick check to see if label box is smaller than primitive box
622 if (pb.width >= nb.getWidth() && pb.height >= nb.getHeight()) {
623
624 final double w = pb.width - nb.getWidth();
625 final double h = pb.height - nb.getHeight();
626
627 final int x2 = pb.x + (int) (w/2.0);
628 final int y2 = pb.y + (int) (h/2.0);
629
630 final int nbw = (int) nb.getWidth();
631 final int nbh = (int) nb.getHeight();
632
633 Rectangle centeredNBounds = new Rectangle(x2, y2, nbw, nbh);
634
635 // slower check to see if label is displayed inside primitive shape
636 boolean labelOK = area.contains(centeredNBounds);
637 if (!labelOK) {
638 // if center position (C) is not inside osm shape, try naively some other positions as follows:
639 // CHECKSTYLE.OFF: SingleSpaceSeparator
640 final int x1 = pb.x + (int) (w/4.0);
641 final int x3 = pb.x + (int) (3*w/4.0);
642 final int y1 = pb.y + (int) (h/4.0);
643 final int y3 = pb.y + (int) (3*h/4.0);
644 // CHECKSTYLE.ON: SingleSpaceSeparator
645 // +-----------+
646 // | 5 1 6 |
647 // | 4 C 2 |
648 // | 8 3 7 |
649 // +-----------+
650 Rectangle[] candidates = new Rectangle[] {
651 new Rectangle(x2, y1, nbw, nbh),
652 new Rectangle(x3, y2, nbw, nbh),
653 new Rectangle(x2, y3, nbw, nbh),
654 new Rectangle(x1, y2, nbw, nbh),
655 new Rectangle(x1, y1, nbw, nbh),
656 new Rectangle(x3, y1, nbw, nbh),
657 new Rectangle(x3, y3, nbw, nbh),
658 new Rectangle(x1, y3, nbw, nbh)
659 };
660 // Dumb algorithm to find a better placement. We could surely find a smarter one but it should
661 // solve most of building issues with only few calculations (8 at most)
662 for (int i = 0; i < candidates.length && !labelOK; i++) {
663 centeredNBounds = candidates[i];
664 labelOK = area.contains(centeredNBounds);
665 }
666 }
667 if (labelOK) {
668 Font defaultFont = g.getFont();
669 int x = (int) (centeredNBounds.getMinX() - nb.getMinX());
670 int y = (int) (centeredNBounds.getMinY() - nb.getMinY());
671 displayText(null, name, x, y, osm.isDisabled(), text);
672 g.setFont(defaultFont);
673 } else if (Main.isTraceEnabled()) {
674 Main.trace("Couldn't find a correct label placement for "+osm+" / "+name);
675 }
676 }
677 }
678 }
679
680 /**
681 * Draws a multipolygon area.
682 * @param r The multipolygon relation
683 * @param color The color to fill the area with.
684 * @param fillImage The image to fill the area with. Overrides color.
685 * @param extent if not null, area will be filled partially; specifies, how
686 * far to fill from the boundary towards the center of the area;
687 * if null, area will be filled completely
688 * @param extentThreshold if not null, determines if the partial filled should
689 * be replaced by plain fill, when it covers a certain fraction of the total area
690 * @param disabled If this should be drawn with a special disabled style.
691 * @param text The text to write on the area.
692 */
693 public void drawArea(Relation r, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) {
694 Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
695 if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) {
696 for (PolyData pd : multipolygon.getCombinedPolygons()) {
697 Path2D.Double p = pd.get();
698 Path2D.Double pfClip = null;
699 if (!isAreaVisible(p)) {
700 continue;
701 }
702 if (extent != null) {
703 if (!usePartialFill(pd.getAreaAndPerimeter(null), extent, extentThreshold)) {
704 extent = null;
705 } else if (!pd.isClosed()) {
706 pfClip = getPFClip(pd, extent * scale);
707 }
708 }
709 drawArea(r, p,
710 pd.isSelected() ? paintSettings.getRelationSelectedColor(color.getAlpha()) : color,
711 fillImage, extent, pfClip, disabled, text);
712 }
713 }
714 }
715
716 /**
717 * Draws an area defined by a way. They way does not need to be closed, but it should.
718 * @param w The way.
719 * @param color The color to fill the area with.
720 * @param fillImage The image to fill the area with. Overrides color.
721 * @param extent if not null, area will be filled partially; specifies, how
722 * far to fill from the boundary towards the center of the area;
723 * if null, area will be filled completely
724 * @param extentThreshold if not null, determines if the partial filled should
725 * be replaced by plain fill, when it covers a certain fraction of the total area
726 * @param disabled If this should be drawn with a special disabled style.
727 * @param text The text to write on the area.
728 */
729 public void drawArea(Way w, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) {
730 Path2D.Double pfClip = null;
731 if (extent != null) {
732 if (!usePartialFill(Geometry.getAreaAndPerimeter(w.getNodes()), extent, extentThreshold)) {
733 extent = null;
734 } else if (!w.isClosed()) {
735 pfClip = getPFClip(w, extent * scale);
736 }
737 }
738 drawArea(w, getPath(w), color, fillImage, extent, pfClip, disabled, text);
739 }
740
741 /**
742 * Determine, if partial fill should be turned off for this object, because
743 * only a small unfilled gap in the center of the area would be left.
744 *
745 * This is used to get a cleaner look for urban regions with many small
746 * areas like buildings, etc.
747 * @param ap the area and the perimeter of the object
748 * @param extent the "width" of partial fill
749 * @param threshold when the partial fill covers that much of the total
750 * area, the partial fill is turned off; can be greater than 100% as the
751 * covered area is estimated as <code>perimeter * extent</code>
752 * @return true, if the partial fill should be used, false otherwise
753 */
754 private boolean usePartialFill(AreaAndPerimeter ap, float extent, Float threshold) {
755 if (threshold == null) return true;
756 return ap.getPerimeter() * extent * scale < threshold * ap.getArea();
757 }
758
759 /**
760 * Draw a text onto a node
761 * @param n The node to draw the text on
762 * @param bs The text and it's alignment.
763 */
764 public void drawBoxText(Node n, BoxTextElement bs) {
765 if (!isShowNames() || bs == null)
766 return;
767
768 MapViewPoint p = mapState.getPointFor(n);
769 TextLabel text = bs.text;
770 String s = text.labelCompositionStrategy.compose(n);
771 if (s == null) return;
772
773 Font defaultFont = g.getFont();
774 g.setFont(text.font);
775
776 int x = (int) (Math.round(p.getInViewX()) + text.xOffset);
777 int y = (int) (Math.round(p.getInViewY()) + text.yOffset);
778 /**
779 *
780 * left-above __center-above___ right-above
781 * left-top| |right-top
782 * | |
783 * left-center| center-center |right-center
784 * | |
785 * left-bottom|_________________|right-bottom
786 * left-below center-below right-below
787 *
788 */
789 Rectangle box = bs.getBox();
790 if (bs.hAlign == HorizontalTextAlignment.RIGHT) {
791 x += box.x + box.width + 2;
792 } else {
793 FontRenderContext frc = g.getFontRenderContext();
794 Rectangle2D bounds = text.font.getStringBounds(s, frc);
795 int textWidth = (int) bounds.getWidth();
796 if (bs.hAlign == HorizontalTextAlignment.CENTER) {
797 x -= textWidth / 2;
798 } else if (bs.hAlign == HorizontalTextAlignment.LEFT) {
799 x -= -box.x + 4 + textWidth;
800 } else throw new AssertionError();
801 }
802
803 if (bs.vAlign == VerticalTextAlignment.BOTTOM) {
804 y += box.y + box.height;
805 } else {
806 FontRenderContext frc = g.getFontRenderContext();
807 LineMetrics metrics = text.font.getLineMetrics(s, frc);
808 if (bs.vAlign == VerticalTextAlignment.ABOVE) {
809 y -= -box.y + (int) metrics.getDescent();
810 } else if (bs.vAlign == VerticalTextAlignment.TOP) {
811 y -= -box.y - (int) metrics.getAscent();
812 } else if (bs.vAlign == VerticalTextAlignment.CENTER) {
813 y += (int) ((metrics.getAscent() - metrics.getDescent()) / 2);
814 } else if (bs.vAlign == VerticalTextAlignment.BELOW) {
815 y += box.y + box.height + (int) metrics.getAscent() + 2;
816 } else throw new AssertionError();
817 }
818 displayText(null, s, x, y, n.isDisabled(), text);
819 g.setFont(defaultFont);
820 }
821
822 /**
823 * Draw an image along a way repeatedly.
824 *
825 * @param way the way
826 * @param pattern the image
827 * @param disabled If this should be drawn with a special disabled style.
828 * @param offset offset from the way
829 * @param spacing spacing between two images
830 * @param phase initial spacing
831 * @param align alignment of the image. The top, center or bottom edge can be aligned with the way.
832 */
833 public void drawRepeatImage(Way way, MapImage pattern, boolean disabled, double offset, double spacing, double phase,
834 LineImageAlignment align) {
835 final int imgWidth = pattern.getWidth();
836 final double repeat = imgWidth + spacing;
837 final int imgHeight = pattern.getHeight();
838
839 int dy1 = (int) ((align.getAlignmentOffset() - .5) * imgHeight);
840 int dy2 = dy1 + imgHeight;
841
842 OffsetIterator it = new OffsetIterator(way.getNodes(), offset);
843 MapViewPath path = new MapViewPath(mapState);
844 if (it.hasNext()) {
845 path.moveTo(it.next());
846 }
847 while (it.hasNext()) {
848 path.lineTo(it.next());
849 }
850
851 double startOffset = phase % repeat;
852 if (startOffset < 0) {
853 startOffset += repeat;
854 }
855
856 BufferedImage image = pattern.getImage(disabled);
857
858 path.visitClippedLine(startOffset, repeat, (inLineOffset, start, end, startIsOldEnd) -> {
859 final double segmentLength = start.distanceToInView(end);
860 if (segmentLength < 0.1) {
861 // avoid odd patterns when zoomed out.
862 return;
863 }
864 if (segmentLength > repeat * 500) {
865 // simply skip drawing so many images - something must be wrong.
866 return;
867 }
868 AffineTransform saveTransform = g.getTransform();
869 g.translate(start.getInViewX(), start.getInViewY());
870 double dx = end.getInViewX() - start.getInViewX();
871 double dy = end.getInViewY() - start.getInViewY();
872 g.rotate(Math.atan2(dy, dx));
873
874 // The start of the next image
875 double imageStart = -(inLineOffset % repeat);
876
877 while (imageStart < segmentLength) {
878 int x = (int) imageStart;
879 int sx1 = Math.max(0, -x);
880 int sx2 = imgWidth - Math.max(0, x + imgWidth - (int) Math.ceil(segmentLength));
881 g.drawImage(image, x + sx1, dy1, x + sx2, dy2, sx1, 0, sx2, imgHeight, null);
882 imageStart += repeat;
883 }
884
885 g.setTransform(saveTransform);
886 });
887 }
888
889 @Override
890 public void drawNode(Node n, Color color, int size, boolean fill) {
891 if (size <= 0 && !n.isHighlighted())
892 return;
893
894 MapViewPoint p = mapState.getPointFor(n);
895
896 if (n.isHighlighted()) {
897 drawPointHighlight(p.getInView(), size);
898 }
899
900 if (size > 1 && p.isInView()) {
901 int radius = size / 2;
902
903 if (isInactiveMode || n.isDisabled()) {
904 g.setColor(inactiveColor);
905 } else {
906 g.setColor(color);
907 }
908 Rectangle2D rect = new Rectangle2D.Double(p.getInViewX()-radius-1, p.getInViewY()-radius-1, size + 1, size + 1);
909 if (fill) {
910 g.fill(rect);
911 } else {
912 g.draw(rect);
913 }
914 }
915 }
916
917 /**
918 * Draw the icon for a given node.
919 * @param n The node
920 * @param img The icon to draw at the node position
921 * @param disabled {@code} true to render disabled version, {@code false} for the standard version
922 * @param selected {@code} true to render it as selected, {@code false} otherwise
923 * @param member {@code} true to render it as a relation member, {@code false} otherwise
924 * @param theta the angle of rotation in radians
925 */
926 public void drawNodeIcon(Node n, MapImage img, boolean disabled, boolean selected, boolean member, double theta) {
927 MapViewPoint p = mapState.getPointFor(n);
928
929 int w = img.getWidth();
930 int h = img.getHeight();
931 if (n.isHighlighted()) {
932 drawPointHighlight(p.getInView(), Math.max(w, h));
933 }
934
935 drawIcon(p, img, disabled, selected, member, theta, (g, r) -> {
936 Color color = getSelectionHintColor(disabled, selected);
937 g.setColor(color);
938 g.draw(r);
939 });
940 }
941
942
943 /**
944 * Draw the icon for a given area. The icon is drawn around the lat/lon center of the area.
945 * @param primitive The node
946 * @param img The icon to draw at the node position
947 * @param disabled {@code} true to render disabled version, {@code false} for the standard version
948 * @param selected {@code} true to render it as selected, {@code false} otherwise
949 * @param member {@code} true to render it as a relation member, {@code false} otherwise
950 * @param theta the angle of rotation in radians
951 */
952 public void drawAreaIcon(OsmPrimitive primitive, MapImage img, boolean disabled, boolean selected, boolean member, double theta) {
953 BBox bbox = null;
954 if (primitive instanceof Way) {
955 bbox = primitive.getBBox();
956 } else if (primitive instanceof Relation) {
957 Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, (Relation) primitive);
958 if (multipolygon != null) {
959 BBox collect = new BBox();
960 multipolygon.getOuterPolygons().forEach(p -> p.getNodes().forEach(n -> collect.add(n.getCoor())));
961 bbox = collect;
962 }
963 }
964
965 if (bbox != null && bbox.isValid()) {
966 MapViewPoint p = mapState.getPointFor(bbox.getCenter());
967 drawIcon(p, img, disabled, selected, member, theta, (g, r) -> {
968 if (useStrokes) {
969 g.setStroke(new BasicStroke(2));
970 }
971 // only draw a minor highlighting, so that users do not confuse this for a point.
972 Color color = getSelectionHintColor(disabled, selected);
973 color = new Color(color.getRed(), color.getGreen(), color.getBlue(), (int) (color.getAlpha() * .2));
974 g.setColor(color);
975 g.draw(r);
976 });
977 }
978 }
979
980 private void drawIcon(MapViewPoint p, MapImage img, boolean disabled, boolean selected, boolean member, double theta,
981 BiConsumer<Graphics2D, Rectangle2D> selectionDrawer) {
982 float alpha = img.getAlphaFloat();
983
984 Graphics2D temporaryGraphics = (Graphics2D) g.create();
985 if (!Utils.equalsEpsilon(alpha, 1f)) {
986 temporaryGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
987 }
988
989 double x = Math.round(p.getInViewX());
990 double y = Math.round(p.getInViewY());
991 temporaryGraphics.translate(x, y);
992 temporaryGraphics.rotate(theta);
993 int drawX = -img.getWidth() / 2 + img.offsetX;
994 int drawY = -img.getHeight() / 2 + img.offsetY;
995 temporaryGraphics.drawImage(img.getImage(disabled), drawX, drawY, nc);
996 if (selected || member) {
997 selectionDrawer.accept(temporaryGraphics, new Rectangle2D.Double(drawX - 2, drawY - 2, img.getWidth() + 4, img.getHeight() + 4));
998 }
999 }
1000
1001 private Color getSelectionHintColor(boolean disabled, boolean selected) {
1002 Color color;
1003 if (disabled) {
1004 color = inactiveColor;
1005 } else if (selected) {
1006 color = selectedColor;
1007 } else {
1008 color = relationSelectedColor;
1009 }
1010 return color;
1011 }
1012
1013 /**
1014 * Draw the symbol and possibly a highlight marking on a given node.
1015 * @param n The position to draw the symbol on
1016 * @param s The symbol to draw
1017 * @param fillColor The color to fill the symbol with
1018 * @param strokeColor The color to use for the outer corner of the symbol
1019 */
1020 public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
1021 MapViewPoint p = mapState.getPointFor(n);
1022
1023 if (n.isHighlighted()) {
1024 drawPointHighlight(p.getInView(), s.size);
1025 }
1026
1027 if (fillColor != null || strokeColor != null) {
1028 Shape shape = s.buildShapeAround(p.getInViewX(), p.getInViewY());
1029
1030 if (fillColor != null) {
1031 g.setColor(fillColor);
1032 g.fill(shape);
1033 }
1034 if (s.stroke != null) {
1035 g.setStroke(s.stroke);
1036 g.setColor(strokeColor);
1037 g.draw(shape);
1038 g.setStroke(new BasicStroke());
1039 }
1040 }
1041 }
1042
1043 /**
1044 * Draw a number of the order of the two consecutive nodes within the
1045 * parents way
1046 *
1047 * @param n1 First node of the way segment.
1048 * @param n2 Second node of the way segment.
1049 * @param orderNumber The number of the segment in the way.
1050 * @param clr The color to use for drawing the text.
1051 */
1052 public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
1053 MapViewPoint p1 = mapState.getPointFor(n1);
1054 MapViewPoint p2 = mapState.getPointFor(n2);
1055 drawOrderNumber(p1, p2, orderNumber, clr);
1056 }
1057
1058 /**
1059 * highlights a given GeneralPath using the settings from BasicStroke to match the line's
1060 * style. Width of the highlight is hard coded.
1061 * @param path path to draw
1062 * @param line line style
1063 */
1064 private void drawPathHighlight(MapViewPath path, BasicStroke line) {
1065 if (path == null)
1066 return;
1067 g.setColor(highlightColorTransparent);
1068 float w = line.getLineWidth() + highlightLineWidth;
1069 if (useWiderHighlight) w += widerHighlight;
1070 while (w >= line.getLineWidth()) {
1071 g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit()));
1072 g.draw(path);
1073 w -= highlightStep;
1074 }
1075 }
1076
1077 /**
1078 * highlights a given point by drawing a rounded rectangle around it. Give the
1079 * size of the object you want to be highlighted, width is added automatically.
1080 * @param p point
1081 * @param size highlight size
1082 */
1083 private void drawPointHighlight(Point2D p, int size) {
1084 g.setColor(highlightColorTransparent);
1085 int s = size + highlightPointRadius;
1086 if (useWiderHighlight) s += widerHighlight;
1087 while (s >= size) {
1088 int r = (int) Math.floor(s/2d);
1089 g.fill(new RoundRectangle2D.Double(p.getX()-r, p.getY()-r, s, s, r, r));
1090 s -= highlightStep;
1091 }
1092 }
1093
1094 public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
1095 // rotate image with direction last node in from to, and scale down image to 16*16 pixels
1096 Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16));
1097 int w = smallImg.getWidth(null), h = smallImg.getHeight(null);
1098 g.drawImage(smallImg, (int) (pVia.x+vx+vx2)-w/2, (int) (pVia.y+vy+vy2)-h/2, nc);
1099
1100 if (selected) {
1101 g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor);
1102 g.drawRect((int) (pVia.x+vx+vx2)-w/2-2, (int) (pVia.y+vy+vy2)-h/2-2, w+4, h+4);
1103 }
1104 }
1105
1106 public void drawRestriction(Relation r, MapImage icon, boolean disabled) {
1107 Way fromWay = null;
1108 Way toWay = null;
1109 OsmPrimitive via = null;
1110
1111 /* find the "from", "via" and "to" elements */
1112 for (RelationMember m : r.getMembers()) {
1113 if (m.getMember().isIncomplete())
1114 return;
1115 else {
1116 if (m.isWay()) {
1117 Way w = m.getWay();
1118 if (w.getNodesCount() < 2) {
1119 continue;
1120 }
1121
1122 switch(m.getRole()) {
1123 case "from":
1124 if (fromWay == null) {
1125 fromWay = w;
1126 }
1127 break;
1128 case "to":
1129 if (toWay == null) {
1130 toWay = w;
1131 }
1132 break;
1133 case "via":
1134 if (via == null) {
1135 via = w;
1136 }
1137 break;
1138 default: // Do nothing
1139 }
1140 } else if (m.isNode()) {
1141 Node n = m.getNode();
1142 if (via == null && "via".equals(m.getRole())) {
1143 via = n;
1144 }
1145 }
1146 }
1147 }
1148
1149 if (fromWay == null || toWay == null || via == null)
1150 return;
1151
1152 Node viaNode;
1153 if (via instanceof Node) {
1154 viaNode = (Node) via;
1155 if (!fromWay.isFirstLastNode(viaNode))
1156 return;
1157 } else {
1158 Way viaWay = (Way) via;
1159 Node firstNode = viaWay.firstNode();
1160 Node lastNode = viaWay.lastNode();
1161 Boolean onewayvia = Boolean.FALSE;
1162
1163 String onewayviastr = viaWay.get("oneway");
1164 if (onewayviastr != null) {
1165 if ("-1".equals(onewayviastr)) {
1166 onewayvia = Boolean.TRUE;
1167 Node tmp = firstNode;
1168 firstNode = lastNode;
1169 lastNode = tmp;
1170 } else {
1171 onewayvia = Optional.ofNullable(OsmUtils.getOsmBoolean(onewayviastr)).orElse(Boolean.FALSE);
1172 }
1173 }
1174
1175 if (fromWay.isFirstLastNode(firstNode)) {
1176 viaNode = firstNode;
1177 } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
1178 viaNode = lastNode;
1179 } else
1180 return;
1181 }
1182
1183 /* find the "direct" nodes before the via node */
1184 Node fromNode;
1185 if (fromWay.firstNode() == via) {
1186 fromNode = fromWay.getNode(1);
1187 } else {
1188 fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
1189 }
1190
1191 Point pFrom = nc.getPoint(fromNode);
1192 Point pVia = nc.getPoint(viaNode);
1193
1194 /* starting from via, go back the "from" way a few pixels
1195 (calculate the vector vx/vy with the specified length and the direction
1196 away from the "via" node along the first segment of the "from" way)
1197 */
1198 double distanceFromVia = 14;
1199 double dx = pFrom.x >= pVia.x ? pFrom.x - pVia.x : pVia.x - pFrom.x;
1200 double dy = pFrom.y >= pVia.y ? pFrom.y - pVia.y : pVia.y - pFrom.y;
1201
1202 double fromAngle;
1203 if (dx == 0) {
1204 fromAngle = Math.PI/2;
1205 } else {
1206 fromAngle = Math.atan(dy / dx);
1207 }
1208 double fromAngleDeg = Math.toDegrees(fromAngle);
1209
1210 double vx = distanceFromVia * Math.cos(fromAngle);
1211 double vy = distanceFromVia * Math.sin(fromAngle);
1212
1213 if (pFrom.x < pVia.x) {
1214 vx = -vx;
1215 }
1216 if (pFrom.y < pVia.y) {
1217 vy = -vy;
1218 }
1219
1220 /* go a few pixels away from the way (in a right angle)
1221 (calculate the vx2/vy2 vector with the specified length and the direction
1222 90degrees away from the first segment of the "from" way)
1223 */
1224 double distanceFromWay = 10;
1225 double vx2 = 0;
1226 double vy2 = 0;
1227 double iconAngle = 0;
1228
1229 if (pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
1230 if (!leftHandTraffic) {
1231 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1232 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1233 } else {
1234 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1235 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1236 }
1237 iconAngle = 270+fromAngleDeg;
1238 }
1239 if (pFrom.x < pVia.x && pFrom.y >= pVia.y) {
1240 if (!leftHandTraffic) {
1241 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1242 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1243 } else {
1244 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1245 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1246 }
1247 iconAngle = 90-fromAngleDeg;
1248 }
1249 if (pFrom.x < pVia.x && pFrom.y < pVia.y) {
1250 if (!leftHandTraffic) {
1251 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1252 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1253 } else {
1254 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1255 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1256 }
1257 iconAngle = 90+fromAngleDeg;
1258 }
1259 if (pFrom.x >= pVia.x && pFrom.y < pVia.y) {
1260 if (!leftHandTraffic) {
1261 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1262 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1263 } else {
1264 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1265 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1266 }
1267 iconAngle = 270-fromAngleDeg;
1268 }
1269
1270 drawRestriction(icon.getImage(disabled),
1271 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1272 }
1273
1274 /**
1275 * A half segment that can be used to place text on it. Used in the drawTextOnPath algorithm.
1276 * @author Michael Zangl
1277 */
1278 private static class HalfSegment {
1279 /**
1280 * start point of half segment (as length along the way)
1281 */
1282 final double start;
1283 /**
1284 * end point of half segment (as length along the way)
1285 */
1286 final double end;
1287 /**
1288 * quality factor (off screen / partly on screen / fully on screen)
1289 */
1290 final double quality;
1291
1292 /**
1293 * Create a new half segment
1294 * @param start The start along the way
1295 * @param end The end of the segment
1296 * @param quality A quality factor.
1297 */
1298 HalfSegment(double start, double end, double quality) {
1299 super();
1300 this.start = start;
1301 this.end = end;
1302 this.quality = quality;
1303 }
1304
1305 @Override
1306 public String toString() {
1307 return "HalfSegment [start=" + start + ", end=" + end + ", quality=" + quality + "]";
1308 }
1309 }
1310
1311 /**
1312 * Draws a text along a given way.
1313 * @param way The way to draw the text on.
1314 * @param text The text definition (font/.../text content) to draw.
1315 */
1316 public void drawTextOnPath(Way way, TextLabel text) {
1317 if (way == null || text == null)
1318 return;
1319 String name = text.getString(way);
1320 if (name == null || name.isEmpty())
1321 return;
1322
1323 FontMetrics fontMetrics = g.getFontMetrics(text.font);
1324 Rectangle2D rec = fontMetrics.getStringBounds(name, g);
1325
1326 Rectangle bounds = g.getClipBounds();
1327
1328 List<MapViewPoint> points = way.getNodes().stream().map(mapState::getPointFor).collect(Collectors.toList());
1329
1330 // find half segments that are long enough to draw text on (don't draw text over the cross hair in the center of each segment)
1331 List<HalfSegment> longHalfSegment = new ArrayList<>();
1332
1333 double pathLength = computePath(2 * (rec.getWidth() + 4), bounds, points, longHalfSegment);
1334
1335 if (rec.getWidth() > pathLength)
1336 return;
1337
1338 double t1, t2;
1339
1340 if (!longHalfSegment.isEmpty()) {
1341 // find the segment with the best quality. If there are several with best quality, the one close to the center is prefered.
1342 Optional<HalfSegment> besto = longHalfSegment.stream().max(
1343 Comparator.comparingDouble(segment ->
1344 segment.quality - 1e-5 * Math.abs(0.5 * (segment.end + segment.start) - 0.5 * pathLength)
1345 ));
1346 if (!besto.isPresent())
1347 throw new IllegalStateException("Unable to find the segment with the best quality for " + way);
1348 HalfSegment best = besto.get();
1349 double remaining = best.end - best.start - rec.getWidth(); // total space left and right from the text
1350 // The space left and right of the text should be distributed 20% - 80% (towards the center),
1351 // but the smaller space should not be less than 7 px.
1352 // However, if the total remaining space is less than 14 px, then distribute it evenly.
1353 double smallerSpace = Math.min(Math.max(0.2 * remaining, 7), 0.5 * remaining);
1354 if ((best.end + best.start)/2 < pathLength/2) {
1355 t2 = best.end - smallerSpace;
1356 t1 = t2 - rec.getWidth();
1357 } else {
1358 t1 = best.start + smallerSpace;
1359 t2 = t1 + rec.getWidth();
1360 }
1361 } else {
1362 // doesn't fit into one half-segment -> just put it in the center of the way
1363 t1 = pathLength/2 - rec.getWidth()/2;
1364 t2 = pathLength/2 + rec.getWidth()/2;
1365 }
1366 t1 /= pathLength;
1367 t2 /= pathLength;
1368
1369 double[] p1 = pointAt(t1, points, pathLength);
1370 double[] p2 = pointAt(t2, points, pathLength);
1371
1372 if (p1 == null || p2 == null)
1373 return;
1374
1375 double angleOffset;
1376 double offsetSign;
1377 double tStart;
1378
1379 if (p1[0] < p2[0] &&
1380 p1[2] < Math.PI/2 &&
1381 p1[2] > -Math.PI/2) {
1382 angleOffset = 0;
1383 offsetSign = 1;
1384 tStart = t1;
1385 } else {
1386 angleOffset = Math.PI;
1387 offsetSign = -1;
1388 tStart = t2;
1389 }
1390
1391 List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext());
1392 double gvOffset = 0;
1393 for (GlyphVector gv : gvs) {
1394 double gvWidth = gv.getLogicalBounds().getBounds2D().getWidth();
1395 for (int i = 0; i < gv.getNumGlyphs(); ++i) {
1396 Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
1397 double t = tStart + offsetSign * (gvOffset + rect.getX() + rect.getWidth()/2) / pathLength;
1398 double[] p = pointAt(t, points, pathLength);
1399 if (p != null) {
1400 AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
1401 trfm.rotate(p[2]+angleOffset);
1402 double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
1403 trfm.translate(-rect.getWidth()/2, off);
1404 if (isGlyphVectorDoubleTranslationBug(text.font)) {
1405 // scale the translation components by one half
1406 AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY());
1407 tmp.concatenate(trfm);
1408 trfm = tmp;
1409 }
1410 gv.setGlyphTransform(i, trfm);
1411 }
1412 }
1413 displayText(gv, null, 0, 0, way.isDisabled(), text);
1414 gvOffset += gvWidth;
1415 }
1416 }
1417
1418 private static double computePath(double minSegmentLength, Rectangle bounds, List<MapViewPoint> points,
1419 List<HalfSegment> longHalfSegment) {
1420 MapViewPoint lastPoint = points.get(0);
1421 double pathLength = 0;
1422 for (MapViewPoint p : points.subList(1, points.size())) {
1423 double segmentLength = p.distanceToInView(lastPoint);
1424 if (segmentLength > minSegmentLength) {
1425 Point2D center = new Point2D.Double((lastPoint.getInViewX() + p.getInViewX())/2, (lastPoint.getInViewY() + p.getInViewY())/2);
1426 double q = computeQuality(bounds, lastPoint, center);
1427 // prefer the first one for quality equality.
1428 longHalfSegment.add(new HalfSegment(pathLength, pathLength + segmentLength / 2, q));
1429
1430 q = 0;
1431 if (bounds != null) {
1432 if (bounds.contains(center) && bounds.contains(p.getInView())) {
1433 q = 2;
1434 } else if (bounds.contains(center) || bounds.contains(p.getInView())) {
1435 q = 1;
1436 }
1437 }
1438 longHalfSegment.add(new HalfSegment(pathLength + segmentLength / 2, pathLength + segmentLength, q));
1439 }
1440 pathLength += segmentLength;
1441 lastPoint = p;
1442 }
1443 return pathLength;
1444 }
1445
1446 private static double computeQuality(Rectangle bounds, MapViewPoint p1, Point2D p2) {
1447 double q = 0;
1448 if (bounds != null) {
1449 if (bounds.contains(p1.getInView())) {
1450 q += 1;
1451 }
1452 if (bounds.contains(p2)) {
1453 q += 1;
1454 }
1455 }
1456 return q;
1457 }
1458
1459 /**
1460 * draw way. This method allows for two draw styles (line using color, dashes using dashedColor) to be passed.
1461 * @param way The way to draw
1462 * @param color The base color to draw the way in
1463 * @param line The line style to use. This is drawn using color.
1464 * @param dashes The dash style to use. This is drawn using dashedColor. <code>null</code> if unused.
1465 * @param dashedColor The color of the dashes.
1466 * @param offset The offset
1467 * @param showOrientation show arrows that indicate the technical orientation of
1468 * the way (defined by order of nodes)
1469 * @param showHeadArrowOnly True if only the arrow at the end of the line but not those on the segments should be displayed.
1470 * @param showOneway show symbols that indicate the direction of the feature,
1471 * e.g. oneway street or waterway
1472 * @param onewayReversed for oneway=-1 and similar
1473 */
1474 public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
1475 boolean showOrientation, boolean showHeadArrowOnly,
1476 boolean showOneway, boolean onewayReversed) {
1477
1478 MapViewPath path = new MapViewPath(mapState);
1479 MapViewPath orientationArrows = showOrientation ? new MapViewPath(mapState) : null;
1480 MapViewPath onewayArrows;
1481 MapViewPath onewayArrowsCasing;
1482 Rectangle bounds = g.getClipBounds();
1483 if (bounds != null) {
1484 // avoid arrow heads at the border
1485 bounds.grow(100, 100);
1486 }
1487
1488 List<Node> wayNodes = way.getNodes();
1489 if (wayNodes.size() < 2) return;
1490
1491 // only highlight the segment if the way itself is not highlighted
1492 if (!way.isHighlighted() && highlightWaySegments != null) {
1493 MapViewPath highlightSegs = null;
1494 for (WaySegment ws : highlightWaySegments) {
1495 if (ws.way != way || ws.lowerIndex < offset) {
1496 continue;
1497 }
1498 if (highlightSegs == null) {
1499 highlightSegs = new MapViewPath(mapState);
1500 }
1501
1502 highlightSegs.moveTo(ws.getFirstNode());
1503 highlightSegs.lineTo(ws.getSecondNode());
1504 }
1505
1506 drawPathHighlight(highlightSegs, line);
1507 }
1508
1509 MapViewPoint lastPoint = null;
1510 Iterator<MapViewPoint> it = new OffsetIterator(wayNodes, offset);
1511 boolean initialMoveToNeeded = true;
1512 while (it.hasNext()) {
1513 MapViewPoint p = it.next();
1514 if (lastPoint != null) {
1515 MapViewPoint p1 = lastPoint;
1516 MapViewPoint p2 = p;
1517
1518 if (initialMoveToNeeded) {
1519 initialMoveToNeeded = false;
1520 path.moveTo(p1);
1521 }
1522 path.lineTo(p2);
1523
1524 /* draw arrow */
1525 if (showHeadArrowOnly ? !it.hasNext() : showOrientation) {
1526 //TODO: Cache
1527 ArrowPaintHelper drawHelper = new ArrowPaintHelper(PHI, 10 + line.getLineWidth());
1528 drawHelper.paintArrowAt(orientationArrows, p2, p1);
1529 }
1530 }
1531 lastPoint = p;
1532 }
1533 if (showOneway) {
1534 onewayArrows = new MapViewPath(mapState);
1535 onewayArrowsCasing = new MapViewPath(mapState);
1536 double interval = 60;
1537
1538 path.visitClippedLine(0, 60, (inLineOffset, start, end, startIsOldEnd) -> {
1539 double segmentLength = start.distanceToInView(end);
1540 if (segmentLength > 0.001) {
1541 final double nx = (end.getInViewX() - start.getInViewX()) / segmentLength;
1542 final double ny = (end.getInViewY() - start.getInViewY()) / segmentLength;
1543
1544 // distance from p1
1545 double dist = interval - (inLineOffset % interval);
1546
1547 while (dist < segmentLength) {
1548 appenOnewayPath(onewayReversed, start, nx, ny, dist, 3d, onewayArrowsCasing);
1549 appenOnewayPath(onewayReversed, start, nx, ny, dist, 2d, onewayArrows);
1550 dist += interval;
1551 }
1552 }
1553 });
1554 } else {
1555 onewayArrows = null;
1556 onewayArrowsCasing = null;
1557 }
1558
1559 if (way.isHighlighted()) {
1560 drawPathHighlight(path, line);
1561 }
1562 displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
1563 }
1564
1565 private static void appenOnewayPath(boolean onewayReversed, MapViewPoint p1, double nx, double ny, double dist,
1566 double onewaySize, Path2D onewayPath) {
1567 // scale such that border is 1 px
1568 final double fac = -(onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI);
1569 final double sx = nx * fac;
1570 final double sy = ny * fac;
1571
1572 // Attach the triangle at the incenter and not at the tip.
1573 // Makes the border even at all sides.
1574 final double x = p1.getInViewX() + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1575 final double y = p1.getInViewY() + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1576
1577 onewayPath.moveTo(x, y);
1578 onewayPath.lineTo(x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
1579 onewayPath.lineTo(x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
1580 onewayPath.lineTo(x, y);
1581 }
1582
1583 /**
1584 * Gets the "circum". This is the distance on the map in meters that 100 screen pixels represent.
1585 * @return The "circum"
1586 */
1587 public double getCircum() {
1588 return circum;
1589 }
1590
1591 @Override
1592 public void getColors() {
1593 super.getColors();
1594 this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
1595 this.backgroundColor = PaintColors.getBackgroundColor();
1596 }
1597
1598 @Override
1599 public void getSettings(boolean virtual) {
1600 super.getSettings(virtual);
1601 paintSettings = MapPaintSettings.INSTANCE;
1602
1603 circum = nc.getDist100Pixel();
1604 scale = nc.getScale();
1605
1606 leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false);
1607
1608 useStrokes = paintSettings.getUseStrokesDistance() > circum;
1609 showNames = paintSettings.getShowNamesDistance() > circum;
1610 showIcons = paintSettings.getShowIconsDistance() > circum;
1611 isOutlineOnly = paintSettings.isOutlineOnly();
1612 orderFont = new Font(Main.pref.get("mappaint.font", "Droid Sans"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8));
1613
1614 antialiasing = Main.pref.getBoolean("mappaint.use-antialiasing", true) ?
1615 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF;
1616 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
1617
1618 Object textAntialiasing;
1619 switch (Main.pref.get("mappaint.text-antialiasing", "default")) {
1620 case "on":
1621 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
1622 break;
1623 case "off":
1624 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
1625 break;
1626 case "gasp":
1627 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
1628 break;
1629 case "lcd-hrgb":
1630 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
1631 break;
1632 case "lcd-hbgr":
1633 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR;
1634 break;
1635 case "lcd-vrgb":
1636 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB;
1637 break;
1638 case "lcd-vbgr":
1639 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR;
1640 break;
1641 default:
1642 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT;
1643 }
1644 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialiasing);
1645
1646 highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4);
1647 highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7);
1648 widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5);
1649 highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4);
1650 }
1651
1652 private static Path2D.Double getPath(Way w) {
1653 Path2D.Double path = new Path2D.Double();
1654 boolean initial = true;
1655 for (Node n : w.getNodes()) {
1656 EastNorth p = n.getEastNorth();
1657 if (p != null) {
1658 if (initial) {
1659 path.moveTo(p.getX(), p.getY());
1660 initial = false;
1661 } else {
1662 path.lineTo(p.getX(), p.getY());
1663 }
1664 }
1665 }
1666 if (w.isClosed()) {
1667 path.closePath();
1668 }
1669 return path;
1670 }
1671
1672 private static Path2D.Double getPFClip(Way w, double extent) {
1673 Path2D.Double clip = new Path2D.Double();
1674 buildPFClip(clip, w.getNodes(), extent);
1675 return clip;
1676 }
1677
1678 private static Path2D.Double getPFClip(PolyData pd, double extent) {
1679 Path2D.Double clip = new Path2D.Double();
1680 clip.setWindingRule(Path2D.WIND_EVEN_ODD);
1681 buildPFClip(clip, pd.getNodes(), extent);
1682 for (PolyData pdInner : pd.getInners()) {
1683 buildPFClip(clip, pdInner.getNodes(), extent);
1684 }
1685 return clip;
1686 }
1687
1688 /**
1689 * Fix the clipping area of unclosed polygons for partial fill.
1690 *
1691 * The current algorithm for partial fill simply strokes the polygon with a
1692 * large stroke width after masking the outside with a clipping area.
1693 * This works, but for unclosed polygons, the mask can crop the corners at
1694 * both ends (see #12104).
1695 *
1696 * This method fixes the clipping area by sort of adding the corners to the
1697 * clip outline.
1698 *
1699 * @param clip the clipping area to modify (initially empty)
1700 * @param nodes nodes of the polygon
1701 * @param extent the extent
1702 */
1703 private static void buildPFClip(Path2D.Double clip, List<Node> nodes, double extent) {
1704 boolean initial = true;
1705 for (Node n : nodes) {
1706 EastNorth p = n.getEastNorth();
1707 if (p != null) {
1708 if (initial) {
1709 clip.moveTo(p.getX(), p.getY());
1710 initial = false;
1711 } else {
1712 clip.lineTo(p.getX(), p.getY());
1713 }
1714 }
1715 }
1716 if (nodes.size() >= 3) {
1717 EastNorth fst = nodes.get(0).getEastNorth();
1718 EastNorth snd = nodes.get(1).getEastNorth();
1719 EastNorth lst = nodes.get(nodes.size() - 1).getEastNorth();
1720 EastNorth lbo = nodes.get(nodes.size() - 2).getEastNorth();
1721
1722 EastNorth cLst = getPFDisplacedEndPoint(lbo, lst, fst, extent);
1723 EastNorth cFst = getPFDisplacedEndPoint(snd, fst, cLst != null ? cLst : lst, extent);
1724 if (cLst == null && cFst != null) {
1725 cLst = getPFDisplacedEndPoint(lbo, lst, cFst, extent);
1726 }
1727 if (cLst != null) {
1728 clip.lineTo(cLst.getX(), cLst.getY());
1729 }
1730 if (cFst != null) {
1731 clip.lineTo(cFst.getX(), cFst.getY());
1732 }
1733 }
1734 }
1735
1736 /**
1737 * Get the point to add to the clipping area for partial fill of unclosed polygons.
1738 *
1739 * <code>(p1,p2)</code> is the first or last way segment and <code>p3</code> the
1740 * opposite endpoint.
1741 *
1742 * @param p1 1st point
1743 * @param p2 2nd point
1744 * @param p3 3rd point
1745 * @param extent the extent
1746 * @return a point q, such that p1,p2,q form a right angle
1747 * and the distance of q to p2 is <code>extent</code>. The point q lies on
1748 * the same side of the line p1,p2 as the point p3.
1749 * Returns null if p1,p2,p3 forms an angle greater 90 degrees. (In this case
1750 * the corner of the partial fill would not be cut off by the mask, so an
1751 * additional point is not necessary.)
1752 */
1753 private static EastNorth getPFDisplacedEndPoint(EastNorth p1, EastNorth p2, EastNorth p3, double extent) {
1754 double dx1 = p2.getX() - p1.getX();
1755 double dy1 = p2.getY() - p1.getY();
1756 double dx2 = p3.getX() - p2.getX();
1757 double dy2 = p3.getY() - p2.getY();
1758 if (dx1 * dx2 + dy1 * dy2 < 0) {
1759 double len = Math.sqrt(dx1 * dx1 + dy1 * dy1);
1760 if (len == 0) return null;
1761 double dxm = -dy1 * extent / len;
1762 double dym = dx1 * extent / len;
1763 if (dx1 * dy2 - dx2 * dy1 < 0) {
1764 dxm = -dxm;
1765 dym = -dym;
1766 }
1767 return new EastNorth(p2.getX() + dxm, p2.getY() + dym);
1768 }
1769 return null;
1770 }
1771
1772 /**
1773 * Test if the area is visible
1774 * @param area The area, interpreted in east/north space.
1775 * @return true if it is visible.
1776 */
1777 private boolean isAreaVisible(Path2D.Double area) {
1778 Rectangle2D bounds = area.getBounds2D();
1779 if (bounds.isEmpty()) return false;
1780 MapViewPoint p = mapState.getPointFor(new EastNorth(bounds.getX(), bounds.getY()));
1781 if (p.getInViewX() > mapState.getViewWidth()) return false;
1782 if (p.getInViewY() < 0) return false;
1783 p = mapState.getPointFor(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
1784 if (p.getInViewX() < 0) return false;
1785 if (p.getInViewY() > mapState.getViewHeight()) return false;
1786 return true;
1787 }
1788
1789 public boolean isInactiveMode() {
1790 return isInactiveMode;
1791 }
1792
1793 public boolean isShowIcons() {
1794 return showIcons;
1795 }
1796
1797 public boolean isShowNames() {
1798 return showNames;
1799 }
1800
1801 private static double[] pointAt(double t, List<MapViewPoint> poly, double pathLength) {
1802 double totalLen = t * pathLength;
1803 double curLen = 0;
1804 double dx, dy;
1805 double segLen;
1806
1807 // Yes, it is inefficient to iterate from the beginning for each glyph.
1808 // Can be optimized if it turns out to be slow.
1809 for (int i = 1; i < poly.size(); ++i) {
1810 dx = poly.get(i).getInViewX() - poly.get(i - 1).getInViewX();
1811 dy = poly.get(i).getInViewY() - poly.get(i - 1).getInViewY();
1812 segLen = Math.sqrt(dx*dx + dy*dy);
1813 if (totalLen > curLen + segLen) {
1814 curLen += segLen;
1815 continue;
1816 }
1817 return new double[] {
1818 poly.get(i - 1).getInViewX() + (totalLen - curLen) / segLen * dx,
1819 poly.get(i - 1).getInViewY() + (totalLen - curLen) / segLen * dy,
1820 Math.atan2(dy, dx)};
1821 }
1822 return null;
1823 }
1824
1825 /**
1826 * Computes the flags for a given OSM primitive.
1827 * @param primitive The primititve to compute the flags for.
1828 * @param checkOuterMember <code>true</code> if we should also add {@link #FLAG_OUTERMEMBER_OF_SELECTED}
1829 * @return The flag.
1830 */
1831 public static int computeFlags(OsmPrimitive primitive, boolean checkOuterMember) {
1832 if (primitive.isDisabled()) {
1833 return FLAG_DISABLED;
1834 } else if (primitive.isSelected()) {
1835 return FLAG_SELECTED;
1836 } else if (checkOuterMember && primitive.isOuterMemberOfSelected()) {
1837 return FLAG_OUTERMEMBER_OF_SELECTED;
1838 } else if (primitive.isMemberOfSelected()) {
1839 return FLAG_MEMBER_OF_SELECTED;
1840 } else {
1841 return FLAG_NORMAL;
1842 }
1843 }
1844
1845 private static class ComputeStyleListWorker extends RecursiveTask<List<StyleRecord>> implements Visitor {
1846 private final transient List<? extends OsmPrimitive> input;
1847 private final transient List<StyleRecord> output;
1848
1849 private final transient ElemStyles styles = MapPaintStyles.getStyles();
1850 private final int directExecutionTaskSize;
1851 private final double circum;
1852 private final NavigatableComponent nc;
1853
1854 private final boolean drawArea;
1855 private final boolean drawMultipolygon;
1856 private final boolean drawRestriction;
1857
1858 /**
1859 * Constructs a new {@code ComputeStyleListWorker}.
1860 * @param circum distance on the map in meters that 100 screen pixels represent
1861 * @param nc navigatable component
1862 * @param input the primitives to process
1863 * @param output the list of styles to which styles will be added
1864 * @param directExecutionTaskSize the threshold deciding whether to subdivide the tasks
1865 */
1866 ComputeStyleListWorker(double circum, NavigatableComponent nc,
1867 final List<? extends OsmPrimitive> input, List<StyleRecord> output, int directExecutionTaskSize) {
1868 this.circum = circum;
1869 this.nc = nc;
1870 this.input = input;
1871 this.output = output;
1872 this.directExecutionTaskSize = directExecutionTaskSize;
1873 this.drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10_000_000);
1874 this.drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
1875 this.drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
1876 this.styles.setDrawMultipolygon(drawMultipolygon);
1877 }
1878
1879 @Override
1880 protected List<StyleRecord> compute() {
1881 if (input.size() <= directExecutionTaskSize) {
1882 return computeDirectly();
1883 } else {
1884 final Collection<ForkJoinTask<List<StyleRecord>>> tasks = new ArrayList<>();
1885 for (int fromIndex = 0; fromIndex < input.size(); fromIndex += directExecutionTaskSize) {
1886 final int toIndex = Math.min(fromIndex + directExecutionTaskSize, input.size());
1887 final List<StyleRecord> output = new ArrayList<>(directExecutionTaskSize);
1888 tasks.add(new ComputeStyleListWorker(circum, nc, input.subList(fromIndex, toIndex), output, directExecutionTaskSize).fork());
1889 }
1890 for (ForkJoinTask<List<StyleRecord>> task : tasks) {
1891 output.addAll(task.join());
1892 }
1893 return output;
1894 }
1895 }
1896
1897 public List<StyleRecord> computeDirectly() {
1898 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
1899 try {
1900 for (final OsmPrimitive osm : input) {
1901 acceptDrawable(osm);
1902 }
1903 return output;
1904 } catch (RuntimeException e) {
1905 throw BugReport.intercept(e).put("input-size", input.size()).put("output-size", output.size());
1906 } finally {
1907 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
1908 }
1909 }
1910
1911 private void acceptDrawable(final OsmPrimitive osm) {
1912 try {
1913 if (osm.isDrawable()) {
1914 osm.accept(this);
1915 }
1916 } catch (RuntimeException e) {
1917 throw BugReport.intercept(e).put("osm", osm);
1918 }
1919 }
1920
1921 @Override
1922 public void visit(Node n) {
1923 add(n, computeFlags(n, false));
1924 }
1925
1926 @Override
1927 public void visit(Way w) {
1928 add(w, computeFlags(w, true));
1929 }
1930
1931 @Override
1932 public void visit(Relation r) {
1933 add(r, computeFlags(r, true));
1934 }
1935
1936 @Override
1937 public void visit(Changeset cs) {
1938 throw new UnsupportedOperationException();
1939 }
1940
1941 public void add(Node osm, int flags) {
1942 StyleElementList sl = styles.get(osm, circum, nc);
1943 for (StyleElement s : sl) {
1944 output.add(new StyleRecord(s, osm, flags));
1945 }
1946 }
1947
1948 public void add(Relation osm, int flags) {
1949 StyleElementList sl = styles.get(osm, circum, nc);
1950 for (StyleElement s : sl) {
1951 if (drawMultipolygon && drawArea && s instanceof AreaElement && (flags & FLAG_DISABLED) == 0) {
1952 output.add(new StyleRecord(s, osm, flags));
1953 } else if (drawRestriction && s instanceof NodeElement) {
1954 output.add(new StyleRecord(s, osm, flags));
1955 }
1956 }
1957 }
1958
1959 public void add(Way osm, int flags) {
1960 StyleElementList sl = styles.get(osm, circum, nc);
1961 for (StyleElement s : sl) {
1962 if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElement) {
1963 continue;
1964 }
1965 output.add(new StyleRecord(s, osm, flags));
1966 }
1967 }
1968 }
1969
1970 /**
1971 * Sets the factory that creates the benchmark data receivers.
1972 * @param benchmarkFactory The factory.
1973 * @since 10697
1974 */
1975 public void setBenchmarkFactory(Supplier<RenderBenchmarkCollector> benchmarkFactory) {
1976 this.benchmarkFactory = benchmarkFactory;
1977 }
1978
1979 @Override
1980 public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) {
1981 RenderBenchmarkCollector benchmark = benchmarkFactory.get();
1982 BBox bbox = bounds.toBBox();
1983 getSettings(renderVirtualNodes);
1984
1985 data.getReadLock().lock();
1986 try {
1987 highlightWaySegments = data.getHighlightedWaySegments();
1988
1989 benchmark.renderStart(circum);
1990
1991 List<Node> nodes = data.searchNodes(bbox);
1992 List<Way> ways = data.searchWays(bbox);
1993 List<Relation> relations = data.searchRelations(bbox);
1994
1995 final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size());
1996
1997 // Need to process all relations first.
1998 // Reason: Make sure, ElemStyles.getStyleCacheWithRange is not called for the same primitive in parallel threads.
1999 // (Could be synchronized, but try to avoid this for performance reasons.)
2000 THREAD_POOL.invoke(new ComputeStyleListWorker(circum, nc, relations, allStyleElems,
2001 Math.max(20, relations.size() / THREAD_POOL.getParallelism() / 3)));
2002 THREAD_POOL.invoke(new ComputeStyleListWorker(circum, nc, new CompositeList<>(nodes, ways), allStyleElems,
2003 Math.max(100, (nodes.size() + ways.size()) / THREAD_POOL.getParallelism() / 3)));
2004
2005 if (!benchmark.renderSort()) {
2006 return;
2007 }
2008
2009 Collections.sort(allStyleElems); // TODO: try parallel sort when switching to Java 8
2010
2011 if (!benchmark.renderDraw(allStyleElems)) {
2012 return;
2013 }
2014
2015 for (StyleRecord record : allStyleElems) {
2016 paintRecord(record);
2017 }
2018
2019 drawVirtualNodes(data, bbox);
2020
2021 benchmark.renderDone();
2022 } catch (RuntimeException e) {
2023 throw BugReport.intercept(e)
2024 .put("data", data)
2025 .put("circum", circum)
2026 .put("scale", scale)
2027 .put("paintSettings", paintSettings)
2028 .put("renderVirtualNodes", renderVirtualNodes);
2029 } finally {
2030 data.getReadLock().unlock();
2031 }
2032 }
2033
2034 private void paintRecord(StyleRecord record) {
2035 try {
2036 record.paintPrimitive(paintSettings, this);
2037 } catch (RuntimeException e) {
2038 throw BugReport.intercept(e).put("record", record);
2039 }
2040 }
2041}
Note: See TracBrowser for help on using the repository browser.