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

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

sonar - squid:S1948 - Fields in a "Serializable" class should either be transient or serializable

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