001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.styleelement;
003
004import java.awt.BasicStroke;
005import java.awt.Color;
006import java.awt.Rectangle;
007import java.awt.Stroke;
008import java.util.Objects;
009import java.util.Optional;
010import java.util.stream.IntStream;
011
012import org.openstreetmap.josm.data.osm.INode;
013import org.openstreetmap.josm.data.osm.IPrimitive;
014import org.openstreetmap.josm.data.osm.IRelation;
015import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings;
016import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer;
017import org.openstreetmap.josm.gui.draw.SymbolShape;
018import org.openstreetmap.josm.gui.mappaint.Cascade;
019import org.openstreetmap.josm.gui.mappaint.Environment;
020import org.openstreetmap.josm.gui.mappaint.Keyword;
021import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.IconReference;
022import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.BoxProvider;
023import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.SimpleBoxProvider;
024import org.openstreetmap.josm.spi.preferences.Config;
025import org.openstreetmap.josm.tools.CheckParameterUtil;
026import org.openstreetmap.josm.tools.Logging;
027import org.openstreetmap.josm.tools.RotationAngle;
028import org.openstreetmap.josm.tools.Utils;
029
030/**
031 * applies for Nodes and turn restriction relations
032 */
033public class NodeElement extends StyleElement {
034    /**
035     * The image that is used to display this node. May be <code>null</code>
036     */
037    public final MapImage mapImage;
038    /**
039     * The angle that is used to rotate {@link #mapImage}. May be <code>null</code> to indicate no rotation.
040     */
041    public final RotationAngle mapImageAngle;
042    /**
043     * The symbol that should be used for drawing this node.
044     */
045    public final Symbol symbol;
046
047    private static final String[] ICON_KEYS = {ICON_IMAGE, ICON_WIDTH, ICON_HEIGHT, ICON_OPACITY, ICON_OFFSET_X, ICON_OFFSET_Y};
048
049    protected NodeElement(Cascade c, MapImage mapImage, Symbol symbol, float defaultMajorZindex, RotationAngle rotationAngle) {
050        super(c, defaultMajorZindex);
051        this.mapImage = mapImage;
052        this.symbol = symbol;
053        this.mapImageAngle = Objects.requireNonNull(rotationAngle, "rotationAngle");
054    }
055
056    /**
057     * Creates a new node element for the given Environment
058     * @param env The environment
059     * @return The node element style or <code>null</code> if the node should not be painted.
060     */
061    public static NodeElement create(Environment env) {
062        return create(env, 4f, false);
063    }
064
065    static NodeElement create(Environment env, float defaultMajorZindex, boolean allowDefault) {
066        MapImage mapImage = createIcon(env);
067        Symbol symbol = null;
068        if (mapImage == null) {
069            symbol = createSymbol(env);
070        }
071
072        // optimization: if we neither have a symbol, nor a mapImage
073        // we don't have to check for the remaining style properties and we don't
074        // have to allocate a node element style.
075        if (!allowDefault && symbol == null && mapImage == null) return null;
076
077        Cascade c = env.mc.getCascade(env.layer);
078        RotationAngle rotationAngle = createRotationAngle(env);
079        return new NodeElement(c, mapImage, symbol, defaultMajorZindex, rotationAngle);
080    }
081
082    /**
083     * Reads the icon-rotation property and creates a rotation angle from it.
084     * @param env The environment
085     * @return The angle
086     * @since 11670
087     */
088    public static RotationAngle createRotationAngle(Environment env) {
089        Cascade c = env.mc.getCascade(env.layer);
090
091        RotationAngle rotationAngle = RotationAngle.NO_ROTATION;
092        final Float angle = c.get(ICON_ROTATION, null, Float.class, true);
093        if (angle != null) {
094            rotationAngle = RotationAngle.buildStaticRotation(angle);
095        } else {
096            final Keyword rotationKW = c.get(ICON_ROTATION, null, Keyword.class);
097            if (rotationKW != null) {
098                if ("way".equals(rotationKW.val)) {
099                    rotationAngle = RotationAngle.buildWayDirectionRotation();
100                } else {
101                    try {
102                        rotationAngle = RotationAngle.buildStaticRotation(rotationKW.val);
103                    } catch (IllegalArgumentException ignore) {
104                        Logging.trace(ignore);
105                    }
106                }
107            }
108        }
109        return rotationAngle;
110    }
111
112    /**
113     * Create a map icon for the environment using the default keys.
114     * @param env The environment to read the icon form
115     * @return The icon or <code>null</code> if no icon is defined
116     * @since 11670
117     */
118    public static MapImage createIcon(final Environment env) {
119        return createIcon(env, ICON_KEYS);
120    }
121
122    /**
123     * Create a map icon for the environment.
124     * @param env The environment to read the icon form
125     * @param keys The keys, indexed by the ICON_..._IDX constants.
126     * @return The icon or <code>null</code> if no icon is defined
127     */
128    public static MapImage createIcon(final Environment env, final String... keys) {
129        CheckParameterUtil.ensureParameterNotNull(env, "env");
130        CheckParameterUtil.ensureParameterNotNull(keys, "keys");
131
132        Cascade c = env.mc.getCascade(env.layer);
133
134        final IconReference iconRef = c.get(keys[ICON_IMAGE_IDX], null, IconReference.class, true);
135        if (iconRef == null)
136            return null;
137
138        Cascade cDef = env.mc.getCascade("default");
139
140        Float widthOnDefault = cDef.get(keys[ICON_WIDTH_IDX], null, Float.class);
141        if (widthOnDefault != null && widthOnDefault <= 0) {
142            widthOnDefault = null;
143        }
144        Float widthF = getWidth(c, keys[ICON_WIDTH_IDX], widthOnDefault);
145
146        Float heightOnDefault = cDef.get(keys[ICON_HEIGHT_IDX], null, Float.class);
147        if (heightOnDefault != null && heightOnDefault <= 0) {
148            heightOnDefault = null;
149        }
150        Float heightF = getWidth(c, keys[ICON_HEIGHT_IDX], heightOnDefault);
151
152        int width = widthF == null ? -1 : Math.round(widthF);
153        int height = heightF == null ? -1 : Math.round(heightF);
154
155        float offsetXF = 0f;
156        float offsetYF = 0f;
157        if (keys[ICON_OFFSET_X_IDX] != null) {
158            offsetXF = c.get(keys[ICON_OFFSET_X_IDX], 0f, Float.class);
159            offsetYF = c.get(keys[ICON_OFFSET_Y_IDX], 0f, Float.class);
160        }
161
162        final MapImage mapImage = new MapImage(iconRef.iconName, iconRef.source);
163
164        mapImage.width = width;
165        mapImage.height = height;
166        mapImage.offsetX = Math.round(offsetXF);
167        mapImage.offsetY = Math.round(offsetYF);
168
169        mapImage.alpha = Utils.clamp(Config.getPref().getInt("mappaint.icon-image-alpha", 255), 0, 255);
170        Integer pAlpha = Utils.colorFloat2int(c.get(keys[ICON_OPACITY_IDX], null, float.class));
171        if (pAlpha != null) {
172            mapImage.alpha = pAlpha;
173        }
174        return mapImage;
175    }
176
177    /**
178     * Create a symbol for the environment
179     * @param env The environment to read the icon form
180     * @return The symbol.
181     */
182    private static Symbol createSymbol(Environment env) {
183        Cascade c = env.mc.getCascade(env.layer);
184
185        Keyword shapeKW = c.get("symbol-shape", null, Keyword.class);
186        if (shapeKW == null)
187            return null;
188        Optional<SymbolShape> shape = SymbolShape.forName(shapeKW.val);
189        if (!shape.isPresent()) {
190            return null;
191        }
192
193        Cascade cDef = env.mc.getCascade("default");
194        Float sizeOnDefault = cDef.get("symbol-size", null, Float.class);
195        if (sizeOnDefault != null && sizeOnDefault <= 0) {
196            sizeOnDefault = null;
197        }
198        Float size = Optional.ofNullable(getWidth(c, "symbol-size", sizeOnDefault)).orElse(10f);
199        if (size <= 0)
200            return null;
201
202        Float strokeWidthOnDefault = getWidth(cDef, "symbol-stroke-width", null);
203        Float strokeWidth = getWidth(c, "symbol-stroke-width", strokeWidthOnDefault);
204
205        Color strokeColor = c.get("symbol-stroke-color", null, Color.class);
206
207        if (strokeWidth == null && strokeColor != null) {
208            strokeWidth = 1f;
209        } else if (strokeWidth != null && strokeColor == null) {
210            strokeColor = Color.ORANGE;
211        }
212
213        Stroke stroke = null;
214        if (strokeColor != null && strokeWidth != null) {
215            Integer strokeAlpha = Utils.colorFloat2int(c.get("symbol-stroke-opacity", null, Float.class));
216            if (strokeAlpha != null) {
217                strokeColor = new Color(strokeColor.getRed(), strokeColor.getGreen(),
218                        strokeColor.getBlue(), strokeAlpha);
219            }
220            stroke = new BasicStroke(strokeWidth);
221        }
222
223        Color fillColor = c.get("symbol-fill-color", null, Color.class);
224        if (stroke == null && fillColor == null) {
225            fillColor = Color.BLUE;
226        }
227
228        if (fillColor != null) {
229            Integer fillAlpha = Utils.colorFloat2int(c.get("symbol-fill-opacity", null, Float.class));
230            if (fillAlpha != null) {
231                fillColor = new Color(fillColor.getRed(), fillColor.getGreen(),
232                        fillColor.getBlue(), fillAlpha);
233            }
234        }
235
236        return new Symbol(shape.get(), Math.round(size), stroke, strokeColor, fillColor);
237    }
238
239    @Override
240    public void paintPrimitive(IPrimitive primitive, MapPaintSettings settings, StyledMapRenderer painter,
241            boolean selected, boolean outermember, boolean member) {
242        if (primitive instanceof INode) {
243            INode n = (INode) primitive;
244            if (mapImage != null && painter.isShowIcons()) {
245                painter.drawNodeIcon(n, mapImage, painter.isInactiveMode() || n.isDisabled(), selected, member,
246                        mapImageAngle == null ? 0.0 : mapImageAngle.getRotationAngle(primitive));
247            } else if (symbol != null) {
248                paintWithSymbol(settings, painter, selected, member, n);
249            } else {
250                Color color;
251                boolean isConnection = n.isConnectionNode();
252
253                if (painter.isInactiveMode() || n.isDisabled()) {
254                    color = settings.getInactiveColor();
255                } else if (selected) {
256                    color = settings.getSelectedColor();
257                } else if (member) {
258                    color = settings.getRelationSelectedColor();
259                } else if (isConnection) {
260                    if (n.isTagged()) {
261                        color = settings.getTaggedConnectionColor();
262                    } else {
263                        color = settings.getConnectionColor();
264                    }
265                } else {
266                    if (n.isTagged()) {
267                        color = settings.getTaggedColor();
268                    } else {
269                        color = settings.getNodeColor();
270                    }
271                }
272
273                final int size = max(
274                        selected ? settings.getSelectedNodeSize() : 0,
275                        n.isTagged() ? settings.getTaggedNodeSize() : 0,
276                        isConnection ? settings.getConnectionNodeSize() : 0,
277                        settings.getUnselectedNodeSize());
278
279                final boolean fill = (selected && settings.isFillSelectedNode()) ||
280                (n.isTagged() && settings.isFillTaggedNode()) ||
281                (isConnection && settings.isFillConnectionNode()) ||
282                settings.isFillUnselectedNode();
283
284                painter.drawNode(n, color, size, fill);
285
286            }
287        } else if (primitive instanceof IRelation && mapImage != null) {
288            painter.drawRestriction((IRelation<?>) primitive, mapImage, painter.isInactiveMode() || primitive.isDisabled());
289        }
290    }
291
292    private void paintWithSymbol(MapPaintSettings settings, StyledMapRenderer painter, boolean selected, boolean member,
293            INode n) {
294        Color fillColor = symbol.fillColor;
295        if (fillColor != null) {
296            if (painter.isInactiveMode() || n.isDisabled()) {
297                fillColor = settings.getInactiveColor();
298            } else if (defaultSelectedHandling && selected) {
299                fillColor = settings.getSelectedColor(fillColor.getAlpha());
300            } else if (member) {
301                fillColor = settings.getRelationSelectedColor(fillColor.getAlpha());
302            }
303        }
304        Color strokeColor = symbol.strokeColor;
305        if (strokeColor != null) {
306            if (painter.isInactiveMode() || n.isDisabled()) {
307                strokeColor = settings.getInactiveColor();
308            } else if (defaultSelectedHandling && selected) {
309                strokeColor = settings.getSelectedColor(strokeColor.getAlpha());
310            } else if (member) {
311                strokeColor = settings.getRelationSelectedColor(strokeColor.getAlpha());
312            }
313        }
314        painter.drawNodeSymbol(n, symbol, fillColor, strokeColor);
315    }
316
317    /**
318     * Gets the selection box for this element.
319     * @return The selection box as {@link BoxProvider} object.
320     */
321    public BoxProvider getBoxProvider() {
322        if (mapImage != null)
323            return mapImage.getBoxProvider();
324        else if (symbol != null)
325            return new SimpleBoxProvider(new Rectangle(-symbol.size/2, -symbol.size/2, symbol.size, symbol.size));
326        else {
327            // This is only executed once, so no performance concerns.
328            // However, it would be better, if the settings could be changed at runtime.
329            int size = max(Config.getPref().getInt("mappaint.node.selected-size", 5),
330                    Config.getPref().getInt("mappaint.node.unselected-size", 3),
331                    Config.getPref().getInt("mappaint.node.connection-size", 5),
332                    Config.getPref().getInt("mappaint.node.tagged-size", 3)
333            );
334            return new SimpleBoxProvider(new Rectangle(-size/2, -size/2, size, size));
335        }
336    }
337
338    private static int max(int... elements) {
339        return IntStream.of(elements).max().orElseThrow(IllegalStateException::new);
340    }
341
342    @Override
343    public int hashCode() {
344        return Objects.hash(super.hashCode(), mapImage, mapImageAngle, symbol);
345    }
346
347    @Override
348    public boolean equals(Object obj) {
349        if (this == obj) return true;
350        if (obj == null || getClass() != obj.getClass()) return false;
351        if (!super.equals(obj)) return false;
352        NodeElement that = (NodeElement) obj;
353        return Objects.equals(mapImage, that.mapImage) &&
354               Objects.equals(mapImageAngle, that.mapImageAngle) &&
355               Objects.equals(symbol, that.symbol);
356    }
357
358    @Override
359    public String toString() {
360        StringBuilder s = new StringBuilder(64).append("NodeElement{").append(super.toString());
361        if (mapImage != null) {
362            s.append(" icon=[" + mapImage + ']');
363        }
364        if (mapImage != null && mapImageAngle != null) {
365            s.append(" mapImageAngle=[" + mapImageAngle + ']');
366        }
367        if (symbol != null) {
368            s.append(" symbol=[" + symbol + ']');
369        }
370        s.append('}');
371        return s.toString();
372    }
373}