001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint.styleelement; 003 004import java.awt.Color; 005import java.awt.Rectangle; 006 007import org.openstreetmap.josm.data.osm.Node; 008import org.openstreetmap.josm.data.osm.OsmPrimitive; 009import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings; 010import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 011import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer; 012import org.openstreetmap.josm.gui.mappaint.Cascade; 013import org.openstreetmap.josm.gui.mappaint.Environment; 014import org.openstreetmap.josm.gui.mappaint.Keyword; 015import org.openstreetmap.josm.gui.mappaint.MultiCascade; 016import org.openstreetmap.josm.tools.CheckParameterUtil; 017 018/** 019 * Text style attached to a style with a bounding box, like an icon or a symbol. 020 */ 021public class BoxTextElement extends StyleElement { 022 023 public enum HorizontalTextAlignment { LEFT, CENTER, RIGHT } 024 025 public enum VerticalTextAlignment { ABOVE, TOP, CENTER, BOTTOM, BELOW } 026 027 public interface BoxProvider { 028 BoxProviderResult get(); 029 } 030 031 public static class BoxProviderResult { 032 private final Rectangle box; 033 private final boolean temporary; 034 035 public BoxProviderResult(Rectangle box, boolean temporary) { 036 this.box = box; 037 this.temporary = temporary; 038 } 039 040 /** 041 * Returns the box. 042 * @return the box 043 */ 044 public Rectangle getBox() { 045 return box; 046 } 047 048 /** 049 * Determines if the box can change in future calls of the {@link BoxProvider#get()} method 050 * @return {@code true} if the box can change in future calls of the {@code BoxProvider#get()} method 051 */ 052 public boolean isTemporary() { 053 return temporary; 054 } 055 } 056 057 public static class SimpleBoxProvider implements BoxProvider { 058 private final Rectangle box; 059 060 /** 061 * Constructs a new {@code SimpleBoxProvider}. 062 * @param box the box 063 */ 064 public SimpleBoxProvider(Rectangle box) { 065 this.box = box; 066 } 067 068 @Override 069 public BoxProviderResult get() { 070 return new BoxProviderResult(box, false); 071 } 072 073 @Override 074 public int hashCode() { 075 return box.hashCode(); 076 } 077 078 @Override 079 public boolean equals(Object obj) { 080 if (!(obj instanceof BoxProvider)) 081 return false; 082 final BoxProvider other = (BoxProvider) obj; 083 BoxProviderResult resultOther = other.get(); 084 if (resultOther.isTemporary()) return false; 085 return box.equals(resultOther.getBox()); 086 } 087 } 088 089 public static final Rectangle ZERO_BOX = new Rectangle(0, 0, 0, 0); 090 091 public TextLabel text; 092 // Either boxProvider or box is not null. If boxProvider is different from 093 // null, this means, that the box can still change in future, otherwise 094 // it is fixed. 095 protected BoxProvider boxProvider; 096 protected Rectangle box; 097 public HorizontalTextAlignment hAlign; 098 public VerticalTextAlignment vAlign; 099 100 public BoxTextElement(Cascade c, TextLabel text, BoxProvider boxProvider, Rectangle box, 101 HorizontalTextAlignment hAlign, VerticalTextAlignment vAlign) { 102 super(c, 5f); 103 CheckParameterUtil.ensureParameterNotNull(text); 104 CheckParameterUtil.ensureParameterNotNull(hAlign); 105 CheckParameterUtil.ensureParameterNotNull(vAlign); 106 this.text = text; 107 this.boxProvider = boxProvider; 108 this.box = box == null ? ZERO_BOX : box; 109 this.hAlign = hAlign; 110 this.vAlign = vAlign; 111 } 112 113 public static BoxTextElement create(Environment env, BoxProvider boxProvider) { 114 return create(env, boxProvider, null); 115 } 116 117 public static BoxTextElement create(Environment env, Rectangle box) { 118 return create(env, null, box); 119 } 120 121 public static BoxTextElement create(Environment env, BoxProvider boxProvider, Rectangle box) { 122 initDefaultParameters(); 123 Cascade c = env.mc.getCascade(env.layer); 124 125 TextLabel text = TextLabel.create(env, DEFAULT_TEXT_COLOR, false); 126 if (text == null) return null; 127 // Skip any primitives that don't have text to draw. (Styles are recreated for any tag change.) 128 // The concrete text to render is not cached in this object, but computed for each 129 // repaint. This way, one BoxTextElement object can be used by multiple primitives (to save memory). 130 if (text.labelCompositionStrategy.compose(env.osm) == null) return null; 131 132 HorizontalTextAlignment hAlign = HorizontalTextAlignment.RIGHT; 133 Keyword hAlignKW = c.get(TEXT_ANCHOR_HORIZONTAL, Keyword.RIGHT, Keyword.class); 134 switch (hAlignKW.val) { 135 case "left": 136 hAlign = HorizontalTextAlignment.LEFT; 137 break; 138 case "center": 139 hAlign = HorizontalTextAlignment.CENTER; 140 } 141 VerticalTextAlignment vAlign = VerticalTextAlignment.BOTTOM; 142 Keyword vAlignKW = c.get(TEXT_ANCHOR_VERTICAL, Keyword.BOTTOM, Keyword.class); 143 switch (vAlignKW.val) { 144 case "bottom": 145 vAlign = VerticalTextAlignment.BOTTOM; 146 break; 147 case "above": 148 vAlign = VerticalTextAlignment.ABOVE; 149 break; 150 case "top": 151 vAlign = VerticalTextAlignment.TOP; 152 break; 153 case "center": 154 vAlign = VerticalTextAlignment.CENTER; 155 break; 156 case "below": 157 vAlign = VerticalTextAlignment.BELOW; 158 } 159 160 return new BoxTextElement(c, text, boxProvider, box, hAlign, vAlign); 161 } 162 163 public Rectangle getBox() { 164 if (boxProvider != null) { 165 BoxProviderResult result = boxProvider.get(); 166 if (!result.isTemporary()) { 167 box = result.getBox(); 168 boxProvider = null; 169 } 170 return result.getBox(); 171 } 172 return box; 173 } 174 175 public static final BoxTextElement SIMPLE_NODE_TEXT_ELEMSTYLE; 176 static { 177 MultiCascade mc = new MultiCascade(); 178 Cascade c = mc.getOrCreateCascade("default"); 179 c.put(TEXT, Keyword.AUTO); 180 Node n = new Node(); 181 n.put("name", "dummy"); 182 SIMPLE_NODE_TEXT_ELEMSTYLE = create(new Environment(n, mc, "default", null), NodeElement.SIMPLE_NODE_ELEMSTYLE.getBoxProvider()); 183 if (SIMPLE_NODE_TEXT_ELEMSTYLE == null) throw new AssertionError(); 184 } 185 186 /* 187 * Caches the default text color from the preferences. 188 * 189 * FIXME: the cache isn't updated if the user changes the preference during a JOSM 190 * session. There should be preference listener updating this cache. 191 */ 192 private static volatile Color DEFAULT_TEXT_COLOR; 193 194 private static void initDefaultParameters() { 195 if (DEFAULT_TEXT_COLOR != null) return; 196 DEFAULT_TEXT_COLOR = PaintColors.TEXT.get(); 197 } 198 199 @Override 200 public void paintPrimitive(OsmPrimitive osm, MapPaintSettings settings, StyledMapRenderer painter, 201 boolean selected, boolean outermember, boolean member) { 202 if (osm instanceof Node) { 203 painter.drawBoxText((Node) osm, this); 204 } 205 } 206 207 @Override 208 public boolean equals(Object obj) { 209 if (!super.equals(obj)) 210 return false; 211 if (obj == null || getClass() != obj.getClass()) 212 return false; 213 final BoxTextElement other = (BoxTextElement) obj; 214 if (!text.equals(other.text)) return false; 215 if (boxProvider != null) { 216 if (!boxProvider.equals(other.boxProvider)) return false; 217 } else if (other.boxProvider != null) 218 return false; 219 else { 220 if (!box.equals(other.box)) return false; 221 } 222 if (hAlign != other.hAlign) return false; 223 if (vAlign != other.vAlign) return false; 224 return true; 225 } 226 227 @Override 228 public int hashCode() { 229 int hash = super.hashCode(); 230 hash = 97 * hash + text.hashCode(); 231 if (boxProvider != null) { 232 hash = 97 * hash + boxProvider.hashCode(); 233 } else { 234 hash = 97 * hash + box.hashCode(); 235 } 236 hash = 97 * hash + hAlign.hashCode(); 237 hash = 97 * hash + vAlign.hashCode(); 238 return hash; 239 } 240 241 @Override 242 public String toString() { 243 return "BoxTextElemStyle{" + super.toString() + ' ' + text.toStringImpl() 244 + " box=" + box + " hAlign=" + hAlign + " vAlign=" + vAlign + '}'; 245 } 246}