// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.osm.visitor.paint;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.LineMetrics;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.swing.AbstractButton;
import javax.swing.FocusManager;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.osm.BBox;
import org.openstreetmap.josm.data.osm.Changeset;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmUtils;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.WaySegment;
import org.openstreetmap.josm.data.osm.visitor.Visitor;
import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
import org.openstreetmap.josm.data.preferences.AbstractProperty;
import org.openstreetmap.josm.data.preferences.BooleanProperty;
import org.openstreetmap.josm.data.preferences.IntegerProperty;
import org.openstreetmap.josm.data.preferences.StringProperty;
import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
import org.openstreetmap.josm.gui.NavigatableComponent;
import org.openstreetmap.josm.gui.draw.MapViewPath;
import org.openstreetmap.josm.gui.draw.MapViewPositionAndRotation;
import org.openstreetmap.josm.gui.mappaint.ElemStyles;
import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
import org.openstreetmap.josm.gui.mappaint.StyleElementList;
import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement;
import org.openstreetmap.josm.gui.mappaint.styleelement.AreaIconElement;
import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement;
import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.HorizontalTextAlignment;
import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.VerticalTextAlignment;
import org.openstreetmap.josm.gui.mappaint.styleelement.MapImage;
import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement;
import org.openstreetmap.josm.gui.mappaint.styleelement.RepeatImageElement.LineImageAlignment;
import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
import org.openstreetmap.josm.gui.mappaint.styleelement.Symbol;
import org.openstreetmap.josm.gui.mappaint.styleelement.TextElement;
import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel;
import org.openstreetmap.josm.gui.mappaint.styleelement.placement.PositionForAreaStrategy;
import org.openstreetmap.josm.tools.CompositeList;
import org.openstreetmap.josm.tools.Geometry;
import org.openstreetmap.josm.tools.Geometry.AreaAndPerimeter;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.JosmRuntimeException;
import org.openstreetmap.josm.tools.Utils;
import org.openstreetmap.josm.tools.bugreport.BugReport;
/**
* A map renderer which renders a map according to style rules in a set of style sheets.
* @since 486
*/
public class StyledMapRenderer extends AbstractMapRenderer {
private static final ForkJoinPool THREAD_POOL =
Utils.newForkJoinPool("mappaint.StyledMapRenderer.style_creation.numberOfThreads", "styled-map-renderer-%d", Thread.NORM_PRIORITY);
/**
* This stores a style and a primitive that should be painted with that style.
*/
public static class StyleRecord implements Comparable<StyleRecord> {
private final StyleElement style;
private final OsmPrimitive osm;
private final int flags;
StyleRecord(StyleElement style, OsmPrimitive osm, int flags) {
this.style = style;
this.osm = osm;
this.flags = flags;
}
@Override
public int compareTo(StyleRecord other) {
if ((this.flags & FLAG_DISABLED) != 0 && (other.flags & FLAG_DISABLED) == 0)
return -1;
if ((this.flags & FLAG_DISABLED) == 0 && (other.flags & FLAG_DISABLED) != 0)
return 1;
int d0 = Float.compare(this.style.majorZIndex, other.style.majorZIndex);
if (d0 != 0)
return d0;
// selected on top of member of selected on top of unselected
// FLAG_DISABLED bit is the same at this point
if (this.flags > other.flags)
return 1;
if (this.flags < other.flags)
return -1;
int dz = Float.compare(this.style.zIndex, other.style.zIndex);
if (dz != 0)
return dz;
// simple node on top of icons and shapes
if (NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(this.style) && !NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(other.style))
return 1;
if (!NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(this.style) && NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(other.style))
return -1;
// newer primitives to the front
long id = this.osm.getUniqueId() - other.osm.getUniqueId();
if (id > 0)
return 1;
if (id < 0)
return -1;
return Float.compare(this.style.objectZIndex, other.style.objectZIndex);
}
/**
* Get the style for this style element.
* @return The style
*/
public StyleElement getStyle() {
return style;
}
/**
* Paints the primitive with the style.
* @param paintSettings The settings to use.
* @param painter The painter to paint the style.
*/
public void paintPrimitive(MapPaintSettings paintSettings, StyledMapRenderer painter) {
style.paintPrimitive(
osm,
paintSettings,
painter,
(flags & FLAG_SELECTED) != 0,
(flags & FLAG_OUTERMEMBER_OF_SELECTED) != 0,
(flags & FLAG_MEMBER_OF_SELECTED) != 0
);
}
@Override
public String toString() {
return "StyleRecord [style=" + style + ", osm=" + osm + ", flags=" + flags + "]";
}
}
private static Map<Font, Boolean> IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = new HashMap<>();
/**
* Check, if this System has the GlyphVector double translation bug.
*
* With this bug, <code>gv.setGlyphTransform(i, trfm)</code> has a different
* effect than on most other systems, namely the translation components
* ("m02" & "m12", {@link AffineTransform}) appear to be twice as large, as
* they actually are. The rotation is unaffected (scale & shear not tested
* so far).
*
* This bug has only been observed on Mac OS X, see #7841.
*
* After switch to Java 7, this test is a false positive on Mac OS X (see #10446),
* i.e. it returns true, but the real rendering code does not require any special
* handling.
* It hasn't been further investigated why the test reports a wrong result in
* this case, but the method has been changed to simply return false by default.
* (This can be changed with a setting in the advanced preferences.)
*
* @param font The font to check.
* @return false by default, but depends on the value of the advanced
* preference glyph-bug=false|true|auto, where auto is the automatic detection
* method which apparently no longer gives a useful result for Java 7.
*/
public static boolean isGlyphVectorDoubleTranslationBug(Font font) {
Boolean cached = IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.get(font);
if (cached != null)
return cached;
String overridePref = Main.pref.get("glyph-bug", "auto");
if ("auto".equals(overridePref)) {
FontRenderContext frc = new FontRenderContext(null, false, false);
GlyphVector gv = font.createGlyphVector(frc, "x");
gv.setGlyphTransform(0, AffineTransform.getTranslateInstance(1000, 1000));
Shape shape = gv.getGlyphOutline(0);
if (Main.isTraceEnabled()) {
Main.trace("#10446: shape: "+shape.getBounds());
}
// x is about 1000 on normal stystems and about 2000 when the bug occurs
int x = shape.getBounds().x;
boolean isBug = x > 1500;
IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, isBug);
return isBug;
} else {
boolean override = Boolean.parseBoolean(overridePref);
IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, override);
return override;
}
}
private double circum;
private double scale;
private MapPaintSettings paintSettings;
private Color highlightColorTransparent;
/**
* Flags used to store the primitive state along with the style. This is the normal style.
* <p>
* Not used in any public interfaces.
*/
private static final int FLAG_NORMAL = 0;
/**
* A primitive with {@link OsmPrimitive#isDisabled()}
*/
private static final int FLAG_DISABLED = 1;
/**
* A primitive with {@link OsmPrimitive#isMemberOfSelected()}
*/
private static final int FLAG_MEMBER_OF_SELECTED = 2;
/**
* A primitive with {@link OsmPrimitive#isSelected()}
*/
private static final int FLAG_SELECTED = 4;
/**
* A primitive with {@link OsmPrimitive#isOuterMemberOfSelected()}
*/
private static final int FLAG_OUTERMEMBER_OF_SELECTED = 8;
private static final double PHI = Math.toRadians(20);
private static final double cosPHI = Math.cos(PHI);
private static final double sinPHI = Math.sin(PHI);
/**
* If we should use left hand traffic.
*/
private static final AbstractProperty<Boolean> PREFERENCE_LEFT_HAND_TRAFFIC
= new BooleanProperty("mappaint.lefthandtraffic", false).cached();
/**
* Indicates that the renderer should enable anti-aliasing
* @since 11758
*/
public static final AbstractProperty<Boolean> PREFERENCE_ANTIALIASING_USE
= new BooleanProperty("mappaint.use-antialiasing", true).cached();
/**
* The mode that is used for anti-aliasing
* @since 11758
*/
public static final AbstractProperty<String> PREFERENCE_TEXT_ANTIALIASING
= new StringProperty("mappaint.text-antialiasing", "default").cached();
/**
* The line with to use for highlighting
*/
private static final AbstractProperty<Integer> HIGHLIGHT_LINE_WIDTH = new IntegerProperty("mappaint.highlight.width", 4).cached();
private static final AbstractProperty<Integer> HIGHLIGHT_POINT_RADIUS = new IntegerProperty("mappaint.highlight.radius", 7).cached();
private static final AbstractProperty<Integer> WIDER_HIGHLIGHT = new IntegerProperty("mappaint.highlight.bigger-increment", 5).cached();
private static final AbstractProperty<Integer> HIGHLIGHT_STEP = new IntegerProperty("mappaint.highlight.step", 4).cached();
private Collection<WaySegment> highlightWaySegments;
//flag that activate wider highlight mode
private boolean useWiderHighlight;
private boolean useStrokes;
private boolean showNames;
private boolean showIcons;
private boolean isOutlineOnly;
private boolean leftHandTraffic;
private Object antialiasing;
private Supplier<RenderBenchmarkCollector> benchmarkFactory = RenderBenchmarkCollector.defaultBenchmarkSupplier();
/**
* Constructs a new {@code StyledMapRenderer}.
*
* @param g the graphics context. Must not be null.
* @param nc the map viewport. Must not be null.
* @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
* look inactive. Example: rendering of data in an inactive layer using light gray as color only.
* @throws IllegalArgumentException if {@code g} is null
* @throws IllegalArgumentException if {@code nc} is null
*/
public StyledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
super(g, nc, isInactiveMode);
Component focusOwner = FocusManager.getCurrentManager().getFocusOwner();
useWiderHighlight = !(focusOwner instanceof AbstractButton || focusOwner == nc);
}
private void displaySegments(MapViewPath path, Path2D orientationArrows, Path2D onewayArrows, Path2D onewayArrowsCasing,
Color color, BasicStroke line, BasicStroke dashes, Color dashedColor) {
g.setColor(isInactiveMode ? inactiveColor : color);
if (useStrokes) {
g.setStroke(line);
}
g.draw(path.computeClippedLine(g.getStroke()));
if (!isInactiveMode && useStrokes && dashes != null) {
g.setColor(dashedColor);
g.setStroke(dashes);
g.draw(path.computeClippedLine(dashes));
}
if (orientationArrows != null) {
g.setColor(isInactiveMode ? inactiveColor : color);
g.setStroke(new BasicStroke(line.getLineWidth(), line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
g.draw(orientationArrows);
}
if (onewayArrows != null) {
g.setStroke(new BasicStroke(1, line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
g.fill(onewayArrowsCasing);
g.setColor(isInactiveMode ? inactiveColor : backgroundColor);
g.fill(onewayArrows);
}
if (useStrokes) {
g.setStroke(new BasicStroke());
}
}
/**
* Worker function for drawing areas.
*
* @param path the path object for the area that should be drawn; in case
* of multipolygons, this can path can be a complex shape with one outer
* polygon and one or more inner polygons
* @param color The color to fill the area with.
* @param fillImage The image to fill the area with. Overrides color.
* @param extent if not null, area will be filled partially; specifies, how
* far to fill from the boundary towards the center of the area;
* if null, area will be filled completely
* @param pfClip clipping area for partial fill (only needed for unclosed
* polygons)
* @param disabled If this should be drawn with a special disabled style.
* @param text Ignored. Use {@link #drawText(OsmPrimitive, TextLabel)} instead.
*/
protected void drawArea(MapViewPath path, Color color,
MapImage fillImage, Float extent, Path2D.Double pfClip, boolean disabled, TextLabel text) {
if (!isOutlineOnly && color.getAlpha() != 0) {
Shape area = path;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
if (fillImage == null) {
if (isInactiveMode) {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.33f));
}
g.setColor(color);
if (extent == null) {
g.fill(area);
} else {
Shape oldClip = g.getClip();
Shape clip = area;
if (pfClip != null) {
clip = pfClip.createTransformedShape(mapState.getAffineTransform());
}
g.clip(clip);
g.setStroke(new BasicStroke(2 * extent, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 4));
g.draw(area);
g.setClip(oldClip);
}
} else {
TexturePaint texture = new TexturePaint(fillImage.getImage(disabled),
new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight()));
g.setPaint(texture);
Float alpha = fillImage.getAlphaFloat();
if (!Utils.equalsEpsilon(alpha, 1f)) {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
}
if (extent == null) {
g.fill(area);
} else {
Shape oldClip = g.getClip();
BasicStroke stroke = new BasicStroke(2 * extent, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
g.clip(stroke.createStrokedShape(area));
Shape fill = area;
if (pfClip != null) {
fill = pfClip.createTransformedShape(mapState.getAffineTransform());
}
g.fill(fill);
g.setClip(oldClip);
}
g.setPaintMode();
}
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
}
}
/**
* Draws a multipolygon area.
* @param r The multipolygon relation
* @param color The color to fill the area with.
* @param fillImage The image to fill the area with. Overrides color.
* @param extent if not null, area will be filled partially; specifies, how
* far to fill from the boundary towards the center of the area;
* if null, area will be filled completely
* @param extentThreshold if not null, determines if the partial filled should
* be replaced by plain fill, when it covers a certain fraction of the total area
* @param disabled If this should be drawn with a special disabled style.
* @param text The text to write on the area.
*/
public void drawArea(Relation r, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) {
Multipolygon multipolygon = MultipolygonCache.getInstance().get(r);
if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) {
for (PolyData pd : multipolygon.getCombinedPolygons()) {
if (!isAreaVisible(pd.get())) {
continue;
}
MapViewPath p = new MapViewPath(mapState);
p.appendFromEastNorth(pd.get());
p.setWindingRule(Path2D.WIND_EVEN_ODD);
Path2D.Double pfClip = null;
if (extent != null) {
if (!usePartialFill(pd.getAreaAndPerimeter(null), extent, extentThreshold)) {
extent = null;
} else if (!pd.isClosed()) {
pfClip = getPFClip(pd, extent * scale);
}
}
drawArea(p,
pd.isSelected() ? paintSettings.getRelationSelectedColor(color.getAlpha()) : color,
fillImage, extent, pfClip, disabled, text);
}
}
}
/**
* Draws an area defined by a way. They way does not need to be closed, but it should.
* @param w The way.
* @param color The color to fill the area with.
* @param fillImage The image to fill the area with. Overrides color.
* @param extent if not null, area will be filled partially; specifies, how
* far to fill from the boundary towards the center of the area;
* if null, area will be filled completely
* @param extentThreshold if not null, determines if the partial filled should
* be replaced by plain fill, when it covers a certain fraction of the total area
* @param disabled If this should be drawn with a special disabled style.
* @param text The text to write on the area.
*/
public void drawArea(Way w, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) {
Path2D.Double pfClip = null;
if (extent != null) {
if (!usePartialFill(Geometry.getAreaAndPerimeter(w.getNodes()), extent, extentThreshold)) {
extent = null;
} else if (!w.isClosed()) {
pfClip = getPFClip(w, extent * scale);
}
}
drawArea(getPath(w), color, fillImage, extent, pfClip, disabled, text);
}
/**
* Determine, if partial fill should be turned off for this object, because
* only a small unfilled gap in the center of the area would be left.
*
* This is used to get a cleaner look for urban regions with many small
* areas like buildings, etc.
* @param ap the area and the perimeter of the object
* @param extent the "width" of partial fill
* @param threshold when the partial fill covers that much of the total
* area, the partial fill is turned off; can be greater than 100% as the
* covered area is estimated as <code>perimeter * extent</code>
* @return true, if the partial fill should be used, false otherwise
*/
private boolean usePartialFill(AreaAndPerimeter ap, float extent, Float threshold) {
if (threshold == null) return true;
return ap.getPerimeter() * extent * scale < threshold * ap.getArea();
}
/**
* Draw a text onto a node
* @param n The node to draw the text on
* @param bs The text and it's alignment.
*/
public void drawBoxText(Node n, BoxTextElement bs) {
if (!isShowNames() || bs == null)
return;
MapViewPoint p = mapState.getPointFor(n);
TextLabel text = bs.text;
String s = text.labelCompositionStrategy.compose(n);
if (s == null || s.isEmpty()) return;
Font defaultFont = g.getFont();
g.setFont(text.font);
FontRenderContext frc = g.getFontRenderContext();
Rectangle2D bounds = text.font.getStringBounds(s, frc);
double x = Math.round(p.getInViewX()) + text.xOffset + bounds.getCenterX();
double y = Math.round(p.getInViewY()) + text.yOffset + bounds.getCenterY();
/**
*
* left-above __center-above___ right-above
* left-top| |right-top
* | |
* left-center| center-center |right-center
* | |
* left-bottom|_________________|right-bottom
* left-below center-below right-below
*
*/
Rectangle box = bs.getBox();
if (bs.hAlign == HorizontalTextAlignment.RIGHT) {
x += box.x + box.width + 2;
} else {
int textWidth = (int) bounds.getWidth();
if (bs.hAlign == HorizontalTextAlignment.CENTER) {
x -= textWidth / 2;
} else if (bs.hAlign == HorizontalTextAlignment.LEFT) {
x -= -box.x + 4 + textWidth;
} else throw new AssertionError();
}
if (bs.vAlign == VerticalTextAlignment.BOTTOM) {
y += box.y + box.height;
} else {
LineMetrics metrics = text.font.getLineMetrics(s, frc);
if (bs.vAlign == VerticalTextAlignment.ABOVE) {
y -= -box.y + (int) metrics.getDescent();
} else if (bs.vAlign == VerticalTextAlignment.TOP) {
y -= -box.y - (int) metrics.getAscent();
} else if (bs.vAlign == VerticalTextAlignment.CENTER) {
y += (int) ((metrics.getAscent() - metrics.getDescent()) / 2);
} else if (bs.vAlign == VerticalTextAlignment.BELOW) {
y += box.y + box.height + (int) metrics.getAscent() + 2;
} else throw new AssertionError();
}
displayText(n, text, s, bounds, new MapViewPositionAndRotation(mapState.getForView(x, y), 0));
g.setFont(defaultFont);
}
/**
* Draw an image along a way repeatedly.
*
* @param way the way
* @param pattern the image
* @param disabled If this should be drawn with a special disabled style.
* @param offset offset from the way
* @param spacing spacing between two images
* @param phase initial spacing
* @param align alignment of the image. The top, center or bottom edge can be aligned with the way.
*/
public void drawRepeatImage(Way way, MapImage pattern, boolean disabled, double offset, double spacing, double phase,
LineImageAlignment align) {
final int imgWidth = pattern.getWidth();
final double repeat = imgWidth + spacing;
final int imgHeight = pattern.getHeight();
int dy1 = (int) ((align.getAlignmentOffset() - .5) * imgHeight);
int dy2 = dy1 + imgHeight;
OffsetIterator it = new OffsetIterator(mapState, way.getNodes(), offset);
MapViewPath path = new MapViewPath(mapState);
if (it.hasNext()) {
path.moveTo(it.next());
}
while (it.hasNext()) {
path.lineTo(it.next());
}
double startOffset = phase % repeat;
if (startOffset < 0) {
startOffset += repeat;
}
BufferedImage image = pattern.getImage(disabled);
path.visitClippedLine(startOffset, repeat, (inLineOffset, start, end, startIsOldEnd) -> {
final double segmentLength = start.distanceToInView(end);
if (segmentLength < 0.1) {
// avoid odd patterns when zoomed out.
return;
}
if (segmentLength > repeat * 500) {
// simply skip drawing so many images - something must be wrong.
return;
}
AffineTransform saveTransform = g.getTransform();
g.translate(start.getInViewX(), start.getInViewY());
double dx = end.getInViewX() - start.getInViewX();
double dy = end.getInViewY() - start.getInViewY();
g.rotate(Math.atan2(dy, dx));
// The start of the next image
double imageStart = -(inLineOffset % repeat);
while (imageStart < segmentLength) {
int x = (int) imageStart;
int sx1 = Math.max(0, -x);
int sx2 = imgWidth - Math.max(0, x + imgWidth - (int) Math.ceil(segmentLength));
g.drawImage(image, x + sx1, dy1, x + sx2, dy2, sx1, 0, sx2, imgHeight, null);
imageStart += repeat;
}
g.setTransform(saveTransform);
});
}
@Override
public void drawNode(Node n, Color color, int size, boolean fill) {
if (size <= 0 && !n.isHighlighted())
return;
MapViewPoint p = mapState.getPointFor(n);
if (n.isHighlighted()) {
drawPointHighlight(p.getInView(), size);
}
if (size > 1 && p.isInView()) {
int radius = size / 2;
if (isInactiveMode || n.isDisabled()) {
g.setColor(inactiveColor);
} else {
g.setColor(color);
}
Rectangle2D rect = new Rectangle2D.Double(p.getInViewX()-radius-1, p.getInViewY()-radius-1, size + 1, size + 1);
if (fill) {
g.fill(rect);
} else {
g.draw(rect);
}
}
}
/**
* Draw the icon for a given node.
* @param n The node
* @param img The icon to draw at the node position
* @param disabled {@code} true to render disabled version, {@code false} for the standard version
* @param selected {@code} true to render it as selected, {@code false} otherwise
* @param member {@code} true to render it as a relation member, {@code false} otherwise
* @param theta the angle of rotation in radians
*/
public void drawNodeIcon(Node n, MapImage img, boolean disabled, boolean selected, boolean member, double theta) {
MapViewPoint p = mapState.getPointFor(n);
int w = img.getWidth();
int h = img.getHeight();
if (n.isHighlighted()) {
drawPointHighlight(p.getInView(), Math.max(w, h));
}
drawIcon(p, img, disabled, selected, member, theta, (g, r) -> {
Color color = getSelectionHintColor(disabled, selected);
g.setColor(color);
g.draw(r);
});
}
/**
* Draw the icon for a given area. Normally, the icon is drawn around the center of the area.
* @param osm The primitive to draw the icon for
* @param img The icon to draw
* @param disabled {@code} true to render disabled version, {@code false} for the standard version
* @param selected {@code} true to render it as selected, {@code false} otherwise
* @param member {@code} true to render it as a relation member, {@code false} otherwise
* @param theta the angle of rotation in radians
* @param iconPosition Where to place the icon.
* @since 11670
*/
public void drawAreaIcon(OsmPrimitive osm, MapImage img, boolean disabled, boolean selected, boolean member, double theta,
PositionForAreaStrategy iconPosition) {
Rectangle2D.Double iconRect = new Rectangle2D.Double(-img.getWidth() / 2.0, -img.getHeight() / 2.0, img.getWidth(), img.getHeight());
forEachPolygon(osm, path -> {
MapViewPositionAndRotation placement = iconPosition.findLabelPlacement(path, iconRect);
if (placement == null) {
return;
}
MapViewPoint p = placement.getPoint();
drawIcon(p, img, disabled, selected, member, theta + placement.getRotation(), (g, r) -> {
if (useStrokes) {
g.setStroke(new BasicStroke(2));
}
// only draw a minor highlighting, so that users do not confuse this for a point.
Color color = getSelectionHintColor(disabled, selected);
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), (int) (color.getAlpha() * .2));
g.setColor(color);
g.draw(r);
});
});
}
private void drawIcon(MapViewPoint p, MapImage img, boolean disabled, boolean selected, boolean member, double theta,
BiConsumer<Graphics2D, Rectangle2D> selectionDrawer) {
float alpha = img.getAlphaFloat();
Graphics2D temporaryGraphics = (Graphics2D) g.create();
if (!Utils.equalsEpsilon(alpha, 1f)) {
temporaryGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
}
double x = Math.round(p.getInViewX());
double y = Math.round(p.getInViewY());
temporaryGraphics.translate(x, y);
temporaryGraphics.rotate(theta);
int drawX = -img.getWidth() / 2 + img.offsetX;
int drawY = -img.getHeight() / 2 + img.offsetY;
temporaryGraphics.drawImage(img.getImage(disabled), drawX, drawY, nc);
if (selected || member) {
selectionDrawer.accept(temporaryGraphics, new Rectangle2D.Double(drawX - 2, drawY - 2, img.getWidth() + 4, img.getHeight() + 4));
}
}
private Color getSelectionHintColor(boolean disabled, boolean selected) {
Color color;
if (disabled) {
color = inactiveColor;
} else if (selected) {
color = selectedColor;
} else {
color = relationSelectedColor;
}
return color;
}
/**
* Draw the symbol and possibly a highlight marking on a given node.
* @param n The position to draw the symbol on
* @param s The symbol to draw
* @param fillColor The color to fill the symbol with
* @param strokeColor The color to use for the outer corner of the symbol
*/
public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
MapViewPoint p = mapState.getPointFor(n);
if (n.isHighlighted()) {
drawPointHighlight(p.getInView(), s.size);
}
if (fillColor != null || strokeColor != null) {
Shape shape = s.buildShapeAround(p.getInViewX(), p.getInViewY());
if (fillColor != null) {
g.setColor(fillColor);
g.fill(shape);
}
if (s.stroke != null) {
g.setStroke(s.stroke);
g.setColor(strokeColor);
g.draw(shape);
g.setStroke(new BasicStroke());
}
}
}
/**
* Draw a number of the order of the two consecutive nodes within the
* parents way
*
* @param n1 First node of the way segment.
* @param n2 Second node of the way segment.
* @param orderNumber The number of the segment in the way.
* @param clr The color to use for drawing the text.
*/
public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
MapViewPoint p1 = mapState.getPointFor(n1);
MapViewPoint p2 = mapState.getPointFor(n2);
drawOrderNumber(p1, p2, orderNumber, clr);
}
/**
* highlights a given GeneralPath using the settings from BasicStroke to match the line's
* style. Width of the highlight can be changed by user preferences
* @param path path to draw
* @param line line style
*/
private void drawPathHighlight(MapViewPath path, BasicStroke line) {
if (path == null)
return;
g.setColor(highlightColorTransparent);
float w = line.getLineWidth() + HIGHLIGHT_LINE_WIDTH.get();
if (useWiderHighlight) {
w += WIDER_HIGHLIGHT.get();
}
int step = Math.max(HIGHLIGHT_STEP.get(), 1);
while (w >= line.getLineWidth()) {
g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit()));
g.draw(path);
w -= step;
}
}
/**
* highlights a given point by drawing a rounded rectangle around it. Give the
* size of the object you want to be highlighted, width is added automatically.
* @param p point
* @param size highlight size
*/
private void drawPointHighlight(Point2D p, int size) {
g.setColor(highlightColorTransparent);
int s = size + HIGHLIGHT_POINT_RADIUS.get();
if (useWiderHighlight) {
s += WIDER_HIGHLIGHT.get();
}
int step = Math.max(HIGHLIGHT_STEP.get(), 1);
while (s >= size) {
int r = (int) Math.floor(s/2d);
g.fill(new RoundRectangle2D.Double(p.getX()-r, p.getY()-r, s, s, r, r));
s -= step;
}
}
public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
// rotate image with direction last node in from to, and scale down image to 16*16 pixels
Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16));
int w = smallImg.getWidth(null), h = smallImg.getHeight(null);
g.drawImage(smallImg, (int) (pVia.x+vx+vx2)-w/2, (int) (pVia.y+vy+vy2)-h/2, nc);
if (selected) {
g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor);
g.drawRect((int) (pVia.x+vx+vx2)-w/2-2, (int) (pVia.y+vy+vy2)-h/2-2, w+4, h+4);
}
}
public void drawRestriction(Relation r, MapImage icon, boolean disabled) {
Way fromWay = null;
Way toWay = null;
OsmPrimitive via = null;
/* find the "from", "via" and "to" elements */
for (RelationMember m : r.getMembers()) {
if (m.getMember().isIncomplete())
return;
else {
if (m.isWay()) {
Way w = m.getWay();
if (w.getNodesCount() < 2) {
continue;
}
switch(m.getRole()) {
case "from":
if (fromWay == null) {
fromWay = w;
}
break;
case "to":
if (toWay == null) {
toWay = w;
}
break;
case "via":
if (via == null) {
via = w;
}
break;
default: // Do nothing
}
} else if (m.isNode()) {
Node n = m.getNode();
if (via == null && "via".equals(m.getRole())) {
via = n;
}
}
}
}
if (fromWay == null || toWay == null || via == null)
return;
Node viaNode;
if (via instanceof Node) {
viaNode = (Node) via;
if (!fromWay.isFirstLastNode(viaNode))
return;
} else {
Way viaWay = (Way) via;
Node firstNode = viaWay.firstNode();
Node lastNode = viaWay.lastNode();
Boolean onewayvia = Boolean.FALSE;
String onewayviastr = viaWay.get("oneway");
if (onewayviastr != null) {
if ("-1".equals(onewayviastr)) {
onewayvia = Boolean.TRUE;
Node tmp = firstNode;
firstNode = lastNode;
lastNode = tmp;
} else {
onewayvia = Optional.ofNullable(OsmUtils.getOsmBoolean(onewayviastr)).orElse(Boolean.FALSE);
}
}
if (fromWay.isFirstLastNode(firstNode)) {
viaNode = firstNode;
} else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
viaNode = lastNode;
} else
return;
}
/* find the "direct" nodes before the via node */
Node fromNode;
if (fromWay.firstNode() == via) {
fromNode = fromWay.getNode(1);
} else {
fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
}
Point pFrom = nc.getPoint(fromNode);
Point pVia = nc.getPoint(viaNode);
/* starting from via, go back the "from" way a few pixels
(calculate the vector vx/vy with the specified length and the direction
away from the "via" node along the first segment of the "from" way)
*/
double distanceFromVia = 14;
double dx = pFrom.x >= pVia.x ? pFrom.x - pVia.x : pVia.x - pFrom.x;
double dy = pFrom.y >= pVia.y ? pFrom.y - pVia.y : pVia.y - pFrom.y;
double fromAngle;
if (dx == 0) {
fromAngle = Math.PI/2;
} else {
fromAngle = Math.atan(dy / dx);
}
double fromAngleDeg = Math.toDegrees(fromAngle);
double vx = distanceFromVia * Math.cos(fromAngle);
double vy = distanceFromVia * Math.sin(fromAngle);
if (pFrom.x < pVia.x) {
vx = -vx;
}
if (pFrom.y < pVia.y) {
vy = -vy;
}
/* go a few pixels away from the way (in a right angle)
(calculate the vx2/vy2 vector with the specified length and the direction
90degrees away from the first segment of the "from" way)
*/
double distanceFromWay = 10;
double vx2 = 0;
double vy2 = 0;
double iconAngle = 0;
if (pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
if (!leftHandTraffic) {
vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
} else {
vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
}
iconAngle = 270+fromAngleDeg;
}
if (pFrom.x < pVia.x && pFrom.y >= pVia.y) {
if (!leftHandTraffic) {
vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
} else {
vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
}
iconAngle = 90-fromAngleDeg;
}
if (pFrom.x < pVia.x && pFrom.y < pVia.y) {
if (!leftHandTraffic) {
vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
} else {
vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
}
iconAngle = 90+fromAngleDeg;
}
if (pFrom.x >= pVia.x && pFrom.y < pVia.y) {
if (!leftHandTraffic) {
vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
} else {
vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
}
iconAngle = 270-fromAngleDeg;
}
drawRestriction(icon.getImage(disabled),
pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
}
/**
* Draws a text for the given primitive
* @param osm The primitive to draw the text for
* @param text The text definition (font/position/.../text content) to draw.
* @since 11722
*/
public void drawText(OsmPrimitive osm, TextLabel text) {
if (!isShowNames()) {
return;
}
String name = text.getString(osm);
if (name == null || name.isEmpty()) {
return;
}
FontMetrics fontMetrics = g.getFontMetrics(text.font); // if slow, use cache
Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
Font defaultFont = g.getFont();
forEachPolygon(osm, path -> {
//TODO: Ignore areas that are out of bounds.
PositionForAreaStrategy position = text.getLabelPositionStrategy();
MapViewPositionAndRotation center = position.findLabelPlacement(path, nb);
if (center != null) {
displayText(osm, text, name, nb, center);
} else if (position.supportsGlyphVector()) {
List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext());
List<GlyphVector> translatedGvs = position.generateGlyphVectors(path, nb, gvs, isGlyphVectorDoubleTranslationBug(text.font));
displayText(() -> translatedGvs.forEach(gv -> g.drawGlyphVector(gv, 0, 0)),
() -> translatedGvs.stream().collect(
Path2D.Double::new,
(p, gv) -> p.append(gv.getOutline(0, 0), false),
(p1, p2) -> p1.append(p2, false)),
osm.isDisabled(), text);
} else if (Main.isTraceEnabled()) {
Main.trace("Couldn't find a correct label placement for " + osm + " / " + name);
}
});
g.setFont(defaultFont);
}
private void displayText(OsmPrimitive osm, TextLabel text, String name, Rectangle2D nb,
MapViewPositionAndRotation center) {
AffineTransform at = new AffineTransform();
if (Math.abs(center.getRotation()) < .01) {
// Explicitly no rotation: move to full pixels.
at.setToTranslation(Math.round(center.getPoint().getInViewX() - nb.getCenterX()),
Math.round(center.getPoint().getInViewY() - nb.getCenterY()));
} else {
at.setToTranslation(center.getPoint().getInViewX(), center.getPoint().getInViewY());
at.rotate(center.getRotation());
at.translate(-nb.getCenterX(), -nb.getCenterY());
}
displayText(() -> {
AffineTransform defaultTransform = g.getTransform();
g.setTransform(at);
g.setFont(text.font);
g.drawString(name, 0, 0);
g.setTransform(defaultTransform);
}, () -> {
FontRenderContext frc = g.getFontRenderContext();
TextLayout tl = new TextLayout(name, text.font, frc);
return tl.getOutline(at);
}, osm.isDisabled(), text);
}
/**
* Displays text at specified position including its halo, if applicable.
*
* @param fill The function that fills the text
* @param outline The function to draw the outline
* @param disabled {@code true} if element is disabled (filtered out)
* @param text text style to use
*/
private void displayText(Runnable fill, Supplier<Shape> outline, boolean disabled, TextLabel text) {
if (isInactiveMode || disabled) {
g.setColor(inactiveColor);
fill.run();
} else if (text.haloRadius != null) {
g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
g.setColor(text.haloColor);
Shape textOutline = outline.get();
g.draw(textOutline);
g.setStroke(new BasicStroke());
g.setColor(text.color);
g.fill(textOutline);
} else {
g.setColor(text.color);
fill.run();
}
}
/**
* Calls a consumer for each path of the area shape-
* @param osm A way or a multipolygon
* @param consumer The consumer to call.
*/
private void forEachPolygon(OsmPrimitive osm, Consumer<MapViewPath> consumer) {
if (osm instanceof Way) {
consumer.accept(getPath((Way) osm));
} else if (osm instanceof Relation) {
Multipolygon multipolygon = MultipolygonCache.getInstance().get((Relation) osm);
if (!multipolygon.getOuterWays().isEmpty()) {
for (PolyData pd : multipolygon.getCombinedPolygons()) {
MapViewPath path = new MapViewPath(mapState);
path.appendFromEastNorth(pd.get());
path.setWindingRule(MapViewPath.WIND_EVEN_ODD);
consumer.accept(path);
}
}
}
}
/**
* Draws a text along a given way.
* @param way The way to draw the text on.
* @param text The text definition (font/.../text content) to draw.
* @deprecated Use {@link #drawText(OsmPrimitive, TextLabel)} instead.
*/
@Deprecated
public void drawTextOnPath(Way way, TextLabel text) {
// NOP.
}
/**
* draw way. This method allows for two draw styles (line using color, dashes using dashedColor) to be passed.
* @param way The way to draw
* @param color The base color to draw the way in
* @param line The line style to use. This is drawn using color.
* @param dashes The dash style to use. This is drawn using dashedColor. <code>null</code> if unused.
* @param dashedColor The color of the dashes.
* @param offset The offset
* @param showOrientation show arrows that indicate the technical orientation of
* the way (defined by order of nodes)
* @param showHeadArrowOnly True if only the arrow at the end of the line but not those on the segments should be displayed.
* @param showOneway show symbols that indicate the direction of the feature,
* e.g. oneway street or waterway
* @param onewayReversed for oneway=-1 and similar
*/
public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
boolean showOrientation, boolean showHeadArrowOnly,
boolean showOneway, boolean onewayReversed) {
MapViewPath path = new MapViewPath(mapState);
MapViewPath orientationArrows = showOrientation ? new MapViewPath(mapState) : null;
MapViewPath onewayArrows;
MapViewPath onewayArrowsCasing;
Rectangle bounds = g.getClipBounds();
if (bounds != null) {
// avoid arrow heads at the border
bounds.grow(100, 100);
}
List<Node> wayNodes = way.getNodes();
if (wayNodes.size() < 2) return;
// only highlight the segment if the way itself is not highlighted
if (!way.isHighlighted() && highlightWaySegments != null) {
MapViewPath highlightSegs = null;
for (WaySegment ws : highlightWaySegments) {
if (ws.way != way || ws.lowerIndex < offset) {
continue;
}
if (highlightSegs == null) {
highlightSegs = new MapViewPath(mapState);
}
highlightSegs.moveTo(ws.getFirstNode());
highlightSegs.lineTo(ws.getSecondNode());
}
drawPathHighlight(highlightSegs, line);
}
MapViewPoint lastPoint = null;
Iterator<MapViewPoint> it = new OffsetIterator(mapState, wayNodes, offset);
boolean initialMoveToNeeded = true;
while (it.hasNext()) {
MapViewPoint p = it.next();
if (lastPoint != null) {
MapViewPoint p1 = lastPoint;
MapViewPoint p2 = p;
if (initialMoveToNeeded) {
initialMoveToNeeded = false;
path.moveTo(p1);
}
path.lineTo(p2);
/* draw arrow */
if (showHeadArrowOnly ? !it.hasNext() : showOrientation) {
//TODO: Cache
ArrowPaintHelper drawHelper = new ArrowPaintHelper(PHI, 10 + line.getLineWidth());
drawHelper.paintArrowAt(orientationArrows, p2, p1);
}
}
lastPoint = p;
}
if (showOneway) {
onewayArrows = new MapViewPath(mapState);
onewayArrowsCasing = new MapViewPath(mapState);
double interval = 60;
path.visitClippedLine(0, 60, (inLineOffset, start, end, startIsOldEnd) -> {
double segmentLength = start.distanceToInView(end);
if (segmentLength > 0.001) {
final double nx = (end.getInViewX() - start.getInViewX()) / segmentLength;
final double ny = (end.getInViewY() - start.getInViewY()) / segmentLength;
// distance from p1
double dist = interval - (inLineOffset % interval);
while (dist < segmentLength) {
appenOnewayPath(onewayReversed, start, nx, ny, dist, 3d, onewayArrowsCasing);
appenOnewayPath(onewayReversed, start, nx, ny, dist, 2d, onewayArrows);
dist += interval;
}
}
});
} else {
onewayArrows = null;
onewayArrowsCasing = null;
}
if (way.isHighlighted()) {
drawPathHighlight(path, line);
}
displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
}
private static void appenOnewayPath(boolean onewayReversed, MapViewPoint p1, double nx, double ny, double dist,
double onewaySize, Path2D onewayPath) {
// scale such that border is 1 px
final double fac = -(onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI);
final double sx = nx * fac;
final double sy = ny * fac;
// Attach the triangle at the incenter and not at the tip.
// Makes the border even at all sides.
final double x = p1.getInViewX() + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
final double y = p1.getInViewY() + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
onewayPath.moveTo(x, y);
onewayPath.lineTo(x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
onewayPath.lineTo(x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
onewayPath.lineTo(x, y);
}
/**
* Gets the "circum". This is the distance on the map in meters that 100 screen pixels represent.
* @return The "circum"
*/
public double getCircum() {
return circum;
}
@Override
public void getColors() {
super.getColors();
this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
this.backgroundColor = PaintColors.getBackgroundColor();
}
@Override
public void getSettings(boolean virtual) {
super.getSettings(virtual);
paintSettings = MapPaintSettings.INSTANCE;
circum = nc.getDist100Pixel();
scale = nc.getScale();
leftHandTraffic = PREFERENCE_LEFT_HAND_TRAFFIC.get();
useStrokes = paintSettings.getUseStrokesDistance() > circum;
showNames = paintSettings.getShowNamesDistance() > circum;
showIcons = paintSettings.getShowIconsDistance() > circum;
isOutlineOnly = paintSettings.isOutlineOnly();
antialiasing = PREFERENCE_ANTIALIASING_USE.get() ?
RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
Object textAntialiasing;
switch (PREFERENCE_TEXT_ANTIALIASING.get()) {
case "on":
textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
break;
case "off":
textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
break;
case "gasp":
textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
break;
case "lcd-hrgb":
textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
break;
case "lcd-hbgr":
textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR;
break;
case "lcd-vrgb":
textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB;
break;
case "lcd-vbgr":
textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR;
break;
default:
textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT;
}
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialiasing);
}
private MapViewPath getPath(Way w) {
MapViewPath path = new MapViewPath(mapState);
if (w.isClosed()) {
path.appendClosed(w.getNodes(), false);
} else {
path.append(w.getNodes(), false);
}
return path;
}
private static Path2D.Double getPFClip(Way w, double extent) {
Path2D.Double clip = new Path2D.Double();
buildPFClip(clip, w.getNodes(), extent);
return clip;
}
private static Path2D.Double getPFClip(PolyData pd, double extent) {
Path2D.Double clip = new Path2D.Double();
clip.setWindingRule(Path2D.WIND_EVEN_ODD);
buildPFClip(clip, pd.getNodes(), extent);
for (PolyData pdInner : pd.getInners()) {
buildPFClip(clip, pdInner.getNodes(), extent);
}
return clip;
}
/**
* Fix the clipping area of unclosed polygons for partial fill.
*
* The current algorithm for partial fill simply strokes the polygon with a
* large stroke width after masking the outside with a clipping area.
* This works, but for unclosed polygons, the mask can crop the corners at
* both ends (see #12104).
*
* This method fixes the clipping area by sort of adding the corners to the
* clip outline.
*
* @param clip the clipping area to modify (initially empty)
* @param nodes nodes of the polygon
* @param extent the extent
*/
private static void buildPFClip(Path2D.Double clip, List<Node> nodes, double extent) {
boolean initial = true;
for (Node n : nodes) {
EastNorth p = n.getEastNorth();
if (p != null) {
if (initial) {
clip.moveTo(p.getX(), p.getY());
initial = false;
} else {
clip.lineTo(p.getX(), p.getY());
}
}
}
if (nodes.size() >= 3) {
EastNorth fst = nodes.get(0).getEastNorth();
EastNorth snd = nodes.get(1).getEastNorth();
EastNorth lst = nodes.get(nodes.size() - 1).getEastNorth();
EastNorth lbo = nodes.get(nodes.size() - 2).getEastNorth();
EastNorth cLst = getPFDisplacedEndPoint(lbo, lst, fst, extent);
EastNorth cFst = getPFDisplacedEndPoint(snd, fst, cLst != null ? cLst : lst, extent);
if (cLst == null && cFst != null) {
cLst = getPFDisplacedEndPoint(lbo, lst, cFst, extent);
}
if (cLst != null) {
clip.lineTo(cLst.getX(), cLst.getY());
}
if (cFst != null) {
clip.lineTo(cFst.getX(), cFst.getY());
}
}
}
/**
* Get the point to add to the clipping area for partial fill of unclosed polygons.
*
* <code>(p1,p2)</code> is the first or last way segment and <code>p3</code> the
* opposite endpoint.
*
* @param p1 1st point
* @param p2 2nd point
* @param p3 3rd point
* @param extent the extent
* @return a point q, such that p1,p2,q form a right angle
* and the distance of q to p2 is <code>extent</code>. The point q lies on
* the same side of the line p1,p2 as the point p3.
* Returns null if p1,p2,p3 forms an angle greater 90 degrees. (In this case
* the corner of the partial fill would not be cut off by the mask, so an
* additional point is not necessary.)
*/
private static EastNorth getPFDisplacedEndPoint(EastNorth p1, EastNorth p2, EastNorth p3, double extent) {
double dx1 = p2.getX() - p1.getX();
double dy1 = p2.getY() - p1.getY();
double dx2 = p3.getX() - p2.getX();
double dy2 = p3.getY() - p2.getY();
if (dx1 * dx2 + dy1 * dy2 < 0) {
double len = Math.sqrt(dx1 * dx1 + dy1 * dy1);
if (len == 0) return null;
double dxm = -dy1 * extent / len;
double dym = dx1 * extent / len;
if (dx1 * dy2 - dx2 * dy1 < 0) {
dxm = -dxm;
dym = -dym;
}
return new EastNorth(p2.getX() + dxm, p2.getY() + dym);
}
return null;
}
/**
* Test if the area is visible
* @param area The area, interpreted in east/north space.
* @return true if it is visible.
*/
private boolean isAreaVisible(Path2D.Double area) {
Rectangle2D bounds = area.getBounds2D();
if (bounds.isEmpty()) return false;
MapViewPoint p = mapState.getPointFor(new EastNorth(bounds.getX(), bounds.getY()));
if (p.getInViewX() > mapState.getViewWidth()) return false;
if (p.getInViewY() < 0) return false;
p = mapState.getPointFor(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
if (p.getInViewX() < 0) return false;
if (p.getInViewY() > mapState.getViewHeight()) return false;
return true;
}
public boolean isInactiveMode() {
return isInactiveMode;
}
public boolean isShowIcons() {
return showIcons;
}
public boolean isShowNames() {
return showNames;
}
/**
* Computes the flags for a given OSM primitive.
* @param primitive The primititve to compute the flags for.
* @param checkOuterMember <code>true</code> if we should also add {@link #FLAG_OUTERMEMBER_OF_SELECTED}
* @return The flag.
*/
public static int computeFlags(OsmPrimitive primitive, boolean checkOuterMember) {
if (primitive.isDisabled()) {
return FLAG_DISABLED;
} else if (primitive.isSelected()) {
return FLAG_SELECTED;
} else if (checkOuterMember && primitive.isOuterMemberOfSelected()) {
return FLAG_OUTERMEMBER_OF_SELECTED;
} else if (primitive.isMemberOfSelected()) {
return FLAG_MEMBER_OF_SELECTED;
} else {
return FLAG_NORMAL;
}
}
private static class ComputeStyleListWorker extends RecursiveTask<List<StyleRecord>> implements Visitor {
private final transient List<? extends OsmPrimitive> input;
private final transient List<StyleRecord> output;
private final transient ElemStyles styles = MapPaintStyles.getStyles();
private final int directExecutionTaskSize;
private final double circum;
private final NavigatableComponent nc;
private final boolean drawArea;
private final boolean drawMultipolygon;
private final boolean drawRestriction;
/**
* Constructs a new {@code ComputeStyleListWorker}.
* @param circum distance on the map in meters that 100 screen pixels represent
* @param nc navigatable component
* @param input the primitives to process
* @param output the list of styles to which styles will be added
* @param directExecutionTaskSize the threshold deciding whether to subdivide the tasks
*/
ComputeStyleListWorker(double circum, NavigatableComponent nc,
final List<? extends OsmPrimitive> input, List<StyleRecord> output, int directExecutionTaskSize) {
this.circum = circum;
this.nc = nc;
this.input = input;
this.output = output;
this.directExecutionTaskSize = directExecutionTaskSize;
this.drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10_000_000);
this.drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
this.drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
this.styles.setDrawMultipolygon(drawMultipolygon);
}
@Override
protected List<StyleRecord> compute() {
if (input.size() <= directExecutionTaskSize) {
return computeDirectly();
} else {
final Collection<ForkJoinTask<List<StyleRecord>>> tasks = new ArrayList<>();
for (int fromIndex = 0; fromIndex < input.size(); fromIndex += directExecutionTaskSize) {
final int toIndex = Math.min(fromIndex + directExecutionTaskSize, input.size());
final List<StyleRecord> output = new ArrayList<>(directExecutionTaskSize);
tasks.add(new ComputeStyleListWorker(circum, nc, input.subList(fromIndex, toIndex), output, directExecutionTaskSize).fork());
}
for (ForkJoinTask<List<StyleRecord>> task : tasks) {
output.addAll(task.join());
}
return output;
}
}
public List<StyleRecord> computeDirectly() {
MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
try {
for (final OsmPrimitive osm : input) {
acceptDrawable(osm);
}
return output;
} catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
throw BugReport.intercept(e).put("input-size", input.size()).put("output-size", output.size());
} finally {
MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
}
}
private void acceptDrawable(final OsmPrimitive osm) {
try {
if (osm.isDrawable()) {
osm.accept(this);
}
} catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
throw BugReport.intercept(e).put("osm", osm);
}
}
@Override
public void visit(Node n) {
add(n, computeFlags(n, false));
}
@Override
public void visit(Way w) {
add(w, computeFlags(w, true));
}
@Override
public void visit(Relation r) {
add(r, computeFlags(r, true));
}
@Override
public void visit(Changeset cs) {
throw new UnsupportedOperationException();
}
public void add(Node osm, int flags) {
StyleElementList sl = styles.get(osm, circum, nc);
for (StyleElement s : sl) {
output.add(new StyleRecord(s, osm, flags));
}
}
public void add(Relation osm, int flags) {
StyleElementList sl = styles.get(osm, circum, nc);
for (StyleElement s : sl) {
if (drawMultipolygon && drawArea && (s instanceof AreaElement || s instanceof AreaIconElement) && (flags & FLAG_DISABLED) == 0) {
output.add(new StyleRecord(s, osm, flags));
} else if (drawMultipolygon && drawArea && s instanceof TextElement) {
output.add(new StyleRecord(s, osm, flags));
} else if (drawRestriction && s instanceof NodeElement) {
output.add(new StyleRecord(s, osm, flags));
}
}
}
public void add(Way osm, int flags) {
StyleElementList sl = styles.get(osm, circum, nc);
for (StyleElement s : sl) {
if ((drawArea && (flags & FLAG_DISABLED) == 0) || !(s instanceof AreaElement)) {
output.add(new StyleRecord(s, osm, flags));
}
}
}
}
/**
* Sets the factory that creates the benchmark data receivers.
* @param benchmarkFactory The factory.
* @since 10697
*/
public void setBenchmarkFactory(Supplier<RenderBenchmarkCollector> benchmarkFactory) {
this.benchmarkFactory = benchmarkFactory;
}
@Override
public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) {
RenderBenchmarkCollector benchmark = benchmarkFactory.get();
BBox bbox = bounds.toBBox();
getSettings(renderVirtualNodes);
data.getReadLock().lock();
try {
highlightWaySegments = data.getHighlightedWaySegments();
benchmark.renderStart(circum);
List<Node> nodes = data.searchNodes(bbox);
List<Way> ways = data.searchWays(bbox);
List<Relation> relations = data.searchRelations(bbox);
final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size());
// Need to process all relations first.
// Reason: Make sure, ElemStyles.getStyleCacheWithRange is not called for the same primitive in parallel threads.
// (Could be synchronized, but try to avoid this for performance reasons.)
THREAD_POOL.invoke(new ComputeStyleListWorker(circum, nc, relations, allStyleElems,
Math.max(20, relations.size() / THREAD_POOL.getParallelism() / 3)));
THREAD_POOL.invoke(new ComputeStyleListWorker(circum, nc, new CompositeList<>(nodes, ways), allStyleElems,
Math.max(100, (nodes.size() + ways.size()) / THREAD_POOL.getParallelism() / 3)));
if (!benchmark.renderSort()) {
return;
}
Collections.sort(allStyleElems); // TODO: try parallel sort when switching to Java 8
if (!benchmark.renderDraw(allStyleElems)) {
return;
}
for (StyleRecord record : allStyleElems) {
paintRecord(record);
}
drawVirtualNodes(data, bbox);
benchmark.renderDone();
} catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
throw BugReport.intercept(e)
.put("data", data)
.put("circum", circum)
.put("scale", scale)
.put("paintSettings", paintSettings)
.put("renderVirtualNodes", renderVirtualNodes);
} finally {
data.getReadLock().unlock();
}
}
private void paintRecord(StyleRecord record) {
try {
record.paintPrimitive(paintSettings, this);
} catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
throw BugReport.intercept(e).put("record", record);
}
}
}