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