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.Polygon;
009import java.awt.Rectangle;
010import java.awt.RenderingHints;
011import java.awt.Stroke;
012import java.awt.geom.GeneralPath;
013import java.util.ArrayList;
014import java.util.Arrays;
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.NavigatableComponent;
031
032/**
033 * A map renderer that paints a simple scheme of every primitive it visits to a
034 * previous set graphic environment.
035 */
036public class WireframeMapRenderer extends AbstractMapRenderer implements Visitor {
037
038    /** Color Preference for ways not matching any other group */
039    protected Color dfltWayColor;
040    /** Color Preference for relations */
041    protected Color relationColor;
042    /** Color Preference for untagged ways */
043    protected Color untaggedWayColor;
044    /** Color Preference for tagged nodes */
045    protected Color taggedColor;
046    /** Color Preference for multiply connected nodes */
047    protected Color connectionColor;
048    /** Color Preference for tagged and multiply connected nodes */
049    protected Color taggedConnectionColor;
050    /** Preference: should directional arrows be displayed */
051    protected boolean showDirectionArrow;
052    /** Preference: should arrows for oneways be displayed */
053    protected boolean showOnewayArrow;
054    /** Preference: should only the last arrow of a way be displayed */
055    protected boolean showHeadArrowOnly;
056    /** Preference: should the segement numbers of ways be displayed */
057    protected boolean showOrderNumber;
058    /** Preference: should selected nodes be filled */
059    protected boolean fillSelectedNode;
060    /** Preference: should unselected nodes be filled */
061    protected boolean fillUnselectedNode;
062    /** Preference: should tagged nodes be filled */
063    protected boolean fillTaggedNode;
064    /** Preference: should multiply connected nodes be filled */
065    protected boolean fillConnectionNode;
066    /** Preference: size of selected nodes */
067    protected int selectedNodeSize;
068    /** Preference: size of unselected nodes */
069    protected int unselectedNodeSize;
070    /** Preference: size of multiply connected nodes */
071    protected int connectionNodeSize;
072    /** Preference: size of tagged nodes */
073    protected int taggedNodeSize;
074
075    /** Color cache to draw subsequent segments of same color as one <code>Path</code>. */
076    protected Color currentColor = null;
077    /** Path store to draw subsequent segments of same color as one <code>Path</code>. */
078    protected GeneralPath currentPath = new GeneralPath();
079    /**
080      * <code>DataSet</code> passed to the @{link render} function to overcome the argument
081      * limitations of @{link Visitor} interface. Only valid until end of rendering call.
082      */
083    private DataSet ds;
084
085    /** Helper variable for {@link #drawSegment} */
086    private static final double PHI = Math.toRadians(20);
087    /** Helper variable for {@link #drawSegment} */
088    private static final double cosPHI = Math.cos(PHI);
089    /** Helper variable for {@link #drawSegment} */
090    private static final double sinPHI = Math.sin(PHI);
091
092    /** Helper variable for {@link #visit(Relation)} */
093    private Stroke relatedWayStroke = new BasicStroke(
094            4, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL);
095
096    /**
097     * Creates an wireframe render
098     *
099     * @param g the graphics context. Must not be null.
100     * @param nc the map viewport. Must not be null.
101     * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
102     * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
103     * @throws IllegalArgumentException thrown if {@code g} is null
104     * @throws IllegalArgumentException thrown if {@code nc} is null
105     */
106    public WireframeMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
107        super(g, nc, isInactiveMode);
108    }
109
110    @Override
111    public void getColors() {
112        super.getColors();
113        dfltWayColor = PaintColors.DEFAULT_WAY.get();
114        relationColor = PaintColors.RELATION.get();
115        untaggedWayColor = PaintColors.UNTAGGED_WAY.get();
116        highlightColor = PaintColors.HIGHLIGHT_WIREFRAME.get();
117        taggedColor = PaintColors.TAGGED.get();
118        connectionColor = PaintColors.CONNECTION.get();
119
120        if (taggedColor != nodeColor) {
121            taggedConnectionColor = taggedColor;
122        } else {
123            taggedConnectionColor = connectionColor;
124        }
125    }
126
127    @Override
128    protected void getSettings(boolean virtual) {
129        super.getSettings(virtual);
130        MapPaintSettings settings = MapPaintSettings.INSTANCE;
131        showDirectionArrow = settings.isShowDirectionArrow();
132        showOnewayArrow = settings.isShowOnewayArrow();
133        showHeadArrowOnly = settings.isShowHeadArrowOnly();
134        showOrderNumber = settings.isShowOrderNumber();
135        selectedNodeSize = settings.getSelectedNodeSize();
136        unselectedNodeSize = settings.getUnselectedNodeSize();
137        connectionNodeSize = settings.getConnectionNodeSize();
138        taggedNodeSize = settings.getTaggedNodeSize();
139        fillSelectedNode = settings.isFillSelectedNode();
140        fillUnselectedNode = settings.isFillUnselectedNode();
141        fillConnectionNode = settings.isFillConnectionNode();
142        fillTaggedNode = settings.isFillTaggedNode();
143
144        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
145                Main.pref.getBoolean("mappaint.wireframe.use-antialiasing", false) ?
146                        RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
147    }
148
149    /**
150     * Renders the dataset for display.
151     *
152     * @param data <code>DataSet</code> to display
153     * @param virtual <code>true</code> if virtual nodes are used
154     * @param bounds display boundaries
155     */
156    @SuppressWarnings("unchecked")
157    @Override
158    public void render(DataSet data, boolean virtual, Bounds bounds) {
159        BBox bbox = bounds.toBBox();
160        this.ds = data;
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<Way>();
171        List<Way> untaggedWays = new ArrayList<Way>();
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        for (List<Way> specialWays : Arrays.asList(new List[]{untaggedWays, highlightedWays})) {
188            for (final Way way : specialWays){
189                way.accept(this);
190            }
191            specialWays.clear();
192            displaySegments();
193        }
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            {
205                osm.accept(this);
206            }
207        }
208        drawVirtualNodes(data, bbox);
209
210        // draw highlighted way segments over the already drawn ways. Otherwise each
211        // way would have to be checked if it contains a way segment to highlight when
212        // in most of the cases there won't be more than one segment. Since the wireframe
213        // renderer does not feature any transparency there should be no visual difference.
214        for (final WaySegment wseg : data.getHighlightedWaySegments()) {
215            drawSegment(nc.getPoint(wseg.getFirstNode()), nc.getPoint(wseg.getSecondNode()), highlightColor, false);
216        }
217        displaySegments();
218    }
219
220    /**
221     * Helper function to calculate maximum of 4 values.
222     *
223     * @param a First value
224     * @param b Second value
225     * @param c Third value
226     * @param d Fourth value
227     */
228    private static final 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 (ds.isSelected(n)) {
250                color = selectedColor;
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 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(ds.isSelected(w)) {
306            wayColor = selectedColor;
307        } else if (!w.isTagged()) {
308            wayColor = untaggedWayColor;
309        } else {
310            wayColor = dfltWayColor;
311        }
312
313        Iterator<Node> it = w.getNodes().iterator();
314        if (it.hasNext()) {
315            Point lastP = nc.getPoint(it.next());
316            for (int orderNumber = 1; it.hasNext(); orderNumber++) {
317                Point p = nc.getPoint(it.next());
318                drawSegment(lastP, p, wayColor,
319                        showOnlyHeadArrowOnly ? !it.hasNext() : showThisDirectionArrow);
320                if (showOrderNumber && !isInactiveMode) {
321                    drawOrderNumber(lastP, p, orderNumber, g.getColor());
322                }
323                lastP = p;
324            }
325        }
326    }
327
328    /**
329     * Draw objects used in relations.
330     * @param r The relation to draw.
331     */
332    @Override
333    public void visit(Relation r) {
334        if (r.isIncomplete()) return;
335
336        Color col;
337        if (isInactiveMode || r.isDisabled()) {
338            col = inactiveColor;
339        } else if (ds.isSelected(r)) {
340            col = selectedColor;
341        } else {
342            col = relationColor;
343        }
344        g.setColor(col);
345
346        for (RelationMember m : r.getMembers()) {
347            if (m.getMember().isIncomplete() || !m.getMember().isDrawable()) {
348                continue;
349            }
350
351            if (m.isNode()) {
352                Point p = nc.getPoint(m.getNode());
353                if (p.x < 0 || p.y < 0
354                        || p.x > nc.getWidth() || p.y > nc.getHeight()) {
355                    continue;
356                }
357
358                g.drawOval(p.x-3, p.y-3, 6, 6);
359            } else if (m.isWay()) {
360                GeneralPath path = new GeneralPath();
361
362                boolean first = true;
363                for (Node n : m.getWay().getNodes()) {
364                    if (!n.isDrawable()) {
365                        continue;
366                    }
367                    Point p = nc.getPoint(n);
368                    if (first) {
369                        path.moveTo(p.x, p.y);
370                        first = false;
371                    } else {
372                        path.lineTo(p.x, p.y);
373                    }
374                }
375
376                g.draw(relatedWayStroke.createStrokedShape(path));
377            }
378        }
379    }
380
381    /**
382     * Visitor for changesets not used in this class
383     * @param cs The changeset for inspection.
384     */
385    @Override
386    public void visit(Changeset cs) {/* ignore */}
387
388    @Override
389    public void drawNode(Node n, Color color, int size, boolean fill) {
390        if (size > 1) {
391            int radius = size / 2;
392            Point p = nc.getPoint(n);
393            if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth())
394                    || (p.y > nc.getHeight()))
395                return;
396            g.setColor(color);
397            if (fill) {
398                g.fillRect(p.x - radius, p.y - radius, size, size);
399                g.drawRect(p.x - radius, p.y - radius, size, size);
400            } else {
401                g.drawRect(p.x - radius, p.y - radius, size, size);
402            }
403        }
404    }
405
406    /**
407     * Draw a line with the given color.
408     *
409     * @param path The path to append this segment.
410     * @param p1 First point of the way segment.
411     * @param p2 Second point of the way segment.
412     * @param showDirection <code>true</code> if segment direction should be indicated
413     */
414    protected void drawSegment(GeneralPath path, Point p1, Point p2, boolean showDirection) {
415        Rectangle bounds = g.getClipBounds();
416        bounds.grow(100, 100);                  // avoid arrow heads at the border
417        LineClip clip = new LineClip(p1, p2, bounds);
418        if (clip.execute()) {
419            p1 = clip.getP1();
420            p2 = clip.getP2();
421            path.moveTo(p1.x, p1.y);
422            path.lineTo(p2.x, p2.y);
423
424            if (showDirection) {
425                final double l =  10. / p1.distance(p2);
426
427                final double sx = l * (p1.x - p2.x);
428                final double sy = l * (p1.y - p2.y);
429
430                path.lineTo (p2.x + (int) Math.round(cosPHI * sx - sinPHI * sy), p2.y + (int) Math.round(sinPHI * sx + cosPHI * sy));
431                path.moveTo (p2.x + (int) Math.round(cosPHI * sx + sinPHI * sy), p2.y + (int) Math.round(- sinPHI * sx + cosPHI * sy));
432                path.lineTo(p2.x, p2.y);
433            }
434        }
435    }
436
437    /**
438     * Draw a line with the given color.
439     *
440     * @param p1 First point of the way segment.
441     * @param p2 Second point of the way segment.
442     * @param col The color to use for drawing line.
443     * @param showDirection <code>true</code> if segment direction should be indicated.
444     */
445    protected void drawSegment(Point p1, Point p2, Color col, boolean showDirection) {
446        if (col != currentColor) {
447            displaySegments(col);
448        }
449        drawSegment(currentPath, p1, p2, showDirection);
450    }
451
452    /**
453     * Checks if a polygon is visible in display.
454     *
455     * @param polygon The polygon to check.
456     * @return <code>true</code> if polygon is visible.
457     */
458    protected boolean isPolygonVisible(Polygon polygon) {
459        Rectangle bounds = polygon.getBounds();
460        if (bounds.width == 0 && bounds.height == 0) return false;
461        if (bounds.x > nc.getWidth()) return false;
462        if (bounds.y > nc.getHeight()) return false;
463        if (bounds.x + bounds.width < 0) return false;
464        if (bounds.y + bounds.height < 0) return false;
465        return true;
466    }
467
468    /**
469     * Finally display all segments in currect path.
470     */
471    protected void displaySegments() {
472        displaySegments(null);
473    }
474
475    /**
476     * Finally display all segments in currect path.
477     *
478     * @param newColor This color is set after the path is drawn.
479     */
480    protected void displaySegments(Color newColor) {
481        if (currentPath != null) {
482            g.setColor(currentColor);
483            g.draw(currentPath);
484            currentPath = new GeneralPath();
485            currentColor = newColor;
486        }
487    }
488}