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