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