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