Ticket #6107: mapcss-patch-2.txt

File mapcss-patch-2.txt, 42.3 KB (added by anonymous, 11 years ago)

Replaces former mapcss-patch.txt

Line 
1Index: src/org/openstreetmap/josm/data/osm/visitor/paint/MapPainter.java
2===================================================================
3--- src/org/openstreetmap/josm/data/osm/visitor/paint/MapPainter.java   (revision 3986)
4+++ src/org/openstreetmap/josm/data/osm/visitor/paint/MapPainter.java   (working copy)
5@@ -38,12 +38,11 @@
6 import org.openstreetmap.josm.gui.NavigatableComponent;
7 import org.openstreetmap.josm.gui.mappaint.NodeElemStyle;
8 import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.HorizontalTextAlignment;
9-import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.Symbol;
10 import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.NodeTextElement;
11+import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.Symbol;
12 import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.VerticalTextAlignment;
13 import org.openstreetmap.josm.gui.mappaint.TextElement;
14 import org.openstreetmap.josm.tools.ImageProvider;
15-import org.openstreetmap.josm.tools.LanguageInfo;
16 import org.openstreetmap.josm.tools.Pair;
17 
18 public class MapPainter {
19@@ -73,8 +72,6 @@
20 
21     private final boolean leftHandTraffic;
22 
23-    private final Collection<String> regionalNameOrder;
24-
25     private static final double PHI = Math.toRadians(20);
26     private static final double cosPHI = Math.cos(PHI);
27     private static final double sinPHI = Math.sin(PHI);
28@@ -103,8 +100,6 @@
29         this.virtualNodeSpace = Main.pref.getInteger("mappaint.node.virtual-space", 70);
30         this.segmentNumberSpace = Main.pref.getInteger("mappaint.segmentnumber.space", 40);
31 
32-        String[] names = {"name:" + LanguageInfo.getJOSMLocaleCode(), "name", "int_name", "ref", "operator", "brand", "addr:housenumber"};
33-        this.regionalNameOrder = Main.pref.getCollection("mappaint.nameOrder", Arrays.asList(names));
34         this.circum = circum;
35         this.leftHandTraffic = leftHandTraffic;
36     }
37@@ -117,7 +112,7 @@
38      *              e.g. oneway street or waterway
39      * @param onewayReversed for oneway=-1 and similar
40      */
41-    public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor,
42+    public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor,
43             TextElement text, boolean showOrientation, boolean showHeadArrowOnly,
44             boolean showOneway, boolean onewayReversed) {
45 
46@@ -252,7 +247,7 @@
47     private void drawTextOnPath(Way way, TextElement text) {
48         if (text == null)
49             return;
50-        String name = text.getString(way, this);
51+        String name = text.getString(way);
52         if (name == null || name.equals(""))
53             return;
54 
55@@ -291,8 +286,8 @@
56         double tStart;
57 
58         if (p1[0] < p2[0] &&
59-            p1[2] < Math.PI/2 &&
60-            p1[2] > -Math.PI/2) {
61+                p1[2] < Math.PI/2 &&
62+                p1[2] > -Math.PI/2) {
63             angleOffset = 0;
64             offsetSign = 1;
65             tStart = t1;
66@@ -346,8 +341,8 @@
67                 continue;
68             }
69             return new double[] {poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx,
70-                                 poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
71-                                 Math.atan2(dy, dx)};
72+                    poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
73+                    Math.atan2(dy, dx)};
74         }
75         return null;
76     }
77@@ -494,9 +489,12 @@
78         if (!isShowNames() || text == null)
79             return;
80 
81-        String s = text.textKey == null ? getNodeName(n) : n.get(text.textKey);
82-        if (s == null)
83-            return;
84+        /*
85+         * abort if we can't compose the label to be rendered
86+         */
87+        if (text.labelCompositionStrategy == null) return;
88+        String s = text.labelCompositionStrategy.compose(n);
89+        if (s == null) return;
90 
91         Font defaultFont = g.getFont();
92         g.setFont(text.font);
93@@ -597,9 +595,12 @@
94         }
95 
96         if (text != null && isShowNames()) {
97-            String name = text.textKey == null ? getAreaName(osm) : osm.get(text.textKey);
98-            if (name == null)
99-                return;
100+            /*
101+             * abort if we can't compose the label to be rendered
102+             */
103+            if (text.labelCompositionStrategy == null) return;
104+            String name = text.labelCompositionStrategy.compose(osm);
105+            if (name == null) return;
106 
107             Rectangle pb = polygon.getBounds();
108             FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache
109@@ -930,34 +931,6 @@
110         }
111     }
112 
113-    //TODO Not a good place for this method
114-    public String getNodeName(Node n) {
115-        String name = null;
116-        if (n.hasKeys()) {
117-            for (String rn : regionalNameOrder) {
118-                name = n.get(rn);
119-                if (name != null) {
120-                    break;
121-                }
122-            }
123-        }
124-        return name;
125-    }
126-
127-    //TODO Not a good place for this method
128-    public String getAreaName(OsmPrimitive w) {
129-        String name = null;
130-        if (w.hasKeys()) {
131-            for (String rn : regionalNameOrder) {
132-                name = w.get(rn);
133-                if (name != null) {
134-                    break;
135-                }
136-            }
137-        }
138-        return name;
139-    }
140-
141     public boolean isInactive() {
142         return inactive;
143     }
144Index: src/org/openstreetmap/josm/gui/mappaint/ElemStyle.java
145===================================================================
146--- src/org/openstreetmap/josm/gui/mappaint/ElemStyle.java      (revision 3986)
147+++ src/org/openstreetmap/josm/gui/mappaint/ElemStyle.java      (working copy)
148@@ -16,7 +16,7 @@
149     public float z_index;
150     public float object_z_index;
151     public boolean isModifier;  // false, if style can serve as main style for the
152-                                // primitive; true, if it is a highlight or modifier
153+    // primitive; true, if it is a highlight or modifier
154 
155     public ElemStyle(float z_index, float object_z_index, boolean isModifier) {
156         this.z_index = z_index;
157@@ -63,7 +63,7 @@
158         String name = c.get("font-family", Main.pref.get("mappaint.font", "Helvetica"), String.class);
159         float size = c.get("font-size", (float) Main.pref.getInteger("mappaint.fontsize", 8), Float.class);
160         int weight = Font.PLAIN;
161-        Keyword weightKW = c.get("font-wheight", null, Keyword.class);
162+        Keyword weightKW = c.get("font-weight", null, Keyword.class);
163         if (weightKW != null && equal(weightKW, "bold")) {
164             weight = Font.BOLD;
165         }
166Index: src/org/openstreetmap/josm/gui/mappaint/LabelCompositionStrategy.java
167===================================================================
168--- src/org/openstreetmap/josm/gui/mappaint/LabelCompositionStrategy.java       (revision 0)
169+++ src/org/openstreetmap/josm/gui/mappaint/LabelCompositionStrategy.java       (revision 0)
170@@ -0,0 +1,244 @@
171+// License: GPL. For details, see LICENSE file.
172+package org.openstreetmap.josm.gui.mappaint;
173+import java.util.ArrayList;
174+import java.util.Arrays;
175+import java.util.Collections;
176+import java.util.List;
177+
178+import org.openstreetmap.josm.Main;
179+import org.openstreetmap.josm.data.osm.OsmPrimitive;
180+import org.openstreetmap.josm.tools.LanguageInfo;
181+
182+/**
183+ * <p>Provides an abstract parent class and three concrete sub classes for various
184+ * strategies on how to compose the text label which can be rendered close to a node
185+ * or within an area in an OSM map.</p>
186+ *
187+ * <p>The three strategies below support three rules for composing a label:
188+ * <ul>
189+ *   <li>{@link StaticLabelCompositionStrategy} - the label is given by a static text
190+ *   specified in the MapCSS style file</li>
191+ *
192+ *   <li>{@link TagLookupCompositionStrategy} - the label is given by the content of a
193+ *   tag whose name specified in the MapCSS style file</li>
194+ *
195+ *   <li>{@link DeriveLabelFromNameTagsCompositionStrategy} - the label is given by the value
196+ *   of one
197+ *   of the configured "name tags". The list of relevant name tags can be configured
198+ *   in the JOSM preferences
199+ *   content of a tag whose name specified in the MapCSS style file, see the preference
200+ *   option <tt>mappaint.nameOrder</tt>.</li>
201+ * </ul>
202+ * </p>
203+ *
204+ */
205+public abstract class LabelCompositionStrategy {
206+
207+    static public class StaticLabelCompositionStrategy extends LabelCompositionStrategy {
208+        private String defaultLabel;
209+        public StaticLabelCompositionStrategy(String defaultLabel){
210+            this.defaultLabel = defaultLabel;
211+        }
212+
213+        @Override
214+        public String compose(OsmPrimitive primitive) {
215+            return defaultLabel;
216+        }
217+
218+        public String getDefaultLabel() {
219+            return defaultLabel;
220+        }
221+
222+        @Override
223+        public String toString() {
224+            return "{"  + getClass().getSimpleName() + " defaultLabel=" + defaultLabel + "}";
225+        }
226+
227+        @Override
228+        public int hashCode() {
229+            final int prime = 31;
230+            int result = 1;
231+            result = prime * result + ((defaultLabel == null) ? 0 : defaultLabel.hashCode());
232+            return result;
233+        }
234+
235+        @Override
236+        public boolean equals(Object obj) {
237+            if (this == obj)
238+                return true;
239+            if (obj == null)
240+                return false;
241+            if (getClass() != obj.getClass())
242+                return false;
243+            StaticLabelCompositionStrategy other = (StaticLabelCompositionStrategy) obj;
244+            if (defaultLabel == null) {
245+                if (other.defaultLabel != null)
246+                    return false;
247+            } else if (!defaultLabel.equals(other.defaultLabel))
248+                return false;
249+            return true;
250+        }
251+    }
252+
253+    static public class TagLookupCompositionStrategy extends LabelCompositionStrategy {
254+
255+        private String defaultLabelTag;
256+        public TagLookupCompositionStrategy(String defaultLabelTag){
257+            if (defaultLabelTag != null) {
258+                defaultLabelTag = defaultLabelTag.trim();
259+                if (defaultLabelTag.isEmpty()) {
260+                    defaultLabelTag = null;
261+                }
262+            }
263+            this.defaultLabelTag = defaultLabelTag;
264+        }
265+
266+        @Override
267+        public String compose(OsmPrimitive primitive) {
268+            if (defaultLabelTag == null) return null;
269+            if (primitive == null) return null;
270+            return primitive.get(defaultLabelTag);
271+        }
272+
273+        public String getDefaultLabelTag() {
274+            return defaultLabelTag;
275+        }
276+
277+        @Override
278+        public String toString() {
279+            return "{" + getClass().getSimpleName() + " defaultLabelTag=" + defaultLabelTag + "}";
280+        }
281+
282+        @Override
283+        public int hashCode() {
284+            final int prime = 31;
285+            int result = 1;
286+            result = prime * result + ((defaultLabelTag == null) ? 0 : defaultLabelTag.hashCode());
287+            return result;
288+        }
289+
290+        @Override
291+        public boolean equals(Object obj) {
292+            if (this == obj)
293+                return true;
294+            if (obj == null)
295+                return false;
296+            if (getClass() != obj.getClass())
297+                return false;
298+            TagLookupCompositionStrategy other = (TagLookupCompositionStrategy) obj;
299+            if (defaultLabelTag == null) {
300+                if (other.defaultLabelTag != null)
301+                    return false;
302+            } else if (!defaultLabelTag.equals(other.defaultLabelTag))
303+                return false;
304+            return true;
305+        }
306+    }
307+
308+    static public class DeriveLabelFromNameTagsCompositionStrategy extends LabelCompositionStrategy {
309+
310+        /**
311+         * The list of default name tags from which a label candidate is derived.
312+         */
313+        static public final String[] DEFAULT_NAME_TAGS = {
314+            "name:" + LanguageInfo.getJOSMLocaleCode(),
315+            "name",
316+            "int_name",
317+            "ref",
318+            "operator",
319+            "brand",
320+            "addr:housenumber"
321+        };
322+
323+        private  List<String> nameTags = new ArrayList<String>();
324+
325+        /**
326+         * <p>Creates the strategy and initializes its name tags from the preferences.</p>
327+         *
328+         * <p><strong>Note:</strong> If the list of name tags in the preferences changes, strategy instances
329+         * are not notified. It's up to the client to listen to preference changes and
330+         * invoke {@link #initNameTagsFromPreferences()} accordingly.</p>
331+         *
332+         */
333+        public DeriveLabelFromNameTagsCompositionStrategy() {
334+            initNameTagsFromPreferences();
335+        }
336+
337+        /**
338+         * Sets the name tags to be looked up in order to build up the label
339+         *
340+         * @param nameTags the name tags. null values are ignore.
341+         */
342+        public void setNameTags(List<String> nameTags){
343+            if (nameTags == null) {
344+                nameTags = Collections.emptyList();
345+            }
346+            this.nameTags = new ArrayList<String>();
347+            for(String tag: nameTags) {
348+                if (tag == null) {
349+                    continue;
350+                }
351+                tag = tag.trim();
352+                if (tag.isEmpty()) {
353+                    continue;
354+                }
355+                this.nameTags.add(tag);
356+            }
357+        }
358+
359+        /**
360+         * Replies an unmodifiable list of the name tags used to compose the label.
361+         *
362+         * @return the list of name tags
363+         */
364+        public List<String> getNameTags() {
365+            return Collections.unmodifiableList(nameTags);
366+        }
367+
368+        /**
369+         * Initializes the name tags to use from a list of default name tags (see
370+         * {@link #DEFAULT_NAME_TAGS}) and from name tags configured in the preferences
371+         * using the preference key <tt>mappaint.nameOrder</tt>.
372+         */
373+        public void initNameTagsFromPreferences() {
374+            if (Main.pref == null){
375+                this.nameTags = new ArrayList<String>(Arrays.asList(DEFAULT_NAME_TAGS));
376+            } else {
377+                this.nameTags = new ArrayList<String>(
378+                        Main.pref.getCollection("mappaint.nameOrder", Arrays.asList(DEFAULT_NAME_TAGS))
379+                );
380+            }
381+        }
382+
383+        private String getPrimitiveName(OsmPrimitive n) {
384+            String name = null;
385+            if (!n.hasKeys()) return null;
386+            for (String rn : nameTags) {
387+                name = n.get(rn);
388+                if (name != null) return name;
389+            }
390+            return null;
391+        }
392+
393+        @Override
394+        public String compose(OsmPrimitive primitive) {
395+            if (primitive == null) return null;
396+            return getPrimitiveName(primitive);
397+        }
398+
399+        @Override
400+        public String toString() {
401+            return "{" + getClass().getSimpleName() +"}";
402+        }
403+    }
404+
405+    /**
406+     * Replies the text value to be rendered as label for the primitive {@code primitive}.
407+     *
408+     * @param primitive the primitive
409+     *
410+     * @return the text value to be rendered or null, if primitive is null or
411+     * if no suitable value could be composed
412+     */
413+    abstract public String compose(OsmPrimitive primitive);
414+}
415Index: src/org/openstreetmap/josm/gui/mappaint/MultiCascade.java
416===================================================================
417--- src/org/openstreetmap/josm/gui/mappaint/MultiCascade.java   (revision 3986)
418+++ src/org/openstreetmap/josm/gui/mappaint/MultiCascade.java   (working copy)
419@@ -6,13 +6,15 @@
420 import java.util.Map;
421 import java.util.Map.Entry;
422 
423+import org.openstreetmap.josm.tools.CheckParameterUtil;
424+
425 /**
426  * Several layers / cascades, e.g. one for the main Line and one for each overlay.
427  * The range is (0,Infinity) at first and it shrinks in the process when
428  * StyleSources apply zoom level dependent properties.
429  */
430 public class MultiCascade {
431-   
432+
433     private Map<String, Cascade> layers;
434     public Range range;
435 
436@@ -27,8 +29,7 @@
437      * a clone of the "*" layer, if it exists.
438      */
439     public Cascade getOrCreateCascade(String layer) {
440-        if (layer == null)
441-            throw new IllegalArgumentException();
442+        CheckParameterUtil.ensureParameterNotNull(layer);
443         Cascade c = layers.get(layer);
444         if (c == null) {
445             if (layers.containsKey("*")) {
446Index: src/org/openstreetmap/josm/gui/mappaint/NodeElemStyle.java
447===================================================================
448--- src/org/openstreetmap/josm/gui/mappaint/NodeElemStyle.java  (revision 3986)
449+++ src/org/openstreetmap/josm/gui/mappaint/NodeElemStyle.java  (working copy)
450@@ -25,6 +25,7 @@
451  * applies for Nodes and turn restriction relations
452  */
453 public class NodeElemStyle extends ElemStyle {
454+    //static private final Logger logger = Logger.getLogger(NodeElemStyle.class.getName());
455 
456     public ImageIcon icon;
457     public int iconAlpha;
458@@ -62,10 +63,10 @@
459                 return false;
460             final Symbol other = (Symbol) obj;
461             return  symbol == other.symbol &&
462-                    size == other.size &&
463-                    equal(stroke, other.stroke) &&
464-                    equal(strokeColor, other.strokeColor) &&
465-                    equal(fillColor, other.fillColor);
466+            size == other.size &&
467+            equal(stroke, other.stroke) &&
468+            equal(strokeColor, other.strokeColor) &&
469+            equal(fillColor, other.fillColor);
470         }
471 
472         @Override
473@@ -82,8 +83,8 @@
474         @Override
475         public String toString() {
476             return "symbol=" + symbol + " size=" + size +
477-                    (stroke != null ? (" stroke=" + stroke + " strokeColor=" + strokeColor) : "") +
478-                    (fillColor != null ? (" fillColor=" + fillColor) : "");
479+            (stroke != null ? (" stroke=" + stroke + " strokeColor=" + strokeColor) : "") +
480+            (fillColor != null ? (" fillColor=" + fillColor) : "");
481         }
482     }
483 
484@@ -107,7 +108,7 @@
485                 return false;
486             final NodeTextElement other = (NodeTextElement) obj;
487             return hAlign == other.hAlign &&
488-                    vAlign == other.vAlign;
489+            vAlign == other.vAlign;
490         }
491 
492         @Override
493@@ -162,9 +163,6 @@
494             symbol = createSymbol(env);
495         }
496 
497-        if (icon == null && symbol == null && !allowOnlyText)
498-            return null;
499-
500         NodeTextElement text = null;
501         TextElement te = TextElement.create(c, PaintColors.TEXT.get());
502         if (te != null) {
503@@ -192,7 +190,7 @@
504             }
505             text = new NodeTextElement(te, hAlign, vAlign);
506         }
507-       
508+
509         return new NodeElemStyle(c, icon, iconAlpha, symbol, text);
510     }
511 
512@@ -224,7 +222,7 @@
513             shape = SymbolShape.DECAGON;
514         } else
515             return null;
516-       
517+
518         Float sizeOnDefault = c_def.get("symbol-size", null, Float.class);
519         if (sizeOnDefault != null && sizeOnDefault <= 0) {
520             sizeOnDefault = null;
521@@ -258,8 +256,9 @@
522         }
523 
524         Color fillColor = c.get("symbol-fill-color", null, Color.class);
525-        if (stroke == null && fillColor == null)
526+        if (stroke == null && fillColor == null) {
527             fillColor = Color.BLUE;
528+        }
529 
530         if (fillColor != null) {
531             float fillAlpha = c.get("symbol-fill-opacity", 1f, Float.class);
532@@ -335,14 +334,14 @@
533                     }
534 
535                     final int size = Utils.max((selected ? settings.getSelectedNodeSize() : 0),
536-                                            (n.isTagged() ? settings.getTaggedNodeSize() : 0),
537-                                            (isConnection ? settings.getConnectionNodeSize() : 0),
538-                                            settings.getUnselectedNodeSize());
539+                            (n.isTagged() ? settings.getTaggedNodeSize() : 0),
540+                            (isConnection ? settings.getConnectionNodeSize() : 0),
541+                            settings.getUnselectedNodeSize());
542 
543                     final boolean fill = (selected && settings.isFillSelectedNode()) ||
544-                                            (n.isTagged() && settings.isFillTaggedNode()) ||
545-                                            (isConnection && settings.isFillConnectionNode()) ||
546-                                            settings.isFillUnselectedNode();
547+                    (n.isTagged() && settings.isFillTaggedNode()) ||
548+                    (isConnection && settings.isFillConnectionNode()) ||
549+                    settings.isFillUnselectedNode();
550 
551                     painter.drawNode(n, color, size, fill, text);
552                 }
553@@ -394,8 +393,8 @@
554     @Override
555     public String toString() {
556         return "NodeElemStyle{" + super.toString() +
557-                (icon != null ? ("icon=" + icon + " iconAlpha=" + iconAlpha) : "") +
558-                (symbol != null ? (" symbol=[" + symbol + "]") : "") + '}';
559+        (icon != null ? ("icon=" + icon + " iconAlpha=" + iconAlpha) : "") +
560+        (symbol != null ? (" symbol=[" + symbol + "]") : "") + '}';
561     }
562 
563 }
564Index: src/org/openstreetmap/josm/gui/mappaint/TextElement.java
565===================================================================
566--- src/org/openstreetmap/josm/gui/mappaint/TextElement.java    (revision 3986)
567+++ src/org/openstreetmap/josm/gui/mappaint/TextElement.java    (working copy)
568@@ -1,31 +1,52 @@
569 // License: GPL. For details, see LICENSE file.
570 package org.openstreetmap.josm.gui.mappaint;
571 
572-import static org.openstreetmap.josm.tools.Utils.equal;
573-
574 import java.awt.Color;
575 import java.awt.Font;
576+
577 import org.openstreetmap.josm.data.osm.OsmPrimitive;
578-import org.openstreetmap.josm.data.osm.visitor.paint.MapPainter;
579-
580+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.DeriveLabelFromNameTagsCompositionStrategy;
581+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.TagLookupCompositionStrategy;
582 import org.openstreetmap.josm.tools.CheckParameterUtil;
583 import org.openstreetmap.josm.tools.Utils;
584 
585+/**
586+ * Represents the rendering style for a textual label placed somewhere on the map.
587+ *
588+ */
589 public class TextElement {
590-    // textKey == null means automatic generation of text string, otherwise
591-    // the corresponding tag value is used
592-    public String textKey;
593+    //static private final Logger logger = Logger.getLogger(TextElement.class.getName());
594+
595+    static private final LabelCompositionStrategy AUTO_LABEL_COMPOSITION_STRATEGY = new DeriveLabelFromNameTagsCompositionStrategy();
596+
597+    /** the font to be used when rendering*/
598     public Font font;
599     public int xOffset;
600     public int yOffset;
601     public Color color;
602     public Float haloRadius;
603     public Color haloColor;
604+    /** the strategy for building the actual label value for a given a {@link OsmPrimitive}.
605+     * Check for null before accessing.
606+     */
607+    public LabelCompositionStrategy labelCompositionStrategy;
608 
609-    public TextElement(String textKey, Font font, int xOffset, int yOffset, Color color, Float haloRadius, Color haloColor) {
610+    /**
611+     * Creates a new text element
612+     *
613+     * @param strategy the strategy indicating how the text is composed for a specific {@link OsmPrimitive} to be rendered.
614+     * If null, no label is rendered.
615+     * @param font the font to be used. Must not be null.
616+     * @param xOffset
617+     * @param yOffset
618+     * @param color the color to be used. Must not be null
619+     * @param haloRadius
620+     * @param haloColor
621+     */
622+    public TextElement(LabelCompositionStrategy strategy, Font font, int xOffset, int yOffset, Color color, Float haloRadius, Color haloColor) {
623         CheckParameterUtil.ensureParameterNotNull(font);
624         CheckParameterUtil.ensureParameterNotNull(color);
625-        this.textKey = textKey;
626+        labelCompositionStrategy = strategy;
627         this.font = font;
628         this.xOffset = xOffset;
629         this.yOffset = yOffset;
630@@ -34,8 +55,13 @@
631         this.haloColor = haloColor;
632     }
633 
634+    /**
635+     * Copy constructor
636+     *
637+     * @param other the other element.
638+     */
639     public TextElement(TextElement other) {
640-        this.textKey = other.textKey;
641+        this.labelCompositionStrategy = other.labelCompositionStrategy;
642         this.font = other.font;
643         this.xOffset = other.xOffset;
644         this.yOffset = other.yOffset;
645@@ -44,17 +70,39 @@
646         this.haloRadius = other.haloRadius;
647     }
648 
649-    public static TextElement create(Cascade c, Color defTextColor) {
650-
651-        String textKey = null;
652+    /**
653+     * Derives a suitable label composition strategy from the style properties in
654+     * {@code c}.
655+     *
656+     * @param c the style properties
657+     * @return the label composition strategy
658+     */
659+    protected static LabelCompositionStrategy buildLabelCompositionStrategy(Cascade c){
660         Keyword textKW = c.get("text", null, Keyword.class, true);
661         if (textKW == null) {
662-            textKey = c.get("text", null, String.class);
663-            if (textKey == null)
664-                return null;
665-        } else if (!textKW.val.equals("auto"))
666-            return null;
667+            String textKey = c.get("text", null, String.class);
668+            if (textKey == null) return null;
669+            return new TagLookupCompositionStrategy(textKey);
670+        } else if (textKW.val.equals("auto"))
671+            return AUTO_LABEL_COMPOSITION_STRATEGY;
672+        else
673+            return new TagLookupCompositionStrategy(textKW.val);
674+    }
675 
676+    /**
677+     * Builds a text element from style properties in {@code c} and the
678+     * default text color {@code defaultTextColor}
679+     *
680+     * @param c the style properties
681+     * @param defaultTextColor the default text color. Must not be null.
682+     * @return the text element or null, if the style properties don't include
683+     * properties for text rendering
684+     * @throws IllegalArgumentException thrown if {@code defaultTextColor} is null
685+     */
686+    public static TextElement create(Cascade c, Color defaultTextColor)  throws IllegalArgumentException{
687+        CheckParameterUtil.ensureParameterNotNull(defaultTextColor, "defaultTextColor");
688+
689+        LabelCompositionStrategy strategy = buildLabelCompositionStrategy(c);
690         Font font = ElemStyle.getFont(c);
691 
692         float xOffset = 0;
693@@ -70,8 +118,8 @@
694         }
695         xOffset = c.get("text-offset-x", xOffset, Float.class);
696         yOffset = c.get("text-offset-y", yOffset, Float.class);
697-       
698-        Color color = c.get("text-color", defTextColor, Color.class);
699+
700+        Color color = c.get("text-color", defaultTextColor, Color.class);
701         float alpha = c.get("text-opacity", 1f, Float.class);
702         color = new Color(color.getRed(), color.getGreen(),
703                 color.getBlue(), Utils.color_float2int(alpha));
704@@ -88,42 +136,86 @@
705                     haloColor.getBlue(), Utils.color_float2int(haloAlpha));
706         }
707 
708-        return new TextElement(textKey, font, (int) xOffset, - (int) yOffset, color, haloRadius, haloColor);
709+        return new TextElement(strategy, font, (int) xOffset, - (int) yOffset, color, haloRadius, haloColor);
710     }
711 
712+    /**
713+     * Replies the label to be rendered for the primitive {@code osm}.
714+     *
715+     * @param osm the the OSM object
716+     * @return the label, or null, if {@code osm} is null or if no label can be
717+     * derived for {@code osm}
718+     */
719+    public String getString(OsmPrimitive osm) {
720+        if (labelCompositionStrategy == null) return null;
721+        return labelCompositionStrategy.compose(osm);
722+    }
723+
724     @Override
725-    public boolean equals(Object obj) {
726-        if (obj == null || getClass() != obj.getClass())
727-            return false;
728-        final TextElement other = (TextElement) obj;
729-        return  equal(textKey, other.textKey) &&
730-                equal(font, other.font) &&
731-                xOffset == other.xOffset &&
732-                yOffset == other.yOffset &&
733-                equal(color, other.color) &&
734-                equal(haloRadius, other.haloRadius) &&
735-                equal(haloColor, other.haloColor);
736+    public String toString() {
737+        StringBuilder sb = new StringBuilder();
738+        sb.append("{TextElement ");
739+        sb.append("strategy=");
740+        sb.append(labelCompositionStrategy == null ? "null" : labelCompositionStrategy.toString());
741+        sb.append("}");
742+        return sb.toString();
743     }
744 
745+    /* -------------------------------------------------------------------------------- */
746+    /* equals and hashCode  (generated by Eclipse, regenerate if necessary)             */
747+    /* -------------------------------------------------------------------------------- */
748     @Override
749     public int hashCode() {
750-        int hash = 3;
751-        hash = 79 * hash + (textKey != null ? textKey.hashCode() : 0);
752-        hash = 79 * hash + font.hashCode();
753-        hash = 79 * hash + xOffset;
754-        hash = 79 * hash + yOffset;
755-        hash = 79 * hash + color.hashCode();
756-        hash = 79 * hash + (haloRadius != null ? Float.floatToIntBits(haloRadius) : 0);
757-        hash = 79 * hash + (haloColor != null ? haloColor.hashCode() : 0);
758-        return hash;
759+        final int prime = 31;
760+        int result = 1;
761+        result = prime * result + ((color == null) ? 0 : color.hashCode());
762+        result = prime * result + ((font == null) ? 0 : font.hashCode());
763+        result = prime * result + ((haloColor == null) ? 0 : haloColor.hashCode());
764+        result = prime * result + ((haloRadius == null) ? 0 : haloRadius.hashCode());
765+        result = prime * result + ((labelCompositionStrategy == null) ? 0 : labelCompositionStrategy.hashCode());
766+        result = prime * result + xOffset;
767+        result = prime * result + yOffset;
768+        return result;
769     }
770 
771-    public String getString(OsmPrimitive osm, MapPainter painter) {
772-        if (textKey == null)
773-            return painter.getAreaName(osm);
774-        else
775-            return osm.get(textKey);
776+    @Override
777+    public boolean equals(Object obj) {
778+        if (this == obj)
779+            return true;
780+        if (obj == null)
781+            return false;
782+        if (getClass() != obj.getClass())
783+            return false;
784+        TextElement other = (TextElement) obj;
785+        if (color == null) {
786+            if (other.color != null)
787+                return false;
788+        } else if (!color.equals(other.color))
789+            return false;
790+        if (font == null) {
791+            if (other.font != null)
792+                return false;
793+        } else if (!font.equals(other.font))
794+            return false;
795+        if (haloColor == null) {
796+            if (other.haloColor != null)
797+                return false;
798+        } else if (!haloColor.equals(other.haloColor))
799+            return false;
800+        if (haloRadius == null) {
801+            if (other.haloRadius != null)
802+                return false;
803+        } else if (!haloRadius.equals(other.haloRadius))
804+            return false;
805+        if (labelCompositionStrategy == null) {
806+            if (other.labelCompositionStrategy != null)
807+                return false;
808+        } else if (!labelCompositionStrategy.equals(other.labelCompositionStrategy))
809+            return false;
810+        if (xOffset != other.xOffset)
811+            return false;
812+        if (yOffset != other.yOffset)
813+            return false;
814+        return true;
815     }
816-
817-
818 }
819Index: src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java
820===================================================================
821--- src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java       (revision 3986)
822+++ src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java       (working copy)
823@@ -27,7 +27,8 @@
824 import org.openstreetmap.josm.tools.Utils;
825 
826 public class MapCSSStyleSource extends StyleSource {
827-   
828+    //static private final Logger logger = Logger.getLogger(MapCSSStyleSource.class.getName());
829+
830     final public List<MapCSSRule> rules;
831     private Color backgroundColorOverride;
832 
833@@ -65,6 +66,7 @@
834         }
835     }
836 
837+    @Override
838     public InputStream getSourceInputStream() throws IOException {
839         MirroredInputStream in = new MirroredInputStream(url);
840         InputStream zip = in.getZipEntry("mapcss", "style");
841@@ -107,26 +109,28 @@
842         Environment env = new Environment(n, mc, "default", this);
843 
844         NEXT_RULE:
845-        for (MapCSSRule r : rules) {
846-            for (Selector s : r.selectors) {
847-                if ((s instanceof GeneralSelector)) {
848-                    GeneralSelector gs = (GeneralSelector) s;
849-                    if (gs.base.equals(type))
850-                     {
851-                        for (Condition cnd : gs.conds) {
852-                            if (!cnd.applies(env))
853-                                continue NEXT_RULE;
854+            for (MapCSSRule r : rules) {
855+                for (Selector s : r.selectors) {
856+                    if ((s instanceof GeneralSelector)) {
857+                        GeneralSelector gs = (GeneralSelector) s;
858+                        if (gs.base.equals(type))
859+                        {
860+                            for (Condition cnd : gs.conds) {
861+                                if (!cnd.applies(env)) {
862+                                    continue NEXT_RULE;
863+                                }
864+                            }
865+                            for (Instruction i : r.declaration) {
866+                                i.execute(env);
867+                            }
868                         }
869-                        for (Instruction i : r.declaration) {
870-                            i.execute(env);
871-                        }
872                     }
873                 }
874             }
875-        }
876         return mc.getCascade("default");
877     }
878 
879+    @Override
880     public Color getBackgroundColorOverride() {
881         return backgroundColorOverride;
882     }
883@@ -159,7 +163,7 @@
884                                 i.execute(env);
885                             }
886                         }
887-                    }
888+                    }
889                     env.layer = sub;
890                     for (Instruction i : r.declaration) {
891                         i.execute(env);
892Index: test/data/styles/label-from-tag.mapcss
893===================================================================
894--- test/data/styles/label-from-tag.mapcss      (revision 0)
895+++ test/data/styles/label-from-tag.mapcss      (revision 0)
896@@ -0,0 +1,20 @@
897+/*
898+ * Simple test style sheet. Includes a style for nodes where the label is derived
899+ * from the value of a specific tag.
900+ *
901+ */
902+
903+meta {
904+    title: "Test style - Deriving labels from tags";
905+}
906+
907+canvas {
908+    background-color: #000000;
909+}
910+
911+node {
912+   text: my_label_tag;  /* take the value of the tag 'my_label_tag' as text */
913+   text-color: white;   
914+   font-size: 12;
915+}
916+
917Index: test/functional/org/openstreetmap/josm/fixtures/JOSMFixture.java
918===================================================================
919--- test/functional/org/openstreetmap/josm/fixtures/JOSMFixture.java    (revision 3986)
920+++ test/functional/org/openstreetmap/josm/fixtures/JOSMFixture.java    (working copy)
921@@ -10,7 +10,7 @@
922 import java.util.logging.Logger;
923 
924 import org.openstreetmap.josm.Main;
925-import org.openstreetmap.josm.data.osm.DataSetMergerTest;
926+import org.openstreetmap.josm.data.Preferences;
927 import org.openstreetmap.josm.data.projection.Mercator;
928 import org.openstreetmap.josm.io.OsmApi;
929 import org.openstreetmap.josm.tools.I18n;
930@@ -39,7 +39,7 @@
931         // load properties
932         //
933         try {
934-            testProperties.load(DataSetMergerTest.class.getResourceAsStream(testPropertiesResourceName));
935+            testProperties.load(JOSMFixture.class.getResourceAsStream(testPropertiesResourceName));
936         } catch(Exception e){
937             logger.log(Level.SEVERE, MessageFormat.format("failed to load property file ''{0}''", testPropertiesResourceName));
938             fail(MessageFormat.format("failed to load property file ''{0}''. \nMake sure the path ''$project_root/test/config'' is on the classpath.", testPropertiesResourceName));
939@@ -57,6 +57,7 @@
940             }
941         }
942         System.setProperty("josm.home", josmHome);
943+        Main.pref = new Preferences();
944         I18n.init();
945         // initialize the plaform hook, and
946         Main.determinePlatformHook();
947Index: test/unit/org/openstreetmap/josm/gui/mappaint/AllMappaintTests.groovy
948===================================================================
949--- test/unit/org/openstreetmap/josm/gui/mappaint/AllMappaintTests.groovy       (revision 0)
950+++ test/unit/org/openstreetmap/josm/gui/mappaint/AllMappaintTests.groovy       (revision 0)
951@@ -0,0 +1,15 @@
952+// License: GPL. For details, see LICENSE file.
953+package org.openstreetmap.josm.gui.mappaint
954+
955+import junit.framework.TestCase;
956+
957+import org.junit.runner.RunWith;
958+import org.junit.runners.Suite;
959+
960+@RunWith(Suite.class)
961+@Suite.SuiteClasses([
962+    LabelCompositionStrategyTest.class,
963+    MapCSSWithExtendedTextDirectivesTest.class
964+])
965+public class AllMappaintTests extends TestCase{}
966+
967Index: test/unit/org/openstreetmap/josm/gui/mappaint/LabelCompositionStrategyTest.groovy
968===================================================================
969--- test/unit/org/openstreetmap/josm/gui/mappaint/LabelCompositionStrategyTest.groovy   (revision 0)
970+++ test/unit/org/openstreetmap/josm/gui/mappaint/LabelCompositionStrategyTest.groovy   (revision 0)
971@@ -0,0 +1,66 @@
972+// License: GPL. For details, see LICENSE file.
973+package org.openstreetmap.josm.gui.mappaint
974+
975+import org.junit.*
976+import org.openstreetmap.josm.fixtures.JOSMFixture;
977+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.DeriveLabelFromNameTagsCompositionStrategy
978+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.StaticLabelCompositionStrategy;
979+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.TagLookupCompositionStrategy
980+import org.openstreetmap.josm.data.osm.Node;
981+
982+class LabelCompositionStrategyTest {
983+   
984+    @BeforeClass
985+    public static void createJOSMFixture(){
986+        JOSMFixture.createUnitTestFixture().init()
987+    }
988+
989+    @Test
990+    public void createStaticLabelCompositionStrategy() {
991+        def n = new Node()
992+       
993+        def strat = new StaticLabelCompositionStrategy(null)
994+        assert strat.compose(n) == null
995+       
996+        strat = new StaticLabelCompositionStrategy("a label")
997+        assert strat.compose(n) == "a label"       
998+    }
999+   
1000+    @Test
1001+    public void createTagLookupCompositionStrategy() {
1002+        def n = new Node()
1003+        n.put("my-tag", "my-value")
1004+       
1005+        def strat = new TagLookupCompositionStrategy(null)
1006+        assert strat.compose(n) == null
1007+       
1008+        strat = new TagLookupCompositionStrategy("name")
1009+        assert strat.compose(n) == null
1010+       
1011+        strat = new TagLookupCompositionStrategy("my-tag")
1012+        assert strat.compose(n) == "my-value"
1013+    }
1014+   
1015+    @Test
1016+    public void createDeriveLabelFromNameTagsCompositionStrategy() {
1017+        def n
1018+        def strat
1019+       
1020+        strat = new DeriveLabelFromNameTagsCompositionStrategy()
1021+        strat.setNameTags(null)
1022+        assert strat.getNameTags() == []
1023+       
1024+        strat = new DeriveLabelFromNameTagsCompositionStrategy()
1025+        strat.setNameTags(["name", "brand"])
1026+        assert strat.getNameTags() == ["name", "brand"]
1027+       
1028+        n = new Node()
1029+        n.put("brand", "my brand")       
1030+        assert strat.compose(n) == "my brand"
1031+       
1032+        n = new Node()
1033+        n.put("name", "my name")
1034+        n.put("brand", "my brand")
1035+        assert strat.compose(n) == "my name"       
1036+    }
1037+}
1038Index: test/unit/org/openstreetmap/josm/gui/mappaint/MapCSSWithExtendedTextDirectivesTest.groovy
1039===================================================================
1040--- test/unit/org/openstreetmap/josm/gui/mappaint/MapCSSWithExtendedTextDirectivesTest.groovy   (revision 0)
1041+++ test/unit/org/openstreetmap/josm/gui/mappaint/MapCSSWithExtendedTextDirectivesTest.groovy   (revision 0)
1042@@ -0,0 +1,58 @@
1043+// License: GPL. For details, see LICENSE file.
1044+package org.openstreetmap.josm.gui.mappaint
1045+
1046+import java.awt.Color;
1047+
1048+import org.junit.*;
1049+import org.openstreetmap.josm.fixtures.JOSMFixture
1050+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.DeriveLabelFromNameTagsCompositionStrategy
1051+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.StaticLabelCompositionStrategy
1052+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.TagLookupCompositionStrategy
1053+class MapCSSWithExtendedTextDirectivesTest {
1054+   
1055+
1056+    @BeforeClass
1057+    public static void createJOSMFixture(){
1058+        JOSMFixture.createUnitTestFixture().init()
1059+    }
1060+
1061+    @Test
1062+    public void createAutoTextElement() {
1063+        Cascade c = new Cascade()
1064+        c.put("text", new Keyword("auto"))
1065+       
1066+        TextElement te = TextElement.create(c, Color.WHITE)
1067+        assert te.labelCompositionStrategy != null
1068+        assert te.labelCompositionStrategy instanceof DeriveLabelFromNameTagsCompositionStrategy
1069+    }
1070+   
1071+    @Test
1072+    public void createTextElementComposingTextFromTag() {
1073+        Cascade c = new Cascade()
1074+        c.put("text", "my_name")
1075+       
1076+        TextElement te = TextElement.create(c, Color.WHITE)
1077+        assert te.labelCompositionStrategy != null
1078+        assert te.labelCompositionStrategy instanceof TagLookupCompositionStrategy
1079+        assert te.labelCompositionStrategy.getDefaultLabelTag() == "my_name"
1080+    }
1081+   
1082+    @Test
1083+    public void createTextElementComposingTextFromTag_2() {
1084+        Cascade c = new Cascade()
1085+        c.put("text", new Keyword("my_name"))
1086+       
1087+        TextElement te = TextElement.create(c, Color.WHITE)
1088+        assert te.labelCompositionStrategy != null
1089+        assert te.labelCompositionStrategy instanceof TagLookupCompositionStrategy
1090+        assert te.labelCompositionStrategy.getDefaultLabelTag() == "my_name"
1091+    }
1092+       
1093+    @Test
1094+    public void createNullStrategy() {
1095+        Cascade c = new Cascade()
1096+       
1097+        TextElement te = TextElement.create(c, Color.WHITE)
1098+        assert te.labelCompositionStrategy == null
1099+    }
1100+}