001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm.visitor.paint;
003
004import java.awt.Color;
005import java.awt.Graphics2D;
006import java.awt.Point;
007import java.awt.geom.GeneralPath;
008import java.awt.geom.Point2D;
009import java.util.Iterator;
010
011import org.openstreetmap.josm.Main;
012import org.openstreetmap.josm.data.osm.BBox;
013import org.openstreetmap.josm.data.osm.DataSet;
014import org.openstreetmap.josm.data.osm.Node;
015import org.openstreetmap.josm.data.osm.Way;
016import org.openstreetmap.josm.data.osm.WaySegment;
017import org.openstreetmap.josm.gui.NavigatableComponent;
018import org.openstreetmap.josm.tools.CheckParameterUtil;
019
020/**
021 * <p>Abstract common superclass for {@link Rendering} implementations.</p>
022 *
023 */
024public abstract class AbstractMapRenderer implements Rendering {
025
026    /** the graphics context to which the visitor renders OSM objects */
027    protected Graphics2D g;
028    /** the map viewport - provides projection and hit detection functionality */
029    protected NavigatableComponent nc;
030
031    /** if true, the paint visitor shall render OSM objects such that they
032     * look inactive. Example: rendering of data in an inactive layer using light gray as color only. */
033    protected boolean isInactiveMode;
034    /** Color Preference for background */
035    protected Color backgroundColor;
036    /** Color Preference for inactive objects */
037    protected Color inactiveColor;
038    /** Color Preference for selected objects */
039    protected Color selectedColor;
040    /** Color Preference for nodes */
041    protected Color nodeColor;
042
043    /** Color Preference for hightlighted objects */
044    protected Color highlightColor;
045    /** Preference: size of virtual nodes (0 displayes display) */
046    protected int virtualNodeSize;
047    /** Preference: minimum space (displayed way length) to display virtual nodes */
048    protected int virtualNodeSpace;
049
050    /** Preference: minimum space (displayed way length) to display segment numbers */
051    protected int segmentNumberSpace;
052
053    /**
054     * <p>Creates an abstract paint visitor</p>
055     *
056     * @param g the graphics context. Must not be null.
057     * @param nc the map viewport. Must not be null.
058     * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
059     * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
060     * @throws IllegalArgumentException thrown if {@code g} is null
061     * @throws IllegalArgumentException thrown if {@code nc} is null
062     */
063    public AbstractMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) throws IllegalArgumentException{
064        CheckParameterUtil.ensureParameterNotNull(g);
065        CheckParameterUtil.ensureParameterNotNull(nc);
066        this.g = g;
067        this.nc = nc;
068        this.isInactiveMode = isInactiveMode;
069    }
070
071    /**
072     * Draw the node as small rectangle with the given color.
073     *
074     * @param n  The node to draw.
075     * @param color The color of the node.
076     */
077    public abstract void drawNode(Node n, Color color, int size, boolean fill);
078
079    /**
080     * Draw an number of the order of the two consecutive nodes within the
081     * parents way
082     *
083     * @param p1 First point of the way segment.
084     * @param p2 Second point of the way segment.
085     * @param orderNumber The number of the segment in the way.
086     */
087    protected void drawOrderNumber(Point p1, Point p2, int orderNumber, Color clr) {
088        if (isSegmentVisible(p1, p2) && isLargeSegment(p1, p2, segmentNumberSpace)) {
089            String on = Integer.toString(orderNumber);
090            int strlen = on.length();
091            int x = (p1.x+p2.x)/2 - 4*strlen;
092            int y = (p1.y+p2.y)/2 + 4;
093
094            if (virtualNodeSize != 0 && isLargeSegment(p1, p2, virtualNodeSpace)) {
095                y = (p1.y+p2.y)/2 - virtualNodeSize - 3;
096            }
097
098            g.setColor(backgroundColor);
099            g.fillRect(x-1, y-12, 8*strlen+1, 14);
100            g.setColor(clr);
101            g.drawString(on, x, y);
102        }
103    }
104
105    /**
106     * Draws virtual nodes.
107     *
108     * @param data The data set being rendered.
109     * @param bbox The bounding box being displayed.
110     */
111    public void drawVirtualNodes(DataSet data, BBox bbox) {
112        if (virtualNodeSize == 0 || data == null || bbox == null)
113            return;
114        // print normal virtual nodes
115        GeneralPath path = new GeneralPath();
116        for (Way osm : data.searchWays(bbox)) {
117            if (osm.isUsable() && !osm.isDisabledAndHidden() && !osm.isDisabled()) {
118                visitVirtual(path, osm);
119            }
120        }
121        g.setColor(nodeColor);
122        g.draw(path);
123        try {
124            // print highlighted virtual nodes. Since only the color changes, simply
125            // drawing them over the existing ones works fine (at least in their current
126            // simple style)
127            path = new GeneralPath();
128            for (WaySegment wseg: data.getHighlightedVirtualNodes()) {
129                if (wseg.way.isUsable() && !wseg.way.isDisabled()) {
130                    visitVirtual(path, wseg.toWay());
131                }
132            }
133            g.setColor(highlightColor);
134            g.draw(path);
135        } catch (ArrayIndexOutOfBoundsException e) {
136            // Silently ignore any ArrayIndexOutOfBoundsException that may be raised
137            // if the way has changed while being rendered (fix #7979)
138            // TODO: proper solution ?
139            // Idea from bastiK: avoid the WaySegment class and add another data class with { Way way; Node firstNode, secondNode; int firstIdx; }.
140            // On read, it would first check, if the way still has firstIdx+2 nodes, then check if the corresponding way nodes are still the same
141            // and report changes in a more controlled manner.
142        }
143    }
144
145    /**
146     * Reads the color definitions from preferences. This function is <code>public</code>, so that
147     * color names in preferences can be displayed even without calling the wireframe display before.
148     */
149    public void getColors() {
150        this.backgroundColor = PaintColors.BACKGROUND.get();
151        this.inactiveColor = PaintColors.INACTIVE.get();
152        this.selectedColor = PaintColors.SELECTED.get();
153        this.nodeColor = PaintColors.NODE.get();
154        this.highlightColor = PaintColors.HIGHLIGHT.get();
155    }
156
157    /**
158     * Reads all the settings from preferences. Calls the @{link #getColors}
159     * function.
160     *
161     * @param virtual <code>true</code> if virtual nodes are used
162     */
163    protected void getSettings(boolean virtual) {
164        this.virtualNodeSize = virtual ? Main.pref.getInteger("mappaint.node.virtual-size", 8) / 2 : 0;
165        this.virtualNodeSpace = Main.pref.getInteger("mappaint.node.virtual-space", 70);
166        this.segmentNumberSpace = Main.pref.getInteger("mappaint.segmentnumber.space", 40);
167        getColors();
168    }
169
170    /**
171     * Checks if a way segemnt is large enough for additional information display.
172     *
173     * @param p1 First point of the way segment.
174     * @param p2 Second point of the way segment.
175     * @param space The free space to check against.
176     * @return <code>true</code> if segment is larger than required space
177     */
178    public static boolean isLargeSegment(Point2D p1, Point2D p2, int space) {
179        double xd = Math.abs(p1.getX()-p2.getX());
180        double yd = Math.abs(p1.getY()-p2.getY());
181        return (xd+yd > space);
182    }
183
184    /**
185     * Checks if segment is visible in display.
186     *
187     * @param p1 First point of the way segment.
188     * @param p2 Second point of the way segment.
189     * @return <code>true</code> if segment is visible.
190     */
191    protected boolean isSegmentVisible(Point p1, Point p2) {
192        if ((p1.x < 0) && (p2.x < 0)) return false;
193        if ((p1.y < 0) && (p2.y < 0)) return false;
194        if ((p1.x > nc.getWidth()) && (p2.x > nc.getWidth())) return false;
195        if ((p1.y > nc.getHeight()) && (p2.y > nc.getHeight())) return false;
196        return true;
197    }
198
199    /**
200     * Creates path for drawing virtual nodes for one way.
201     *
202     * @param path The path to append drawing to.
203     * @param w The ways to draw node for.
204     */
205    public void visitVirtual(GeneralPath path, Way w) {
206        Iterator<Node> it = w.getNodes().iterator();
207        if (it.hasNext()) {
208            Point lastP = nc.getPoint(it.next());
209            while (it.hasNext())
210            {
211                Point p = nc.getPoint(it.next());
212                if (isSegmentVisible(lastP, p) && isLargeSegment(lastP, p, virtualNodeSpace))
213                {
214                    int x = (p.x+lastP.x)/2;
215                    int y = (p.y+lastP.y)/2;
216                    path.moveTo(x-virtualNodeSize, y);
217                    path.lineTo(x+virtualNodeSize, y);
218                    path.moveTo(x, y-virtualNodeSize);
219                    path.lineTo(x, y+virtualNodeSize);
220                }
221                lastP = p;
222            }
223        }
224    }
225}