001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import java.util.Arrays;
005import java.util.Collection;
006import java.util.HashSet;
007import java.util.Locale;
008import java.util.Map;
009import java.util.Set;
010
011import org.openstreetmap.josm.data.coor.LatLon;
012import org.openstreetmap.josm.tools.CheckParameterUtil;
013import org.openstreetmap.josm.tools.TextTagParser;
014
015/**
016 * Utility methods/constants that are useful for generic OSM tag handling.
017 */
018public final class OsmUtils {
019
020    private static final Set<String> TRUE_VALUES = new HashSet<>(Arrays
021            .asList("true", "yes", "1", "on"));
022    private static final Set<String> FALSE_VALUES = new HashSet<>(Arrays
023            .asList("false", "no", "0", "off"));
024    private static final Set<String> REVERSE_VALUES = new HashSet<>(Arrays
025            .asList("reverse", "-1"));
026
027    /**
028     * A value that should be used to indicate true
029     * @since 12186
030     */
031    public static final String TRUE_VALUE = "yes";
032    /**
033     * A value that should be used to indicate false
034     * @since 12186
035     */
036    public static final String FALSE_VALUE = "no";
037    /**
038     * A value that should be used to indicate that a property applies reversed on the way
039     * @since 12186
040     */
041    public static final String REVERSE_VALUE = "-1";
042
043    /**
044     * Discouraged synonym for {@link #TRUE_VALUE}
045     */
046    public static final String trueval = TRUE_VALUE;
047    /**
048     * Discouraged synonym for {@link #FALSE_VALUE}
049     */
050    public static final String falseval = FALSE_VALUE;
051    /**
052     * Discouraged synonym for {@link #REVERSE_VALUE}
053     */
054    public static final String reverseval = REVERSE_VALUE;
055
056    private OsmUtils() {
057        // Hide default constructor for utils classes
058    }
059
060    /**
061     * Converts a string to a boolean value
062     * @param value The string to convert
063     * @return {@link Boolean#TRUE} if that string represents a true value,
064     *         {@link Boolean#FALSE} if it represents a false value,
065     *         <code>null</code> otherwise.
066     */
067    public static Boolean getOsmBoolean(String value) {
068        if (value == null) return null;
069        String lowerValue = value.toLowerCase(Locale.ENGLISH);
070        if (TRUE_VALUES.contains(lowerValue)) return Boolean.TRUE;
071        if (FALSE_VALUES.contains(lowerValue)) return Boolean.FALSE;
072        return null;
073    }
074
075    /**
076     * Normalizes the OSM boolean value
077     * @param value The tag value
078     * @return The best true/false value or the old value if the input cannot be converted.
079     * @see #TRUE_VALUE
080     * @see #FALSE_VALUE
081     */
082    public static String getNamedOsmBoolean(String value) {
083        Boolean res = getOsmBoolean(value);
084        return res == null ? value : (res ? trueval : falseval);
085    }
086
087    /**
088     * Check if the value is a value indicating that a property applies reversed.
089     * @param value The value to check
090     * @return true if it is reversed.
091     */
092    public static boolean isReversed(String value) {
093        return REVERSE_VALUES.contains(value);
094    }
095
096    /**
097     * Check if a tag value represents a boolean true value
098     * @param value The value to check
099     * @return true if it is a true value.
100     */
101    public static boolean isTrue(String value) {
102        return TRUE_VALUES.contains(value);
103    }
104
105    /**
106     * Check if a tag value represents a boolean false value
107     * @param value The value to check
108     * @return true if it is a true value.
109     */
110    public static boolean isFalse(String value) {
111        return FALSE_VALUES.contains(value);
112    }
113
114    /**
115     * Creates a new OSM primitive around (0,0) according to the given assertion. Originally written for unit tests,
116     * this can also be used in another places like validation of local MapCSS validator rules.
117     * Ways and relations created using this method are empty.
118     * @param assertion The assertion describing OSM primitive (ex: "way name=Foo railway=rail")
119     * @return a new OSM primitive according to the given assertion
120     * @throws IllegalArgumentException if assertion is null or if the primitive type cannot be deduced from it
121     * @since 7356
122     */
123    public static OsmPrimitive createPrimitive(String assertion) {
124        return createPrimitive(assertion, LatLon.ZERO, false);
125    }
126
127    /**
128     * Creates a new OSM primitive according to the given assertion. Originally written for unit tests,
129     * this can also be used in another places like validation of local MapCSS validator rules.
130     * @param assertion The assertion describing OSM primitive (ex: "way name=Foo railway=rail")
131     * @param around the coordinate at which the primitive will be located
132     * @param enforceLocation if {@code true}, ways and relations will not be empty to force a physical location
133     * @return a new OSM primitive according to the given assertion
134     * @throws IllegalArgumentException if assertion is null or if the primitive type cannot be deduced from it
135     * @since 14486
136     */
137    public static OsmPrimitive createPrimitive(String assertion, LatLon around, boolean enforceLocation) {
138        CheckParameterUtil.ensureParameterNotNull(assertion, "assertion");
139        final String[] x = assertion.split("\\s+", 2);
140        final OsmPrimitive p = "n".equals(x[0]) || "node".equals(x[0])
141                ? newNode(around)
142                : "w".equals(x[0]) || "way".equals(x[0]) || /*for MapCSS related usage*/ "area".equals(x[0])
143                ? newWay(around, enforceLocation)
144                : "r".equals(x[0]) || "relation".equals(x[0])
145                ? newRelation(around, enforceLocation)
146                : null;
147        if (p == null) {
148            throw new IllegalArgumentException("Expecting n/node/w/way/r/relation/area, but got '" + x[0] + '\'');
149        }
150        if (x.length > 1) {
151            for (final Map.Entry<String, String> i : TextTagParser.readTagsFromText(x[1]).entrySet()) {
152                p.put(i.getKey(), i.getValue());
153            }
154        }
155        return p;
156    }
157
158    private static Node newNode(LatLon around) {
159        return new Node(around);
160    }
161
162    private static Way newWay(LatLon around, boolean enforceLocation) {
163        Way w = new Way();
164        if (enforceLocation) {
165            w.addNode(newNode(new LatLon(around.lat()+0.1, around.lon())));
166            w.addNode(newNode(new LatLon(around.lat()-0.1, around.lon())));
167        }
168        return w;
169    }
170
171    private static Relation newRelation(LatLon around, boolean enforceLocation) {
172        Relation r = new Relation();
173        if (enforceLocation) {
174            r.addMember(new RelationMember(null, newNode(around)));
175        }
176        return r;
177    }
178
179    /**
180     * Returns the layer value of primitive (null for layer 0).
181     * @param w OSM primitive
182     * @return the value of "layer" key, or null if absent or set to 0 (default value)
183     * @since 12986
184     * @since 13637 (signature)
185     */
186    public static String getLayer(IPrimitive w) {
187        String layer1 = w.get("layer");
188        if ("0".equals(layer1)) {
189            layer1 = null; // 0 is default value for layer.
190        }
191        return layer1;
192    }
193
194    /**
195     * Determines if the given collection contains primitives, and that none of them belong to a locked layer.
196     * @param collection collection of OSM primitives
197     * @return {@code true} if the given collection is not empty and does not contain any primitive in a locked layer.
198     * @since 13611
199     * @since 13957 (signature)
200     */
201    public static boolean isOsmCollectionEditable(Collection<? extends IPrimitive> collection) {
202        if (collection == null || collection.isEmpty()) {
203            return false;
204        }
205        // see #16510: optimization: only consider the first primitive, as collection always refer to the same dataset
206        OsmData<?, ?, ?, ?> ds = collection.iterator().next().getDataSet();
207        return ds == null || !ds.isLocked();
208    }
209}