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