001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint.styleelement; 003 004import java.util.ArrayList; 005import java.util.Arrays; 006import java.util.Collections; 007import java.util.List; 008import java.util.Objects; 009 010import org.openstreetmap.josm.data.osm.IPrimitive; 011import org.openstreetmap.josm.spi.preferences.Config; 012import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 013import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 014import org.openstreetmap.josm.tools.LanguageInfo; 015 016/** 017 * <p>Provides an abstract parent class and three concrete sub classes for various 018 * strategies on how to compose the text label which can be rendered close to a node 019 * or within an area in an OSM map.</p> 020 * 021 * <p>The three strategies below support three rules for composing a label: 022 * <ul> 023 * <li>{@link StaticLabelCompositionStrategy} - the label is given by a static text 024 * specified in the MapCSS style file</li> 025 * 026 * <li>{@link TagLookupCompositionStrategy} - the label is given by the content of a 027 * tag whose name specified in the MapCSS style file</li> 028 * 029 * <li>{@link DeriveLabelFromNameTagsCompositionStrategy} - the label is given by the value 030 * of one of the configured "name tags". The list of relevant name tags can be configured 031 * in the JOSM preferences 032 * see the preference options <code>mappaint.nameOrder</code> and <code>mappaint.nameComplementOrder</code>.</li> 033 * </ul> 034 * @since 3987 (creation) 035 * @since 10599 (functional interface) 036 */ 037@FunctionalInterface 038public interface LabelCompositionStrategy { 039 040 /** 041 * Replies the text value to be rendered as label for the primitive {@code primitive}. 042 * 043 * @param primitive the primitive 044 * 045 * @return the text value to be rendered or null, if primitive is null or 046 * if no suitable value could be composed 047 */ 048 String compose(IPrimitive primitive); 049 050 /** 051 * Strategy where the label is given by a static text specified in the MapCSS style file. 052 */ 053 class StaticLabelCompositionStrategy implements LabelCompositionStrategy { 054 private final String defaultLabel; 055 056 public StaticLabelCompositionStrategy(String defaultLabel) { 057 this.defaultLabel = defaultLabel; 058 } 059 060 @Override 061 public String compose(IPrimitive primitive) { 062 return defaultLabel; 063 } 064 065 public String getDefaultLabel() { 066 return defaultLabel; 067 } 068 069 @Override 070 public String toString() { 071 return '{' + getClass().getSimpleName() + " defaultLabel=" + defaultLabel + '}'; 072 } 073 074 @Override 075 public int hashCode() { 076 return Objects.hash(defaultLabel); 077 } 078 079 @Override 080 public boolean equals(Object obj) { 081 if (this == obj) return true; 082 if (obj == null || getClass() != obj.getClass()) return false; 083 StaticLabelCompositionStrategy that = (StaticLabelCompositionStrategy) obj; 084 return Objects.equals(defaultLabel, that.defaultLabel); 085 } 086 } 087 088 /** 089 * Strategy where the label is given by the content of a tag whose name specified in the MapCSS style file. 090 */ 091 class TagLookupCompositionStrategy implements LabelCompositionStrategy { 092 093 private final String defaultLabelTag; 094 095 public TagLookupCompositionStrategy(String defaultLabelTag) { 096 if (defaultLabelTag != null) { 097 defaultLabelTag = defaultLabelTag.trim(); 098 if (defaultLabelTag.isEmpty()) { 099 defaultLabelTag = null; 100 } 101 } 102 this.defaultLabelTag = defaultLabelTag; 103 } 104 105 @Override 106 public String compose(IPrimitive primitive) { 107 if (defaultLabelTag == null) return null; 108 if (primitive == null) return null; 109 return primitive.get(defaultLabelTag); 110 } 111 112 public String getDefaultLabelTag() { 113 return defaultLabelTag; 114 } 115 116 @Override 117 public String toString() { 118 return '{' + getClass().getSimpleName() + " defaultLabelTag=" + defaultLabelTag + '}'; 119 } 120 121 @Override 122 public int hashCode() { 123 return Objects.hash(defaultLabelTag); 124 } 125 126 @Override 127 public boolean equals(Object obj) { 128 if (this == obj) return true; 129 if (obj == null || getClass() != obj.getClass()) return false; 130 TagLookupCompositionStrategy that = (TagLookupCompositionStrategy) obj; 131 return Objects.equals(defaultLabelTag, that.defaultLabelTag); 132 } 133 } 134 135 /** 136 * Strategy where the label is given by the value of one of the configured "name tags". 137 * The list of relevant name tags can be configured in the JOSM preferences 138 * see the preference options <code>mappaint.nameOrder</code> and <code>mappaint.nameComplementOrder</code> 139 */ 140 class DeriveLabelFromNameTagsCompositionStrategy implements LabelCompositionStrategy, PreferenceChangedListener { 141 142 /** 143 * The list of default name tags from which a label candidate is derived. 144 */ 145 private static final String[] DEFAULT_NAME_TAGS = { 146 "name:" + LanguageInfo.getJOSMLocaleCode(), 147 "name", 148 "int_name", 149 "distance", 150 "ref", 151 "operator", 152 "brand", 153 "addr:housenumber" 154 }; 155 156 /** 157 * The list of default name complement tags from which a label candidate is derived. 158 */ 159 private static final String[] DEFAULT_NAME_COMPLEMENT_TAGS = { 160 "capacity" 161 }; 162 163 private List<String> nameTags = new ArrayList<>(); 164 private List<String> nameComplementTags = new ArrayList<>(); 165 166 /** 167 * <p>Creates the strategy and initializes its name tags from the preferences.</p> 168 */ 169 public DeriveLabelFromNameTagsCompositionStrategy() { 170 Config.getPref().addPreferenceChangeListener(this); 171 initNameTagsFromPreferences(); 172 } 173 174 private static List<String> buildNameTags(List<String> nameTags) { 175 List<String> result = new ArrayList<>(); 176 if (nameTags != null) { 177 for (String tag: nameTags) { 178 if (tag == null) { 179 continue; 180 } 181 tag = tag.trim(); 182 if (tag.isEmpty()) { 183 continue; 184 } 185 result.add(tag); 186 } 187 } 188 return result; 189 } 190 191 /** 192 * Sets the name tags to be looked up in order to build up the label. 193 * 194 * @param nameTags the name tags. null values are ignored. 195 */ 196 public void setNameTags(List<String> nameTags) { 197 this.nameTags = buildNameTags(nameTags); 198 } 199 200 /** 201 * Sets the name complement tags to be looked up in order to build up the label. 202 * 203 * @param nameComplementTags the name complement tags. null values are ignored. 204 * @since 6541 205 */ 206 public void setNameComplementTags(List<String> nameComplementTags) { 207 this.nameComplementTags = buildNameTags(nameComplementTags); 208 } 209 210 /** 211 * Replies an unmodifiable list of the name tags used to compose the label. 212 * 213 * @return the list of name tags 214 */ 215 public List<String> getNameTags() { 216 return Collections.unmodifiableList(nameTags); 217 } 218 219 /** 220 * Replies an unmodifiable list of the name complement tags used to compose the label. 221 * 222 * @return the list of name complement tags 223 * @since 6541 224 */ 225 public List<String> getNameComplementTags() { 226 return Collections.unmodifiableList(nameComplementTags); 227 } 228 229 /** 230 * Initializes the name tags to use from a list of default name tags (see 231 * {@link #DEFAULT_NAME_TAGS} and {@link #DEFAULT_NAME_COMPLEMENT_TAGS}) 232 * and from name tags configured in the preferences using the keys 233 * <code>mappaint.nameOrder</code> and <code>mappaint.nameComplementOrder</code>. 234 */ 235 public final void initNameTagsFromPreferences() { 236 if (Config.getPref() == null) { 237 this.nameTags = new ArrayList<>(Arrays.asList(DEFAULT_NAME_TAGS)); 238 this.nameComplementTags = new ArrayList<>(Arrays.asList(DEFAULT_NAME_COMPLEMENT_TAGS)); 239 } else { 240 this.nameTags = new ArrayList<>( 241 Config.getPref().getList("mappaint.nameOrder", Arrays.asList(DEFAULT_NAME_TAGS)) 242 ); 243 this.nameComplementTags = new ArrayList<>( 244 Config.getPref().getList("mappaint.nameComplementOrder", Arrays.asList(DEFAULT_NAME_COMPLEMENT_TAGS)) 245 ); 246 } 247 } 248 249 private String getPrimitiveName(IPrimitive n) { 250 StringBuilder name = new StringBuilder(); 251 if (!n.hasKeys()) return null; 252 for (String rn : nameTags) { 253 String val = n.get(rn); 254 if (val != null) { 255 name.append(val); 256 break; 257 } 258 } 259 for (String rn : nameComplementTags) { 260 String comp = n.get(rn); 261 if (comp != null) { 262 if (name.length() == 0) { 263 name.append(comp); 264 } else { 265 name.append(" (").append(comp).append(')'); 266 } 267 break; 268 } 269 } 270 return name.toString(); 271 } 272 273 @Override 274 public String compose(IPrimitive primitive) { 275 if (primitive == null) return null; 276 return getPrimitiveName(primitive); 277 } 278 279 @Override 280 public String toString() { 281 return '{' + getClass().getSimpleName() + '}'; 282 } 283 284 @Override 285 public void preferenceChanged(PreferenceChangeEvent e) { 286 if (e.getKey() != null && e.getKey().startsWith("mappaint.name")) { 287 initNameTagsFromPreferences(); 288 } 289 } 290 } 291}