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

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

sonar - squid:S2325 - "private" methods that don't access instance data should be "static"

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