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

Last change on this file since 8085 was 8085, checked in by bastiK, 9 years ago

fixed #10216 Offsetting node icon in MapCSS

also: rework of rendering of disabled layers and objects

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