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