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

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

use of Utils.getDurationString()

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