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