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