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

Last change on this file since 9952 was 9952, checked in by simon04, 8 years ago

see #11516 - Compute multipolygon area, include in MapCSS, JOSM search

  • Property svn:eol-style set to native
File size: 80.0 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(dxNext*dxNext + 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 = null;
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 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 = thisP.x - lastP.x;
817 final double dy = 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 }
1094 } else if (m.isNode()) {
1095 Node n = m.getNode();
1096 if ("via".equals(m.getRole()) && via == null) {
1097 via = n;
1098 }
1099 }
1100 }
1101 }
1102
1103 if (fromWay == null || toWay == null || via == null)
1104 return;
1105
1106 Node viaNode;
1107 if (via instanceof Node) {
1108 viaNode = (Node) via;
1109 if (!fromWay.isFirstLastNode(viaNode))
1110 return;
1111 } else {
1112 Way viaWay = (Way) via;
1113 Node firstNode = viaWay.firstNode();
1114 Node lastNode = viaWay.lastNode();
1115 Boolean onewayvia = Boolean.FALSE;
1116
1117 String onewayviastr = viaWay.get("oneway");
1118 if (onewayviastr != null) {
1119 if ("-1".equals(onewayviastr)) {
1120 onewayvia = Boolean.TRUE;
1121 Node tmp = firstNode;
1122 firstNode = lastNode;
1123 lastNode = tmp;
1124 } else {
1125 onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
1126 if (onewayvia == null) {
1127 onewayvia = Boolean.FALSE;
1128 }
1129 }
1130 }
1131
1132 if (fromWay.isFirstLastNode(firstNode)) {
1133 viaNode = firstNode;
1134 } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
1135 viaNode = lastNode;
1136 } else
1137 return;
1138 }
1139
1140 /* find the "direct" nodes before the via node */
1141 Node fromNode;
1142 if (fromWay.firstNode() == via) {
1143 fromNode = fromWay.getNode(1);
1144 } else {
1145 fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
1146 }
1147
1148 Point pFrom = nc.getPoint(fromNode);
1149 Point pVia = nc.getPoint(viaNode);
1150
1151 /* starting from via, go back the "from" way a few pixels
1152 (calculate the vector vx/vy with the specified length and the direction
1153 away from the "via" node along the first segment of the "from" way)
1154 */
1155 double distanceFromVia = 14;
1156 double dx = pFrom.x >= pVia.x ? pFrom.x - pVia.x : pVia.x - pFrom.x;
1157 double dy = pFrom.y >= pVia.y ? pFrom.y - pVia.y : pVia.y - pFrom.y;
1158
1159 double fromAngle;
1160 if (dx == 0) {
1161 fromAngle = Math.PI/2;
1162 } else {
1163 fromAngle = Math.atan(dy / dx);
1164 }
1165 double fromAngleDeg = Math.toDegrees(fromAngle);
1166
1167 double vx = distanceFromVia * Math.cos(fromAngle);
1168 double vy = distanceFromVia * Math.sin(fromAngle);
1169
1170 if (pFrom.x < pVia.x) {
1171 vx = -vx;
1172 }
1173 if (pFrom.y < pVia.y) {
1174 vy = -vy;
1175 }
1176
1177 /* go a few pixels away from the way (in a right angle)
1178 (calculate the vx2/vy2 vector with the specified length and the direction
1179 90degrees away from the first segment of the "from" way)
1180 */
1181 double distanceFromWay = 10;
1182 double vx2 = 0;
1183 double vy2 = 0;
1184 double iconAngle = 0;
1185
1186 if (pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
1187 if (!leftHandTraffic) {
1188 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1189 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1190 } else {
1191 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1192 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1193 }
1194 iconAngle = 270+fromAngleDeg;
1195 }
1196 if (pFrom.x < pVia.x && pFrom.y >= pVia.y) {
1197 if (!leftHandTraffic) {
1198 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1199 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1200 } else {
1201 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1202 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1203 }
1204 iconAngle = 90-fromAngleDeg;
1205 }
1206 if (pFrom.x < pVia.x && pFrom.y < pVia.y) {
1207 if (!leftHandTraffic) {
1208 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1209 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1210 } else {
1211 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1212 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1213 }
1214 iconAngle = 90+fromAngleDeg;
1215 }
1216 if (pFrom.x >= pVia.x && pFrom.y < pVia.y) {
1217 if (!leftHandTraffic) {
1218 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1219 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1220 } else {
1221 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1222 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1223 }
1224 iconAngle = 270-fromAngleDeg;
1225 }
1226
1227 drawRestriction(icon.getImage(disabled),
1228 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1229 }
1230
1231 /**
1232 * Draws a text along a given way.
1233 * @param way The way to draw the text on.
1234 * @param text The text definition (font/.../text content) to draw.
1235 */
1236 public void drawTextOnPath(Way way, TextLabel text) {
1237 if (way == null || text == null)
1238 return;
1239 String name = text.getString(way);
1240 if (name == null || name.isEmpty())
1241 return;
1242
1243 FontMetrics fontMetrics = g.getFontMetrics(text.font);
1244 Rectangle2D rec = fontMetrics.getStringBounds(name, g);
1245
1246 Rectangle bounds = g.getClipBounds();
1247
1248 Polygon poly = new Polygon();
1249 Point lastPoint = null;
1250 Iterator<Node> it = way.getNodes().iterator();
1251 double pathLength = 0;
1252 long dx, dy;
1253
1254 // 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)
1255 List<Double> longHalfSegmentStart = new ArrayList<>(); // start point of half segment (as length along the way)
1256 List<Double> longHalfSegmentEnd = new ArrayList<>(); // end point of half segment (as length along the way)
1257 List<Double> longHalfsegmentQuality = new ArrayList<>(); // quality factor (off screen / partly on screen / fully on screen)
1258
1259 while (it.hasNext()) {
1260 Node n = it.next();
1261 Point p = nc.getPoint(n);
1262 poly.addPoint(p.x, p.y);
1263
1264 if (lastPoint != null) {
1265 dx = p.x - lastPoint.x;
1266 dy = p.y - lastPoint.y;
1267 double segmentLength = Math.sqrt(dx*dx + dy*dy);
1268 if (segmentLength > 2*(rec.getWidth()+4)) {
1269 Point center = new Point((lastPoint.x + p.x)/2, (lastPoint.y + p.y)/2);
1270 double q = 0;
1271 if (bounds != null) {
1272 if (bounds.contains(lastPoint) && bounds.contains(center)) {
1273 q = 2;
1274 } else if (bounds.contains(lastPoint) || bounds.contains(center)) {
1275 q = 1;
1276 }
1277 }
1278 longHalfSegmentStart.add(pathLength);
1279 longHalfSegmentEnd.add(pathLength + segmentLength / 2);
1280 longHalfsegmentQuality.add(q);
1281
1282 q = 0;
1283 if (bounds != null) {
1284 if (bounds.contains(center) && bounds.contains(p)) {
1285 q = 2;
1286 } else if (bounds.contains(center) || bounds.contains(p)) {
1287 q = 1;
1288 }
1289 }
1290 longHalfSegmentStart.add(pathLength + segmentLength / 2);
1291 longHalfSegmentEnd.add(pathLength + segmentLength);
1292 longHalfsegmentQuality.add(q);
1293 }
1294 pathLength += segmentLength;
1295 }
1296 lastPoint = p;
1297 }
1298
1299 if (rec.getWidth() > pathLength)
1300 return;
1301
1302 double t1, t2;
1303
1304 if (!longHalfSegmentStart.isEmpty()) {
1305 if (way.getNodesCount() == 2) {
1306 // For 2 node ways, the two half segments are exactly the same size and distance from the center.
1307 // Prefer the first one for consistency.
1308 longHalfsegmentQuality.set(0, longHalfsegmentQuality.get(0) + 0.5);
1309 }
1310
1311 // find the long half segment that is closest to the center of the way
1312 // candidates with higher quality value are preferred
1313 double bestStart = Double.NaN;
1314 double bestEnd = Double.NaN;
1315 double bestDistanceToCenter = Double.MAX_VALUE;
1316 double bestQuality = -1;
1317 for (int i = 0; i < longHalfSegmentStart.size(); i++) {
1318 double start = longHalfSegmentStart.get(i);
1319 double end = longHalfSegmentEnd.get(i);
1320 double dist = Math.abs(0.5 * (end + start) - 0.5 * pathLength);
1321 if (longHalfsegmentQuality.get(i) > bestQuality
1322 || (dist < bestDistanceToCenter && Utils.equalsEpsilon(longHalfsegmentQuality.get(i), bestQuality))) {
1323 bestStart = start;
1324 bestEnd = end;
1325 bestDistanceToCenter = dist;
1326 bestQuality = longHalfsegmentQuality.get(i);
1327 }
1328 }
1329 double remaining = bestEnd - bestStart - rec.getWidth(); // total space left and right from the text
1330 // The space left and right of the text should be distributed 20% - 80% (towards the center),
1331 // but the smaller space should not be less than 7 px.
1332 // However, if the total remaining space is less than 14 px, then distribute it evenly.
1333 double smallerSpace = Math.min(Math.max(0.2 * remaining, 7), 0.5 * remaining);
1334 if ((bestEnd + bestStart)/2 < pathLength/2) {
1335 t2 = bestEnd - smallerSpace;
1336 t1 = t2 - rec.getWidth();
1337 } else {
1338 t1 = bestStart + smallerSpace;
1339 t2 = t1 + rec.getWidth();
1340 }
1341 } else {
1342 // doesn't fit into one half-segment -> just put it in the center of the way
1343 t1 = pathLength/2 - rec.getWidth()/2;
1344 t2 = pathLength/2 + rec.getWidth()/2;
1345 }
1346 t1 /= pathLength;
1347 t2 /= pathLength;
1348
1349 double[] p1 = pointAt(t1, poly, pathLength);
1350 double[] p2 = pointAt(t2, poly, pathLength);
1351
1352 if (p1 == null || p2 == null)
1353 return;
1354
1355 double angleOffset;
1356 double offsetSign;
1357 double tStart;
1358
1359 if (p1[0] < p2[0] &&
1360 p1[2] < Math.PI/2 &&
1361 p1[2] > -Math.PI/2) {
1362 angleOffset = 0;
1363 offsetSign = 1;
1364 tStart = t1;
1365 } else {
1366 angleOffset = Math.PI;
1367 offsetSign = -1;
1368 tStart = t2;
1369 }
1370
1371 List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext());
1372 double gvOffset = 0;
1373 for (GlyphVector gv : gvs) {
1374 double gvWidth = gv.getLogicalBounds().getBounds2D().getWidth();
1375 for (int i = 0; i < gv.getNumGlyphs(); ++i) {
1376 Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
1377 double t = tStart + offsetSign * (gvOffset + rect.getX() + rect.getWidth()/2) / pathLength;
1378 double[] p = pointAt(t, poly, pathLength);
1379 if (p != null) {
1380 AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
1381 trfm.rotate(p[2]+angleOffset);
1382 double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
1383 trfm.translate(-rect.getWidth()/2, off);
1384 if (isGlyphVectorDoubleTranslationBug(text.font)) {
1385 // scale the translation components by one half
1386 AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY());
1387 tmp.concatenate(trfm);
1388 trfm = tmp;
1389 }
1390 gv.setGlyphTransform(i, trfm);
1391 }
1392 }
1393 displayText(gv, null, 0, 0, way.isDisabled(), text);
1394 gvOffset += gvWidth;
1395 }
1396 }
1397
1398 /**
1399 * draw way. This method allows for two draw styles (line using color, dashes using dashedColor) to be passed.
1400 * @param way The way to draw
1401 * @param color The base color to draw the way in
1402 * @param line The line style to use. This is drawn using color.
1403 * @param dashes The dash style to use. This is drawn using dashedColor. <code>null</code> if unused.
1404 * @param dashedColor The color of the dashes.
1405 * @param offset The offset
1406 * @param showOrientation show arrows that indicate the technical orientation of
1407 * the way (defined by order of nodes)
1408 * @param showHeadArrowOnly True if only the arrow at the end of the line but not those on the segments should be displayed.
1409 * @param showOneway show symbols that indicate the direction of the feature,
1410 * e.g. oneway street or waterway
1411 * @param onewayReversed for oneway=-1 and similar
1412 */
1413 public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
1414 boolean showOrientation, boolean showHeadArrowOnly,
1415 boolean showOneway, boolean onewayReversed) {
1416
1417 GeneralPath path = new GeneralPath();
1418 GeneralPath orientationArrows = showOrientation ? new GeneralPath() : null;
1419 GeneralPath onewayArrows = showOneway ? new GeneralPath() : null;
1420 GeneralPath onewayArrowsCasing = showOneway ? new GeneralPath() : null;
1421 Rectangle bounds = g.getClipBounds();
1422 if (bounds != null) {
1423 // avoid arrow heads at the border
1424 bounds.grow(100, 100);
1425 }
1426
1427 double wayLength = 0;
1428 Point lastPoint = null;
1429 boolean initialMoveToNeeded = true;
1430 List<Node> wayNodes = way.getNodes();
1431 if (wayNodes.size() < 2) return;
1432
1433 // only highlight the segment if the way itself is not highlighted
1434 if (!way.isHighlighted() && highlightWaySegments != null) {
1435 GeneralPath highlightSegs = null;
1436 for (WaySegment ws : highlightWaySegments) {
1437 if (ws.way != way || ws.lowerIndex < offset) {
1438 continue;
1439 }
1440 if (highlightSegs == null) {
1441 highlightSegs = new GeneralPath();
1442 }
1443
1444 Point p1 = nc.getPoint(ws.getFirstNode());
1445 Point p2 = nc.getPoint(ws.getSecondNode());
1446 highlightSegs.moveTo(p1.x, p1.y);
1447 highlightSegs.lineTo(p2.x, p2.y);
1448 }
1449
1450 drawPathHighlight(highlightSegs, line);
1451 }
1452
1453 Iterator<Point> it = new OffsetIterator(wayNodes, offset);
1454 while (it.hasNext()) {
1455 Point p = it.next();
1456 if (lastPoint != null) {
1457 Point p1 = lastPoint;
1458 Point p2 = p;
1459
1460 /**
1461 * Do custom clipping to work around openjdk bug. It leads to
1462 * drawing artefacts when zooming in a lot. (#4289, #4424)
1463 * (Looks like int overflow.)
1464 */
1465 LineClip clip = new LineClip(p1, p2, bounds);
1466 if (clip.execute()) {
1467 if (!p1.equals(clip.getP1())) {
1468 p1 = clip.getP1();
1469 path.moveTo(p1.x, p1.y);
1470 } else if (initialMoveToNeeded) {
1471 initialMoveToNeeded = false;
1472 path.moveTo(p1.x, p1.y);
1473 }
1474 p2 = clip.getP2();
1475 path.lineTo(p2.x, p2.y);
1476
1477 /* draw arrow */
1478 if (showHeadArrowOnly ? !it.hasNext() : showOrientation) {
1479 final double segmentLength = p1.distance(p2);
1480 if (segmentLength != 0) {
1481 final double l = (10. + line.getLineWidth()) / segmentLength;
1482
1483 final double sx = l * (p1.x - p2.x);
1484 final double sy = l * (p1.y - p2.y);
1485
1486 orientationArrows.moveTo(p2.x + cosPHI * sx - sinPHI * sy, p2.y + sinPHI * sx + cosPHI * sy);
1487 orientationArrows.lineTo(p2.x, p2.y);
1488 orientationArrows.lineTo(p2.x + cosPHI * sx + sinPHI * sy, p2.y - sinPHI * sx + cosPHI * sy);
1489 }
1490 }
1491 if (showOneway) {
1492 final double segmentLength = p1.distance(p2);
1493 if (segmentLength != 0) {
1494 final double nx = (p2.x - p1.x) / segmentLength;
1495 final double ny = (p2.y - p1.y) / segmentLength;
1496
1497 final double interval = 60;
1498 // distance from p1
1499 double dist = interval - (wayLength % interval);
1500
1501 while (dist < segmentLength) {
1502 for (int i = 0; i < 2; ++i) {
1503 float onewaySize = i == 0 ? 3f : 2f;
1504 GeneralPath onewayPath = i == 0 ? onewayArrowsCasing : onewayArrows;
1505
1506 // scale such that border is 1 px
1507 final double fac = -(onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI);
1508 final double sx = nx * fac;
1509 final double sy = ny * fac;
1510
1511 // Attach the triangle at the incenter and not at the tip.
1512 // Makes the border even at all sides.
1513 final double x = p1.x + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1514 final double y = p1.y + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1515
1516 onewayPath.moveTo(x, y);
1517 onewayPath.lineTo(x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
1518 onewayPath.lineTo(x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
1519 onewayPath.lineTo(x, y);
1520 }
1521 dist += interval;
1522 }
1523 }
1524 wayLength += segmentLength;
1525 }
1526 }
1527 }
1528 lastPoint = p;
1529 }
1530 if (way.isHighlighted()) {
1531 drawPathHighlight(path, line);
1532 }
1533 displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
1534 }
1535
1536 /**
1537 * Gets the "circum". This is the distance on the map in meters that 100 screen pixels represent.
1538 * @return The "circum"
1539 */
1540 public double getCircum() {
1541 return circum;
1542 }
1543
1544 @Override
1545 public void getColors() {
1546 super.getColors();
1547 this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
1548 this.backgroundColor = PaintColors.getBackgroundColor();
1549 }
1550
1551 @Override
1552 public void getSettings(boolean virtual) {
1553 super.getSettings(virtual);
1554 paintSettings = MapPaintSettings.INSTANCE;
1555
1556 circum = nc.getDist100Pixel();
1557 scale = nc.getScale();
1558
1559 leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false);
1560
1561 useStrokes = paintSettings.getUseStrokesDistance() > circum;
1562 showNames = paintSettings.getShowNamesDistance() > circum;
1563 showIcons = paintSettings.getShowIconsDistance() > circum;
1564 isOutlineOnly = paintSettings.isOutlineOnly();
1565 orderFont = new Font(Main.pref.get("mappaint.font", "Droid Sans"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8));
1566
1567 antialiasing = Main.pref.getBoolean("mappaint.use-antialiasing", true) ?
1568 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF;
1569 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
1570
1571 Object textAntialiasing;
1572 switch (Main.pref.get("mappaint.text-antialiasing", "default")) {
1573 case "on":
1574 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
1575 break;
1576 case "off":
1577 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
1578 break;
1579 case "gasp":
1580 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
1581 break;
1582 case "lcd-hrgb":
1583 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
1584 break;
1585 case "lcd-hbgr":
1586 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR;
1587 break;
1588 case "lcd-vrgb":
1589 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB;
1590 break;
1591 case "lcd-vbgr":
1592 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR;
1593 break;
1594 default:
1595 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT;
1596 }
1597 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialiasing);
1598
1599 highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4);
1600 highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7);
1601 widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5);
1602 highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4);
1603 }
1604
1605 private static Path2D.Double getPath(Way w) {
1606 Path2D.Double path = new Path2D.Double();
1607 boolean initial = true;
1608 for (Node n : w.getNodes()) {
1609 EastNorth p = n.getEastNorth();
1610 if (p != null) {
1611 if (initial) {
1612 path.moveTo(p.getX(), p.getY());
1613 initial = false;
1614 } else {
1615 path.lineTo(p.getX(), p.getY());
1616 }
1617 }
1618 }
1619 if (w.isClosed()) {
1620 path.closePath();
1621 }
1622 return path;
1623 }
1624
1625 private static Path2D.Double getPFClip(Way w, double extent) {
1626 Path2D.Double clip = new Path2D.Double();
1627 buildPFClip(clip, w.getNodes(), extent);
1628 return clip;
1629 }
1630
1631 private static Path2D.Double getPFClip(PolyData pd, double extent) {
1632 Path2D.Double clip = new Path2D.Double();
1633 clip.setWindingRule(Path2D.WIND_EVEN_ODD);
1634 buildPFClip(clip, pd.getNodes(), extent);
1635 for (PolyData pdInner : pd.getInners()) {
1636 buildPFClip(clip, pdInner.getNodes(), extent);
1637 }
1638 return clip;
1639 }
1640
1641 /**
1642 * Fix the clipping area of unclosed polygons for partial fill.
1643 *
1644 * The current algorithm for partial fill simply strokes the polygon with a
1645 * large stroke width after masking the outside with a clipping area.
1646 * This works, but for unclosed polygons, the mask can crop the corners at
1647 * both ends (see #12104).
1648 *
1649 * This method fixes the clipping area by sort of adding the corners to the
1650 * clip outline.
1651 *
1652 * @param clip the clipping area to modify (initially empty)
1653 * @param nodes nodes of the polygon
1654 * @param extent the extent
1655 */
1656 private static void buildPFClip(Path2D.Double clip, List<Node> nodes, double extent) {
1657 boolean initial = true;
1658 for (Node n : nodes) {
1659 EastNorth p = n.getEastNorth();
1660 if (p != null) {
1661 if (initial) {
1662 clip.moveTo(p.getX(), p.getY());
1663 initial = false;
1664 } else {
1665 clip.lineTo(p.getX(), p.getY());
1666 }
1667 }
1668 }
1669 if (nodes.size() >= 3) {
1670 EastNorth fst = nodes.get(0).getEastNorth();
1671 EastNorth snd = nodes.get(1).getEastNorth();
1672 EastNorth lst = nodes.get(nodes.size() - 1).getEastNorth();
1673 EastNorth lbo = nodes.get(nodes.size() - 2).getEastNorth();
1674
1675 EastNorth cLst = getPFDisplacedEndPoint(lbo, lst, fst, extent);
1676 EastNorth cFst = getPFDisplacedEndPoint(snd, fst, cLst != null ? cLst : lst, extent);
1677 if (cLst == null && cFst != null) {
1678 cLst = getPFDisplacedEndPoint(lbo, lst, cFst, extent);
1679 }
1680 if (cLst != null) {
1681 clip.lineTo(cLst.getX(), cLst.getY());
1682 }
1683 if (cFst != null) {
1684 clip.lineTo(cFst.getX(), cFst.getY());
1685 }
1686 }
1687 }
1688
1689 /**
1690 * Get the point to add to the clipping area for partial fill of unclosed polygons.
1691 *
1692 * <code>(p1,p2)</code> is the first or last way segment and <code>p3</code> the
1693 * opposite endpoint.
1694 *
1695 * @param p1 1st point
1696 * @param p2 2nd point
1697 * @param p3 3rd point
1698 * @param extent the extent
1699 * @return a point q, such that p1,p2,q form a right angle
1700 * and the distance of q to p2 is <code>extent</code>. The point q lies on
1701 * the same side of the line p1,p2 as the point p3.
1702 * Returns null if p1,p2,p3 forms an angle greater 90 degrees. (In this case
1703 * the corner of the partial fill would not be cut off by the mask, so an
1704 * additional point is not necessary.)
1705 */
1706 private static EastNorth getPFDisplacedEndPoint(EastNorth p1, EastNorth p2, EastNorth p3, double extent) {
1707 double dx1 = p2.getX() - p1.getX();
1708 double dy1 = p2.getY() - p1.getY();
1709 double dx2 = p3.getX() - p2.getX();
1710 double dy2 = p3.getY() - p2.getY();
1711 if (dx1 * dx2 + dy1 * dy2 < 0) {
1712 double len = Math.sqrt(dx1 * dx1 + dy1 * dy1);
1713 if (len == 0) return null;
1714 double dxm = -dy1 * extent / len;
1715 double dym = dx1 * extent / len;
1716 if (dx1 * dy2 - dx2 * dy1 < 0) {
1717 dxm = -dxm;
1718 dym = -dym;
1719 }
1720 return new EastNorth(p2.getX() + dxm, p2.getY() + dym);
1721 }
1722 return null;
1723 }
1724
1725 private boolean isAreaVisible(Path2D.Double area) {
1726 Rectangle2D bounds = area.getBounds2D();
1727 if (bounds.isEmpty()) return false;
1728 Point2D p = nc.getPoint2D(new EastNorth(bounds.getX(), bounds.getY()));
1729 if (p.getX() > nc.getWidth()) return false;
1730 if (p.getY() < 0) return false;
1731 p = nc.getPoint2D(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
1732 if (p.getX() < 0) return false;
1733 if (p.getY() > nc.getHeight()) return false;
1734 return true;
1735 }
1736
1737 public boolean isInactiveMode() {
1738 return isInactiveMode;
1739 }
1740
1741 public boolean isShowIcons() {
1742 return showIcons;
1743 }
1744
1745 public boolean isShowNames() {
1746 return showNames;
1747 }
1748
1749 private static double[] pointAt(double t, Polygon poly, double pathLength) {
1750 double totalLen = t * pathLength;
1751 double curLen = 0;
1752 long dx, dy;
1753 double segLen;
1754
1755 // Yes, it is inefficient to iterate from the beginning for each glyph.
1756 // Can be optimized if it turns out to be slow.
1757 for (int i = 1; i < poly.npoints; ++i) {
1758 dx = poly.xpoints[i] - poly.xpoints[i-1];
1759 dy = poly.ypoints[i] - poly.ypoints[i-1];
1760 segLen = Math.sqrt(dx*dx + dy*dy);
1761 if (totalLen > curLen + segLen) {
1762 curLen += segLen;
1763 continue;
1764 }
1765 return new double[] {
1766 poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx,
1767 poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
1768 Math.atan2(dy, dx)};
1769 }
1770 return null;
1771 }
1772
1773 /**
1774 * Computes the flags for a given OSM primitive.
1775 * @param primitive The primititve to compute the flags for.
1776 * @param checkOuterMember <code>true</code> if we should also add {@link #FLAG_OUTERMEMBER_OF_SELECTED}
1777 * @return The flag.
1778 */
1779 public static int computeFlags(OsmPrimitive primitive, boolean checkOuterMember) {
1780 if (primitive.isDisabled()) {
1781 return FLAG_DISABLED;
1782 } else if (primitive.isSelected()) {
1783 return FLAG_SELECTED;
1784 } else if (checkOuterMember && primitive.isOuterMemberOfSelected()) {
1785 return FLAG_OUTERMEMBER_OF_SELECTED;
1786 } else if (primitive.isMemberOfSelected()) {
1787 return FLAG_MEMBER_OF_SELECTED;
1788 } else {
1789 return FLAG_NORMAL;
1790 }
1791 }
1792
1793 private class ComputeStyleListWorker extends RecursiveTask<List<StyleRecord>> implements Visitor {
1794 private final List<? extends OsmPrimitive> input;
1795 private final List<StyleRecord> output;
1796
1797 private final ElemStyles styles = MapPaintStyles.getStyles();
1798 private final int directExecutionTaskSize;
1799
1800 private final boolean drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10000000);
1801 private final boolean drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
1802 private final boolean drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
1803
1804 /**
1805 * Constructs a new {@code ComputeStyleListWorker}.
1806 * @param input the primitives to process
1807 * @param output the list of styles to which styles will be added
1808 * @param directExecutionTaskSize the threshold deciding whether to subdivide the tasks
1809 */
1810 ComputeStyleListWorker(final List<? extends OsmPrimitive> input, List<StyleRecord> output, int directExecutionTaskSize) {
1811 this.input = input;
1812 this.output = output;
1813 this.directExecutionTaskSize = directExecutionTaskSize;
1814 this.styles.setDrawMultipolygon(drawMultipolygon);
1815 }
1816
1817 @Override
1818 protected List<StyleRecord> compute() {
1819 if (input.size() <= directExecutionTaskSize) {
1820 return computeDirectly();
1821 } else {
1822 final Collection<ForkJoinTask<List<StyleRecord>>> tasks = new ArrayList<>();
1823 for (int fromIndex = 0; fromIndex < input.size(); fromIndex += directExecutionTaskSize) {
1824 final int toIndex = Math.min(fromIndex + directExecutionTaskSize, input.size());
1825 final List<StyleRecord> output = new ArrayList<>(directExecutionTaskSize);
1826 tasks.add(new ComputeStyleListWorker(input.subList(fromIndex, toIndex), output, directExecutionTaskSize).fork());
1827 }
1828 for (ForkJoinTask<List<StyleRecord>> task : tasks) {
1829 output.addAll(task.join());
1830 }
1831 return output;
1832 }
1833 }
1834
1835 public List<StyleRecord> computeDirectly() {
1836 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
1837 try {
1838 for (final OsmPrimitive osm : input) {
1839 if (osm.isDrawable()) {
1840 osm.accept(this);
1841 }
1842 }
1843 return output;
1844 } finally {
1845 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
1846 }
1847 }
1848
1849 @Override
1850 public void visit(Node n) {
1851 add(n, computeFlags(n, false));
1852 }
1853
1854 @Override
1855 public void visit(Way w) {
1856 add(w, computeFlags(w, true));
1857 }
1858
1859 @Override
1860 public void visit(Relation r) {
1861 add(r, computeFlags(r, true));
1862 }
1863
1864 @Override
1865 public void visit(Changeset cs) {
1866 throw new UnsupportedOperationException();
1867 }
1868
1869 public void add(Node osm, int flags) {
1870 StyleElementList sl = styles.get(osm, circum, nc);
1871 for (StyleElement s : sl) {
1872 output.add(new StyleRecord(s, osm, flags));
1873 }
1874 }
1875
1876 public void add(Relation osm, int flags) {
1877 StyleElementList sl = styles.get(osm, circum, nc);
1878 for (StyleElement s : sl) {
1879 if (drawMultipolygon && drawArea && s instanceof AreaElement && (flags & FLAG_DISABLED) == 0) {
1880 output.add(new StyleRecord(s, osm, flags));
1881 } else if (drawRestriction && s instanceof NodeElement) {
1882 output.add(new StyleRecord(s, osm, flags));
1883 }
1884 }
1885 }
1886
1887 public void add(Way osm, int flags) {
1888 StyleElementList sl = styles.get(osm, circum, nc);
1889 for (StyleElement s : sl) {
1890 if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElement) {
1891 continue;
1892 }
1893 output.add(new StyleRecord(s, osm, flags));
1894 }
1895 }
1896 }
1897
1898 @Override
1899 public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) {
1900 BBox bbox = bounds.toBBox();
1901 getSettings(renderVirtualNodes);
1902 boolean benchmarkOutput = Main.isTraceEnabled() || Main.pref.getBoolean("mappaint.render.benchmark", false);
1903 boolean benchmark = benchmarkOutput || benchmarkData != null;
1904
1905 data.getReadLock().lock();
1906 try {
1907 highlightWaySegments = data.getHighlightedWaySegments();
1908
1909 long timeStart = 0, timeGenerateDone = 0, timeSortingDone = 0, timeFinished;
1910 if (benchmark) {
1911 timeStart = System.currentTimeMillis();
1912 if (benchmarkOutput) {
1913 System.err.print("BENCHMARK: rendering ");
1914 }
1915 }
1916
1917 List<Node> nodes = data.searchNodes(bbox);
1918 List<Way> ways = data.searchWays(bbox);
1919 List<Relation> relations = data.searchRelations(bbox);
1920
1921 final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size());
1922
1923 // Need to process all relations first.
1924 // Reason: Make sure, ElemStyles.getStyleCacheWithRange is
1925 // not called for the same primitive in parallel threads.
1926 // (Could be synchronized, but try to avoid this for
1927 // performance reasons.)
1928 THREAD_POOL.invoke(new ComputeStyleListWorker(relations, allStyleElems,
1929 Math.max(20, relations.size() / THREAD_POOL.getParallelism() / 3)));
1930 THREAD_POOL.invoke(new ComputeStyleListWorker(new CompositeList<>(nodes, ways), allStyleElems,
1931 Math.max(100, (nodes.size() + ways.size()) / THREAD_POOL.getParallelism() / 3)));
1932
1933 if (benchmark) {
1934 timeGenerateDone = System.currentTimeMillis();
1935 if (benchmarkOutput) {
1936 System.err.print("phase 1 (calculate styles): " + Utils.getDurationString(timeGenerateDone - timeStart));
1937 }
1938 if (benchmarkData != null) {
1939 benchmarkData.generateTime = timeGenerateDone - timeStart;
1940 }
1941 }
1942
1943 Collections.sort(allStyleElems); // TODO: try parallel sort when switching to Java 8
1944
1945 if (benchmarkData != null) {
1946 timeSortingDone = System.currentTimeMillis();
1947 benchmarkData.sortTime = timeSortingDone - timeGenerateDone;
1948 if (benchmarkData.skipDraw) {
1949 benchmarkData.recordElementStats(allStyleElems);
1950 return;
1951 }
1952 }
1953
1954 for (StyleRecord r : allStyleElems) {
1955 r.style.paintPrimitive(
1956 r.osm,
1957 paintSettings,
1958 this,
1959 (r.flags & FLAG_SELECTED) != 0,
1960 (r.flags & FLAG_OUTERMEMBER_OF_SELECTED) != 0,
1961 (r.flags & FLAG_MEMBER_OF_SELECTED) != 0
1962 );
1963 }
1964
1965 if (benchmark) {
1966 timeFinished = System.currentTimeMillis();
1967 if (benchmarkData != null) {
1968 benchmarkData.drawTime = timeFinished - timeGenerateDone;
1969 benchmarkData.recordElementStats(allStyleElems);
1970 }
1971 if (benchmarkOutput) {
1972 System.err.println("; phase 2 (draw): " + Utils.getDurationString(timeFinished - timeGenerateDone) +
1973 "; total: " + Utils.getDurationString(timeFinished - timeStart) +
1974 " (scale: " + circum + " zoom level: " + Selector.GeneralSelector.scale2level(circum) + ')');
1975 }
1976 }
1977
1978 drawVirtualNodes(data, bbox);
1979 } finally {
1980 data.getReadLock().unlock();
1981 }
1982 }
1983}
Note: See TracBrowser for help on using the repository browser.