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