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 Cascade c = env.mc.getCascade(env.layer); 123 124 TextLabel text = TextLabel.create(env, DEFAULT_TEXT_COLOR, false); 125 if (text == null) return null; 126 // Skip any primitives that don't have text to draw. (Styles are recreated for any tag change.) 127 // The concrete text to render is not cached in this object, but computed for each 128 // repaint. This way, one BoxTextElement object can be used by multiple primitives (to save memory). 129 if (text.labelCompositionStrategy.compose(env.osm) == null) return null; 130 131 HorizontalTextAlignment hAlign = HorizontalTextAlignment.RIGHT; 132 Keyword hAlignKW = c.get(TEXT_ANCHOR_HORIZONTAL, Keyword.RIGHT, Keyword.class); 133 switch (hAlignKW.val) { 134 case "left": 135 hAlign = HorizontalTextAlignment.LEFT; 136 break; 137 case "center": 138 hAlign = HorizontalTextAlignment.CENTER; 139 } 140 VerticalTextAlignment vAlign = VerticalTextAlignment.BOTTOM; 141 Keyword vAlignKW = c.get(TEXT_ANCHOR_VERTICAL, Keyword.BOTTOM, Keyword.class); 142 switch (vAlignKW.val) { 143 case "bottom": 144 vAlign = VerticalTextAlignment.BOTTOM; 145 break; 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 } 158 159 return new BoxTextElement(c, text, boxProvider, box, hAlign, vAlign); 160 } 161 162 public Rectangle getBox() { 163 if (boxProvider != null) { 164 BoxProviderResult result = boxProvider.get(); 165 if (!result.isTemporary()) { 166 box = result.getBox(); 167 boxProvider = null; 168 } 169 return result.getBox(); 170 } 171 return box; 172 } 173 174 public static final BoxTextElement SIMPLE_NODE_TEXT_ELEMSTYLE; 175 static { 176 MultiCascade mc = new MultiCascade(); 177 Cascade c = mc.getOrCreateCascade("default"); 178 c.put(TEXT, Keyword.AUTO); 179 Node n = new Node(); 180 n.put("name", "dummy"); 181 SIMPLE_NODE_TEXT_ELEMSTYLE = create(new Environment(n, mc, "default", null), NodeElement.SIMPLE_NODE_ELEMSTYLE.getBoxProvider()); 182 if (SIMPLE_NODE_TEXT_ELEMSTYLE == null) throw new AssertionError(); 183 } 184 185 /* 186 * Caches the default text color from the preferences. 187 * 188 * FIXME: the cache isn't updated if the user changes the preference during a JOSM 189 * session. There should be preference listener updating this cache. 190 */ 191 private static volatile Color DEFAULT_TEXT_COLOR; 192 193 private static void initDefaultParameters() { 194 if (DEFAULT_TEXT_COLOR != null) return; 195 DEFAULT_TEXT_COLOR = PaintColors.TEXT.get(); 196 } 197 198 @Override 199 public void paintPrimitive(OsmPrimitive osm, MapPaintSettings settings, StyledMapRenderer painter, 200 boolean selected, boolean outermember, boolean member) { 201 if (osm instanceof Node) { 202 painter.drawBoxText((Node) osm, this); 203 } 204 } 205 206 @Override 207 public boolean equals(Object obj) { 208 if (this == obj) return true; 209 if (obj == null || getClass() != obj.getClass()) return false; 210 if (!super.equals(obj)) return false; 211 BoxTextElement that = (BoxTextElement) obj; 212 return Objects.equals(text, that.text) && 213 Objects.equals(boxProvider, that.boxProvider) && 214 Objects.equals(box, that.box) && 215 hAlign == that.hAlign && 216 vAlign == that.vAlign; 217 } 218 219 @Override 220 public int hashCode() { 221 return Objects.hash(super.hashCode(), text, boxProvider, box, hAlign, vAlign); 222 } 223 224 @Override 225 public String toString() { 226 return "BoxTextElemStyle{" + super.toString() + ' ' + text.toStringImpl() 227 + " box=" + box + " hAlign=" + hAlign + " vAlign=" + vAlign + '}'; 228 } 229}