001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm.visitor.paint;
003
004import java.awt.BasicStroke;
005import java.awt.Color;
006import java.awt.Graphics2D;
007import java.awt.Point;
008import java.awt.Rectangle;
009import java.awt.RenderingHints;
010import java.awt.Stroke;
011import java.awt.geom.GeneralPath;
012import java.util.ArrayList;
013import java.util.Iterator;
014import java.util.List;
015
016import org.openstreetmap.josm.Main;
017import org.openstreetmap.josm.data.Bounds;
018import org.openstreetmap.josm.data.osm.BBox;
019import org.openstreetmap.josm.data.osm.Changeset;
020import org.openstreetmap.josm.data.osm.DataSet;
021import org.openstreetmap.josm.data.osm.Node;
022import org.openstreetmap.josm.data.osm.OsmPrimitive;
023import org.openstreetmap.josm.data.osm.Relation;
024import org.openstreetmap.josm.data.osm.RelationMember;
025import org.openstreetmap.josm.data.osm.Way;
026import org.openstreetmap.josm.data.osm.WaySegment;
027import org.openstreetmap.josm.data.osm.visitor.Visitor;
028import org.openstreetmap.josm.gui.NavigatableComponent;
029
030/**
031 * A map renderer that paints a simple scheme of every primitive it visits to a
032 * previous set graphic environment.
033 * @since 23
034 */
035public class WireframeMapRenderer extends AbstractMapRenderer implements Visitor {
036
037    /** Color Preference for ways not matching any other group */
038    protected Color dfltWayColor;
039    /** Color Preference for relations */
040    protected Color relationColor;
041    /** Color Preference for untagged ways */
042    protected Color untaggedWayColor;
043    /** Color Preference for tagged nodes */
044    protected Color taggedColor;
045    /** Color Preference for multiply connected nodes */
046    protected Color connectionColor;
047    /** Color Preference for tagged and multiply connected nodes */
048    protected Color taggedConnectionColor;
049    /** Preference: should directional arrows be displayed */
050    protected boolean showDirectionArrow;
051    /** Preference: should arrows for oneways be displayed */
052    protected boolean showOnewayArrow;
053    /** Preference: should only the last arrow of a way be displayed */
054    protected boolean showHeadArrowOnly;
055    /** Preference: should the segment numbers of ways be displayed */
056    protected boolean showOrderNumber;
057    /** Preference: should selected nodes be filled */
058    protected boolean fillSelectedNode;
059    /** Preference: should unselected nodes be filled */
060    protected boolean fillUnselectedNode;
061    /** Preference: should tagged nodes be filled */
062    protected boolean fillTaggedNode;
063    /** Preference: should multiply connected nodes be filled */
064    protected boolean fillConnectionNode;
065    /** Preference: size of selected nodes */
066    protected int selectedNodeSize;
067    /** Preference: size of unselected nodes */
068    protected int unselectedNodeSize;
069    /** Preference: size of multiply connected nodes */
070    protected int connectionNodeSize;
071    /** Preference: size of tagged nodes */
072    protected int taggedNodeSize;
073
074    /** Color cache to draw subsequent segments of same color as one <code>Path</code>. */
075    protected Color currentColor;
076    /** Path store to draw subsequent segments of same color as one <code>Path</code>. */
077    protected GeneralPath currentPath = new GeneralPath();
078    /**
079      * <code>DataSet</code> passed to the @{link render} function to overcome the argument
080      * limitations of @{link Visitor} interface. Only valid until end of rendering call.
081      */
082    private DataSet ds;
083
084    /** Helper variable for {@link #drawSegment} */
085    private static final double PHI = Math.toRadians(20);
086    /** Helper variable for {@link #drawSegment} */
087    private static final double cosPHI = Math.cos(PHI);
088    /** Helper variable for {@link #drawSegment} */
089    private static final double sinPHI = Math.sin(PHI);
090
091    /** Helper variable for {@link #visit(Relation)} */
092    private final Stroke relatedWayStroke = new BasicStroke(
093            4, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL);
094
095    /**
096     * Creates an wireframe render
097     *
098     * @param g the graphics context. Must not be null.
099     * @param nc the map viewport. Must not be null.
100     * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
101     * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
102     * @throws IllegalArgumentException if {@code g} is null
103     * @throws IllegalArgumentException if {@code nc} is null
104     */
105    public WireframeMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
106        super(g, nc, isInactiveMode);
107    }
108
109    @Override
110    public void getColors() {
111        super.getColors();
112        dfltWayColor = PaintColors.DEFAULT_WAY.get();
113        relationColor = PaintColors.RELATION.get();
114        untaggedWayColor = PaintColors.UNTAGGED_WAY.get();
115        highlightColor = PaintColors.HIGHLIGHT_WIREFRAME.get();
116        taggedColor = PaintColors.TAGGED.get();
117        connectionColor = PaintColors.CONNECTION.get();
118
119        if (taggedColor != nodeColor) {
120            taggedConnectionColor = taggedColor;
121        } else {
122            taggedConnectionColor = connectionColor;
123        }
124    }
125
126    @Override
127    protected void getSettings(boolean virtual) {
128        super.getSettings(virtual);
129        MapPaintSettings settings = MapPaintSettings.INSTANCE;
130        showDirectionArrow = settings.isShowDirectionArrow();
131        showOnewayArrow = settings.isShowOnewayArrow();
132        showHeadArrowOnly = settings.isShowHeadArrowOnly();
133        showOrderNumber = settings.isShowOrderNumber();
134        selectedNodeSize = settings.getSelectedNodeSize();
135        unselectedNodeSize = settings.getUnselectedNodeSize();
136        connectionNodeSize = settings.getConnectionNodeSize();
137        taggedNodeSize = settings.getTaggedNodeSize();
138        fillSelectedNode = settings.isFillSelectedNode();
139        fillUnselectedNode = settings.isFillUnselectedNode();
140        fillConnectionNode = settings.isFillConnectionNode();
141        fillTaggedNode = settings.isFillTaggedNode();
142
143        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
144                Main.pref.getBoolean("mappaint.wireframe.use-antialiasing", false) ?
145                        RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
146    }
147
148    /**
149     * Renders the dataset for display.
150     *
151     * @param data <code>DataSet</code> to display
152     * @param virtual <code>true</code> if virtual nodes are used
153     * @param bounds display boundaries
154     */
155    @Override
156    public void render(DataSet data, boolean virtual, Bounds bounds) {
157        BBox bbox = bounds.toBBox();
158        this.ds = data;
159        getSettings(virtual);
160
161        for (final Relation rel : data.searchRelations(bbox)) {
162            if (rel.isDrawable() && !ds.isSelected(rel) && !rel.isDisabledAndHidden()) {
163                rel.accept(this);
164            }
165        }
166
167        // draw tagged ways first, then untagged ways, then highlighted ways
168        List<Way> highlightedWays = new ArrayList<>();
169        List<Way> untaggedWays = new ArrayList<>();
170
171        for (final Way way : data.searchWays(bbox)) {
172            if (way.isDrawable() && !ds.isSelected(way) && !way.isDisabledAndHidden()) {
173                if (way.isHighlighted()) {
174                    highlightedWays.add(way);
175                } else if (!way.isTagged()) {
176                    untaggedWays.add(way);
177                } else {
178                    way.accept(this);
179                }
180            }
181        }
182        displaySegments();
183
184        // Display highlighted ways after the other ones (fix #8276)
185        List<Way> specialWays = new ArrayList<>(untaggedWays);
186        specialWays.addAll(highlightedWays);
187        for (final Way way : specialWays) {
188            way.accept(this);
189        }
190        specialWays.clear();
191        displaySegments();
192
193        for (final OsmPrimitive osm : data.getSelected()) {
194            if (osm.isDrawable()) {
195                osm.accept(this);
196            }
197        }
198        displaySegments();
199
200        for (final OsmPrimitive osm: data.searchNodes(bbox)) {
201            if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden()) {
202                osm.accept(this);
203            }
204        }
205        drawVirtualNodes(data, bbox);
206
207        // draw highlighted way segments over the already drawn ways. Otherwise each
208        // way would have to be checked if it contains a way segment to highlight when
209        // in most of the cases there won't be more than one segment. Since the wireframe
210        // renderer does not feature any transparency there should be no visual difference.
211        for (final WaySegment wseg : data.getHighlightedWaySegments()) {
212            drawSegment(nc.getPoint(wseg.getFirstNode()), nc.getPoint(wseg.getSecondNode()), highlightColor, false);
213        }
214        displaySegments();
215    }
216
217    /**
218     * Helper function to calculate maximum of 4 values.
219     *
220     * @param a First value
221     * @param b Second value
222     * @param c Third value
223     * @param d Fourth value
224     * @return maximumof {@code a}, {@code b}, {@code c}, {@code d}
225     */
226    private static int max(int a, int b, int c, int d) {
227        return Math.max(Math.max(a, b), Math.max(c, d));
228    }
229
230    /**
231     * Draw a small rectangle.
232     * White if selected (as always) or red otherwise.
233     *
234     * @param n The node to draw.
235     */
236    @Override
237    public void visit(Node n) {
238        if (n.isIncomplete()) return;
239
240        if (n.isHighlighted()) {
241            drawNode(n, highlightColor, selectedNodeSize, fillSelectedNode);
242        } else {
243            Color color;
244
245            if (isInactiveMode || n.isDisabled()) {
246                color = inactiveColor;
247            } else if (n.isSelected()) {
248                color = selectedColor;
249            } else if (n.isMemberOfSelected()) {
250                color = relationSelectedColor;
251            } else if (n.isConnectionNode()) {
252                if (isNodeTagged(n)) {
253                    color = taggedConnectionColor;
254                } else {
255                    color = connectionColor;
256                }
257            } else {
258                if (isNodeTagged(n)) {
259                    color = taggedColor;
260                } else {
261                    color = nodeColor;
262                }
263            }
264
265            final int size = max(ds.isSelected(n) ? selectedNodeSize : 0,
266                    isNodeTagged(n) ? taggedNodeSize : 0,
267                    n.isConnectionNode() ? connectionNodeSize : 0,
268                    unselectedNodeSize);
269
270            final boolean fill = (ds.isSelected(n) && fillSelectedNode) ||
271            (isNodeTagged(n) && fillTaggedNode) ||
272            (n.isConnectionNode() && fillConnectionNode) ||
273            fillUnselectedNode;
274
275            drawNode(n, color, size, fill);
276        }
277    }
278
279    private static boolean isNodeTagged(Node n) {
280        return n.isTagged() || n.isAnnotated();
281    }
282
283    /**
284     * Draw a line for all way segments.
285     * @param w The way to draw.
286     */
287    @Override
288    public void visit(Way w) {
289        if (w.isIncomplete() || w.getNodesCount() < 2)
290            return;
291
292        /* show direction arrows, if draw.segment.relevant_directions_only is not set, the way is tagged with a direction key
293           (even if the tag is negated as in oneway=false) or the way is selected */
294
295        boolean showThisDirectionArrow = ds.isSelected(w) || showDirectionArrow;
296        /* head only takes over control if the option is true,
297           the direction should be shown at all and not only because it's selected */
298        boolean showOnlyHeadArrowOnly = showThisDirectionArrow && !ds.isSelected(w) && showHeadArrowOnly;
299        Color wayColor;
300
301        if (isInactiveMode || w.isDisabled()) {
302            wayColor = inactiveColor;
303        } else if (w.isHighlighted()) {
304            wayColor = highlightColor;
305        } else if (w.isSelected()) {
306            wayColor = selectedColor;
307        } else if (w.isMemberOfSelected()) {
308            wayColor = relationSelectedColor;
309        } else if (!w.isTagged()) {
310            wayColor = untaggedWayColor;
311        } else {
312            wayColor = dfltWayColor;
313        }
314
315        Iterator<Node> it = w.getNodes().iterator();
316        if (it.hasNext()) {
317            Point lastP = nc.getPoint(it.next());
318            for (int orderNumber = 1; it.hasNext(); orderNumber++) {
319                Point p = nc.getPoint(it.next());
320                drawSegment(lastP, p, wayColor,
321                        showOnlyHeadArrowOnly ? !it.hasNext() : showThisDirectionArrow);
322                if (showOrderNumber && !isInactiveMode) {
323                    drawOrderNumber(lastP, p, orderNumber, g.getColor());
324                }
325                lastP = p;
326            }
327        }
328    }
329
330    /**
331     * Draw objects used in relations.
332     * @param r The relation to draw.
333     */
334    @Override
335    public void visit(Relation r) {
336        if (r.isIncomplete()) return;
337
338        Color col;
339        if (isInactiveMode || r.isDisabled()) {
340            col = inactiveColor;
341        } else if (r.isSelected()) {
342            col = selectedColor;
343        } else if (r.isMultipolygon() && r.isMemberOfSelected()) {
344            col = relationSelectedColor;
345        } else {
346            col = relationColor;
347        }
348        g.setColor(col);
349
350        for (RelationMember m : r.getMembers()) {
351            if (m.getMember().isIncomplete() || !m.getMember().isDrawable()) {
352                continue;
353            }
354
355            if (m.isNode()) {
356                Point p = nc.getPoint(m.getNode());
357                if (p.x < 0 || p.y < 0
358                        || p.x > nc.getWidth() || p.y > nc.getHeight()) {
359                    continue;
360                }
361
362                g.drawOval(p.x-4, p.y-4, 9, 9);
363            } else if (m.isWay()) {
364                GeneralPath path = new GeneralPath();
365
366                boolean first = true;
367                for (Node n : m.getWay().getNodes()) {
368                    if (!n.isDrawable()) {
369                        continue;
370                    }
371                    Point p = nc.getPoint(n);
372                    if (first) {
373                        path.moveTo(p.x, p.y);
374                        first = false;
375                    } else {
376                        path.lineTo(p.x, p.y);
377                    }
378                }
379
380                g.draw(relatedWayStroke.createStrokedShape(path));
381            }
382        }
383    }
384
385    /**
386     * Visitor for changesets not used in this class
387     * @param cs The changeset for inspection.
388     */
389    @Override
390    public void visit(Changeset cs) {/* ignore */}
391
392    @Override
393    public void drawNode(Node n, Color color, int size, boolean fill) {
394        if (size > 1) {
395            int radius = size / 2;
396            Point p = nc.getPoint(n);
397            if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth())
398                    || (p.y > nc.getHeight()))
399                return;
400            g.setColor(color);
401            if (fill) {
402                g.fillRect(p.x - radius, p.y - radius, size, size);
403                g.drawRect(p.x - radius, p.y - radius, size, size);
404            } else {
405                g.drawRect(p.x - radius, p.y - radius, size, size);
406            }
407        }
408    }
409
410    /**
411     * Draw a line with the given color.
412     *
413     * @param path The path to append this segment.
414     * @param p1 First point of the way segment.
415     * @param p2 Second point of the way segment.
416     * @param showDirection <code>true</code> if segment direction should be indicated
417     */
418    protected void drawSegment(GeneralPath path, Point p1, Point p2, boolean showDirection) {
419        Rectangle bounds = g.getClipBounds();
420        bounds.grow(100, 100);                  // avoid arrow heads at the border
421        LineClip clip = new LineClip(p1, p2, bounds);
422        if (clip.execute()) {
423            p1 = clip.getP1();
424            p2 = clip.getP2();
425            path.moveTo(p1.x, p1.y);
426            path.lineTo(p2.x, p2.y);
427
428            if (showDirection) {
429                final double l = 10. / p1.distance(p2);
430
431                final double sx = l * (p1.x - p2.x);
432                final double sy = l * (p1.y - p2.y);
433
434                path.lineTo(p2.x + (double) Math.round(cosPHI * sx - sinPHI * sy), p2.y + (double) Math.round(sinPHI * sx + cosPHI * sy));
435                path.moveTo(p2.x + (double) Math.round(cosPHI * sx + sinPHI * sy), p2.y + (double) Math.round(-sinPHI * sx + cosPHI * sy));
436                path.lineTo(p2.x, p2.y);
437            }
438        }
439    }
440
441    /**
442     * Draw a line with the given color.
443     *
444     * @param p1 First point of the way segment.
445     * @param p2 Second point of the way segment.
446     * @param col The color to use for drawing line.
447     * @param showDirection <code>true</code> if segment direction should be indicated.
448     */
449    protected void drawSegment(Point p1, Point p2, Color col, boolean showDirection) {
450        if (col != currentColor) {
451            displaySegments(col);
452        }
453        drawSegment(currentPath, p1, p2, showDirection);
454    }
455
456    /**
457     * Finally display all segments in currect path.
458     */
459    protected void displaySegments() {
460        displaySegments(null);
461    }
462
463    /**
464     * Finally display all segments in currect path.
465     *
466     * @param newColor This color is set after the path is drawn.
467     */
468    protected void displaySegments(Color newColor) {
469        if (currentPath != null) {
470            g.setColor(currentColor);
471            g.draw(currentPath);
472            currentPath = new GeneralPath();
473            currentColor = newColor;
474        }
475    }
476}