001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint.styleelement; 003 004import java.awt.Color; 005import java.awt.Rectangle; 006import java.awt.geom.Point2D; 007import java.util.Objects; 008 009import org.openstreetmap.josm.data.osm.Node; 010import org.openstreetmap.josm.data.osm.OsmPrimitive; 011import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings; 012import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 013import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer; 014import org.openstreetmap.josm.gui.mappaint.Cascade; 015import org.openstreetmap.josm.gui.mappaint.Environment; 016import org.openstreetmap.josm.gui.mappaint.Keyword; 017import org.openstreetmap.josm.gui.mappaint.MultiCascade; 018import org.openstreetmap.josm.tools.CheckParameterUtil; 019 020/** 021 * Text style attached to a style with a bounding box, like an icon or a symbol. 022 */ 023public class BoxTextElement extends StyleElement { 024 025 /** 026 * MapCSS text-anchor-horizontal 027 */ 028 public enum HorizontalTextAlignment { 029 /** 030 * Align to the left 031 */ 032 LEFT, 033 /** 034 * Align in the center 035 */ 036 CENTER, 037 /** 038 * Align to the right 039 */ 040 RIGHT 041 } 042 043 /** 044 * MapCSS text-anchor-vertical 045 */ 046 public enum VerticalTextAlignment { 047 /** 048 * Render above the box 049 */ 050 ABOVE, 051 /** 052 * Align to the top of the box 053 */ 054 TOP, 055 /** 056 * Render at the center of the box 057 */ 058 CENTER, 059 /** 060 * Align to the bottom of the box 061 */ 062 BOTTOM, 063 /** 064 * Render below the box 065 */ 066 BELOW 067 } 068 069 /** 070 * Something that provides us with a {@link BoxProviderResult} 071 * @since 10600 (functional interface) 072 */ 073 @FunctionalInterface 074 public interface BoxProvider { 075 /** 076 * Compute and get the {@link BoxProviderResult}. The temporary flag is set if the result of the computation may change in the future. 077 * @return The result of the computation. 078 */ 079 BoxProviderResult get(); 080 } 081 082 /** 083 * A box rectangle with a flag if it is temporary. 084 */ 085 public static class BoxProviderResult { 086 private final Rectangle box; 087 private final boolean temporary; 088 089 /** 090 * Create a new box provider result 091 * @param box The box 092 * @param temporary The temporary flag, will be returned by {@link #isTemporary()} 093 */ 094 public BoxProviderResult(Rectangle box, boolean temporary) { 095 this.box = box; 096 this.temporary = temporary; 097 } 098 099 /** 100 * Returns the box. 101 * @return the box 102 */ 103 public Rectangle getBox() { 104 return box; 105 } 106 107 /** 108 * Determines if the box can change in future calls of the {@link BoxProvider#get()} method 109 * @return {@code true} if the box can change in future calls of the {@code BoxProvider#get()} method 110 */ 111 public boolean isTemporary() { 112 return temporary; 113 } 114 } 115 116 /** 117 * A {@link BoxProvider} that always returns the same non-temporary rectangle 118 */ 119 public static class SimpleBoxProvider implements BoxProvider { 120 private final Rectangle box; 121 122 /** 123 * Constructs a new {@code SimpleBoxProvider}. 124 * @param box the box 125 */ 126 public SimpleBoxProvider(Rectangle box) { 127 this.box = box; 128 } 129 130 @Override 131 public BoxProviderResult get() { 132 return new BoxProviderResult(box, false); 133 } 134 135 @Override 136 public int hashCode() { 137 return Objects.hash(box); 138 } 139 140 @Override 141 public boolean equals(Object obj) { 142 if (this == obj) return true; 143 if (obj == null || getClass() != obj.getClass()) return false; 144 SimpleBoxProvider that = (SimpleBoxProvider) obj; 145 return Objects.equals(box, that.box); 146 } 147 } 148 149 /** 150 * The default style a simple node should use for it's text 151 */ 152 public static final BoxTextElement SIMPLE_NODE_TEXT_ELEMSTYLE; 153 static { 154 MultiCascade mc = new MultiCascade(); 155 Cascade c = mc.getOrCreateCascade("default"); 156 c.put(TEXT, Keyword.AUTO); 157 Node n = new Node(); 158 n.put("name", "dummy"); 159 SIMPLE_NODE_TEXT_ELEMSTYLE = create(new Environment(n, mc, "default", null), NodeElement.SIMPLE_NODE_ELEMSTYLE.getBoxProvider()); 160 if (SIMPLE_NODE_TEXT_ELEMSTYLE == null) throw new AssertionError(); 161 } 162 163 /** 164 * Caches the default text color from the preferences. 165 * 166 * FIXME: the cache isn't updated if the user changes the preference during a JOSM 167 * session. There should be preference listener updating this cache. 168 */ 169 private static volatile Color defaultTextColorCache; 170 171 /** 172 * The text this element should display. 173 */ 174 public TextLabel text; 175 /** 176 * The x offset of the text. 177 */ 178 public int xOffset; 179 /** 180 * The y offset of the text. In screen space (inverted to user space) 181 */ 182 public int yOffset; 183 /** 184 * The {@link HorizontalTextAlignment} for this text. 185 */ 186 public HorizontalTextAlignment hAlign; 187 /** 188 * The {@link VerticalTextAlignment} for this text. 189 */ 190 public VerticalTextAlignment vAlign; 191 protected BoxProvider boxProvider; 192 193 /** 194 * Create a new {@link BoxTextElement} 195 * @param c The current cascade 196 * @param text The text to display 197 * @param boxProvider The box provider to use 198 * @param offsetX x offset, in screen space 199 * @param offsetY y offset, in screen space 200 * @param hAlign The {@link HorizontalTextAlignment} 201 * @param vAlign The {@link VerticalTextAlignment} 202 */ 203 public BoxTextElement(Cascade c, TextLabel text, BoxProvider boxProvider, 204 int offsetX, int offsetY, HorizontalTextAlignment hAlign, VerticalTextAlignment vAlign) { 205 super(c, 5f); 206 xOffset = offsetX; 207 yOffset = offsetY; 208 CheckParameterUtil.ensureParameterNotNull(text); 209 CheckParameterUtil.ensureParameterNotNull(hAlign); 210 CheckParameterUtil.ensureParameterNotNull(vAlign); 211 this.text = text; 212 this.boxProvider = boxProvider; 213 this.hAlign = hAlign; 214 this.vAlign = vAlign; 215 } 216 217 /** 218 * Create a new {@link BoxTextElement} with a boxprovider and a box. 219 * @param env The MapCSS environment 220 * @param boxProvider The box provider. 221 * @return A new {@link BoxTextElement} or <code>null</code> if the creation failed. 222 */ 223 public static BoxTextElement create(Environment env, BoxProvider boxProvider) { 224 initDefaultParameters(); 225 226 TextLabel text = TextLabel.create(env, defaultTextColorCache, false); 227 if (text == null) return null; 228 // Skip any primitives that don't have text to draw. (Styles are recreated for any tag change.) 229 // The concrete text to render is not cached in this object, but computed for each 230 // repaint. This way, one BoxTextElement object can be used by multiple primitives (to save memory). 231 if (text.labelCompositionStrategy.compose(env.osm) == null) return null; 232 233 Cascade c = env.mc.getCascade(env.layer); 234 235 HorizontalTextAlignment hAlign; 236 switch (c.get(TEXT_ANCHOR_HORIZONTAL, Keyword.RIGHT, Keyword.class).val) { 237 case "left": 238 hAlign = HorizontalTextAlignment.LEFT; 239 break; 240 case "center": 241 hAlign = HorizontalTextAlignment.CENTER; 242 break; 243 case "right": 244 default: 245 hAlign = HorizontalTextAlignment.RIGHT; 246 } 247 VerticalTextAlignment vAlign; 248 switch (c.get(TEXT_ANCHOR_VERTICAL, Keyword.BOTTOM, Keyword.class).val) { 249 case "above": 250 vAlign = VerticalTextAlignment.ABOVE; 251 break; 252 case "top": 253 vAlign = VerticalTextAlignment.TOP; 254 break; 255 case "center": 256 vAlign = VerticalTextAlignment.CENTER; 257 break; 258 case "below": 259 vAlign = VerticalTextAlignment.BELOW; 260 break; 261 case "bottom": 262 default: 263 vAlign = VerticalTextAlignment.BOTTOM; 264 } 265 Point2D offset = TextLabel.getTextOffset(c); 266 267 return new BoxTextElement(c, text, boxProvider, (int) offset.getX(), (int) -offset.getY(), hAlign, vAlign); 268 } 269 270 /** 271 * Get the box in which the content should be drawn. 272 * @return The box. 273 */ 274 public Rectangle getBox() { 275 return boxProvider.get().getBox(); 276 } 277 278 private static void initDefaultParameters() { 279 if (defaultTextColorCache != null) return; 280 defaultTextColorCache = PaintColors.TEXT.get(); 281 } 282 283 @Override 284 public void paintPrimitive(OsmPrimitive osm, MapPaintSettings settings, StyledMapRenderer painter, 285 boolean selected, boolean outermember, boolean member) { 286 if (osm instanceof Node) { 287 painter.drawBoxText((Node) osm, this); 288 } 289 } 290 291 @Override 292 public boolean equals(Object obj) { 293 if (this == obj) return true; 294 if (obj == null || getClass() != obj.getClass()) return false; 295 if (!super.equals(obj)) return false; 296 BoxTextElement that = (BoxTextElement) obj; 297 return hAlign == that.hAlign && 298 vAlign == that.vAlign && 299 xOffset == that.xOffset && 300 yOffset == that.yOffset && 301 Objects.equals(text, that.text) && 302 Objects.equals(boxProvider, that.boxProvider); 303 } 304 305 @Override 306 public int hashCode() { 307 return Objects.hash(super.hashCode(), text, boxProvider, hAlign, vAlign, xOffset, yOffset); 308 } 309 310 @Override 311 public String toString() { 312 return "BoxTextElement{" + super.toString() + ' ' + text.toStringImpl() 313 + " box=" + getBox() + " hAlign=" + hAlign + " vAlign=" + vAlign + " xOffset=" + xOffset + " yOffset=" + yOffset + '}'; 314 } 315}