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

Last change on this file since 9077 was 9077, checked in by bastiK, 8 years ago

mapcss: partial fill - fix linecap (see #12104)

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