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