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

Last change on this file since 10856 was 10856, checked in by Don-vip, 8 years ago

fix #13375 - Fix icon rendering (patch by michael2402) - gsoc-core + add unit test

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