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.geom.GeneralPath;
007import java.awt.geom.Path2D;
008import java.awt.geom.Rectangle2D;
009import java.util.Iterator;
010
011import org.openstreetmap.josm.data.osm.BBox;
012import org.openstreetmap.josm.data.osm.INode;
013import org.openstreetmap.josm.data.osm.IWay;
014import org.openstreetmap.josm.data.osm.OsmData;
015import org.openstreetmap.josm.data.osm.Way;
016import org.openstreetmap.josm.data.osm.WaySegment;
017import org.openstreetmap.josm.gui.MapViewState;
018import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
019import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle;
020import org.openstreetmap.josm.gui.NavigatableComponent;
021import org.openstreetmap.josm.spi.preferences.Config;
022import org.openstreetmap.josm.tools.CheckParameterUtil;
023import org.openstreetmap.josm.tools.Logging;
024
025/**
026 * <p>Abstract common superclass for {@link Rendering} implementations.</p>
027 * @since 4087
028 */
029public abstract class AbstractMapRenderer implements Rendering {
030
031    /** the graphics context to which the visitor renders OSM objects */
032    protected final Graphics2D g;
033    /** the map viewport - provides projection and hit detection functionality */
034    protected final NavigatableComponent nc;
035
036    /**
037     * The {@link MapViewState} to use to convert between coordinates.
038     */
039    protected final MapViewState mapState;
040
041    /** if true, the paint visitor shall render OSM objects such that they
042     * look inactive. Example: rendering of data in an inactive layer using light gray as color only. */
043    protected boolean isInactiveMode;
044    /** Color Preference for background */
045    protected Color backgroundColor;
046    /** Color Preference for inactive objects */
047    protected Color inactiveColor;
048    /** Color Preference for selected objects */
049    protected Color selectedColor;
050    /** Color Preference for members of selected relations */
051    protected Color relationSelectedColor;
052    /** Color Preference for nodes */
053    protected Color nodeColor;
054
055    /** Color Preference for highlighted objects */
056    protected Color highlightColor;
057    /** Preference: size of virtual nodes (0 displays display) */
058    protected int virtualNodeSize;
059    /** Preference: minimum space (displayed way length) to display virtual nodes */
060    protected int virtualNodeSpace;
061
062    /** Preference: minimum space (displayed way length) to display segment numbers */
063    protected int segmentNumberSpace;
064
065    /** Performs slow operations by default. Can be disabled when fast partial rendering is required */
066    protected boolean doSlowOperations = true;
067
068    /**
069     * <p>Creates an abstract paint visitor</p>
070     *
071     * @param g the graphics context. Must not be null.
072     * @param nc the map viewport. Must not be null.
073     * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
074     * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
075     * @throws IllegalArgumentException if {@code g} is null
076     * @throws IllegalArgumentException if {@code nc} is null
077     */
078    protected AbstractMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
079        CheckParameterUtil.ensureParameterNotNull(g);
080        CheckParameterUtil.ensureParameterNotNull(nc);
081        this.g = g;
082        this.nc = nc;
083        this.mapState = nc.getState();
084        this.isInactiveMode = isInactiveMode;
085    }
086
087    /**
088     * Draw the node as small square with the given color.
089     *
090     * @param n  The node to draw.
091     * @param color The color of the node.
092     * @param size size in pixels
093     * @param fill determines if the square must be filled
094     */
095    public abstract void drawNode(INode n, Color color, int size, boolean fill);
096
097    /**
098     * Draw an number of the order of the two consecutive nodes within the
099     * parents way
100     *
101     * @param p1 First point of the way segment.
102     * @param p2 Second point of the way segment.
103     * @param orderNumber The number of the segment in the way.
104     * @param clr The color to use for drawing the text.
105     * @since 10827
106     */
107    protected void drawOrderNumber(MapViewPoint p1, MapViewPoint p2, int orderNumber, Color clr) {
108        if (isSegmentVisible(p1, p2) && isLargeSegment(p1, p2, segmentNumberSpace)) {
109            String on = Integer.toString(orderNumber);
110            int strlen = on.length();
111            double centerX = (p1.getInViewX()+p2.getInViewX())/2;
112            double centerY = (p1.getInViewY()+p2.getInViewY())/2;
113            double x = centerX - 4*strlen;
114            double y = centerY + 4;
115
116            if (virtualNodeSize != 0 && isLargeSegment(p1, p2, virtualNodeSpace)) {
117                y = centerY - virtualNodeSize - 3;
118            }
119
120            g.setColor(backgroundColor);
121            g.fill(new Rectangle2D.Double(x-1, y-12, 8*strlen+1d, 14));
122            g.setColor(clr);
123            g.drawString(on, (int) x, (int) y);
124        }
125    }
126
127    /**
128     * Draws virtual nodes.
129     *
130     * @param data The data set being rendered.
131     * @param bbox The bounding box being displayed.
132     * @since 13810 (signature)
133     */
134    public void drawVirtualNodes(OsmData<?, ?, ?, ?> data, BBox bbox) {
135        if (virtualNodeSize == 0 || data == null || bbox == null || data.isLocked())
136            return;
137        // print normal virtual nodes
138        GeneralPath path = new GeneralPath();
139        for (IWay<?> osm : data.searchWays(bbox)) {
140            if (osm.isUsable() && !osm.isDisabledAndHidden() && !osm.isDisabled()) {
141                visitVirtual(path, osm);
142            }
143        }
144        g.setColor(nodeColor);
145        g.draw(path);
146        try {
147            // print highlighted virtual nodes. Since only the color changes, simply
148            // drawing them over the existing ones works fine (at least in their current simple style)
149            path = new GeneralPath();
150            for (WaySegment wseg: data.getHighlightedVirtualNodes()) {
151                if (wseg.getWay().isUsable() && !wseg.getWay().isDisabled()) {
152                    Way tmpWay = wseg.toWay();
153                    visitVirtual(path, tmpWay);
154                    tmpWay.setNodes(null);
155                }
156            }
157            g.setColor(highlightColor);
158            g.draw(path);
159        } catch (ArrayIndexOutOfBoundsException e) {
160            // Silently ignore any ArrayIndexOutOfBoundsException that may be raised
161            // if the way has changed while being rendered (fix #7979)
162            // TODO: proper solution ?
163            // Idea from bastiK:
164            // avoid the WaySegment class and add another data class with { Way way; Node firstNode, secondNode; int firstIdx; }.
165            // On read, it would first check, if the way still has firstIdx+2 nodes, then check if the corresponding way nodes are still
166            // the same and report changes in a more controlled manner.
167            Logging.trace(e);
168        }
169    }
170
171    /**
172     * Reads the color definitions from preferences. This function is <code>public</code>, so that
173     * color names in preferences can be displayed even without calling the wireframe display before.
174     */
175    public void getColors() {
176        this.backgroundColor = PaintColors.BACKGROUND.get();
177        this.inactiveColor = PaintColors.INACTIVE.get();
178        this.selectedColor = PaintColors.SELECTED.get();
179        this.relationSelectedColor = PaintColors.RELATIONSELECTED.get();
180        this.nodeColor = PaintColors.NODE.get();
181        this.highlightColor = PaintColors.HIGHLIGHT.get();
182    }
183
184    /**
185     * Reads all the settings from preferences. Calls the @{link #getColors}
186     * function.
187     *
188     * @param virtual <code>true</code> if virtual nodes are used
189     */
190    protected void getSettings(boolean virtual) {
191        this.virtualNodeSize = virtual ? Config.getPref().getInt("mappaint.node.virtual-size", 8) / 2 : 0;
192        this.virtualNodeSpace = Config.getPref().getInt("mappaint.node.virtual-space", 70);
193        this.segmentNumberSpace = Config.getPref().getInt("mappaint.segmentnumber.space", 40);
194        getColors();
195    }
196
197    /**
198     * Checks if a way segment is large enough for additional information display.
199     *
200     * @param p1 First point of the way segment.
201     * @param p2 Second point of the way segment.
202     * @param space The free space to check against.
203     * @return <code>true</code> if segment is larger than required space
204     * @since 10827
205     */
206    public static boolean isLargeSegment(MapViewPoint p1, MapViewPoint p2, int space) {
207        return p1.oneNormInView(p2) > space;
208    }
209
210    /**
211     * Checks if segment is visible in display.
212     *
213     * @param p1 First point of the way segment.
214     * @param p2 Second point of the way segment.
215     * @return <code>true</code> if segment may be visible.
216     * @since 10827
217     */
218    protected boolean isSegmentVisible(MapViewPoint p1, MapViewPoint p2) {
219        MapViewRectangle view = mapState.getViewArea();
220        // not outside in the same direction
221        return (p1.getOutsideRectangleFlags(view) & p2.getOutsideRectangleFlags(view)) == 0;
222    }
223
224    /**
225     * Creates path for drawing virtual nodes for one way.
226     *
227     * @param path The path to append drawing to.
228     * @param w The ways to draw node for.
229     * @since 10827
230     * @since 13810 (signature)
231     */
232    public void visitVirtual(Path2D path, IWay<?> w) {
233        Iterator<? extends INode> it = w.getNodes().iterator();
234        MapViewPoint lastP = null;
235        while (it.hasNext()) {
236            INode n = it.next();
237            if (n.isLatLonKnown()) {
238                MapViewPoint p = mapState.getPointFor(n);
239                if (lastP != null && isSegmentVisible(lastP, p) && isLargeSegment(lastP, p, virtualNodeSpace)) {
240                    double x = (p.getInViewX()+lastP.getInViewX())/2;
241                    double y = (p.getInViewY()+lastP.getInViewY())/2;
242                    path.moveTo(x-virtualNodeSize, y);
243                    path.lineTo(x+virtualNodeSize, y);
244                    path.moveTo(x, y-virtualNodeSize);
245                    path.lineTo(x, y+virtualNodeSize);
246                }
247                lastP = p;
248            }
249        }
250    }
251
252    /**
253     * Sets whether slow operations such as text rendering must be performed (true by default).
254     * @param enable whether slow operations such as text rendering must be performed
255     * @since 13987
256     */
257    public final void enableSlowOperations(boolean enable) {
258        doSlowOperations = enable;
259    }
260}