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