Ticket #6107: mapcss-patch-3.txt

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

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