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

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

mappaint: added Einstein-pref to set text antialiasing hint (see #12085)

(mappaint.text-antialiasing=default|on|off|gasp|lcd-hrgb|lcd-hbgr|lcd-vrgb|lcd-vbgr)

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