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

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

fix #10054 - Multipolygons as members not highlighted when relation is selected

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