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

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

checkstyle

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