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