001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint;
003
004import java.awt.BasicStroke;
005import java.awt.Color;
006import java.awt.Rectangle;
007import java.awt.Stroke;
008import java.util.Objects;
009
010import org.openstreetmap.josm.Main;
011import org.openstreetmap.josm.data.osm.Node;
012import org.openstreetmap.josm.data.osm.OsmPrimitive;
013import org.openstreetmap.josm.data.osm.Relation;
014import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings;
015import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer;
016import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.BoxProvider;
017import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.SimpleBoxProvider;
018import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.IconReference;
019import org.openstreetmap.josm.gui.mappaint.StyleCache.StyleList;
020import org.openstreetmap.josm.gui.util.RotationAngle;
021import org.openstreetmap.josm.tools.Utils;
022
023/**
024 * applies for Nodes and turn restriction relations
025 */
026public class NodeElemStyle extends ElemStyle implements StyleKeys {
027    public final MapImage mapImage;
028    public final RotationAngle mapImageAngle;
029    public final Symbol symbol;
030
031    public enum SymbolShape { SQUARE, CIRCLE, TRIANGLE, PENTAGON, HEXAGON, HEPTAGON, OCTAGON, NONAGON, DECAGON }
032
033    public static class Symbol {
034        public SymbolShape symbol;
035        public int size;
036        public Stroke stroke;
037        public Color strokeColor;
038        public Color fillColor;
039
040        public Symbol(SymbolShape symbol, int size, Stroke stroke, Color strokeColor, Color fillColor) {
041            if (stroke != null && strokeColor == null)
042                throw new IllegalArgumentException("Stroke given without color");
043            if (stroke == null && fillColor == null)
044                throw new IllegalArgumentException("Either a stroke or a fill color must be given");
045            this.symbol = symbol;
046            this.size = size;
047            this.stroke = stroke;
048            this.strokeColor = strokeColor;
049            this.fillColor = fillColor;
050        }
051
052        @Override
053        public boolean equals(Object obj) {
054            if (obj == null || getClass() != obj.getClass())
055                return false;
056            final Symbol other = (Symbol) obj;
057            return  symbol == other.symbol &&
058                    size == other.size &&
059                    Objects.equals(stroke, other.stroke) &&
060                    Objects.equals(strokeColor, other.strokeColor) &&
061                    Objects.equals(fillColor, other.fillColor);
062        }
063
064        @Override
065        public int hashCode() {
066            int hash = 7;
067            hash = 67 * hash + symbol.hashCode();
068            hash = 67 * hash + size;
069            hash = 67 * hash + (stroke != null ? stroke.hashCode() : 0);
070            hash = 67 * hash + (strokeColor != null ? strokeColor.hashCode() : 0);
071            hash = 67 * hash + (fillColor != null ? fillColor.hashCode() : 0);
072            return hash;
073        }
074
075        @Override
076        public String toString() {
077            return "symbol=" + symbol + " size=" + size +
078                    (stroke != null ? " stroke=" + stroke + " strokeColor=" + strokeColor : "") +
079                    (fillColor != null ? " fillColor=" + fillColor : "");
080        }
081    }
082
083    public static final NodeElemStyle SIMPLE_NODE_ELEMSTYLE;
084    public static final BoxProvider SIMPLE_NODE_ELEMSTYLE_BOXPROVIDER;
085    static {
086        MultiCascade mc = new MultiCascade();
087        mc.getOrCreateCascade("default");
088        SIMPLE_NODE_ELEMSTYLE = create(new Environment(null, mc, "default", null), 4.1f, true);
089        if (SIMPLE_NODE_ELEMSTYLE == null) throw new AssertionError();
090        SIMPLE_NODE_ELEMSTYLE_BOXPROVIDER = SIMPLE_NODE_ELEMSTYLE.getBoxProvider();
091    }
092
093    public static final StyleList DEFAULT_NODE_STYLELIST = new StyleList(NodeElemStyle.SIMPLE_NODE_ELEMSTYLE);
094    public static final StyleList DEFAULT_NODE_STYLELIST_TEXT = new StyleList(NodeElemStyle.SIMPLE_NODE_ELEMSTYLE,
095            BoxTextElemStyle.SIMPLE_NODE_TEXT_ELEMSTYLE);
096
097    protected NodeElemStyle(Cascade c, MapImage mapImage, Symbol symbol, float default_major_z_index, RotationAngle rotationAngle) {
098        super(c, default_major_z_index);
099        this.mapImage = mapImage;
100        this.symbol = symbol;
101        this.mapImageAngle = rotationAngle;
102    }
103
104    public static NodeElemStyle create(Environment env) {
105        return create(env, 4f, false);
106    }
107
108    private static NodeElemStyle create(Environment env, float default_major_z_index, boolean allowDefault) {
109        Cascade c = env.mc.getCascade(env.layer);
110
111        MapImage mapImage = createIcon(env, ICON_KEYS);
112        Symbol symbol = null;
113        if (mapImage == null) {
114            symbol = createSymbol(env);
115        }
116        RotationAngle rotationAngle = null;
117        final Float angle = c.get(ICON_ROTATION, null, Float.class, true);
118        if (angle != null) {
119            rotationAngle = RotationAngle.buildStaticRotation(angle);
120        } else {
121            final Keyword rotationKW = c.get(ICON_ROTATION, null, Keyword.class);
122            if (rotationKW != null) {
123                if ("way".equals(rotationKW.val)) {
124                    rotationAngle = RotationAngle.buildWayDirectionRotation();
125                } else {
126                    try {
127                        rotationAngle = RotationAngle.buildStaticRotation(rotationKW.val);
128                    } catch (IllegalArgumentException ignore) {
129                        if (Main.isTraceEnabled()) {
130                            Main.trace(ignore.getMessage());
131                        }
132                    }
133                }
134            }
135        }
136
137        // optimization: if we neither have a symbol, nor a mapImage
138        // we don't have to check for the remaining style properties and we don't
139        // have to allocate a node element style.
140        if (!allowDefault && symbol == null && mapImage == null) return null;
141
142        return new NodeElemStyle(c, mapImage, symbol, default_major_z_index, rotationAngle);
143    }
144
145    public static MapImage createIcon(final Environment env, final String[] keys) {
146        Cascade c = env.mc.getCascade(env.layer);
147
148        final IconReference iconRef = c.get(keys[ICON_IMAGE_IDX], null, IconReference.class, true);
149        if (iconRef == null)
150            return null;
151
152        Cascade c_def = env.mc.getCascade("default");
153
154        Float widthOnDefault = c_def.get(keys[ICON_WIDTH_IDX], null, Float.class);
155        if (widthOnDefault != null && widthOnDefault <= 0) {
156            widthOnDefault = null;
157        }
158        Float widthF = getWidth(c, keys[ICON_WIDTH_IDX], widthOnDefault);
159
160        Float heightOnDefault = c_def.get(keys[ICON_HEIGHT_IDX], null, Float.class);
161        if (heightOnDefault != null && heightOnDefault <= 0) {
162            heightOnDefault = null;
163        }
164        Float heightF = getWidth(c, keys[ICON_HEIGHT_IDX], heightOnDefault);
165
166        int width = widthF == null ? -1 : Math.round(widthF);
167        int height = heightF == null ? -1 : Math.round(heightF);
168
169        float offsetXF = 0f;
170        float offsetYF = 0f;
171        if (keys[ICON_OFFSET_X_IDX] != null) {
172            offsetXF = c.get(keys[ICON_OFFSET_X_IDX], 0f, Float.class);
173            offsetYF = c.get(keys[ICON_OFFSET_Y_IDX], 0f, Float.class);
174        }
175
176        final MapImage mapImage = new MapImage(iconRef.iconName, iconRef.source);
177
178        mapImage.width = width;
179        mapImage.height = height;
180        mapImage.offsetX = Math.round(offsetXF);
181        mapImage.offsetY = Math.round(offsetYF);
182
183        mapImage.alpha = Math.min(255, Math.max(0, Integer.valueOf(Main.pref.getInteger("mappaint.icon-image-alpha", 255))));
184        Integer pAlpha = Utils.color_float2int(c.get(keys[ICON_OPACITY_IDX], null, float.class));
185        if (pAlpha != null) {
186            mapImage.alpha = pAlpha;
187        }
188        return mapImage;
189    }
190
191    private static Symbol createSymbol(Environment env) {
192        Cascade c = env.mc.getCascade(env.layer);
193        Cascade c_def = env.mc.getCascade("default");
194
195        SymbolShape shape;
196        Keyword shapeKW = c.get("symbol-shape", null, Keyword.class);
197        if (shapeKW == null)
198            return null;
199        if ("square".equals(shapeKW.val)) {
200            shape = SymbolShape.SQUARE;
201        } else if ("circle".equals(shapeKW.val)) {
202            shape = SymbolShape.CIRCLE;
203        } else if ("triangle".equals(shapeKW.val)) {
204            shape = SymbolShape.TRIANGLE;
205        } else if ("pentagon".equals(shapeKW.val)) {
206            shape = SymbolShape.PENTAGON;
207        } else if ("hexagon".equals(shapeKW.val)) {
208            shape = SymbolShape.HEXAGON;
209        } else if ("heptagon".equals(shapeKW.val)) {
210            shape = SymbolShape.HEPTAGON;
211        } else if ("octagon".equals(shapeKW.val)) {
212            shape = SymbolShape.OCTAGON;
213        } else if ("nonagon".equals(shapeKW.val)) {
214            shape = SymbolShape.NONAGON;
215        } else if ("decagon".equals(shapeKW.val)) {
216            shape = SymbolShape.DECAGON;
217        } else
218            return null;
219
220        Float sizeOnDefault = c_def.get("symbol-size", null, Float.class);
221        if (sizeOnDefault != null && sizeOnDefault <= 0) {
222            sizeOnDefault = null;
223        }
224        Float size = getWidth(c, "symbol-size", sizeOnDefault);
225
226        if (size == null) {
227            size = 10f;
228        }
229
230        if (size <= 0)
231            return null;
232
233        Float strokeWidthOnDefault = getWidth(c_def, "symbol-stroke-width", null);
234        Float strokeWidth = getWidth(c, "symbol-stroke-width", strokeWidthOnDefault);
235
236        Color strokeColor = c.get("symbol-stroke-color", null, Color.class);
237
238        if (strokeWidth == null && strokeColor != null) {
239            strokeWidth = 1f;
240        } else if (strokeWidth != null && strokeColor == null) {
241            strokeColor = Color.ORANGE;
242        }
243
244        Stroke stroke = null;
245        if (strokeColor != null) {
246            Integer strokeAlpha = Utils.color_float2int(c.get("symbol-stroke-opacity", null, Float.class));
247            if (strokeAlpha != null) {
248                strokeColor = new Color(strokeColor.getRed(), strokeColor.getGreen(),
249                        strokeColor.getBlue(), strokeAlpha);
250            }
251            stroke = new BasicStroke(strokeWidth);
252        }
253
254        Color fillColor = c.get("symbol-fill-color", null, Color.class);
255        if (stroke == null && fillColor == null) {
256            fillColor = Color.BLUE;
257        }
258
259        if (fillColor != null) {
260            Integer fillAlpha = Utils.color_float2int(c.get("symbol-fill-opacity", null, Float.class));
261            if (fillAlpha != null) {
262                fillColor = new Color(fillColor.getRed(), fillColor.getGreen(),
263                        fillColor.getBlue(), fillAlpha);
264            }
265        }
266
267        return new Symbol(shape, Math.round(size), stroke, strokeColor, fillColor);
268    }
269
270    @Override
271    public void paintPrimitive(OsmPrimitive primitive, MapPaintSettings settings, StyledMapRenderer painter,
272            boolean selected, boolean outermember, boolean member) {
273        if (primitive instanceof Node) {
274            Node n = (Node) primitive;
275            if (mapImage != null && painter.isShowIcons()) {
276                painter.drawNodeIcon(n, mapImage, painter.isInactiveMode() || n.isDisabled(), selected, member,
277                        mapImageAngle == null ? 0.0 : mapImageAngle.getRotationAngle(primitive));
278            } else if (symbol != null) {
279                Color fillColor = symbol.fillColor;
280                if (fillColor != null) {
281                    if (painter.isInactiveMode() || n.isDisabled()) {
282                        fillColor = settings.getInactiveColor();
283                    } else if (selected) {
284                        fillColor = settings.getSelectedColor(fillColor.getAlpha());
285                    } else if (member) {
286                        fillColor = settings.getRelationSelectedColor(fillColor.getAlpha());
287                    }
288                }
289                Color strokeColor = symbol.strokeColor;
290                if (strokeColor != null) {
291                    if (painter.isInactiveMode() || n.isDisabled()) {
292                        strokeColor = settings.getInactiveColor();
293                    } else if (selected) {
294                        strokeColor = settings.getSelectedColor(strokeColor.getAlpha());
295                    } else if (member) {
296                        strokeColor = settings.getRelationSelectedColor(strokeColor.getAlpha());
297                    }
298                }
299                painter.drawNodeSymbol(n, symbol, fillColor, strokeColor);
300            } else {
301                Color color;
302                boolean isConnection = n.isConnectionNode();
303
304                if (painter.isInactiveMode() || n.isDisabled()) {
305                    color = settings.getInactiveColor();
306                } else if (selected) {
307                    color = settings.getSelectedColor();
308                } else if (member) {
309                    color = settings.getRelationSelectedColor();
310                } else if (isConnection) {
311                    if (n.isTagged()) {
312                        color = settings.getTaggedConnectionColor();
313                    } else {
314                        color = settings.getConnectionColor();
315                    }
316                } else {
317                    if (n.isTagged()) {
318                        color = settings.getTaggedColor();
319                    } else {
320                        color = settings.getNodeColor();
321                    }
322                }
323
324                final int size = Utils.max(selected ? settings.getSelectedNodeSize() : 0,
325                        n.isTagged() ? settings.getTaggedNodeSize() : 0,
326                        isConnection ? settings.getConnectionNodeSize() : 0,
327                        settings.getUnselectedNodeSize());
328
329                final boolean fill = (selected && settings.isFillSelectedNode()) ||
330                (n.isTagged() && settings.isFillTaggedNode()) ||
331                (isConnection && settings.isFillConnectionNode()) ||
332                settings.isFillUnselectedNode();
333
334                painter.drawNode(n, color, size, fill);
335
336            }
337        } else if (primitive instanceof Relation && mapImage != null) {
338            painter.drawRestriction((Relation) primitive, mapImage, painter.isInactiveMode() || primitive.isDisabled());
339        }
340    }
341
342    public BoxProvider getBoxProvider() {
343        if (mapImage != null)
344            return mapImage.getBoxProvider();
345        else if (symbol != null)
346            return new SimpleBoxProvider(new Rectangle(-symbol.size/2, -symbol.size/2, symbol.size, symbol.size));
347        else {
348            // This is only executed once, so no performance concerns.
349            // However, it would be better, if the settings could be changed at runtime.
350            int size = Utils.max(
351                    Main.pref.getInteger("mappaint.node.selected-size", 5),
352                    Main.pref.getInteger("mappaint.node.unselected-size", 3),
353                    Main.pref.getInteger("mappaint.node.connection-size", 5),
354                    Main.pref.getInteger("mappaint.node.tagged-size", 3)
355            );
356            return new SimpleBoxProvider(new Rectangle(-size/2, -size/2, size, size));
357        }
358    }
359
360    @Override
361    public int hashCode() {
362        int hash = super.hashCode();
363        hash = 17 * hash + (mapImage != null ? mapImage.hashCode() : 0);
364        hash = 17 * hash + (symbol != null ? symbol.hashCode() : 0);
365        hash = 17 * hash + (mapImageAngle != null ? mapImageAngle.hashCode() : 0);
366        return hash;
367    }
368
369    @Override
370    public boolean equals(Object obj) {
371        if (obj == null || getClass() != obj.getClass())
372            return false;
373        if (!super.equals(obj))
374            return false;
375
376        final NodeElemStyle other = (NodeElemStyle) obj;
377        // we should get the same image object due to caching
378        if (!Objects.equals(mapImage, other.mapImage))
379            return false;
380        if (!Objects.equals(symbol, other.symbol))
381            return false;
382        if (!Objects.equals(mapImageAngle, other.mapImageAngle))
383            return false;
384        return true;
385    }
386
387    @Override
388    public String toString() {
389        StringBuilder s = new StringBuilder("NodeElemStyle{");
390        s.append(super.toString());
391        if (mapImage != null) {
392            s.append(" icon=[" + mapImage + ']');
393        }
394        if (symbol != null) {
395            s.append(" symbol=[" + symbol + ']');
396        }
397        if (mapImageAngle != null) {
398            s.append(" mapImageAngle=[" + mapImageAngle + ']');
399        }
400        s.append('}');
401        return s.toString();
402    }
403}