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