/* License: GPL. For details, see LICENSE file. */
package org.openstreetmap.josm.data.osm.visitor.paint;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.GeneralPath;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.Bounds;
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.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.gui.NavigatableComponent;
/**
* A map renderer that paints a simple scheme of every primitive it visits to a
* previous set graphic environment.
*/
public class WireframeMapRenderer extends AbstractMapRenderer implements Visitor {
/** Color Preference for ways not matching any other group */
protected Color dfltWayColor;
/** Color Preference for relations */
protected Color relationColor;
/** Color Preference for untagged ways */
protected Color untaggedWayColor;
/** Color Preference for tagged nodes */
protected Color taggedColor;
/** Color Preference for multiply connected nodes */
protected Color connectionColor;
/** Color Preference for tagged and multiply connected nodes */
protected Color taggedConnectionColor;
/** Preference: should directional arrows be displayed */
protected boolean showDirectionArrow;
/** Preference: should arrows for oneways be displayed */
protected boolean showOnewayArrow;
/** Preference: should only the last arrow of a way be displayed */
protected boolean showHeadArrowOnly;
/** Preference: should the segement numbers of ways be displayed */
protected boolean showOrderNumber;
/** Preference: should selected nodes be filled */
protected boolean fillSelectedNode;
/** Preference: should unselected nodes be filled */
protected boolean fillUnselectedNode;
/** Preference: should tagged nodes be filled */
protected boolean fillTaggedNode;
/** Preference: should multiply connected nodes be filled */
protected boolean fillConnectionNode;
/** Preference: size of selected nodes */
protected int selectedNodeSize;
/** Preference: size of unselected nodes */
protected int unselectedNodeSize;
/** Preference: size of multiply connected nodes */
protected int connectionNodeSize;
/** Preference: size of tagged nodes */
protected int taggedNodeSize;
/** Color cache to draw subsequent segments of same color as one <code>Path</code>. */
protected Color currentColor = null;
/** Path store to draw subsequent segments of same color as one <code>Path</code>. */
protected GeneralPath currentPath = new GeneralPath();
/**
* <code>DataSet</code> passed to the @{link render} function to overcome the argument
* limitations of @{link Visitor} interface. Only valid until end of rendering call.
*/
private DataSet ds;
/** Helper variable for {@link #drawSegment} */
private static final double PHI = Math.toRadians(20);
/** Helper variable for {@link #drawSegment} */
private static final double cosPHI = Math.cos(PHI);
/** Helper variable for {@link #drawSegment} */
private static final double sinPHI = Math.sin(PHI);
/** Helper variable for {@link #visit(Relation)} */
private Stroke relatedWayStroke = new BasicStroke(
4, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL);
/**
* Creates an wireframe render
*
* @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 thrown if {@code g} is null
* @throws IllegalArgumentException thrown if {@code nc} is null
*/
public WireframeMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
super(g, nc, isInactiveMode);
}
@Override
public void getColors() {
super.getColors();
dfltWayColor = PaintColors.DEFAULT_WAY.get();
relationColor = PaintColors.RELATION.get();
untaggedWayColor = PaintColors.UNTAGGED_WAY.get();
highlightColor = PaintColors.HIGHLIGHT_WIREFRAME.get();
taggedColor = PaintColors.TAGGED.get();
connectionColor = PaintColors.CONNECTION.get();
if (taggedColor != nodeColor) {
taggedConnectionColor = taggedColor;
} else {
taggedConnectionColor = connectionColor;
}
}
@Override
protected void getSettings(boolean virtual) {
super.getSettings(virtual);
MapPaintSettings settings = MapPaintSettings.INSTANCE;
showDirectionArrow = settings.isShowDirectionArrow();
showOnewayArrow = settings.isShowOnewayArrow();
showHeadArrowOnly = settings.isShowHeadArrowOnly();
showOrderNumber = settings.isShowOrderNumber();
selectedNodeSize = settings.getSelectedNodeSize();
unselectedNodeSize = settings.getUnselectedNodeSize();
connectionNodeSize = settings.getConnectionNodeSize();
taggedNodeSize = settings.getTaggedNodeSize();
fillSelectedNode = settings.isFillSelectedNode();
fillUnselectedNode = settings.isFillUnselectedNode();
fillConnectionNode = settings.isFillConnectionNode();
fillTaggedNode = settings.isFillTaggedNode();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
Main.pref.getBoolean("mappaint.wireframe.use-antialiasing", false) ?
RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
}
/**
* Renders the dataset for display.
*
* @param data <code>DataSet</code> to display
* @param virtual <code>true</code> if virtual nodes are used
* @param bounds display boundaries
*/
@SuppressWarnings("unchecked")
@Override
public void render(DataSet data, boolean virtual, Bounds bounds) {
BBox bbox = bounds.toBBox();
this.ds = data;
getSettings(virtual);
for (final Relation rel : data.searchRelations(bbox)) {
if (rel.isDrawable() && !ds.isSelected(rel) && !rel.isDisabledAndHidden()) {
rel.accept(this);
}
}
// draw tagged ways first, then untagged ways, then highlighted ways
List<Way> highlightedWays = new ArrayList<>();
List<Way> untaggedWays = new ArrayList<>();
for (final Way way : data.searchWays(bbox)){
if (way.isDrawable() && !ds.isSelected(way) && !way.isDisabledAndHidden()) {
if (way.isHighlighted()) {
highlightedWays.add(way);
} else if (!way.isTagged()) {
untaggedWays.add(way);
} else {
way.accept(this);
}
}
}
displaySegments();
// Display highlighted ways after the other ones (fix #8276)
List<Way> specialWays = new ArrayList<>(untaggedWays);
specialWays.addAll(highlightedWays);
for (final Way way : specialWays){
way.accept(this);
}
specialWays.clear();
displaySegments();
for (final OsmPrimitive osm : data.getSelected()) {
if (osm.isDrawable()) {
osm.accept(this);
}
}
displaySegments();
for (final OsmPrimitive osm: data.searchNodes(bbox)) {
if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden())
{
osm.accept(this);
}
}
drawVirtualNodes(data, bbox);
// draw highlighted way segments over the already drawn ways. Otherwise each
// way would have to be checked if it contains a way segment to highlight when
// in most of the cases there won't be more than one segment. Since the wireframe
// renderer does not feature any transparency there should be no visual difference.
for (final WaySegment wseg : data.getHighlightedWaySegments()) {
drawSegment(nc.getPoint(wseg.getFirstNode()), nc.getPoint(wseg.getSecondNode()), highlightColor, false);
}
displaySegments();
}
/**
* Helper function to calculate maximum of 4 values.
*
* @param a First value
* @param b Second value
* @param c Third value
* @param d Fourth value
*/
private static final int max(int a, int b, int c, int d) {
return Math.max(Math.max(a, b), Math.max(c, d));
}
/**
* Draw a small rectangle.
* White if selected (as always) or red otherwise.
*
* @param n The node to draw.
*/
@Override
public void visit(Node n) {
if (n.isIncomplete()) return;
if (n.isHighlighted()) {
drawNode(n, highlightColor, selectedNodeSize, fillSelectedNode);
} else {
Color color;
if (isInactiveMode || n.isDisabled()) {
color = inactiveColor;
} else if (ds.isSelected(n)) {
color = selectedColor;
} else if (n.isConnectionNode()) {
if (isNodeTagged(n)) {
color = taggedConnectionColor;
} else {
color = connectionColor;
}
} else {
if (isNodeTagged(n)) {
color = taggedColor;
} else {
color = nodeColor;
}
}
final int size = max((ds.isSelected(n) ? selectedNodeSize : 0),
(isNodeTagged(n) ? taggedNodeSize : 0),
(n.isConnectionNode() ? connectionNodeSize : 0),
unselectedNodeSize);
final boolean fill = (ds.isSelected(n) && fillSelectedNode) ||
(isNodeTagged(n) && fillTaggedNode) ||
(n.isConnectionNode() && fillConnectionNode) ||
fillUnselectedNode;
drawNode(n, color, size, fill);
}
}
private boolean isNodeTagged(Node n) {
return n.isTagged() || n.isAnnotated();
}
/**
* Draw a line for all way segments.
* @param w The way to draw.
*/
@Override
public void visit(Way w) {
if (w.isIncomplete() || w.getNodesCount() < 2)
return;
/* show direction arrows, if draw.segment.relevant_directions_only is not set, the way is tagged with a direction key
(even if the tag is negated as in oneway=false) or the way is selected */
boolean showThisDirectionArrow = ds.isSelected(w) || showDirectionArrow;
/* head only takes over control if the option is true,
the direction should be shown at all and not only because it's selected */
boolean showOnlyHeadArrowOnly = showThisDirectionArrow && !ds.isSelected(w) && showHeadArrowOnly;
Color wayColor;
if (isInactiveMode || w.isDisabled()) {
wayColor = inactiveColor;
} else if(w.isHighlighted()) {
wayColor = highlightColor;
} else if(ds.isSelected(w)) {
wayColor = selectedColor;
} else if (!w.isTagged()) {
wayColor = untaggedWayColor;
} else {
wayColor = dfltWayColor;
}
Iterator<Node> it = w.getNodes().iterator();
if (it.hasNext()) {
Point lastP = nc.getPoint(it.next());
for (int orderNumber = 1; it.hasNext(); orderNumber++) {
Point p = nc.getPoint(it.next());
drawSegment(lastP, p, wayColor,
showOnlyHeadArrowOnly ? !it.hasNext() : showThisDirectionArrow);
if (showOrderNumber && !isInactiveMode) {
drawOrderNumber(lastP, p, orderNumber, g.getColor());
}
lastP = p;
}
}
}
/**
* Draw objects used in relations.
* @param r The relation to draw.
*/
@Override
public void visit(Relation r) {
if (r.isIncomplete()) return;
Color col;
if (isInactiveMode || r.isDisabled()) {
col = inactiveColor;
} else if (ds.isSelected(r)) {
col = selectedColor;
} else {
col = relationColor;
}
g.setColor(col);
for (RelationMember m : r.getMembers()) {
if (m.getMember().isIncomplete() || !m.getMember().isDrawable()) {
continue;
}
if (m.isNode()) {
Point p = nc.getPoint(m.getNode());
if (p.x < 0 || p.y < 0
|| p.x > nc.getWidth() || p.y > nc.getHeight()) {
continue;
}
g.drawOval(p.x-3, p.y-3, 6, 6);
} else if (m.isWay()) {
GeneralPath path = new GeneralPath();
boolean first = true;
for (Node n : m.getWay().getNodes()) {
if (!n.isDrawable()) {
continue;
}
Point p = nc.getPoint(n);
if (first) {
path.moveTo(p.x, p.y);
first = false;
} else {
path.lineTo(p.x, p.y);
}
}
g.draw(relatedWayStroke.createStrokedShape(path));
}
}
}
/**
* Visitor for changesets not used in this class
* @param cs The changeset for inspection.
*/
@Override
public void visit(Changeset cs) {/* ignore */}
@Override
public void drawNode(Node n, Color color, int size, boolean fill) {
if (size > 1) {
int radius = size / 2;
Point p = nc.getPoint(n);
if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth())
|| (p.y > nc.getHeight()))
return;
g.setColor(color);
if (fill) {
g.fillRect(p.x - radius, p.y - radius, size, size);
g.drawRect(p.x - radius, p.y - radius, size, size);
} else {
g.drawRect(p.x - radius, p.y - radius, size, size);
}
}
}
/**
* Draw a line with the given color.
*
* @param path The path to append this segment.
* @param p1 First point of the way segment.
* @param p2 Second point of the way segment.
* @param showDirection <code>true</code> if segment direction should be indicated
*/
protected void drawSegment(GeneralPath path, Point p1, Point p2, boolean showDirection) {
Rectangle bounds = g.getClipBounds();
bounds.grow(100, 100); // avoid arrow heads at the border
LineClip clip = new LineClip(p1, p2, bounds);
if (clip.execute()) {
p1 = clip.getP1();
p2 = clip.getP2();
path.moveTo(p1.x, p1.y);
path.lineTo(p2.x, p2.y);
if (showDirection) {
final double l = 10. / p1.distance(p2);
final double sx = l * (p1.x - p2.x);
final double sy = l * (p1.y - p2.y);
path.lineTo (p2.x + (int) Math.round(cosPHI * sx - sinPHI * sy), p2.y + (int) Math.round(sinPHI * sx + cosPHI * sy));
path.moveTo (p2.x + (int) Math.round(cosPHI * sx + sinPHI * sy), p2.y + (int) Math.round(- sinPHI * sx + cosPHI * sy));
path.lineTo(p2.x, p2.y);
}
}
}
/**
* Draw a line with the given color.
*
* @param p1 First point of the way segment.
* @param p2 Second point of the way segment.
* @param col The color to use for drawing line.
* @param showDirection <code>true</code> if segment direction should be indicated.
*/
protected void drawSegment(Point p1, Point p2, Color col, boolean showDirection) {
if (col != currentColor) {
displaySegments(col);
}
drawSegment(currentPath, p1, p2, showDirection);
}
/**
* Checks if a polygon is visible in display.
*
* @param polygon The polygon to check.
* @return <code>true</code> if polygon is visible.
*/
protected boolean isPolygonVisible(Polygon polygon) {
Rectangle bounds = polygon.getBounds();
if (bounds.width == 0 && bounds.height == 0) return false;
if (bounds.x > nc.getWidth()) return false;
if (bounds.y > nc.getHeight()) return false;
if (bounds.x + bounds.width < 0) return false;
if (bounds.y + bounds.height < 0) return false;
return true;
}
/**
* Finally display all segments in currect path.
*/
protected void displaySegments() {
displaySegments(null);
}
/**
* Finally display all segments in currect path.
*
* @param newColor This color is set after the path is drawn.
*/
protected void displaySegments(Color newColor) {
if (currentPath != null) {
g.setColor(currentColor);
g.draw(currentPath);
currentPath = new GeneralPath();
currentColor = newColor;
}
}
}