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