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