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 initNameTagsFromPreferences(); 171 } 172 173 private static List<String> buildNameTags(List<String> nameTags) { 174 List<String> result = new ArrayList<>(); 175 if (nameTags != null) { 176 for (String tag: nameTags) { 177 if (tag == null) { 178 continue; 179 } 180 tag = tag.trim(); 181 if (tag.isEmpty()) { 182 continue; 183 } 184 result.add(tag); 185 } 186 } 187 return result; 188 } 189 190 /** 191 * Sets the name tags to be looked up in order to build up the label. 192 * 193 * @param nameTags the name tags. null values are ignored. 194 */ 195 public void setNameTags(List<String> nameTags) { 196 this.nameTags = buildNameTags(nameTags); 197 } 198 199 /** 200 * Sets the name complement tags to be looked up in order to build up the label. 201 * 202 * @param nameComplementTags the name complement tags. null values are ignored. 203 * @since 6541 204 */ 205 public void setNameComplementTags(List<String> nameComplementTags) { 206 this.nameComplementTags = buildNameTags(nameComplementTags); 207 } 208 209 /** 210 * Replies an unmodifiable list of the name tags used to compose the label. 211 * 212 * @return the list of name tags 213 */ 214 public List<String> getNameTags() { 215 return Collections.unmodifiableList(nameTags); 216 } 217 218 /** 219 * Replies an unmodifiable list of the name complement tags used to compose the label. 220 * 221 * @return the list of name complement tags 222 * @since 6541 223 */ 224 public List<String> getNameComplementTags() { 225 return Collections.unmodifiableList(nameComplementTags); 226 } 227 228 /** 229 * Initializes the name tags to use from a list of default name tags (see 230 * {@link #DEFAULT_NAME_TAGS} and {@link #DEFAULT_NAME_COMPLEMENT_TAGS}) 231 * and from name tags configured in the preferences using the keys 232 * <code>mappaint.nameOrder</code> and <code>mappaint.nameComplementOrder</code>. 233 */ 234 public final void initNameTagsFromPreferences() { 235 if (Config.getPref() == null) { 236 this.nameTags = new ArrayList<>(Arrays.asList(DEFAULT_NAME_TAGS)); 237 this.nameComplementTags = new ArrayList<>(Arrays.asList(DEFAULT_NAME_COMPLEMENT_TAGS)); 238 } else { 239 this.nameTags = new ArrayList<>( 240 Config.getPref().getList("mappaint.nameOrder", Arrays.asList(DEFAULT_NAME_TAGS)) 241 ); 242 this.nameComplementTags = new ArrayList<>( 243 Config.getPref().getList("mappaint.nameComplementOrder", Arrays.asList(DEFAULT_NAME_COMPLEMENT_TAGS)) 244 ); 245 } 246 } 247 248 private String getPrimitiveName(IPrimitive n) { 249 StringBuilder name = new StringBuilder(); 250 if (!n.hasKeys()) return null; 251 for (String rn : nameTags) { 252 String val = n.get(rn); 253 if (val != null) { 254 name.append(val); 255 break; 256 } 257 } 258 for (String rn : nameComplementTags) { 259 String comp = n.get(rn); 260 if (comp != null) { 261 if (name.length() == 0) { 262 name.append(comp); 263 } else { 264 name.append(" (").append(comp).append(')'); 265 } 266 break; 267 } 268 } 269 return name.toString(); 270 } 271 272 @Override 273 public String compose(IPrimitive primitive) { 274 if (primitive == null) return null; 275 return getPrimitiveName(primitive); 276 } 277 278 @Override 279 public String toString() { 280 return '{' + getClass().getSimpleName() + '}'; 281 } 282 283 @Override 284 public void preferenceChanged(PreferenceChangeEvent e) { 285 if (e.getKey() != null && e.getKey().startsWith("mappaint.name")) { 286 initNameTagsFromPreferences(); 287 } 288 } 289 } 290}