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