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