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