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