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

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

javadoc update

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