/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2017, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.mbstyle.parse; import java.awt.Color; import java.lang.reflect.Array; import java.util.Arrays; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.geotools.factory.CommonFactoryFinder; import org.geotools.factory.Hints; import org.geotools.mbstyle.layer.LineMBLayer.LineJoin; import org.geotools.styling.Displacement; import org.geotools.styling.StyleFactory2; import org.geotools.util.ColorConverterFactory; import org.geotools.util.Converters; import org.geotools.util.logging.Logging; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.ParseException; import org.opengis.filter.FilterFactory2; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; /** * Helper class used to perform JSON traversal of {@link JSONObject} and perform Expression and Filter * conversions. These utilities are used by the MBStyle to convert JSON to simple Java objects, * process functions and perform common JSON manipulation tasks. * * <h2>Access methods</h2> * Example of transformation to Expression, using the fallback value if provided: * * <pre><code> MBObjectParser parse = new MBObjectParser( ff ); * * Expression fillOpacity = parse.percent( json, "fill-opacity", 1.0 ); * Expression fillColor = parse.color( json, "fill-color", Color.BLACK ); * </code></pre> * * <h2>Get Methods</h2> * * Generic "get" methods are also available for safely accessing required fields. These methods will throw a * validation error if the required tag was is not available. * * <pre><code> String id = parse.get("id"); * String visibility = parse.getBoolean("visibility"); * String source = parse.get("source"); * </code></pre> * * Non generic "get" methods, like {@link #paint(JSONObject)}, are in position to provide an appropriate default value. * <pre><code> JSONObject paint = parse.paint( layer ); * </code></pre> * * @author Torben Barsballe (Boundless) */ public class MBObjectParser { /** Wrapper class (used to provide better error messages). */ Class<?> context; final FilterFactory2 ff; final StyleFactory2 sf; private static final Logger LOGGER = Logging.getLogger(MBObjectParser.class); /** * Parser used to in the provided context. * * @param context Context this parser is being used in (for better error reporting) */ public MBObjectParser(Class<?> context) { this.context = context; this.sf = (StyleFactory2) CommonFactoryFinder.getStyleFactory(); this.ff = CommonFactoryFinder.getFilterFactory2(); } /** * Copy constructor allowing reuse of factories, while returning correct {@link #context}. * * @param context Context this parser is being used in (for better error reporting) * @param parse Parent parser used to configure factories consistently */ public MBObjectParser(Class<MBFilter> context, MBObjectParser parse) { this.context = context; sf = parse == null ? (StyleFactory2) CommonFactoryFinder.getStyleFactory() : parse.getStyleFactory(); ff = parse == null ? CommonFactoryFinder.getFilterFactory2() : parse.getFilterFactory(); } // // Utility methods for required lookup // // These methods throw a validation error if tag is not available // /** Shared FilterFactory */ public FilterFactory2 getFilterFactory() { return this.ff; } /** Shared StyleFactory */ public StyleFactory2 getStyleFactory() { return sf; } /** Safely look up paint in provided layer json. * <p> * Paint is optional, returning an empty JSONObject (to prevent the need for null checks).</p> * @param layer * @return paint definition, optional so may be an empty JSONObject * @throws MBFormatException If paint is provided as an invalid type (such as boolean). */ public JSONObject paint(JSONObject layer) { if (layer.containsKey("paint")) { Object paint = layer.get("paint"); if( paint == null ){ String type = get( layer, "type", "layer"); throw new MBFormatException( type + " paint requires JSONObject"); } else if (paint instanceof JSONObject) { return (JSONObject) paint; } else { String type = get( layer, "type", "layer"); throw new MBFormatException( type + " paint requires JSONObject"); } } else { // paint is optional, having a value here prevents need for null checks return new JSONObject(); } } /** Safely look up layout in provided layer json. * <p> * Layout is optional, returning an empty JSONObject (to prevent the need for null checks).</p> * @param layer * @return layout definition, optional so may be an empty JSONObject * @throws MBFormatException If layout is provided as an invalid type (such as boolean). */ public JSONObject layout(JSONObject layer) { if(layer.containsKey("layout")) { Object layout = layer.get("layout"); if (layout == null ) { String type = get( layer, "type", "layer"); throw new MBFormatException( type + " layout requires JSONObject"); } else if (layout instanceof JSONObject) { return (JSONObject) layout; } else { String type = get( layer, "type", "layer"); throw new MBFormatException( type + " paint requires JSONObject"); } } else { // paint is optional, having a value here prevents need for null checks return new JSONObject(); } } /** * Access JSONObject for the indicated tag. * <p> * Confirms json contains the provided tag as a JSONObject, correctly * throwing {@link MBFormatException} if not available. * * @param json The JSONObject in which to lookup the provided tag and return a JSONObject * @param tag The tag to look up in the provided JSONObject * @return The JSONObject at the provided tag * @throws MBFormatException If JSONObject not available for the provided tag */ public JSONObject getJSONObject(JSONObject json, String tag) { if (json == null) { throw new IllegalArgumentException("json required"); } if (tag == null) { throw new IllegalArgumentException("tag required for json access"); } if (json.containsKey(tag) && json.get(tag) instanceof JSONObject) { return (JSONObject) json.get(tag); } else { throw new MBFormatException("\""+tag+"\" requires JSONObject"); } } /** * Access JSONObject for the indicated tag, with the provided fallback if the the json does not contain a JSONObject for that tag. * * @param json The JSONObject in which to lookup the provided tag and return a JSONObject * @param tag The tag to look up in the provided JSONObject * @param fallback The JSONObject to return if the provided json does not contain a JSONObject for that tag. * @return The JSONObject at the provided tag, or the fallback object. */ public JSONObject getJSONObject(JSONObject json, String tag, JSONObject fallback) { if (json == null) { throw new IllegalArgumentException("json required"); } if (tag == null) { throw new IllegalArgumentException("tag required for json access"); } if (json.containsKey(tag) && json.get(tag) instanceof JSONObject) { return (JSONObject) json.get(tag); } else { return fallback; } } /** * Access json contains a JSONArray for the indicated tag. * <p> * Confirms json contains the provided tag as a JSONArray, correctly * throwing {@link MBFormatException} if not available. * * @param json * @param tag * @return JSONObject * @throws MBFormatException If JSONObject not available for the provided tag */ public JSONArray getJSONArray(JSONObject json, String tag) { if (json == null) { throw new IllegalArgumentException("json required"); } if (tag == null) { throw new IllegalArgumentException("tag required for json access"); } if (json.containsKey(tag) && json.get(tag) instanceof JSONArray) { return (JSONArray) json.get(tag); } else { throw new MBFormatException("\""+tag+"\" requires JSONArray"); } } /** * Access a JSONArray at the provided tag in the provided JSONObject, with a fallback if no JSONArray is found at that tag. * * @param json The JSONObject in which to lookup the provided tag and return a JSONArray * @param tag The tag to look up in the provided JSONObject * @param fallback The JSONArray to return if the provided json does not contain a JSONArray for that tag. * @return The JSONArray at the provided tag, or the fallback JSONArray. */ public JSONArray getJSONArray(JSONObject json, String tag, JSONArray fallback) { if (json == null) { throw new IllegalArgumentException("json required"); } else if (tag == null) { throw new IllegalArgumentException("tag required for json access"); } else if (!json.containsKey(tag) || json.get(tag)==null) { return fallback; } else if (json.containsKey(tag) && json.get(tag) instanceof JSONArray) { return (JSONArray) json.get(tag); } else { throw new MBFormatException("\""+tag+"\" requires JSONArray"); } } /** * Access a literal value (string, numeric, or boolean). * * @param index * @return required string, numeric or boolean * @throws MBFormatException if required index not available. */ public Object value(JSONArray json, int index) { if (json == null) { throw new IllegalArgumentException("json required"); } if (index < json.size()) { Object value = json.get(index); if (value instanceof String || value instanceof Boolean || value instanceof Number) { return value; } } throw new MBFormatException(context.getSimpleName() + " requires [" + index + "] string, numeric or boolean"); } /** * Access a literal value (string, numeric, or boolean). * * @param tag * @return required string, numeric or boolean * @throws MBFormatException if required tag not available. */ public Object value(JSONObject json, String tag) { if (json == null) { throw new IllegalArgumentException("json required"); } if (json.containsKey(tag)) { Object value = json.get(tag); if (value == null || value instanceof String || value instanceof Boolean || value instanceof Number) { return value; } throw new MBFormatException(context.getSimpleName() + " requires \"" + tag + "\" literal required (was " + value.getClass().getSimpleName() + ")"); } throw new MBFormatException(context.getSimpleName() + " requires \"" + tag + "\" required."); } /** * Quickly access required json index (as a String). * * @param index * @return required string * @throws MBFormatException if required index not available. */ public String get(JSONArray json, int index) { if (json == null) { throw new IllegalArgumentException("json required"); } if (index < json.size() && json.get(index) instanceof String) { return (String) json.get(index); } else { throw new MBFormatException( context.getSimpleName() + " requires [" + index + "] string"); } } /** * Quickly access required json tag (as a String). * * @param tag * @return required string * @throws MBFormatException if required tag not available. */ public String get(JSONObject json, String tag) { if (json == null) { throw new IllegalArgumentException("json required"); } if (tag == null) { throw new IllegalArgumentException("tag required for json access"); } if (json.containsKey(tag) && json.get(tag) instanceof String) { return (String) json.get(tag); } else { throw new MBFormatException( context.getSimpleName() + " requires \"" + tag + "\" string field"); } } /** * Quickly access required json tag. * * @param json * @param tag * @param fallback * @return required string, or fallback if unavailable */ public String get(JSONObject json, String tag, String fallback) { if (tag == null || json == null) { return fallback; } else if(!json.containsKey(tag) || json.get(tag)==null){ return fallback; } if (json.containsKey(tag) && json.get(tag) instanceof String) { return (String) json.get(tag); } else { throw new MBFormatException( context.getSimpleName() + " requires \"" + tag + "\" string field"); } } /** * Look up a double in the provided {@link JSONArray} at the provided index, or throw an {@link MBFormatException}. * * @param json The array in which to look up the double * @param index The index at which to look up the double * @return The double from the array at index. */ public double getNumeric(JSONArray json, int index) { if (json == null) { throw new IllegalArgumentException("json required"); } if (index < json.size() && json.get(index) instanceof Number) { return ((Number) json.get(index)).doubleValue(); } else { throw new MBFormatException( context.getSimpleName() + " requires [" + index + "] numeric value"); } } /** * Look up a Double in the provided {@link JSONObject} at the provided 'tag', or thrown an {@link MBFormatException}. * * @param json The object in which to look up the Double * @param tag The tag at which to look up the Double * @return The Double from the object at 'tag' */ public Double getNumeric(JSONObject json, String tag) { if (json == null) { throw new IllegalArgumentException("json required"); } if (tag == null) { throw new IllegalArgumentException("tag required for json access"); } if (json.containsKey(tag) && json.get(tag) instanceof Number) { return ((Number) json.get(tag)).doubleValue(); } else { throw new MBFormatException( context.getSimpleName() + " requires \"" + tag + "\" numeric field"); } } /** * Look up a Boolean in the provided {@link JSONArray} at the provided index, or throw an {@link MBFormatException}. * * @param json The array in which to look up the Boolean * @param index The index at which to look up the Boolean * @return The Boolean from the array at index. */ public Boolean getBoolean(JSONArray json, int index) { if (json == null) { throw new IllegalArgumentException("json required"); } if (index < json.size() && json.get(index) instanceof Boolean) { return (Boolean) json.get(index); } else { throw new MBFormatException( context.getSimpleName() + " requires [" + index + "] boolean"); } } /** * Look up a Boolean in the provided {@link JSONObject} at the provided 'tag', or thrown an {@link MBFormatException}. * * @param json The object in which to look up the Boolean * @param tag The tag at which to look up the Boolean * @return The Boolean from the object at 'tag' */ public Boolean getBoolean(JSONObject json, String tag) { if (json == null) { throw new IllegalArgumentException("json required"); } if (tag == null) { throw new IllegalArgumentException("tag required for json access"); } if (json.containsKey(tag) && json.get(tag) instanceof Boolean) { return (Boolean) json.get(tag); } else { throw new MBFormatException( context.getSimpleName() + " requires \"" + tag + "\" boolean field"); } } /** * Look up a Boolean in the provided {@link JSONObject} at the provided 'tag', or thrown an {@link MBFormatException}, * with a fallback if the json is null or contains no such 'tag'. * * @param json The object in which to look up the Boolean * @param tag The tag at which to look up the Boolean * @param fallback The value to return if the json is null or contains no such 'tag'. * @return The Boolean from the object at 'tag', or the fallback value */ public Boolean getBoolean(JSONObject json, String tag, Boolean fallback) { if (json == null) { throw new IllegalArgumentException("json required"); } else if (tag == null) { throw new IllegalArgumentException("tag required for json access"); } else if (!json.containsKey(tag) || json.get(tag)==null){ return fallback; } else if (json.get(tag) instanceof Boolean) { return (Boolean) json.get(tag); } else { throw new MBFormatException(context.getSimpleName() + " requires \"" + tag + "\" boolean field"); } } /** * Retrieve an object of the provided type in the JSONArray at this index, throwing an {@link MBFormatException} if * no object of that type is found at that index of the array. * * @param type The type of the object to retrieve. * @param json The JSONArray in which to retrieve the object. * @param index The index in the JSONArray at which to retrieve the object. * @return The object of the required type in the array at index. */ public <T> T require(Class<T> type, JSONArray json, int index) { if (json == null) { throw new IllegalArgumentException("json required"); } if (index >= 0 && index <= json.size() && type.isInstance(json.get(index))) { return type.cast(json.get(index)); } else { throw new MBFormatException(context.getSimpleName() + " requires [" + index + "] "+type.getSimpleName()); } } /** * Retrieve an object of the provided type in the JSONObject at this tag, throwing an {@link MBFormatException} if * no object of that type is found at that tag in the object. * * @param type The type of the object to retrieve. * @param json The JSONObject in which to retrieve the object. * @param tag The index in the JSONObject at which to retrieve the object. * @return The object of the required type in the JSONObject at the tag. */ public <T> T require(Class<T> type, JSONObject json, String tag) { if (json == null) { throw new IllegalArgumentException("json required"); } if (tag == null) { throw new IllegalArgumentException("tag required for json access"); } if (json.containsKey(tag) && type.isInstance(json.get(tag))) { return type.cast(json.get(tag)); } else { throw new MBFormatException(context.getSimpleName() + " requires \"" + tag + "\" "+type.getSimpleName()+" field"); } } /** * Optional lookup, will return fallback if not available. * * @param type Type to lookup * @param json The JSONObject in which to lookup the value * @param tag The tag at which to lookupthe value in the JSONObject * @param fallback The fallback value to use if the JSONObject is null or does not contain the provided tag * @return value for the provided tag, or fallback if not available * @throws MBFormatException If value is found and is not the expected type */ public <T> T optional(Class<T> type, JSONObject json, String tag, T fallback) { if (json == null) { throw new IllegalArgumentException("json required"); } else if (tag == null) { throw new IllegalArgumentException("tag required for json access"); } else if (!json.containsKey(tag)){ return fallback; } else { Object value = json.get(tag); if (!json.containsKey(tag) || json.get(tag)==null) { return fallback; } else if (Number.class.isAssignableFrom(type)) { T number = Converters.convert(value, type); if( number == null){ throw new MBFormatException(context.getSimpleName() + " optional \"" + tag + "\" expects "+type.getSimpleName()+" value"); } else { return type.cast(number); } } else if (type.isInstance(value)) { return type.cast(value); } else { throw new MBFormatException(context.getSimpleName() + " optional \"" + tag + "\" expects "+type.getSimpleName()+" value"); } } } /** * * Convert a String value to an enum value, ignoring case, with the provided fallback. * * @param json The json object to parse the value from * @param tag The key used to retrieve the value from the json. * @param enumeration The enum to convert the value to. * * @return The enum value from the string, or the fallback value. * @throws MBFormatException if the value is not a String, or it is not a valid value for the enumeration. */ public <T extends Enum<?>> T getEnum(JSONObject json, String tag, Class<T> enumeration, T fallback) { Object value = json.get(tag); if (value == null) { return fallback; } else if (value instanceof String) { String stringVal = (String) value; if ("".equals(stringVal.trim())) { return fallback; } for (T enumValue : enumeration.getEnumConstants()) { if (enumValue.toString().equalsIgnoreCase(stringVal.trim())) { return enumValue; } } throw new MBFormatException("\"" + tag + "\" contains invalid value for enumeration " + enumeration.getSimpleName()); } else { throw new MBFormatException( "Conversion of \"" + tag + "\" value from " + value.getClass().getSimpleName() + " to " + enumeration.getSimpleName() + " not supported."); } } /** * * <p>Parse a Mapbox enumeration property to a GeoTools Expression that evaluates to a GeoTools constant (supports the property being specified as a * mapbox function). </p> * * <p>For example, converts {@link LineJoin#BEVEL} to an expression that evaluates to the String value "bevel", or * {@link LineJoin#MITER} to an expression that evaluates to "mitre".</p> * * @param json The json object containing the property * @param tag The json key corresponding to the property * @param enumeration The Mapbox enumeration that the value should be an instance of * @param fallback The fallback enumeration value, if the value is missing or invalid for the provided enumeration. * @return A GeoTools expression corresponding to the Mapbox enumeration value, evaluating to a GeoTools constant. */ public <T extends Enum<?>> Expression enumToExpression(JSONObject json, String tag, Class<T> enumeration, T fallback) { // Function name is inconsistent because "enum" is not a valid function name. Object value = json.get(tag); if (value == null) { return constant(fallback.toString(), enumeration); } else if (value instanceof String) { String stringVal = (String) value; if ("".equals(stringVal.trim())) { return constant(fallback.toString(), enumeration); } try { return constant(stringVal, enumeration); } catch (Exception e) { LOGGER.log(Level.WARNING, "\"" + stringVal + "\" Exception parsing value for enumeration " + enumeration.getSimpleName() + ", falling back to default value."); return constant(fallback.toString(), enumeration); } } else if (value instanceof JSONObject) { MBFunction function = new MBFunction(this, (JSONObject) value); return function.enumeration(enumeration); } else { throw new MBFormatException( "Conversion of \"" + tag + "\" value from " + value.getClass().getSimpleName() + " to " + enumeration.getSimpleName() + " not supported."); } } /** * Utility method used to convert enumerations to an appropriate GeoTools literal string. * <p> * Any conversion between mapbox constants and geotools constants will be done here. * * @param value The value to be converted to the appropriate GeoTools literal * @param enumeration The type of the mapbox enumeration * @return Literal, or null if unavailable */ public Literal constant(Object value, Class<? extends Enum<?>> enumeration) { if( value == null ){ return null; } if( value instanceof String){ // step 1 look up enumValue String stringVal = (String) value; if ("".equals(stringVal.trim())) { return null; } Object enumValue = null; for (Object constant : enumeration.getEnumConstants()) { if (constant.toString().equalsIgnoreCase(stringVal.trim())) { enumValue = constant; break; } } if( enumValue == null ){ throw new MBFormatException("\"" + stringVal + "\" invalid value for enumeration " + enumeration.getSimpleName()); } // step 2 - convert to geotools constant // (Converting the string value to lowercase takes care of most cases). if (enumValue instanceof LineJoin && LineJoin.MITER.equals(enumValue)) { return ff.literal("mitre"); } String literal = enumValue.toString().toLowerCase(); return ff.literal(literal); } return null; } /** * Casts the provided obj to a JSONObject (safely reporting format exception * * @param obj * @return JSONObject * @throws MBFormatException */ public JSONObject jsonObject(Object obj) throws MBFormatException { if (obj instanceof JSONObject) { return (JSONObject) obj; } else if (obj == null) { throw new MBFormatException("Not a JSONObject: null"); } else { throw new MBFormatException("Not a JSONObject: " + obj.toString()); } } /** * Casts the provided obj to a JSONObject (safely reporting format exception, with the provided message). * * @param obj The object to cast * @param message The message for the exception of the object is not a JSONObject * @return The object, cast to JSONObject * @throws MBFormatException */ public JSONObject jsonObect(Object obj, String message) throws MBFormatException { if (obj instanceof JSONObject) { return (JSONObject) obj; } else { throw new MBFormatException(message); } } /** * Casts the provided obj to a JSONArray (otherwise throws an {@link MBFormatException}). * * @param obj The object to cast * @return The object, cast to JSONArray * @throws MBFormatException */ public JSONArray jsonArray(Object obj) throws MBFormatException { if( obj instanceof JSONArray){ return (JSONArray) obj; } else if (obj == null) { throw new MBFormatException("Not a JSONArray: null"); } else { throw new MBFormatException("Not a JSONArray: " + obj.toString() ); } } /** * Casts the provided obj to a JSONArray (otherwise throws an {@link MBFormatException} with the provided message). * * @param obj The object to cast * @param message The message for the exception of the object is not a JSONArray * @return The object, cast to JSONArray * @throws MBFormatException */ public JSONArray jsonArray(Object obj, String message) throws MBFormatException { if (obj instanceof JSONArray) { return (JSONArray) obj; } else { throw new MBFormatException(message); } } /** * Lookup an array of the provided type in the provided JSONObject at 'tag', with a fallback if that tag is not found. Throws an exception if * that tag is something other than an array, or if its contents cannot be cast to type. * * @param type The type of the array * @param json The JSONObject in which to look up the array * @param tag The tag at which to look up the array * @param fallback The fallback array * @return An array of the provided type, or the fallback array. */ @SuppressWarnings("unchecked") public <T> T[] array(Class<T> type, JSONObject json, String tag, T[] fallback) { if (json.containsKey(tag)) { Object obj = json.get(tag); if (obj instanceof JSONArray) { JSONArray array = (JSONArray) obj; T[] returnArray = (T[]) Array.newInstance(type, array.size()); for (int i = 0; i < array.size(); i++) { Object value = array.get(i); if (Number.class.isAssignableFrom(type) && value instanceof Number) { if (type == Double.class) { returnArray[i] = (T)new Double(((Number)value).doubleValue()); continue; } else if (type == Integer.class) { returnArray[i] = (T)new Integer(((Number)value).intValue()); continue; } } returnArray[i] = type.cast(array.get(i)); } return returnArray; } else { throw new MBFormatException("\"" + tag + "\" required as JSONArray of " + type.getSimpleName() + ": Unexpected " + obj.getClass().getSimpleName()); } } return fallback; } /** Convert to int[] */ public int[] array(JSONObject json, String tag, int[] fallback) { Integer[] array = array(Integer.class, json, tag, fallback == null ? null : Arrays.stream(fallback).mapToObj(i -> i).toArray(Integer[]::new)); return array == null ? fallback : Arrays.stream(array).mapToInt(i -> i).toArray(); } /** Convert to double[] */ public double[] array(JSONObject json, String tag, double[] fallback) { Double[] array = array(Double.class, json, tag, fallback == null ? null : Arrays.stream(fallback).mapToObj(i -> i).toArray(Double[]::new)); return array == null ? fallback : Arrays.stream(array).mapToDouble(i -> i).toArray(); } /** * Convert json to Expression number between 0 and 1, or a function. * * @param json json representation * @return Expression based on provided json, or null if not provided * @throws MBFormatException */ public Expression percentage(JSONObject json, String tag) throws MBFormatException { return percentage(json, tag, null); } /** * Convert json to Expression number between 0 and 1, or a function. * * @param json json representation * @param fallback default value if json is null * @return Expression based on provided json, or literal if json was null. * @throws MBFormatException */ public Expression percentage(JSONObject json, String tag, Number fallback) throws MBFormatException { if (json == null) { return fallback == null ? null : ff.literal(fallback); } Object obj = json.get(tag); if (obj == null) { return ff.literal(fallback); } else if (obj instanceof String) { String str = (String) obj; return ff.literal(str); } else if (obj instanceof Number) { Number number = (Number) obj; return ff.literal(number); } else if (obj instanceof Boolean) { throw new MBFormatException("\"" + tag + "\" percentage from boolean " + obj + " not supported, expected value between 0 and 1"); } else if (obj instanceof JSONObject) { MBFunction function = new MBFunction(this, (JSONObject) obj); return function.numeric(); } else if (obj instanceof JSONArray) { throw new MBFormatException("\"" + tag + "\" percentage from JSONArray not supported, expected value between 0 and 1"); } else { throw new IllegalArgumentException("json contents invalid, \"" + tag + "\" value limited to Number or JSONObject but was " + obj.getClass().getSimpleName()); } } // // NUMBER // /** * Convert the provided object to a numeric Expression (or function), with a fallback value if the object is null. * * @param context The json context of the object, used for error messages. * @param obj The object to convert * @param fallback The fallback value, used when the provided object is null. * @return A numeric expression for the provided object */ private Expression number(String context, Object obj, Number fallback) { if (obj == null) { return fallback == null ? null : ff.literal(fallback); } if (obj instanceof String) { String str = (String) obj; return ff.literal(str); } else if (obj instanceof Number) { Number number = (Number) obj; return ff.literal(number.toString()); } else if (obj instanceof Boolean) { Boolean bool = (Boolean) obj; throw new MBFormatException( context + " number from Boolean " + bool + " not supported"); } else if (obj instanceof JSONObject) { MBFunction function = new MBFunction(this, (JSONObject) obj); return function.numeric(); } else if (obj instanceof JSONArray) { throw new MBFormatException(context + " number from JSONArray not supported"); } else { throw new IllegalArgumentException("json contents invalid, " + context + " value limited to String, Number, Boolean or JSONObject but was " + obj.getClass().getSimpleName()); } } /** * Convert the value at 'index' in the provided JSONArray to a numeric Expression or a Function. * * @param json The JSONArray in which to look up a value * @param index The index in the JSONArray * @return Numeric Expression based on provided json, or null. * @throws MBFormatException */ public Expression number(JSONArray json, int index) throws MBFormatException { return number(json, index, null); } /** * Convert the value in the provided JSONArray at index to a numeric Expression, or a function, with a fallback if the json is null. * * @param json The JSONArray in which to look up the value * @param index The index in the JSONArray at which to look up the value * @param fallback default value if json is null * @return Expression based on provided json, or literal if json was null. * @throws MBFormatException */ public Expression number(JSONArray json, int index, Number fallback) throws MBFormatException { if (json == null) { return fallback == null ? null : ff.literal(fallback); } Object obj = json.get(index); return number("index " + index, obj, fallback); } /** * Convert the value in the provided JSONObject at 'tag' to a numeric Expression, or a function. * * @param json The JSONObject in which to look up the value * @param tag The tag in the JSONObject at which to look up the value * @return Expression based on provided json, or null * @throws MBFormatException */ public Expression number(JSONObject json, String tag) throws MBFormatException { return number(json, tag, null); } /** * Convert the value in the provided JSONObject at 'tag' to a numeric Expression, or a function, with a fallback if the json is null. * * @param json The JSONObject in which to look up the value * @param tag The tag in the JSONObject at which to look up the value * @param fallback default value if the JSONObject is null * @return Expression based on provided json, or literal if json was null. * @throws MBFormatException */ public Expression number(JSONObject json, String tag, Number fallback) throws MBFormatException { if (json == null) { return ff.literal(fallback); } Object obj = json.get(tag); return number("\"" + tag + "\"", obj, fallback); } // // STRING // /** * Convert the provided object to a string Expression (or function), with a fallback value if the object is null. * @param context The json context of the object, used for error messages. * @param obj The object to convert * @param fallback The fallback value, used when the provided object is null. * @return A string expression for the provided object */ private Expression string(String context, Object obj, String fallback) { if (obj == null) { return fallback == null ? null : ff.literal(fallback); } else if (obj instanceof String) { String str = (String) obj; return ff.literal(str); } else if (obj instanceof Number) { Number number = (Number) obj; return ff.literal(number.toString()); } else if (obj instanceof Boolean) { Boolean bool = (Boolean) obj; return ff.literal(bool.toString()); } else if (obj instanceof JSONObject) { MBFunction function = new MBFunction(this, (JSONObject) obj); return function.function(String.class); } else if (obj instanceof JSONArray) { throw new MBFormatException(context + " string from JSONArray not supported"); } else { throw new IllegalArgumentException("json contents invalid, " + context + " value limited to String, Number, Boolean or JSONObject but was " + obj.getClass().getSimpleName()); } } /** * Convert json to Expression string, or a function. * * @param json json representation * @param fallback default value if json is null * @return Expression based on provided json, or literal if json was null. * @throws MBFormatException */ public Expression string(JSONObject json, String tag, String fallback) throws MBFormatException { if (json == null) { return fallback == null ? null : ff.literal(fallback); } return string( "\""+tag+"\"", json.get(tag), fallback ); } /** * Convert json to Expression color, or a function. * * @param json json representation * @param fallback default value (string representation of color) if json is null * @return Expression based on provided json, or literal if json was null. * @throws MBFormatException */ public Expression color(JSONObject json, String tag, Color fallback) throws MBFormatException { if (json.get(tag) == null) { return fallback == null ? null : ff.literal(fallback); } Object obj = json.get(tag); return color( "\""+tag+"\"", obj, fallback ); } /** * Handles literal color definitions supplied as a string, returning a {@link Literal}. * * <ul> * <li><pre>{"line-color": "yellow"</pre> named: a few have been put in pass test cases, prnding: plan to use {@link Hints#COLOR_DEFINITION} to allow for web colors.</li> * <li><pre>{"line-color": "#ffff00"}</pre> hex: hex color conversion are supplied by {@link ColorConverterFactory}</li> * <li><pre>{"line-color": "#ff0"}</pre> hex: we will need to special case this</li> * <li><pre>{"line-color": "rgb(255, 255, 0)"}</pre> - we will need to special case this </li> * <li><pre>{"line-color": "rgba(255, 255, 0, 1)"}</pre> - we will need to special case this </li> * <li><pre>{"line-color": "hsl(100, 50%, 50%)"}</pre> - we will need to special case this </li> * <li><pre>{"line-color": "hsla(100, 50%, 50%, 1)"}</pre> - we will need to special case this </li> * <li> * </ul> * * This method uses {@link Hints#COLOR_DEFINITION} "CSS" to support the use of web colors names. * * @param color name of color (CSS or "web" colors) * @return appropriate java color, or null if not available. */ public Literal color(String color) { if (color == null) { return null; } return ff.literal(convertToColor(color)); } /** * Parse obj into a color expression (literal or function). * * @param context * @param obj * @param fallback * @return color expression (literal or function) */ private Expression color(String context, Object obj, Color fallback) { if (obj == null) { return fallback == null ? null : ff.literal(fallback); } else if (obj instanceof String) { String str = (String) obj; return color( str ); } else if (obj instanceof Number) { throw new MBFormatException(context + " color from Number not supported"); } else if (obj instanceof Boolean) { throw new MBFormatException(context + " color from Boolean not supported"); } else if (obj instanceof JSONObject) { MBFunction function = new MBFunction( (JSONObject) obj ); return function.color(); } else if (obj instanceof JSONArray) { throw new MBFormatException(context + " color from JSONArray not supported"); } else { throw new IllegalArgumentException("json contents invalid, " + context + " limited to String or JSONObject but was " + obj.getClass().getSimpleName()); } } /** * Converts color definitions supplied as a string to Color objects: * * <ul> * <li><pre>{"line-color": "yellow"</pre> named: a few have been put in pass test cases, prnding: plan to use {@link Hints#COLOR_DEFINITION} to allow for web colors.</li> * <li><pre>{"line-color": "#ffff00"}</pre> hex: hex color conversion are supplied by {@link ColorConverterFactory}</li> * <li><pre>{"line-color": "#ff0"}</pre> hex: we will need to special case this</li> * <li><pre>{"line-color": "rgb(255, 255, 0)"}</pre> - we will need to special case this </li> * <li><pre>{"line-color": "rgba(255, 255, 0, 1)"}</pre> - we will need to special case this </li> * <li><pre>{"line-color": "hsl(100, 50%, 50%)"}</pre> - we will need to special case this </li> * <li><pre>{"line-color": "hsla(100, 50%, 50%, 1)"}</pre> - we will need to special case this </li> * <li> * </ul> * * This method uses {@link Hints#COLOR_DEFINITION} "CSS" to support the use of web colors names. * * @param color name of color (CSS or "web" colors) * @return appropriate java color, or null if not available. */ public Color convertToColor(String color) { if (color == null) { return null; } // quick examples to pass test case (while we work on color converter) if ("red".equalsIgnoreCase(color)) { return Color.RED; } else if ("blue".equalsIgnoreCase(color)) { return Color.BLUE; } Hints h = new Hints(Hints.COLOR_DEFINITION, "CSS"); return Converters.convert(color, Color.class, h); } /** * Convert json to Expression boolean, or a function. * * @param json json representation * @param fallback default value if json is null * @return Expression based on provided json, or literal if json was null. * @throws MBFormatException */ public Expression bool(JSONObject json, String tag, boolean fallback) throws MBFormatException { if (json.get(tag) == null) { return ff.literal(fallback); } Object obj = json.get(tag); if (obj instanceof String) { String str = (String) obj; return ff.literal(str); } else if (obj instanceof Number) { throw new UnsupportedOperationException( "\"" + tag + "\": boolean from Number not supported"); } else if (obj instanceof Boolean) { Boolean b = (Boolean) obj; return ff.literal(b); } else if (obj instanceof JSONObject) { MBFunction function = new MBFunction(this, (JSONObject) obj); return function.function(Boolean.class); } else if (obj instanceof JSONArray) { throw new MBFormatException("\"" + tag + "\": boolean from JSONArray not supported"); } else { throw new IllegalArgumentException("json contents invalid, \"" + tag + "\" value limited to String, Boolean or JSONObject but was " + obj.getClass().getSimpleName()); } } /** * Maps a json value at 'tag' in the provided JSONObject to a {@link Displacement}. * * @param json The JSONObject in which to look up a displacement value * @param tag The tag in the JSONObject * @param fallback The fallback displacement, if no value is found at that tag. * @return A displacement from the json */ public Displacement displacement(JSONObject json, String tag, Displacement fallback) { Object defn = json.get(tag); if (defn == null) { return fallback; } else if (defn instanceof JSONArray) { JSONArray array = (JSONArray) defn; return sf.displacement(number(array, 0, 0), number(array, 1, 0)); } else if (defn instanceof JSONObject) { // Function case MBFunction function = new MBFunction(this, (JSONObject) defn); if (!function.isArrayFunction()) { throw new MBFormatException("\"" + tag + "\": Exception parsing displacement from Mapbox function: function values must all be arrays with length 2."); } List<MBFunction> functionForEachDimension; try { functionForEachDimension = function.splitArrayFunction(); } catch (ParseException pe) { throw new MBFormatException( "\"" + tag + "\": Exception parsing displacement from Mapbox function: " + pe.getMessage(), pe); } catch (Exception e) { throw new MBFormatException( "\"" + tag + "\": Exception parsing displacement from Mapbox function: " + e.getMessage(), e); } if (functionForEachDimension.size() != 2) { throw new MBFormatException("\"" + tag + "\": Exception parsing displacement from Mapbox function: function values must all be arrays with length 2."); } Expression xFn = functionForEachDimension.get(0).numeric(); Expression yFn = functionForEachDimension.get(1).numeric(); return sf.displacement(xFn, yFn); } else { throw new MBFormatException("\"" + tag + "\": Expected array or function, but was " + defn.getClass().getSimpleName()); } } /** * * @return True if the layer has the provided property explicitly provided, False otherwise. */ public boolean isPropertyDefined(JSONObject json, String propertyName) throws MBFormatException { return json.containsKey(propertyName) && json.get(propertyName) != null; } }