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    }