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