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