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

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

see #11447 - partial revert of r8384

  • Property svn:eol-style set to native
File size: 66.9 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 final List<Node> nodes;
102 private final double 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, 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 public 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 * @return false by default, but depends on the value of the advanced
261 * preference glyph-bug=false|true|auto, where auto is the automatic detection
262 * method which apparently no longer gives a useful result for Java 7.
263 */
264 public static boolean isGlyphVectorDoubleTranslationBug(Font font) {
265 Boolean cached = IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.get(font);
266 if (cached != null)
267 return cached;
268 String overridePref = Main.pref.get("glyph-bug", "auto");
269 if ("auto".equals(overridePref)) {
270 FontRenderContext frc = new FontRenderContext(null, false, false);
271 GlyphVector gv = font.createGlyphVector(frc, "x");
272 gv.setGlyphTransform(0, AffineTransform.getTranslateInstance(1000, 1000));
273 Shape shape = gv.getGlyphOutline(0);
274 Main.trace("#10446: shape: "+shape.getBounds());
275 // x is about 1000 on normal stystems and about 2000 when the bug occurs
276 int x = shape.getBounds().x;
277 boolean isBug = x > 1500;
278 IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, isBug);
279 return isBug;
280 } else {
281 boolean override = Boolean.parseBoolean(overridePref);
282 IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, override);
283 return override;
284 }
285 }
286
287 private double circum;
288
289 private MapPaintSettings paintSettings;
290
291 private Color highlightColorTransparent;
292
293 private static final int FLAG_NORMAL = 0;
294 private static final int FLAG_DISABLED = 1;
295 private static final int FLAG_MEMBER_OF_SELECTED = 2;
296 private static final int FLAG_SELECTED = 4;
297 private static final int FLAG_OUTERMEMBER_OF_SELECTED = 8;
298
299 private static final double PHI = Math.toRadians(20);
300 private static final double cosPHI = Math.cos(PHI);
301 private static final double sinPHI = Math.sin(PHI);
302
303 private Collection<WaySegment> highlightWaySegments;
304
305 // highlight customization fields
306 private int highlightLineWidth;
307 private int highlightPointRadius;
308 private int widerHighlight;
309 private int highlightStep;
310
311 //flag that activate wider highlight mode
312 private boolean useWiderHighlight;
313
314 private boolean useStrokes;
315 private boolean showNames;
316 private boolean showIcons;
317 private boolean isOutlineOnly;
318
319 private Font orderFont;
320
321 private boolean leftHandTraffic;
322 private Object antialiasing;
323
324 /**
325 * Constructs a new {@code StyledMapRenderer}.
326 *
327 * @param g the graphics context. Must not be null.
328 * @param nc the map viewport. Must not be null.
329 * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
330 * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
331 * @throws IllegalArgumentException if {@code g} is null
332 * @throws IllegalArgumentException if {@code nc} is null
333 */
334 public StyledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
335 super(g, nc, isInactiveMode);
336
337 if (nc!=null) {
338 Component focusOwner = FocusManager.getCurrentManager().getFocusOwner();
339 useWiderHighlight = !(focusOwner instanceof AbstractButton || focusOwner == nc);
340 }
341 }
342
343 private Polygon buildPolygon(Point center, int radius, int sides) {
344 return buildPolygon(center, radius, sides, 0.0);
345 }
346
347 private Polygon buildPolygon(Point center, int radius, int sides, double rotation) {
348 Polygon polygon = new Polygon();
349 for (int i = 0; i < sides; i++) {
350 double angle = ((2 * Math.PI / sides) * i) - rotation;
351 int x = (int) Math.round(center.x + radius * Math.cos(angle));
352 int y = (int) Math.round(center.y + radius * Math.sin(angle));
353 polygon.addPoint(x, y);
354 }
355 return polygon;
356 }
357
358 private void displaySegments(GeneralPath path, GeneralPath orientationArrows, GeneralPath onewayArrows, GeneralPath onewayArrowsCasing,
359 Color color, BasicStroke line, BasicStroke dashes, Color dashedColor) {
360 g.setColor(isInactiveMode ? inactiveColor : color);
361 if (useStrokes) {
362 g.setStroke(line);
363 }
364 g.draw(path);
365
366 if(!isInactiveMode && useStrokes && dashes != null) {
367 g.setColor(dashedColor);
368 g.setStroke(dashes);
369 g.draw(path);
370 }
371
372 if (orientationArrows != null) {
373 g.setColor(isInactiveMode ? inactiveColor : color);
374 g.setStroke(new BasicStroke(line.getLineWidth(), line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
375 g.draw(orientationArrows);
376 }
377
378 if (onewayArrows != null) {
379 g.setStroke(new BasicStroke(1, line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
380 g.fill(onewayArrowsCasing);
381 g.setColor(isInactiveMode ? inactiveColor : backgroundColor);
382 g.fill(onewayArrows);
383 }
384
385 if (useStrokes) {
386 g.setStroke(new BasicStroke());
387 }
388 }
389
390 /**
391 * Displays text at specified position including its halo, if applicable.
392 *
393 * @param gv Text's glyphs to display. If {@code null}, use text from {@code s} instead.
394 * @param s text to display if {@code gv} is {@code null}
395 * @param x X position
396 * @param y Y position
397 * @param disabled {@code true} if element is disabled (filtered out)
398 * @param text text style to use
399 */
400 private void displayText(GlyphVector gv, String s, int x, int y, boolean disabled, TextElement text) {
401 if (isInactiveMode || disabled) {
402 g.setColor(inactiveColor);
403 if (gv != null) {
404 g.drawGlyphVector(gv, x, y);
405 } else {
406 g.setFont(text.font);
407 g.drawString(s, x, y);
408 }
409 } else if (text.haloRadius != null) {
410 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
411 g.setColor(text.haloColor);
412 Shape textOutline;
413 if (gv == null) {
414 if (s.isEmpty()) return;
415 FontRenderContext frc = g.getFontRenderContext();
416 TextLayout tl = new TextLayout(s, text.font, frc);
417 textOutline = tl.getOutline(AffineTransform.getTranslateInstance(x, y));
418 } else {
419 textOutline = gv.getOutline(x, y);
420 }
421 g.draw(textOutline);
422 g.setStroke(new BasicStroke());
423 g.setColor(text.color);
424 g.fill(textOutline);
425 } else {
426 g.setColor(text.color);
427 if (gv != null) {
428 g.drawGlyphVector(gv, x, y);
429 } else {
430 g.setFont(text.font);
431 g.drawString(s, x, y);
432 }
433 }
434 }
435
436 protected void drawArea(OsmPrimitive osm, Path2D.Double path, Color color, MapImage fillImage, boolean disabled, TextElement text) {
437
438 Shape area = path.createTransformedShape(nc.getAffineTransform());
439
440 if (!isOutlineOnly) {
441 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
442 if (fillImage == null) {
443 if (isInactiveMode) {
444 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.33f));
445 }
446 g.setColor(color);
447 g.fill(area);
448 } else {
449 TexturePaint texture = new TexturePaint(fillImage.getImage(disabled),
450 new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight()));
451 g.setPaint(texture);
452 Float alpha = fillImage.getAlphaFloat();
453 if (!Utils.equalsEpsilon(alpha, 1f)) {
454 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
455 }
456 g.fill(area);
457 g.setPaintMode();
458 }
459 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
460 }
461
462 drawAreaText(osm, text, area);
463 }
464
465 private void drawAreaText(OsmPrimitive osm, TextElement text, Shape area) {
466 if (text != null && isShowNames()) {
467 // abort if we can't compose the label to be rendered
468 if (text.labelCompositionStrategy == null) return;
469 String name = text.labelCompositionStrategy.compose(osm);
470 if (name == null) return;
471
472 Rectangle pb = area.getBounds();
473 FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache
474 Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
475
476 // Using the Centroid is Nicer for buildings like: +--------+
477 // but this needs to be fast. As most houses are | 42 |
478 // boxes anyway, the center of the bounding box +---++---+
479 // will have to do. ++
480 // Centroids are not optimal either, just imagine a U-shaped house.
481
482 // quick check to see if label box is smaller than primitive box
483 if (pb.width >= nb.getWidth() && pb.height >= nb.getHeight()) {
484
485 final double w = pb.width - nb.getWidth();
486 final double h = pb.height - nb.getHeight();
487
488 final int x2 = pb.x + (int)(w/2.0);
489 final int y2 = pb.y + (int)(h/2.0);
490
491 final int nbw = (int) nb.getWidth();
492 final int nbh = (int) nb.getHeight();
493
494 Rectangle centeredNBounds = new Rectangle(x2, y2, nbw, nbh);
495
496 // slower check to see if label is displayed inside primitive shape
497 boolean labelOK = area.contains(centeredNBounds);
498 if (!labelOK) {
499 // if center position (C) is not inside osm shape, try naively some other positions as follows:
500 final int x1 = pb.x + (int)( w/4.0);
501 final int x3 = pb.x + (int)(3*w/4.0);
502 final int y1 = pb.y + (int)( h/4.0);
503 final int y3 = pb.y + (int)(3*h/4.0);
504 // +-----------+
505 // | 5 1 6 |
506 // | 4 C 2 |
507 // | 8 3 7 |
508 // +-----------+
509 Rectangle[] candidates = new Rectangle[] {
510 new Rectangle(x2, y1, nbw, nbh),
511 new Rectangle(x3, y2, nbw, nbh),
512 new Rectangle(x2, y3, nbw, nbh),
513 new Rectangle(x1, y2, nbw, nbh),
514 new Rectangle(x1, y1, nbw, nbh),
515 new Rectangle(x3, y1, nbw, nbh),
516 new Rectangle(x3, y3, nbw, nbh),
517 new Rectangle(x1, y3, nbw, nbh)
518 };
519 // Dumb algorithm to find a better placement. We could surely find a smarter one but it should
520 // solve most of building issues with only few calculations (8 at most)
521 for (int i = 0; i < candidates.length && !labelOK; i++) {
522 centeredNBounds = candidates[i];
523 labelOK = area.contains(centeredNBounds);
524 }
525 }
526 if (labelOK) {
527 Font defaultFont = g.getFont();
528 int x = (int)(centeredNBounds.getMinX() - nb.getMinX());
529 int y = (int)(centeredNBounds.getMinY() - nb.getMinY());
530 displayText(null, name, x, y, osm.isDisabled(), text);
531 g.setFont(defaultFont);
532 } else if (Main.isDebugEnabled()) {
533 Main.debug("Couldn't find a correct label placement for "+osm+" / "+name);
534 }
535 }
536 }
537 }
538
539 public void drawArea(Relation r, Color color, MapImage fillImage, boolean disabled, TextElement text) {
540 Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
541 if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) {
542 for (PolyData pd : multipolygon.getCombinedPolygons()) {
543 Path2D.Double p = pd.get();
544 if (!isAreaVisible(p)) {
545 continue;
546 }
547 drawArea(r, p,
548 pd.selected ? paintSettings.getRelationSelectedColor(color.getAlpha()) : color,
549 fillImage, disabled, text);
550 }
551 }
552 }
553
554 public void drawArea(Way w, Color color, MapImage fillImage, boolean disabled, TextElement text) {
555 drawArea(w, getPath(w), color, fillImage, disabled, text);
556 }
557
558 public void drawBoxText(Node n, BoxTextElemStyle bs) {
559 if (!isShowNames() || bs == null)
560 return;
561
562 Point p = nc.getPoint(n);
563 TextElement text = bs.text;
564 String s = text.labelCompositionStrategy.compose(n);
565 if (s == null) return;
566
567 Font defaultFont = g.getFont();
568 g.setFont(text.font);
569
570 int x = p.x + text.xOffset;
571 int y = p.y + text.yOffset;
572 /**
573 *
574 * left-above __center-above___ right-above
575 * left-top| |right-top
576 * | |
577 * left-center| center-center |right-center
578 * | |
579 * left-bottom|_________________|right-bottom
580 * left-below center-below right-below
581 *
582 */
583 Rectangle box = bs.getBox();
584 if (bs.hAlign == HorizontalTextAlignment.RIGHT) {
585 x += box.x + box.width + 2;
586 } else {
587 FontRenderContext frc = g.getFontRenderContext();
588 Rectangle2D bounds = text.font.getStringBounds(s, frc);
589 int textWidth = (int) bounds.getWidth();
590 if (bs.hAlign == HorizontalTextAlignment.CENTER) {
591 x -= textWidth / 2;
592 } else if (bs.hAlign == HorizontalTextAlignment.LEFT) {
593 x -= - box.x + 4 + textWidth;
594 } else throw new AssertionError();
595 }
596
597 if (bs.vAlign == VerticalTextAlignment.BOTTOM) {
598 y += box.y + box.height;
599 } else {
600 FontRenderContext frc = g.getFontRenderContext();
601 LineMetrics metrics = text.font.getLineMetrics(s, frc);
602 if (bs.vAlign == VerticalTextAlignment.ABOVE) {
603 y -= -box.y + metrics.getDescent();
604 } else if (bs.vAlign == VerticalTextAlignment.TOP) {
605 y -= -box.y - metrics.getAscent();
606 } else if (bs.vAlign == VerticalTextAlignment.CENTER) {
607 y += (metrics.getAscent() - metrics.getDescent()) / 2;
608 } else if (bs.vAlign == VerticalTextAlignment.BELOW) {
609 y += box.y + box.height + metrics.getAscent() + 2;
610 } else throw new AssertionError();
611 }
612 displayText(null, s, x, y, n.isDisabled(), text);
613 g.setFont(defaultFont);
614 }
615
616 /**
617 * Draw an image along a way repeatedly.
618 *
619 * @param way the way
620 * @param pattern the image
621 * @param offset offset from the way
622 * @param spacing spacing between two images
623 * @param phase initial spacing
624 * @param align alignment of the image. The top, center or bottom edge can be aligned with the way.
625 */
626 public void drawRepeatImage(Way way, MapImage pattern, boolean disabled, double offset, double spacing, double phase,
627 LineImageAlignment align) {
628 final int imgWidth = pattern.getWidth();
629 final double repeat = imgWidth + spacing;
630 final int imgHeight = pattern.getHeight();
631
632 Point lastP = null;
633 double currentWayLength = phase % repeat;
634 if (currentWayLength < 0) {
635 currentWayLength += repeat;
636 }
637
638 int dy1, dy2;
639 switch (align) {
640 case TOP:
641 dy1 = 0;
642 dy2 = imgHeight;
643 break;
644 case CENTER:
645 dy1 = - imgHeight / 2;
646 dy2 = imgHeight + dy1;
647 break;
648 case BOTTOM:
649 dy1 = -imgHeight;
650 dy2 = 0;
651 break;
652 default:
653 throw new AssertionError();
654 }
655
656 OffsetIterator it = new OffsetIterator(way.getNodes(), offset);
657 while (it.hasNext()) {
658 Point thisP = it.next();
659
660 if (lastP != null) {
661 final double segmentLength = thisP.distance(lastP);
662
663 final double dx = thisP.x - lastP.x;
664 final double dy = thisP.y - lastP.y;
665
666 // pos is the position from the beginning of the current segment
667 // where an image should be painted
668 double pos = repeat - (currentWayLength % repeat);
669
670 AffineTransform saveTransform = g.getTransform();
671 g.translate(lastP.x, lastP.y);
672 g.rotate(Math.atan2(dy, dx));
673
674 // draw the rest of the image from the last segment in case it
675 // is cut off
676 if (pos > spacing) {
677 // segment is too short for a complete image
678 if (pos > segmentLength + spacing) {
679 g.drawImage(pattern.getImage(disabled), 0, dy1, (int) segmentLength, dy2,
680 (int) (repeat - pos), 0,
681 (int) (repeat - pos + segmentLength), imgHeight, null);
682 // rest of the image fits fully on the current segment
683 } else {
684 g.drawImage(pattern.getImage(disabled), 0, dy1, (int) (pos - spacing), dy2,
685 (int) (repeat - pos), 0, imgWidth, imgHeight, null);
686 }
687 }
688 // draw remaining images for this segment
689 while (pos < segmentLength) {
690 // cut off at the end?
691 if (pos + imgWidth > segmentLength) {
692 g.drawImage(pattern.getImage(disabled), (int) pos, dy1, (int) segmentLength, dy2,
693 0, 0, (int) segmentLength - (int) pos, imgHeight, null);
694 } else {
695 g.drawImage(pattern.getImage(disabled), (int) pos, dy1, nc);
696 }
697 pos += repeat;
698 }
699 g.setTransform(saveTransform);
700
701 currentWayLength += segmentLength;
702 }
703 lastP = thisP;
704 }
705 }
706
707 @Override
708 public void drawNode(Node n, Color color, int size, boolean fill) {
709 if(size <= 0 && !n.isHighlighted())
710 return;
711
712 Point p = nc.getPoint(n);
713
714 if(n.isHighlighted()) {
715 drawPointHighlight(p, size);
716 }
717
718 if (size > 1) {
719 if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return;
720 int radius = size / 2;
721
722 if (isInactiveMode || n.isDisabled()) {
723 g.setColor(inactiveColor);
724 } else {
725 g.setColor(color);
726 }
727 if (fill) {
728 g.fillRect(p.x-radius-1, p.y-radius-1, size + 1, size + 1);
729 } else {
730 g.drawRect(p.x-radius-1, p.y-radius-1, size, size);
731 }
732 }
733 }
734
735 public void drawNodeIcon(Node n, MapImage img, boolean disabled, boolean selected, boolean member, double theta) {
736 Point p = nc.getPoint(n);
737
738 final int w = img.getWidth(), h = img.getHeight();
739 if(n.isHighlighted()) {
740 drawPointHighlight(p, Math.max(w, h));
741 }
742
743 float alpha = img.getAlphaFloat();
744
745 if (!Utils.equalsEpsilon(alpha, 1f)) {
746 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
747 }
748 g.rotate(theta, p.x, p.y);
749 g.drawImage(img.getImage(disabled), p.x - w/2 + img.offsetX, p.y - h/2 + img.offsetY, nc);
750 g.rotate(-theta, p.x, p.y);
751 g.setPaintMode();
752 if (selected || member)
753 {
754 Color color;
755 if (disabled) {
756 color = inactiveColor;
757 } else if (selected) {
758 color = selectedColor;
759 } else {
760 color = relationSelectedColor;
761 }
762 g.setColor(color);
763 g.drawRect(p.x - w/2 + img.offsetX - 2, p.y - h/2 + img.offsetY - 2, w + 4, h + 4);
764 }
765 }
766
767 public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
768 Point p = nc.getPoint(n);
769 int radius = s.size / 2;
770
771 if(n.isHighlighted()) {
772 drawPointHighlight(p, s.size);
773 }
774
775 if (fillColor != null) {
776 g.setColor(fillColor);
777 switch (s.symbol) {
778 case SQUARE:
779 g.fillRect(p.x - radius, p.y - radius, s.size, s.size);
780 break;
781 case CIRCLE:
782 g.fillOval(p.x - radius, p.y - radius, s.size, s.size);
783 break;
784 case TRIANGLE:
785 g.fillPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
786 break;
787 case PENTAGON:
788 g.fillPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
789 break;
790 case HEXAGON:
791 g.fillPolygon(buildPolygon(p, radius, 6));
792 break;
793 case HEPTAGON:
794 g.fillPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
795 break;
796 case OCTAGON:
797 g.fillPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
798 break;
799 case NONAGON:
800 g.fillPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
801 break;
802 case DECAGON:
803 g.fillPolygon(buildPolygon(p, radius, 10));
804 break;
805 default:
806 throw new AssertionError();
807 }
808 }
809 if (s.stroke != null) {
810 g.setStroke(s.stroke);
811 g.setColor(strokeColor);
812 switch (s.symbol) {
813 case SQUARE:
814 g.drawRect(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
815 break;
816 case CIRCLE:
817 g.drawOval(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
818 break;
819 case TRIANGLE:
820 g.drawPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
821 break;
822 case PENTAGON:
823 g.drawPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
824 break;
825 case HEXAGON:
826 g.drawPolygon(buildPolygon(p, radius, 6));
827 break;
828 case HEPTAGON:
829 g.drawPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
830 break;
831 case OCTAGON:
832 g.drawPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
833 break;
834 case NONAGON:
835 g.drawPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
836 break;
837 case DECAGON:
838 g.drawPolygon(buildPolygon(p, radius, 10));
839 break;
840 default:
841 throw new AssertionError();
842 }
843 g.setStroke(new BasicStroke());
844 }
845 }
846
847 /**
848 * Draw a number of the order of the two consecutive nodes within the
849 * parents way
850 */
851 public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
852 Point p1 = nc.getPoint(n1);
853 Point p2 = nc.getPoint(n2);
854 StyledMapRenderer.this.drawOrderNumber(p1, p2, orderNumber, clr);
855 }
856
857 /**
858 * highlights a given GeneralPath using the settings from BasicStroke to match the line's
859 * style. Width of the highlight is hard coded.
860 * @param path
861 * @param line
862 */
863 private void drawPathHighlight(GeneralPath path, BasicStroke line) {
864 if(path == null)
865 return;
866 g.setColor(highlightColorTransparent);
867 float w = line.getLineWidth() + highlightLineWidth;
868 if (useWiderHighlight) w+=widerHighlight;
869 while(w >= line.getLineWidth()) {
870 g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit()));
871 g.draw(path);
872 w -= highlightStep;
873 }
874 }
875 /**
876 * highlights a given point by drawing a rounded rectangle around it. Give the
877 * size of the object you want to be highlighted, width is added automatically.
878 */
879 private void drawPointHighlight(Point p, int size) {
880 g.setColor(highlightColorTransparent);
881 int s = size + highlightPointRadius;
882 if (useWiderHighlight) s+=widerHighlight;
883 while(s >= size) {
884 int r = (int) Math.floor(s/2d);
885 g.fillRoundRect(p.x-r, p.y-r, s, s, r, r);
886 s -= highlightStep;
887 }
888 }
889
890 public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
891 // rotate image with direction last node in from to, and scale down image to 16*16 pixels
892 Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16));
893 int w = smallImg.getWidth(null), h=smallImg.getHeight(null);
894 g.drawImage(smallImg, (int)(pVia.x+vx+vx2)-w/2, (int)(pVia.y+vy+vy2)-h/2, nc);
895
896 if (selected) {
897 g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor);
898 g.drawRect((int)(pVia.x+vx+vx2)-w/2-2,(int)(pVia.y+vy+vy2)-h/2-2, w+4, h+4);
899 }
900 }
901
902 public void drawRestriction(Relation r, MapImage icon, boolean disabled) {
903 Way fromWay = null;
904 Way toWay = null;
905 OsmPrimitive via = null;
906
907 /* find the "from", "via" and "to" elements */
908 for (RelationMember m : r.getMembers()) {
909 if(m.getMember().isIncomplete())
910 return;
911 else {
912 if(m.isWay()) {
913 Way w = m.getWay();
914 if(w.getNodesCount() < 2) {
915 continue;
916 }
917
918 switch(m.getRole()) {
919 case "from":
920 if(fromWay == null) {
921 fromWay = w;
922 }
923 break;
924 case "to":
925 if(toWay == null) {
926 toWay = w;
927 }
928 break;
929 case "via":
930 if(via == null) {
931 via = w;
932 }
933 }
934 } else if(m.isNode()) {
935 Node n = m.getNode();
936 if("via".equals(m.getRole()) && via == null) {
937 via = n;
938 }
939 }
940 }
941 }
942
943 if (fromWay == null || toWay == null || via == null)
944 return;
945
946 Node viaNode;
947 if(via instanceof Node) {
948 viaNode = (Node) via;
949 if(!fromWay.isFirstLastNode(viaNode))
950 return;
951 } else {
952 Way viaWay = (Way) via;
953 Node firstNode = viaWay.firstNode();
954 Node lastNode = viaWay.lastNode();
955 Boolean onewayvia = Boolean.FALSE;
956
957 String onewayviastr = viaWay.get("oneway");
958 if(onewayviastr != null) {
959 if("-1".equals(onewayviastr)) {
960 onewayvia = Boolean.TRUE;
961 Node tmp = firstNode;
962 firstNode = lastNode;
963 lastNode = tmp;
964 } else {
965 onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
966 if (onewayvia == null) {
967 onewayvia = Boolean.FALSE;
968 }
969 }
970 }
971
972 if(fromWay.isFirstLastNode(firstNode)) {
973 viaNode = firstNode;
974 } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
975 viaNode = lastNode;
976 } else
977 return;
978 }
979
980 /* find the "direct" nodes before the via node */
981 Node fromNode;
982 if(fromWay.firstNode() == via) {
983 fromNode = fromWay.getNode(1);
984 } else {
985 fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
986 }
987
988 Point pFrom = nc.getPoint(fromNode);
989 Point pVia = nc.getPoint(viaNode);
990
991 /* starting from via, go back the "from" way a few pixels
992 (calculate the vector vx/vy with the specified length and the direction
993 away from the "via" node along the first segment of the "from" way)
994 */
995 double distanceFromVia=14;
996 double dx = pFrom.x >= pVia.x ? pFrom.x - pVia.x : pVia.x - pFrom.x;
997 double dy = pFrom.y >= pVia.y ? pFrom.y - pVia.y : pVia.y - pFrom.y;
998
999 double fromAngle;
1000 if (dx == 0) {
1001 fromAngle = Math.PI/2;
1002 } else {
1003 fromAngle = Math.atan(dy / dx);
1004 }
1005 double fromAngleDeg = Math.toDegrees(fromAngle);
1006
1007 double vx = distanceFromVia * Math.cos(fromAngle);
1008 double vy = distanceFromVia * Math.sin(fromAngle);
1009
1010 if(pFrom.x < pVia.x) {
1011 vx = -vx;
1012 }
1013 if(pFrom.y < pVia.y) {
1014 vy = -vy;
1015 }
1016
1017 /* go a few pixels away from the way (in a right angle)
1018 (calculate the vx2/vy2 vector with the specified length and the direction
1019 90degrees away from the first segment of the "from" way)
1020 */
1021 double distanceFromWay=10;
1022 double vx2 = 0;
1023 double vy2 = 0;
1024 double iconAngle = 0;
1025
1026 if(pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
1027 if(!leftHandTraffic) {
1028 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1029 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1030 } else {
1031 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1032 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1033 }
1034 iconAngle = 270+fromAngleDeg;
1035 }
1036 if(pFrom.x < pVia.x && pFrom.y >= pVia.y) {
1037 if(!leftHandTraffic) {
1038 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1039 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1040 } else {
1041 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1042 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1043 }
1044 iconAngle = 90-fromAngleDeg;
1045 }
1046 if(pFrom.x < pVia.x && pFrom.y < pVia.y) {
1047 if(!leftHandTraffic) {
1048 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1049 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1050 } else {
1051 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1052 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1053 }
1054 iconAngle = 90+fromAngleDeg;
1055 }
1056 if(pFrom.x >= pVia.x && pFrom.y < pVia.y) {
1057 if(!leftHandTraffic) {
1058 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1059 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1060 } else {
1061 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1062 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1063 }
1064 iconAngle = 270-fromAngleDeg;
1065 }
1066
1067 drawRestriction(icon.getImage(disabled),
1068 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1069 }
1070
1071 public void drawTextOnPath(Way way, TextElement text) {
1072 if (way == null || text == null)
1073 return;
1074 String name = text.getString(way);
1075 if (name == null || name.isEmpty())
1076 return;
1077
1078 FontMetrics fontMetrics = g.getFontMetrics(text.font);
1079 Rectangle2D rec = fontMetrics.getStringBounds(name, g);
1080
1081 Rectangle bounds = g.getClipBounds();
1082
1083 Polygon poly = new Polygon();
1084 Point lastPoint = null;
1085 Iterator<Node> it = way.getNodes().iterator();
1086 double pathLength = 0;
1087 long dx, dy;
1088
1089 // find half segments that are long enough to draw text on
1090 // (don't draw text over the cross hair in the center of each segment)
1091 List<Double> longHalfSegmentStart = new ArrayList<>(); // start point of half segment (as length along the way)
1092 List<Double> longHalfSegmentEnd = new ArrayList<>(); // end point of half segment (as length along the way)
1093 List<Double> longHalfsegmentQuality = new ArrayList<>(); // quality factor (off screen / partly on screen / fully on screen)
1094
1095 while (it.hasNext()) {
1096 Node n = it.next();
1097 Point p = nc.getPoint(n);
1098 poly.addPoint(p.x, p.y);
1099
1100 if(lastPoint != null) {
1101 dx = p.x - lastPoint.x;
1102 dy = p.y - lastPoint.y;
1103 double segmentLength = Math.sqrt(dx*dx + dy*dy);
1104 if (segmentLength > 2*(rec.getWidth()+4)) {
1105 Point center = new Point((lastPoint.x + p.x)/2, (lastPoint.y + p.y)/2);
1106 double q = 0;
1107 if (bounds != null) {
1108 if (bounds.contains(lastPoint) && bounds.contains(center)) {
1109 q = 2;
1110 } else if (bounds.contains(lastPoint) || bounds.contains(center)) {
1111 q = 1;
1112 }
1113 }
1114 longHalfSegmentStart.add(pathLength);
1115 longHalfSegmentEnd.add(pathLength + segmentLength / 2);
1116 longHalfsegmentQuality.add(q);
1117
1118 q = 0;
1119 if (bounds != null) {
1120 if (bounds.contains(center) && bounds.contains(p)) {
1121 q = 2;
1122 } else if (bounds.contains(center) || bounds.contains(p)) {
1123 q = 1;
1124 }
1125 }
1126 longHalfSegmentStart.add(pathLength + segmentLength / 2);
1127 longHalfSegmentEnd.add(pathLength + segmentLength);
1128 longHalfsegmentQuality.add(q);
1129 }
1130 pathLength += segmentLength;
1131 }
1132 lastPoint = p;
1133 }
1134
1135 if (rec.getWidth() > pathLength)
1136 return;
1137
1138 double t1, t2;
1139
1140 if (!longHalfSegmentStart.isEmpty()) {
1141 if (way.getNodesCount() == 2) {
1142 // For 2 node ways, the two half segments are exactly
1143 // the same size and distance from the center.
1144 // Prefer the first one for consistency.
1145 longHalfsegmentQuality.set(0, longHalfsegmentQuality.get(0) + 0.5);
1146 }
1147
1148 // find the long half segment that is closest to the center of the way
1149 // candidates with higher quality value are preferred
1150 double bestStart = Double.NaN;
1151 double bestEnd = Double.NaN;
1152 double bestDistanceToCenter = Double.MAX_VALUE;
1153 double bestQuality = -1;
1154 for (int i=0; i<longHalfSegmentStart.size(); i++) {
1155 double start = longHalfSegmentStart.get(i);
1156 double end = longHalfSegmentEnd.get(i);
1157 double dist = Math.abs(0.5 * (end + start) - 0.5 * pathLength);
1158 if (longHalfsegmentQuality.get(i) > bestQuality
1159 || (dist < bestDistanceToCenter && Utils.equalsEpsilon(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) {
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) {
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.