Ticket #6107: mapcss-patch.txt

File mapcss-patch.txt, 42.6 KB (added by anonymous, 11 years ago)
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 3980)
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 3980)
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 3980)
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 3980)
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 3980)
567+++ src/org/openstreetmap/josm/gui/mappaint/TextElement.java    (working copy)
568@@ -1,31 +1,53 @@
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.StaticLabelCompositionStrategy;
582+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.TagLookupCompositionStrategy;
583 import org.openstreetmap.josm.tools.CheckParameterUtil;
584 import org.openstreetmap.josm.tools.Utils;
585 
586+/**
587+ * Represents the rendering style for a textual label placed somewhere on the map.
588+ *
589+ */
590 public class TextElement {
591-    // textKey == null means automatic generation of text string, otherwise
592-    // the corresponding tag value is used
593-    public String textKey;
594+    //static private final Logger logger = Logger.getLogger(TextElement.class.getName());
595+
596+    static private final LabelCompositionStrategy AUTO_LABEL_COMPOSITION_STRATEGY = new DeriveLabelFromNameTagsCompositionStrategy();
597+
598+    /** the font to be used when rendering*/
599     public Font font;
600     public int xOffset;
601     public int yOffset;
602     public Color color;
603     public Float haloRadius;
604     public Color haloColor;
605+    /** the strategy for building the actual label value for a given a {@link OsmPrimitive}.
606+     * Check for null before accessing.
607+     */
608+    public LabelCompositionStrategy labelCompositionStrategy;
609 
610-    public TextElement(String textKey, Font font, int xOffset, int yOffset, Color color, Float haloRadius, Color haloColor) {
611+    /**
612+     * Creates a new text element
613+     *
614+     * @param strategy the strategy indicating how the text is composed for a specific {@link OsmPrimitive} to be rendered.
615+     * If null, no label is rendered.
616+     * @param font the font to be used. Must not be null.
617+     * @param xOffset
618+     * @param yOffset
619+     * @param color the color to be used. Must not be null
620+     * @param haloRadius
621+     * @param haloColor
622+     */
623+    public TextElement(LabelCompositionStrategy strategy, Font font, int xOffset, int yOffset, Color color, Float haloRadius, Color haloColor) {
624         CheckParameterUtil.ensureParameterNotNull(font);
625         CheckParameterUtil.ensureParameterNotNull(color);
626-        this.textKey = textKey;
627+        labelCompositionStrategy = strategy;
628         this.font = font;
629         this.xOffset = xOffset;
630         this.yOffset = yOffset;
631@@ -34,8 +56,13 @@
632         this.haloColor = haloColor;
633     }
634 
635+    /**
636+     * Copy constructor
637+     *
638+     * @param other the other element.
639+     */
640     public TextElement(TextElement other) {
641-        this.textKey = other.textKey;
642+        this.labelCompositionStrategy = other.labelCompositionStrategy;
643         this.font = other.font;
644         this.xOffset = other.xOffset;
645         this.yOffset = other.yOffset;
646@@ -44,17 +71,47 @@
647         this.haloRadius = other.haloRadius;
648     }
649 
650-    public static TextElement create(Cascade c, Color defTextColor) {
651-
652-        String textKey = null;
653+    /**
654+     * Derives a suitable label composition strategy from the style properties in
655+     * {@code c}.
656+     *
657+     * @param c the style properties
658+     * @return the label composition strategy
659+     */
660+    protected static LabelCompositionStrategy buildLabelCompositionStrategy(Cascade c){
661+        LabelCompositionStrategy strategy;
662         Keyword textKW = c.get("text", null, Keyword.class, true);
663         if (textKW == null) {
664-            textKey = c.get("text", null, String.class);
665-            if (textKey == null)
666-                return null;
667+            String textKey = c.get("text", null, String.class);
668+            if (textKey == null) return null;
669+            strategy = new StaticLabelCompositionStrategy(textKey);
670         } else if (!textKW.val.equals("auto"))
671             return null;
672+        else {
673+            String defaultLabelTag = c.get("text-from-tag", null, String.class);
674+            if (defaultLabelTag == null){
675+                strategy = AUTO_LABEL_COMPOSITION_STRATEGY;
676+            } else {
677+                strategy = new TagLookupCompositionStrategy(defaultLabelTag);
678+            }
679+        }
680+        return strategy;
681+    }
682 
683+    /**
684+     * Builds a text element from style properties in {@code c} and the
685+     * default text color {@code defaultTextColor}
686+     *
687+     * @param c the style properties
688+     * @param defaultTextColor the default text color. Must not be null.
689+     * @return the text element or null, if the style properties don't include
690+     * properties for text rendering
691+     * @throws IllegalArgumentException thrown if {@code defaultTextColor} is null
692+     */
693+    public static TextElement create(Cascade c, Color defaultTextColor)  throws IllegalArgumentException{
694+        CheckParameterUtil.ensureParameterNotNull(defaultTextColor, "defaultTextColor");
695+
696+        LabelCompositionStrategy strategy = buildLabelCompositionStrategy(c);
697         Font font = ElemStyle.getFont(c);
698 
699         float xOffset = 0;
700@@ -70,8 +127,8 @@
701         }
702         xOffset = c.get("text-offset-x", xOffset, Float.class);
703         yOffset = c.get("text-offset-y", yOffset, Float.class);
704-       
705-        Color color = c.get("text-color", defTextColor, Color.class);
706+
707+        Color color = c.get("text-color", defaultTextColor, Color.class);
708         float alpha = c.get("text-opacity", 1f, Float.class);
709         color = new Color(color.getRed(), color.getGreen(),
710                 color.getBlue(), Utils.color_float2int(alpha));
711@@ -88,42 +145,86 @@
712                     haloColor.getBlue(), Utils.color_float2int(haloAlpha));
713         }
714 
715-        return new TextElement(textKey, font, (int) xOffset, - (int) yOffset, color, haloRadius, haloColor);
716+        return new TextElement(strategy, font, (int) xOffset, - (int) yOffset, color, haloRadius, haloColor);
717     }
718 
719+    /**
720+     * Replies the label to be rendered for the primitive {@code osm}.
721+     *
722+     * @param osm the the OSM object
723+     * @return the label, or null, if {@code osm} is null or if no label can be
724+     * derived for {@code osm}
725+     */
726+    public String getString(OsmPrimitive osm) {
727+        if (labelCompositionStrategy == null) return null;
728+        return labelCompositionStrategy.compose(osm);
729+    }
730+
731     @Override
732-    public boolean equals(Object obj) {
733-        if (obj == null || getClass() != obj.getClass())
734-            return false;
735-        final TextElement other = (TextElement) obj;
736-        return  equal(textKey, other.textKey) &&
737-                equal(font, other.font) &&
738-                xOffset == other.xOffset &&
739-                yOffset == other.yOffset &&
740-                equal(color, other.color) &&
741-                equal(haloRadius, other.haloRadius) &&
742-                equal(haloColor, other.haloColor);
743+    public String toString() {
744+        StringBuilder sb = new StringBuilder();
745+        sb.append("{TextElement ");
746+        sb.append("strategy=");
747+        sb.append(labelCompositionStrategy == null ? "null" : labelCompositionStrategy.toString());
748+        sb.append("}");
749+        return sb.toString();
750     }
751 
752+    /* -------------------------------------------------------------------------------- */
753+    /* equals and hashCode  (generated by Eclipse, regenerate if necessary)             */
754+    /* -------------------------------------------------------------------------------- */
755     @Override
756     public int hashCode() {
757-        int hash = 3;
758-        hash = 79 * hash + (textKey != null ? textKey.hashCode() : 0);
759-        hash = 79 * hash + font.hashCode();
760-        hash = 79 * hash + xOffset;
761-        hash = 79 * hash + yOffset;
762-        hash = 79 * hash + color.hashCode();
763-        hash = 79 * hash + (haloRadius != null ? Float.floatToIntBits(haloRadius) : 0);
764-        hash = 79 * hash + (haloColor != null ? haloColor.hashCode() : 0);
765-        return hash;
766+        final int prime = 31;
767+        int result = 1;
768+        result = prime * result + ((color == null) ? 0 : color.hashCode());
769+        result = prime * result + ((font == null) ? 0 : font.hashCode());
770+        result = prime * result + ((haloColor == null) ? 0 : haloColor.hashCode());
771+        result = prime * result + ((haloRadius == null) ? 0 : haloRadius.hashCode());
772+        result = prime * result + ((labelCompositionStrategy == null) ? 0 : labelCompositionStrategy.hashCode());
773+        result = prime * result + xOffset;
774+        result = prime * result + yOffset;
775+        return result;
776     }
777 
778-    public String getString(OsmPrimitive osm, MapPainter painter) {
779-        if (textKey == null)
780-            return painter.getAreaName(osm);
781-        else
782-            return osm.get(textKey);
783+    @Override
784+    public boolean equals(Object obj) {
785+        if (this == obj)
786+            return true;
787+        if (obj == null)
788+            return false;
789+        if (getClass() != obj.getClass())
790+            return false;
791+        TextElement other = (TextElement) obj;
792+        if (color == null) {
793+            if (other.color != null)
794+                return false;
795+        } else if (!color.equals(other.color))
796+            return false;
797+        if (font == null) {
798+            if (other.font != null)
799+                return false;
800+        } else if (!font.equals(other.font))
801+            return false;
802+        if (haloColor == null) {
803+            if (other.haloColor != null)
804+                return false;
805+        } else if (!haloColor.equals(other.haloColor))
806+            return false;
807+        if (haloRadius == null) {
808+            if (other.haloRadius != null)
809+                return false;
810+        } else if (!haloRadius.equals(other.haloRadius))
811+            return false;
812+        if (labelCompositionStrategy == null) {
813+            if (other.labelCompositionStrategy != null)
814+                return false;
815+        } else if (!labelCompositionStrategy.equals(other.labelCompositionStrategy))
816+            return false;
817+        if (xOffset != other.xOffset)
818+            return false;
819+        if (yOffset != other.yOffset)
820+            return false;
821+        return true;
822     }
823-
824-
825 }
826Index: src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java
827===================================================================
828--- src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java       (revision 3980)
829+++ src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java       (working copy)
830@@ -27,7 +27,8 @@
831 import org.openstreetmap.josm.tools.Utils;
832 
833 public class MapCSSStyleSource extends StyleSource {
834-   
835+    //static private final Logger logger = Logger.getLogger(MapCSSStyleSource.class.getName());
836+
837     final public List<MapCSSRule> rules;
838     private Color backgroundColorOverride;
839 
840@@ -65,6 +66,7 @@
841         }
842     }
843 
844+    @Override
845     public InputStream getSourceInputStream() throws IOException {
846         MirroredInputStream in = new MirroredInputStream(url);
847         InputStream zip = in.getZipEntry("mapcss", "style");
848@@ -107,26 +109,28 @@
849         Environment env = new Environment(n, mc, "default", this);
850 
851         NEXT_RULE:
852-        for (MapCSSRule r : rules) {
853-            for (Selector s : r.selectors) {
854-                if ((s instanceof GeneralSelector)) {
855-                    GeneralSelector gs = (GeneralSelector) s;
856-                    if (gs.base.equals(type))
857-                     {
858-                        for (Condition cnd : gs.conds) {
859-                            if (!cnd.applies(env))
860-                                continue NEXT_RULE;
861+            for (MapCSSRule r : rules) {
862+                for (Selector s : r.selectors) {
863+                    if ((s instanceof GeneralSelector)) {
864+                        GeneralSelector gs = (GeneralSelector) s;
865+                        if (gs.base.equals(type))
866+                        {
867+                            for (Condition cnd : gs.conds) {
868+                                if (!cnd.applies(env)) {
869+                                    continue NEXT_RULE;
870+                                }
871+                            }
872+                            for (Instruction i : r.declaration) {
873+                                i.execute(env);
874+                            }
875                         }
876-                        for (Instruction i : r.declaration) {
877-                            i.execute(env);
878-                        }
879                     }
880                 }
881             }
882-        }
883         return mc.getCascade("default");
884     }
885 
886+    @Override
887     public Color getBackgroundColorOverride() {
888         return backgroundColorOverride;
889     }
890@@ -159,7 +163,7 @@
891                                 i.execute(env);
892                             }
893                         }
894-                    }
895+                    }
896                     env.layer = sub;
897                     for (Instruction i : r.declaration) {
898                         i.execute(env);
899Index: test/data/styles/label-from-tag.mapcss
900===================================================================
901--- test/data/styles/label-from-tag.mapcss      (revision 0)
902+++ test/data/styles/label-from-tag.mapcss      (revision 0)
903@@ -0,0 +1,21 @@
904+/*
905+ * Simple test style sheet. Includes a style with the new style declaration 'text-from-tag'.
906+ *
907+ */
908+
909+meta {
910+    title: "Test style - Deriving labels from tags";
911+}
912+
913+canvas {
914+    background-color: #000000;
915+}
916+
917+node {
918+   text: auto;
919+   text-color: white;   
920+   font-size: 12;
921+   /* the value of the tag 'my_label_tag' will be rendered as text */
922+   text-from-tag: my_label_tag;
923+}
924+
925Index: test/functional/org/openstreetmap/josm/fixtures/JOSMFixture.java
926===================================================================
927--- test/functional/org/openstreetmap/josm/fixtures/JOSMFixture.java    (revision 3980)
928+++ test/functional/org/openstreetmap/josm/fixtures/JOSMFixture.java    (working copy)
929@@ -10,7 +10,7 @@
930 import java.util.logging.Logger;
931 
932 import org.openstreetmap.josm.Main;
933-import org.openstreetmap.josm.data.osm.DataSetMergerTest;
934+import org.openstreetmap.josm.data.Preferences;
935 import org.openstreetmap.josm.data.projection.Mercator;
936 import org.openstreetmap.josm.io.OsmApi;
937 import org.openstreetmap.josm.tools.I18n;
938@@ -39,7 +39,7 @@
939         // load properties
940         //
941         try {
942-            testProperties.load(DataSetMergerTest.class.getResourceAsStream(testPropertiesResourceName));
943+            testProperties.load(JOSMFixture.class.getResourceAsStream(testPropertiesResourceName));
944         } catch(Exception e){
945             logger.log(Level.SEVERE, MessageFormat.format("failed to load property file ''{0}''", testPropertiesResourceName));
946             fail(MessageFormat.format("failed to load property file ''{0}''. \nMake sure the path ''$project_root/test/config'' is on the classpath.", testPropertiesResourceName));
947@@ -57,6 +57,7 @@
948             }
949         }
950         System.setProperty("josm.home", josmHome);
951+        Main.pref = new Preferences();
952         I18n.init();
953         // initialize the plaform hook, and
954         Main.determinePlatformHook();
955Index: test/unit/org/openstreetmap/josm/gui/mappaint/AllMappaintTests.groovy
956===================================================================
957--- test/unit/org/openstreetmap/josm/gui/mappaint/AllMappaintTests.groovy       (revision 0)
958+++ test/unit/org/openstreetmap/josm/gui/mappaint/AllMappaintTests.groovy       (revision 0)
959@@ -0,0 +1,15 @@
960+// License: GPL. For details, see LICENSE file.
961+package org.openstreetmap.josm.gui.mappaint
962+
963+import junit.framework.TestCase;
964+
965+import org.junit.runner.RunWith;
966+import org.junit.runners.Suite;
967+
968+@RunWith(Suite.class)
969+@Suite.SuiteClasses([
970+    LabelCompositionStrategyTest.class,
971+    MapCSSWithExtendedTextDirectivesTest.class
972+])
973+public class AllMappaintTests extends TestCase{}
974+
975Index: test/unit/org/openstreetmap/josm/gui/mappaint/LabelCompositionStrategyTest.groovy
976===================================================================
977--- test/unit/org/openstreetmap/josm/gui/mappaint/LabelCompositionStrategyTest.groovy   (revision 0)
978+++ test/unit/org/openstreetmap/josm/gui/mappaint/LabelCompositionStrategyTest.groovy   (revision 0)
979@@ -0,0 +1,66 @@
980+// License: GPL. For details, see LICENSE file.
981+package org.openstreetmap.josm.gui.mappaint
982+
983+import org.junit.*
984+import org.openstreetmap.josm.fixtures.JOSMFixture;
985+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.DeriveLabelFromNameTagsCompositionStrategy
986+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.StaticLabelCompositionStrategy;
987+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.TagLookupCompositionStrategy
988+import org.openstreetmap.josm.data.osm.Node;
989+
990+class LabelCompositionStrategyTest {
991+   
992+    @BeforeClass
993+    public static void createJOSMFixture(){
994+        JOSMFixture.createUnitTestFixture().init()
995+    }
996+
997+    @Test
998+    public void createStaticLabelCompositionStrategy() {
999+        def n = new Node()
1000+       
1001+        def strat = new StaticLabelCompositionStrategy(null)
1002+        assert strat.compose(n) == null
1003+       
1004+        strat = new StaticLabelCompositionStrategy("a label")
1005+        assert strat.compose(n) == "a label"       
1006+    }
1007+   
1008+    @Test
1009+    public void createTagLookupCompositionStrategy() {
1010+        def n = new Node()
1011+        n.put("my-tag", "my-value")
1012+       
1013+        def strat = new TagLookupCompositionStrategy(null)
1014+        assert strat.compose(n) == null
1015+       
1016+        strat = new TagLookupCompositionStrategy("name")
1017+        assert strat.compose(n) == null
1018+       
1019+        strat = new TagLookupCompositionStrategy("my-tag")
1020+        assert strat.compose(n) == "my-value"
1021+    }
1022+   
1023+    @Test
1024+    public void createDeriveLabelFromNameTagsCompositionStrategy() {
1025+        def n
1026+        def strat
1027+       
1028+        strat = new DeriveLabelFromNameTagsCompositionStrategy()
1029+        strat.setNameTags(null)
1030+        assert strat.getNameTags() == []
1031+       
1032+        strat = new DeriveLabelFromNameTagsCompositionStrategy()
1033+        strat.setNameTags(["name", "brand"])
1034+        assert strat.getNameTags() == ["name", "brand"]
1035+       
1036+        n = new Node()
1037+        n.put("brand", "my brand")       
1038+        assert strat.compose(n) == "my brand"
1039+       
1040+        n = new Node()
1041+        n.put("name", "my name")
1042+        n.put("brand", "my brand")
1043+        assert strat.compose(n) == "my name"       
1044+    }
1045+}
1046Index: test/unit/org/openstreetmap/josm/gui/mappaint/MapCSSWithExtendedTextDirectivesTest.groovy
1047===================================================================
1048--- test/unit/org/openstreetmap/josm/gui/mappaint/MapCSSWithExtendedTextDirectivesTest.groovy   (revision 0)
1049+++ test/unit/org/openstreetmap/josm/gui/mappaint/MapCSSWithExtendedTextDirectivesTest.groovy   (revision 0)
1050@@ -0,0 +1,58 @@
1051+// License: GPL. For details, see LICENSE file.
1052+package org.openstreetmap.josm.gui.mappaint
1053+
1054+import java.awt.Color;
1055+
1056+import org.junit.*;
1057+import org.openstreetmap.josm.fixtures.JOSMFixture
1058+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.DeriveLabelFromNameTagsCompositionStrategy
1059+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.StaticLabelCompositionStrategy
1060+import org.openstreetmap.josm.gui.mappaint.LabelCompositionStrategy.TagLookupCompositionStrategy
1061+class MapCSSWithExtendedTextDirectivesTest {
1062+   
1063+
1064+    @BeforeClass
1065+    public static void createJOSMFixture(){
1066+        JOSMFixture.createUnitTestFixture().init()
1067+    }
1068+
1069+    @Test
1070+    public void createAutoTextElement() {
1071+        Cascade c = new Cascade()
1072+        c.put("text", new Keyword("auto"))
1073+       
1074+        TextElement te = TextElement.create(c, Color.WHITE)
1075+        assert te.labelCompositionStrategy != null
1076+        assert te.labelCompositionStrategy instanceof DeriveLabelFromNameTagsCompositionStrategy
1077+    }
1078+   
1079+    @Test
1080+    public void createTextElementComposingTextFromTag() {
1081+        Cascade c = new Cascade()
1082+        c.put("text", new Keyword("auto"))
1083+        c.put("text-from-tag", "my_name")
1084+       
1085+        TextElement te = TextElement.create(c, Color.WHITE)
1086+        assert te.labelCompositionStrategy != null
1087+        assert te.labelCompositionStrategy instanceof TagLookupCompositionStrategy
1088+        assert te.labelCompositionStrategy.getDefaultLabelTag() == "my_name"
1089+    }
1090+   
1091+    @Test
1092+    public void createTextElementWithStaticText() {
1093+        Cascade c = new Cascade()
1094+        c.put("text","my static label")
1095+       
1096+        TextElement te = TextElement.create(c, Color.WHITE)
1097+        assert te.labelCompositionStrategy != null
1098+        assert te.labelCompositionStrategy instanceof StaticLabelCompositionStrategy       
1099+    }
1100+   
1101+    @Test
1102+    public void createNullStrategy() {
1103+        Cascade c = new Cascade()
1104+       
1105+        TextElement te = TextElement.create(c, Color.WHITE)
1106+        assert te.labelCompositionStrategy == null
1107+    }
1108+}