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

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

fix #10419 - improved label placement inside areas (ex: addr:housenumber on buildings)

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