package adql.db; /* * This file is part of ADQLLibrary. * * ADQLLibrary 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, either version 3 of the License, or * (at your option) any later version. * * ADQLLibrary 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. * * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2014-2017 - Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import adql.parser.ADQLQueryFactory; import adql.parser.ParseException; import adql.query.TextPosition; import adql.query.operand.ADQLOperand; import adql.query.operand.NegativeOperand; import adql.query.operand.NumericConstant; import adql.query.operand.StringConstant; import adql.query.operand.function.ADQLFunction; import adql.query.operand.function.geometry.BoxFunction; import adql.query.operand.function.geometry.CircleFunction; import adql.query.operand.function.geometry.GeometryFunction; import adql.query.operand.function.geometry.PointFunction; import adql.query.operand.function.geometry.PolygonFunction; import adql.query.operand.function.geometry.RegionFunction; /** * <p>This class helps dealing with the subset of STC-S expressions described by the section "6 Use of STC-S in TAP (informative)" * of the TAP Recommendation 1.0 (27th March 2010). This subset is limited to the most common coordinate systems and regions.</p> * * <p><i>Note: * No instance of this class can be created. Its usage is only limited to its static functions and classes. * </i></p> * * <h3>Coordinate system</h3> * <p> * The function {@link #parseCoordSys(String)} is able to parse a string containing only the STC-S expression of a coordinate system * (or an empty string or null which would be interpreted as the default coordinate system - UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2). * When successful, this parsing returns an object representation of the coordinate system: {@link CoordSys}. * </p> * <p> * To serialize into STC-S a coordinate system, you have to create a {@link CoordSys} instance with the desired values * and to call the function {@link CoordSys#toSTCS()}. The static function {@link #toSTCS(CoordSys)} is just calling the * {@link CoordSys#toSTCS()} on the given coordinate system. * </p> * * <h3>Geometrical region</h3> * <p> * As for the coordinate system, there is a static function to parse the STC-S representation of a geometrical region: {@link #parseRegion(String)}. * Here again, when the parsing is successful an object representation is returned: {@link Region}. * </p> * <p> * This class lets also serializing into STC-S a region. The procedure is the same as with a coordinate system: create a {@link Region} and then * call {@link Region#toString()}. * </p> * <p> * The class {@link Region} lets also dealing with the {@link ADQLFunction} implementing a region. It is then possible to create a {@link Region} * object from a such {@link ADQLFunction} and to get the corresponding STC-S representation. The static function {@link #toSTCS(GeometryFunction)} * is a helpful function which do these both actions in once. * </p> * <p><i>Note: * The conversion from {@link ADQLFunction} to {@link Region} or STC-S is possible only if the {@link ADQLFunction} contains constants as parameter. * Thus, a such function using a column, a concatenation, a math operation or using another function can not be converted into STC-S using this class. * </i></p> * * @author Grégory Mantelet (ARI) * @version 1.4 (04/2017) * @since 1.3 */ public final class STCS { /** * Empty private constructor ; in order to prevent any instance creation. */ private STCS(){} /* ***************** */ /* COORDINATE SYSTEM */ /* ***************** */ /** Regular expression for a STC-S representation of a coordinate system. It takes into account the fact that each part of * a coordinate system is optional and so that a full coordinate system expression can be reduced to an empty string. */ private final static String coordSysRegExp = Frame.regexp + "?\\s*" + RefPos.regexp + "?\\s*" + Flavor.regexp + "?"; /** Regular expression of an expression exclusively limited to a coordinate system. */ private final static String onlyCoordSysRegExp = "^\\s*" + coordSysRegExp + "\\s*$"; /** Regular expression of a default coordinate system: either an empty string or a string containing only default values. */ private final static String defaultCoordSysRegExp = "^\\s*" + Frame.DEFAULT + "?\\s*" + RefPos.DEFAULT + "?\\s*" + Flavor.DEFAULT + "?\\s*$"; /** Regular expression of a pattern describing a set of allowed coordinate systems. <i>See {@link #buildAllowedRegExp(String)} for more details.</i> */ /* With this regular expression, we get the following matching groups: * 0: All the expression * 1+(6*N): The N-th part of the coordinate system (N is an unsigned integer between 0 and 2 (included) ; it is reduced to '*' if the two following groups are NULL * 2+(6*N): A single value for the N-th part * 3+(6*N): A list of values for the N-th part * 4+(6*N): First value of the list for the N-th part * 5+(6*N): All the other values (starting with a |) of the list for the N-th part * 6+(6*N): Last value of the list for the N-th part. */ private final static String allowedCoordSysRegExp = "^\\s*" + buildAllowedRegExp(Frame.regexp) + "\\s+" + buildAllowedRegExp(RefPos.regexp) + "\\s+" + buildAllowedRegExp(Flavor.regexp) + "\\s*$"; /** Pattern of an allowed coordinate system pattern. This object has been compiled with {@link #allowedCoordSysRegExp}. */ private final static Pattern allowedCoordSysPattern = Pattern.compile(allowedCoordSysRegExp); /** Human description of the syntax of a full coordinate system expression. */ private final static String COORD_SYS_SYNTAX = "\"[" + Frame.regexp + "] [" + RefPos.regexp + "] [" + Flavor.regexp + "]\" ; an empty string is also allowed and will be interpreted as the coordinate system locally used"; /** * Build the regular expression of a string defining the allowed values for one part of the whole coordinate system. * * @param rootRegExp All allowed part values. * * @return The corresponding regular expression. */ private static String buildAllowedRegExp(final String rootRegExp){ return "(" + rootRegExp + "|\\*|(\\(\\s*" + rootRegExp + "\\s*(\\|\\s*" + rootRegExp + "\\s*)*\\)))"; } /** * <p>List of all possible frames in an STC expression.</p> * * <p> * When no value is specified, the default one is {@link #UNKNOWNFRAME}. * The default value is also accessible through the attribute {@link #DEFAULT} * and it is possible to test whether a frame is the default with the function {@link #isDefault()}. * </p> * * <p><i>Note: * The possible values listed in this enumeration are limited to the subset of STC-S described by the section "6 Use of STC-S in TAP (informative)" * of the TAP Recommendation 1.0 (27th March 2010). * </i></p> * * @author Grégory Mantelet (ARI) * @version 1.4 (04/2017) * @since 1.3 */ public static enum Frame{ ECLIPTIC, FK4, FK5, J2000, GALACTIC, ICRS, UNKNOWNFRAME; /** Default value for a frame: {@link #UNKNOWNFRAME}. */ public static final Frame DEFAULT = UNKNOWNFRAME; /** Regular expression to test whether a string is a valid frame or not. This regular expression does not take into account * the case of an empty string (which means "default frame"). */ public static final String regexp = buildRegexp(Frame.class); /** * Tell whether this frame is the default one. * * @return <i>true</i> if this is the default frame, <i>false</i> */ public final boolean isDefault(){ return this == DEFAULT; } } /** * <p>List of all possible reference positions in an STC expression.</p> * * <p> * When no value is specified, the default one is {@link #UNKNOWNREFPOS}. * The default value is also accessible through the attribute {@link #DEFAULT} * and it is possible to test whether a reference position is the default with the function {@link #isDefault()}. * </p> * * <p><i>Note: * The possible values listed in this enumeration are limited to the subset of STC-S described by the section "6 Use of STC-S in TAP (informative)" * of the TAP Recommendation 1.0 (27th March 2010). * </i></p> * * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * @since 1.3 */ public static enum RefPos{ BARYCENTER, GEOCENTER, HELIOCENTER, LSR, TOPOCENTER, RELOCATABLE, UNKNOWNREFPOS; /** Default value for a reference position: {@link #UNKNOWNREFPOS}. */ public static final RefPos DEFAULT = UNKNOWNREFPOS; /** Regular expression to test whether a string is a valid reference position or not. This regular expression does not take into account * the case of an empty string (which means "default reference position"). */ public static final String regexp = buildRegexp(RefPos.class); /** * Tell whether this reference position is the default one. * * @return <i>true</i> if this is the default reference position, <i>false</i> */ public final boolean isDefault(){ return this == DEFAULT; } } /** * <p>List of all possible flavors in an STC expression.</p> * * <p> * When no value is specified, the default one is {@link #SPHERICAL2}. * The default value is also accessible through the attribute {@link #DEFAULT} * and it is possible to test whether a flavor is the default with the function {@link #isDefault()}. * </p> * * <p><i>Note: * The possible values listed in this enumeration are limited to the subset of STC-S described by the section "6 Use of STC-S in TAP (informative)" * of the TAP Recommendation 1.0 (27th March 2010). * </i></p> * * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * @since 1.3 */ public static enum Flavor{ CARTESIAN2, CARTESIAN3, SPHERICAL2; /** Default value for a flavor: {@link #SPHERICAL2}. */ public static final Flavor DEFAULT = SPHERICAL2; /** Regular expression to test whether a string is a valid flavor or not. This regular expression does not take into account * the case of an empty string (which means "default flavor"). */ public static final String regexp = buildRegexp(Flavor.class); /** * Tell whether this flavor is the default one. * * @return <i>true</i> if this is the default flavor, <i>false</i> */ public final boolean isDefault(){ return this == DEFAULT; } } /** * Build a regular expression covering all possible values of the given enumeration. * * @param enumType Class of an enumeration type. * * @return The build regular expression or "\s*" if the given enumeration contains no constants/values. * * @throws IllegalArgumentException If the given class is not an enumeration type. */ private static String buildRegexp(final Class<?> enumType) throws IllegalArgumentException{ // The given class must be an enumeration type: if (!enumType.isEnum()) throw new IllegalArgumentException("An enum class was expected, but a " + enumType.getName() + " has been given!"); // Get the enumeration constants/values: Object[] constants = enumType.getEnumConstants(); if (constants == null || constants.length == 0) return "\\s*"; // Concatenate all constants with pipe to build a choice regular expression: StringBuffer buf = new StringBuffer("("); for(int i = 0; i < constants.length; i++){ buf.append(constants[i]); if ((i + 1) < constants.length) buf.append('|'); } return buf.append(')').toString(); } /** * <p>Object representation of an STC coordinate system.</p> * * <p> * A coordinate system is composed of three parts: a frame ({@link #frame}), * a reference position ({@link #refpos}) and a flavor ({@link #flavor}). * </p> * * <p> * The default value - also corresponding to an empty string - should be: * {@link Frame#UNKNOWNFRAME} {@link RefPos#UNKNOWNREFPOS} {@link Flavor#SPHERICAL2}. * Once built, it is possible to know whether the coordinate system is the default one * or not thanks to function {@link #isDefault()}. * </p> * * <p> * An instance of this class can be easily serialized into STC-S using {@link #toSTCS()}, {@link #toFullSTCS()} * or {@link #toString()}. {@link #toFullSTCS()} will display default values explicitly * on the contrary to {@link #toSTCS()} which will replace them by empty strings. * </p> * * <p><i><b>Important note:</b> * The flavors CARTESIAN2 and CARTESIAN3 can not be used with other frame and reference position than * UNKNOWNFRAME and UNKNOWNREFPOS. In the contrary case an {@link IllegalArgumentException} is throw. * </i></p> * * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * @since 1.3 */ public static class CoordSys { /** First item of a coordinate system expression: the frame. */ public final Frame frame; /** Second item of a coordinate system expression: the reference position. */ public final RefPos refpos; /** Third and last item of a coordinate system expression: the flavor. */ public final Flavor flavor; /** Indicate whether all parts of the coordinate system are set to their default value. */ private final boolean isDefault; /** STC-S representation of this coordinate system. Default items are not written (that's to say, they are replaced by an empty string). */ private final String stcs; /** STC-S representation of this coordinate system. Default items are explicitly written. */ private final String fullStcs; /** * Build a default coordinate system (UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2). */ public CoordSys(){ this(null, null, null); } /** * Build a coordinate system with the given parts. * * @param fr Frame part. * @param rp Reference position part. * @param fl Flavor part. * * @throws IllegalArgumentException If a cartesian flavor is used with a frame and reference position other than UNKNOWNFRAME and UNKNOWNREFPOS. */ public CoordSys(final Frame fr, final RefPos rp, final Flavor fl) throws IllegalArgumentException{ frame = (fr == null) ? Frame.DEFAULT : fr; refpos = (rp == null) ? RefPos.DEFAULT : rp; flavor = (fl == null) ? Flavor.DEFAULT : fl; if (flavor != Flavor.SPHERICAL2 && (frame != Frame.UNKNOWNFRAME || refpos != RefPos.UNKNOWNREFPOS)) throw new IllegalArgumentException("a coordinate system expressed with a cartesian flavor MUST have an UNKNOWNFRAME and UNKNOWNREFPOS!"); isDefault = frame.isDefault() && refpos.isDefault() && flavor.isDefault(); stcs = ((!frame.isDefault() ? frame + " " : "") + (!refpos.isDefault() ? refpos + " " : "") + (!flavor.isDefault() ? flavor : "")).trim(); fullStcs = frame + " " + refpos + " " + flavor; } /** * Build a coordinate system by parsing the given STC-S expression. * * @param coordsys STC-S expression representing a coordinate system. <i>Empty string and NULL are allowed values ; they correspond to a default coordinate system.</i> * * @throws ParseException If the syntax of the given STC-S expression is wrong or if it is not a coordinate system only. */ public CoordSys(final String coordsys) throws ParseException{ CoordSys tmp = new STCSParser().parseCoordSys(coordsys); frame = tmp.frame; refpos = tmp.refpos; flavor = tmp.flavor; isDefault = tmp.isDefault; stcs = tmp.stcs; fullStcs = tmp.fullStcs; } /** * Tell whether this is the default coordinate system (UNKNOWNFRAME UNKNOWNREFPOS SPHERICAL2). * * @return <i>true</i> if it is the default coordinate system, <i>false</i> otherwise. */ public final boolean isDefault(){ return isDefault; } /** * Get the STC-S expression of this coordinate system, * in which default values are not written (they are replaced by empty strings). * * @return STC-S representation of this coordinate system. */ public String toSTCS(){ return stcs; } /** * Get the STC-S expression of this coordinate system, * in which default values are explicitly written. * * @return STC-S representation of this coordinate system. */ public String toFullSTCS(){ return fullStcs; } /** * Convert this coordinate system into a STC-S expression. * * @see java.lang.Object#toString() * @see #toSTCS() */ @Override public String toString(){ return stcs; } } /** * Parse the given STC-S representation of a coordinate system. * * @param stcs STC-S expression of a coordinate system. <i>Note: a NULL or empty string will be interpreted as a default coordinate system.</i> * * @return The object representation of the specified coordinate system. * * @throws ParseException If the given expression has a wrong STC-S syntax. */ public static CoordSys parseCoordSys(final String stcs) throws ParseException{ return (new STCSParser().parseCoordSys(stcs)); } /** * <p>Convert an object representation of a coordinate system into an STC-S expression.</p> * * <p><i>Note: * A NULL object will be interpreted as the default coordinate system and so an empty string will be returned. * Otherwise, this function is equivalent to {@link CoordSys#toSTCS()} (in which default values for each * coordinate system part is not displayed). * </i></p> * * @param coordSys The object representation of the coordinate system to convert into STC-S. * * @return The corresponding STC-S expression. * * @see CoordSys#toSTCS() * @see CoordSys#toFullSTCS() */ public static String toSTCS(final CoordSys coordSys){ if (coordSys == null) return ""; else return coordSys.toSTCS(); } /** * <p>Build a big regular expression gathering all of the given coordinate system syntaxes.</p> * * <p> * Each item of the given list must respect a strict syntax. Each part of the coordinate system * may be a single value, a list of values or a '*' (meaning all values are allowed). * A list of values must have the following syntax: <code>({value1}|{value2}|...)</code>. * An empty string is NOT here accepted. * </p> * * <p><i>Example: * <code>(ICRS|FK4|FK5) * SPHERICAL2</code> is OK, * but <code>(ICRS|FK4|FK5) *</code> is not valid because the flavor value is not defined. * </i></p> * * <p> * Since the default value of each part of a coordinate system should always be possible, * this function ensure these default values are always possible in the returned regular expression. * Thus, if some values except the default one are specified, the default value is automatically appended. * </p> * * <p><i>Note: * If the given array is NULL, all coordinate systems are allowed. * But if the given array is empty, none except an empty string or the default value will be allowed. * </i></p> * * @param allowedCoordSys List of all coordinate systems that are allowed. * * @return The corresponding regular expression. * * @throws ParseException If the syntax of one of the given allowed coordinate system is wrong. */ public static String buildCoordSysRegExp(final String[] allowedCoordSys) throws ParseException{ // NULL array => all coordinate systems are allowed: if (allowedCoordSys == null) return onlyCoordSysRegExp; // Empty array => no coordinate system (except the default one) is allowed: else if (allowedCoordSys.length == 0) return defaultCoordSysRegExp; // The final regular expression must be reduced to a coordinate system and nothing else before: StringBuffer finalRegExp = new StringBuffer("^\\s*("); // For each allowed coordinate system: Matcher m; int nbCoordSys = 0; for(int i = 0; i < allowedCoordSys.length; i++){ // NULL item => skipped! if (allowedCoordSys[i] == null) continue; else{ if (nbCoordSys > 0) finalRegExp.append('|'); nbCoordSys++; } // Check its syntax and identify all of its parts: m = allowedCoordSysPattern.matcher(allowedCoordSys[i].toUpperCase()); if (m.matches()){ finalRegExp.append('('); for(int g = 0; g < 3; g++){ // See the comment after the Javadoc of #allowedCoordSysRegExp for a complete list of available groups returned by the pattern. // SINGLE VALUE: if (m.group(2 + (6 * g)) != null) finalRegExp.append('(').append(defaultChoice(g, m.group(2 + (6 * g)))).append(m.group(2 + (6 * g))).append(')'); // LIST OF VALUES: else if (m.group(3 + (6 * g)) != null) finalRegExp.append('(').append(defaultChoice(g, m.group(3 + (6 * g)))).append(m.group(3 + (6 * g)).replaceAll("\\s", "").substring(1)); // JOKER (*): else{ switch(g){ case 0: finalRegExp.append(Frame.regexp); break; case 1: finalRegExp.append(RefPos.regexp); break; case 2: finalRegExp.append(Flavor.regexp); break; } finalRegExp.append('?'); } finalRegExp.append("\\s*"); } finalRegExp.append(')'); }else throw new ParseException("Wrong allowed coordinate system syntax for the " + (i + 1) + "-th item: \"" + allowedCoordSys[i] + "\"! Expected: \"frameRegExp refposRegExp flavorRegExp\" ; where each xxxRegExp = (xxx | '*' | '('xxx ('|' xxx)*')'), frame=\"" + Frame.regexp + "\", refpos=\"" + RefPos.regexp + "\" and flavor=\"" + Flavor.regexp + "\" ; an empty string is also allowed and will be interpreted as '*' (so all possible values)."); } // The final regular expression must be reduced to a coordinate system and nothing else after: finalRegExp.append(")\\s*$"); return (nbCoordSys > 0) ? finalRegExp.toString() : defaultCoordSysRegExp; } /** * Get the default value appended by a '|' character, ONLY IF the given value does not already contain the default value. * * @param g Index of the coordinate system part (0: Frame, 1: RefPos, 2: Flavor, another value will return an empty string). * @param value Value in which the default value must prefix. * * @return A prefix for the given value (the default value and a '|' if the default value is not already in the given value, "" otherwise). */ private static String defaultChoice(final int g, final String value){ switch(g){ case 0: return value.contains(Frame.DEFAULT.toString()) ? "" : Frame.DEFAULT + "|"; case 1: return value.contains(RefPos.DEFAULT.toString()) ? "" : RefPos.DEFAULT + "|"; case 2: return value.contains(Flavor.DEFAULT.toString()) ? "" : Flavor.DEFAULT + "|"; default: return ""; } } /* ****** */ /* REGION */ /* ****** */ /** * <p>List all possible region types allowed in an STC-S expression.</p> * * <p><i>Note: * The possible values listed in this enumeration are limited to the subset of STC-S described by the section "6 Use of STC-S in TAP (informative)" * of the TAP Recommendation 1.0 (27th March 2010). * </i></p> * * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * @since 1.3 */ public static enum RegionType{ POSITION, CIRCLE, BOX, POLYGON, UNION, INTERSECTION, NOT; } /** * <p>Object representation of an STC region.</p> * * <p> * This class contains a field for each possible parameter of a region. Depending of the region type * some are not used. In such case, these unused fields are set to NULL. * </p> * * <p> * An instance of this class can be easily serialized into STC-S using {@link #toSTCS()}, {@link #toFullSTCS()} * or {@link #toString()}. {@link #toFullSTCS()} will display default value explicit * on the contrary to {@link #toSTCS()} which will replace them by empty strings. * </p> * * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * @since 1.3 */ public static class Region { /** Type of the region. */ public final RegionType type; /** Coordinate system used by this region. * <i>Note: only the NOT region does not declare a coordinate system ; so only for this region this field is NULL.</i> */ public final CoordSys coordSys; /** List of coordinates' pairs. The second dimension of this array represents a pair of coordinates ; it is then an array of two elements. * <i>Note: this field is used by POINT, BOX, CIRCLE and POLYGON.</i> */ public final double[][] coordinates; /** Width of the BOX region. */ public final double width; /** Height of the BOX region. */ public final double height; /** Radius of the CIRCLE region. */ public final double radius; /** List of regions unified (UNION), intersected (INTERSECTION) or avoided (NOT). */ public final Region[] regions; /** STC-S representation of this region, in which default values of the coordinate system (if any) are not written (they are replaced by empty strings). * <i>Note: This attribute is NULL until the first call of the function {@link #toSTCS()} where it is built.</i> */ private String stcs = null; /** STC-S representation of this region, in which default values of the coordinate system (if any) are explicitly written. * <i>Note: This attribute is NULL until the first call of the function {@link #toFullSTCS()} where it is built.</i> */ private String fullStcs = null; /** The ADQL function object representing this region. * <i>Note: this attribute is NULL until the first call of the function {@link #toGeometry()} or {@link #toGeometry(ADQLQueryFactory)}.</i> */ private GeometryFunction geometry = null; /** * <p>Constructor for a POINT/POSITION region.</p> * * <p><i><b>Important note:</b> * The array of coordinates is used like that. No copy is done. * </i></p> * * @param coordSys Coordinate system. <i>note: It MAY BE null ; if so, the default coordinate system will be chosen</li> * @param coordinates A pair of coordinates ; coordinates[0] and coordinates[1]. */ public Region(final CoordSys coordSys, final double[] coordinates){ this(coordSys, new double[][]{coordinates}); } /** * <p>Constructor for a POINT/POSITION or a POLYGON region.</p> * * <p>Whether it is a polygon or a point depends on the number of given coordinates:</p> * <ul> * <li>1 item => POINT/POSITION</li> * <li>more items => POLYGON</li> * </ul> * * <p><i><b>Important note:</b> * The array of coordinates is used like that. No copy is done. * </i></p> * * @param coordSys Coordinate system. <i>note: It MAY BE null ; if so, the default coordinate system will be chosen</li> * @param coordinates List of coordinates' pairs ; coordinates[n] = 1 pair = 2 items (coordinates[n][0] and coordinates[n][1]) ; if 1 pair, it is a POINT/POSITION, but if more, it is a POLYGON. */ public Region(final CoordSys coordSys, final double[][] coordinates){ // Check roughly the coordinates: if (coordinates == null || coordinates.length == 0) throw new NullPointerException("Missing coordinates!"); else if (coordinates[0].length != 2) throw new IllegalArgumentException("Wrong number of coordinates! Expected at least 2 pairs of coordinates (so coordinates[0], coordinates[1] and coordinates[n].length = 2)."); // Decide of the region type in function of the number of coordinates' pairs: type = (coordinates.length > 1) ? RegionType.POLYGON : RegionType.POSITION; // Set the coordinate system (if NULL, choose the default one): this.coordSys = (coordSys == null ? new CoordSys() : coordSys); // Set the coordinates: this.coordinates = coordinates; // Set the other fields as not used: width = Double.NaN; height = Double.NaN; radius = Double.NaN; regions = null; } /** * <p>Constructor for a CIRCLE region.</p> * * <p><i><b>Important note:</b> * The array of coordinates is used like that. No copy is done. * </i></p> * * @param coordSys Coordinate system. <i>note: It MAY BE null ; if so, the default coordinate system will be chosen</li> * @param coordinates A pair of coordinates ; coordinates[0] and coordinates[1]. * @param radius The circle radius. */ public Region(final CoordSys coordSys, final double[] coordinates, final double radius){ // Check roughly the coordinates: if (coordinates == null || coordinates.length == 0) throw new NullPointerException("Missing coordinates!"); else if (coordinates.length != 2) throw new IllegalArgumentException("Wrong number of coordinates! Expected exactly 2 values."); // Set the region type: type = RegionType.CIRCLE; // Set the coordinate system (if NULL, choose the default one): this.coordSys = (coordSys == null ? new CoordSys() : coordSys); // Set the coordinates: this.coordinates = new double[][]{coordinates}; // Set the radius: this.radius = radius; // Set the other fields as not used: width = Double.NaN; height = Double.NaN; regions = null; } /** * <p>Constructor for a BOX region.</p> * * <p><i><b>Important note:</b> * The array of coordinates is used like that. No copy is done. * </i></p> * * @param coordSys Coordinate system. <i>note: It MAY BE null ; if so, the default coordinate system will be chosen</li> * @param coordinates A pair of coordinates ; coordinates[0] and coordinates[1]. * @param width Width of the box. * @param height Height of the box. */ public Region(final CoordSys coordSys, final double[] coordinates, final double width, final double height){ // Check roughly the coordinates: if (coordinates == null || coordinates.length == 0) throw new NullPointerException("Missing coordinates!"); else if (coordinates.length != 2) throw new IllegalArgumentException("Wrong number of coordinates! Expected exactly 2 values."); // Set the region type: type = RegionType.BOX; // Set the coordinate system (if NULL, choose the default one): this.coordSys = (coordSys == null ? new CoordSys() : coordSys); // Set the coordinates: this.coordinates = new double[][]{coordinates}; // Set the size of the box: this.width = width; this.height = height; // Set the other fields as not used: radius = Double.NaN; regions = null; } /** * <p>Constructor for a UNION or INTERSECTION region.</p> * * <p><i><b>Important note:</b> * The array of regions is used like that. No copy is done. * </i></p> * * @param unionOrIntersection Type of the region to create. <i>Note: It can be ONLY a UNION or INTERSECTION. Another value will throw an IllegalArgumentException).</i> * @param coordSys Coordinate system. <i>note: It MAY BE null ; if so, the default coordinate system will be chosen</li> * @param regions Regions to unite or to intersect. <i>Note: At least two regions must be provided.</i> */ public Region(final RegionType unionOrIntersection, final CoordSys coordSys, final Region[] regions){ // Check the type: if (unionOrIntersection == null) throw new NullPointerException("Missing type of region (UNION or INTERSECTION here)!"); else if (unionOrIntersection != RegionType.UNION && unionOrIntersection != RegionType.INTERSECTION) throw new IllegalArgumentException("Wrong region type: \"" + unionOrIntersection + "\"! This constructor lets create only an UNION or INTERSECTION region."); // Check the list of regions: if (regions == null || regions.length == 0) throw new NullPointerException("Missing regions to " + (unionOrIntersection == RegionType.UNION ? "unite" : "intersect") + "!"); else if (regions.length < 2) throw new IllegalArgumentException("Wrong number of regions! Expected at least 2 regions."); // Set the region type: type = unionOrIntersection; // Set the coordinate system (if NULL, choose the default one): this.coordSys = (coordSys == null ? new CoordSys() : coordSys); // Set the regions: this.regions = regions; // Set the other fields as not used: coordinates = null; radius = Double.NaN; width = Double.NaN; height = Double.NaN; } /** * Constructor for a NOT region. * * @param region Any region to not select. */ public Region(final Region region){ // Check the region parameter: if (region == null) throw new NullPointerException("Missing region to NOT select!"); // Set the region type: type = RegionType.NOT; // Set the regions: this.regions = new Region[]{region}; // Set the other fields as not used: coordSys = null; coordinates = null; radius = Double.NaN; width = Double.NaN; height = Double.NaN; } /** * <p>Build a Region from the given ADQL representation.</p> * * <p><i>Note: * Only {@link PointFunction}, {@link CircleFunction}, {@link BoxFunction}, {@link PolygonFunction} and {@link RegionFunction} * are accepted here. Other extensions of {@link GeometryFunction} will throw an {@link IllegalArgumentException}. * </i></p> * * @param geometry The ADQL representation of the region to create here. * * @throws IllegalArgumentException If the given geometry is neither of {@link PointFunction}, {@link BoxFunction}, {@link PolygonFunction} and {@link RegionFunction}. * @throws ParseException If the declared coordinate system, the coordinates or the STC-S definition has a wrong syntax. */ public Region(final GeometryFunction geometry) throws IllegalArgumentException, ParseException{ if (geometry == null) throw new NullPointerException("Missing geometry to convert into STCS.Region!"); if (geometry instanceof PointFunction){ type = RegionType.POSITION; coordSys = STCS.parseCoordSys(extractString(geometry.getCoordinateSystem())); coordinates = new double[][]{{extractNumeric(((PointFunction)geometry).getCoord1()),extractNumeric(((PointFunction)geometry).getCoord2())}}; width = Double.NaN; height = Double.NaN; radius = Double.NaN; regions = null; }else if (geometry instanceof CircleFunction){ type = RegionType.CIRCLE; coordSys = STCS.parseCoordSys(extractString(geometry.getCoordinateSystem())); coordinates = new double[][]{{extractNumeric(((CircleFunction)geometry).getCoord1()),extractNumeric(((CircleFunction)geometry).getCoord2())}}; radius = extractNumeric(((CircleFunction)geometry).getRadius()); width = Double.NaN; height = Double.NaN; regions = null; }else if (geometry instanceof BoxFunction){ type = RegionType.BOX; coordSys = STCS.parseCoordSys(extractString(geometry.getCoordinateSystem())); coordinates = new double[][]{{extractNumeric(((BoxFunction)geometry).getCoord1()),extractNumeric(((BoxFunction)geometry).getCoord2())}}; width = extractNumeric(((BoxFunction)geometry).getWidth()); height = extractNumeric(((BoxFunction)geometry).getHeight()); radius = Double.NaN; regions = null; }else if (geometry instanceof PolygonFunction){ PolygonFunction poly = (PolygonFunction)geometry; type = RegionType.POLYGON; coordSys = STCS.parseCoordSys(extractString(poly.getCoordinateSystem())); coordinates = new double[(poly.getNbParameters() - 1) / 2][2]; for(int i = 0; i < coordinates.length; i++) coordinates[i] = new double[]{extractNumeric(poly.getParameter(1 + i * 2)),extractNumeric(poly.getParameter(2 + i * 2))}; width = Double.NaN; height = Double.NaN; radius = Double.NaN; regions = null; }else if (geometry instanceof RegionFunction){ Region r = STCS.parseRegion(extractString(((RegionFunction)geometry).getParameter(0))); type = r.type; coordSys = r.coordSys; coordinates = r.coordinates; width = r.width; height = r.height; radius = r.radius; regions = r.regions; }else throw new IllegalArgumentException("Unknown region type! Only geometrical function PointFunction, CircleFunction, BoxFunction, PolygonFunction and RegionFunction are allowed."); } /** * Extract a string value from the given {@link ADQLOperand} * which is <b>expected to be a {@link StringConstant} instance</b>. * * @param op A string operand. * * @return The string value embedded in the given operand. * * @throws ParseException If the given operand is not an instance of {@link StringConstant}. */ private static String extractString(final ADQLOperand op) throws ParseException{ if (op == null) throw new NullPointerException("Missing operand!"); else if (op instanceof StringConstant) return ((StringConstant)op).getValue(); else throw new ParseException("Can not convert into STC-S a non string argument (including ADQLColumn and Concatenation)!"); } /** * Extract a numeric value from the given {@link ADQLOperand} * which is <b>expected to be a {@link NumericConstant} instance</b> * or a {@link NegativeOperand} embedding a {@link NumericConstant}. * * @param op A numeric operand. * * @return The numeric value embedded in the given operand. * * @throws ParseException If the given operand is not an instance of {@link NumericConstant} or a {@link NegativeOperand}. */ private static double extractNumeric(final ADQLOperand op) throws ParseException{ if (op == null) throw new NullPointerException("Missing operand!"); else if (op instanceof NumericConstant) return Double.parseDouble(((NumericConstant)op).getValue()); else if (op instanceof NegativeOperand) return extractNumeric(((NegativeOperand)op).getOperand()) * -1; else throw new ParseException("Can not convert into STC-S a non numeric argument (including ADQLColumn and Operation)!"); } /** * <p>Get the STC-S representation of this region (in which default values * of the coordinate system are not written ; they are replaced by empty strings).</p> * * <p><i>Note: * This function build the STC-S just once and store it in a class attribute. * The value of this attribute is then returned at next calls of this function. * </i></p> * * @return Its STC-S representation. */ public String toSTCS(){ if (stcs != null) return stcs; else{ // Write the region type: StringBuffer buf = new StringBuffer(type.toString()); // Write the coordinate system (except for NOT): if (type != RegionType.NOT){ String coordSysStr = coordSys.toSTCS(); if (coordSysStr != null && coordSysStr.length() > 0) buf.append(' ').append(coordSysStr); buf.append(' '); } // Write the other parameters (coordinates, regions, ...): switch(type){ case POSITION: case POLYGON: appendCoordinates(buf, coordinates); break; case CIRCLE: appendCoordinates(buf, coordinates); buf.append(' ').append(radius); break; case BOX: appendCoordinates(buf, coordinates); buf.append(' ').append(width).append(' ').append(height); break; case UNION: case INTERSECTION: case NOT: buf.append('('); appendRegions(buf, regions, false); buf.append(')'); break; } // Return the built STC-S: return (stcs = buf.toString()); } } /** * <p>Get the STC-S representation of this region (in which default values * of the coordinate system are explicitly written).</p> * * <p><i>Note: * This function build the STC-S just once and store it in a class attribute. * The value of this attribute is then returned at next calls of this function. * </i></p> * * @return Its STC-S representation. */ public String toFullSTCS(){ if (fullStcs != null) return fullStcs; else{ // Write the region type: StringBuffer buf = new StringBuffer(type.toString()); // Write the coordinate system (except for NOT): if (type != RegionType.NOT){ String coordSysStr = coordSys.toFullSTCS(); if (coordSysStr != null && coordSysStr.length() > 0) buf.append(' ').append(coordSysStr); buf.append(' '); } // Write the other parameters (coordinates, regions, ...): switch(type){ case POSITION: case POLYGON: appendCoordinates(buf, coordinates); break; case CIRCLE: appendCoordinates(buf, coordinates); buf.append(' ').append(radius); break; case BOX: appendCoordinates(buf, coordinates); buf.append(' ').append(width).append(' ').append(height); break; case UNION: case INTERSECTION: case NOT: buf.append('('); appendRegions(buf, regions, true); buf.append(')'); break; } // Return the built STC-S: return (fullStcs = buf.toString()); } } /** * Append all the given coordinates to the given buffer. * * @param buf Buffer in which coordinates must be appended. * @param coords Coordinates to append. */ private static void appendCoordinates(final StringBuffer buf, final double[][] coords){ for(int i = 0; i < coords.length; i++){ if (i > 0) buf.append(' '); buf.append(coords[i][0]).append(' ').append(coords[i][1]); } } /** * Append all the given regions in the given buffer. * * @param buf Buffer in which regions must be appended. * @param regions Regions to append. * @param fullCoordSys Indicate whether the coordinate system of the regions must explicitly display the default values. */ private static void appendRegions(final StringBuffer buf, final Region[] regions, final boolean fullCoordSys){ for(int i = 0; i < regions.length; i++){ if (i > 0) buf.append(' '); if (fullCoordSys) buf.append(regions[i].toFullSTCS()); else buf.append(regions[i].toSTCS()); } } @Override public String toString(){ return toSTCS(); } /** * <p>Convert this region into its corresponding ADQL representation.</p> * * <ul> * <li><b>POSITION:</b> {@link PointFunction}</li> * <li><b>CIRCLE:</b> {@link CircleFunction}</li> * <li><b>BOX:</b> {@link BoxFunction}</li> * <li><b>POLYGON:</b> {@link PolygonFunction}</li> * <li><b>UNION, INTERSECTION, NOT:</b> {@link RegionFunction}</li> * </ul> * * <p><i>Note: * This function is using the default ADQL factory, built using {@link ADQLQueryFactory#ADQLQueryFactory()}. * </i></p> * * @return The corresponding ADQL representation. * * @see #toGeometry(ADQLQueryFactory) */ public GeometryFunction toGeometry(){ return toGeometry(null); } /** * <p>Convert this region into its corresponding ADQL representation.</p> * * <ul> * <li><b>POSITION:</b> {@link PointFunction}</li> * <li><b>CIRCLE:</b> {@link CircleFunction}</li> * <li><b>BOX:</b> {@link BoxFunction}</li> * <li><b>POLYGON:</b> {@link PolygonFunction}</li> * <li><b>UNION, INTERSECTION, NOT:</b> {@link RegionFunction}</li> * </ul> * * <p><i>Note: * This function build the ADQL representation just once and store it in a class attribute. * The value of this attribute is then returned at next calls of this function. * </i></p> * * @param factory The factory of ADQL objects to use. * * @return The corresponding ADQL representation. */ public GeometryFunction toGeometry(ADQLQueryFactory factory){ if (factory == null) factory = new ADQLQueryFactory(); try{ if (geometry != null) return geometry; else{ StringConstant coordSysObj = factory.createStringConstant(coordSys == null ? "" : coordSys.toString()); switch(type){ case POSITION: return (geometry = factory.createPoint(coordSysObj, toNumericObj(coordinates[0][0], factory), toNumericObj(coordinates[0][1], factory))); case CIRCLE: return (geometry = factory.createCircle(coordSysObj, toNumericObj(coordinates[0][0], factory), toNumericObj(coordinates[0][1], factory), toNumericObj(radius, factory))); case BOX: return (geometry = factory.createBox(coordSysObj, toNumericObj(coordinates[0][0], factory), toNumericObj(coordinates[0][1], factory), toNumericObj(width, factory), toNumericObj(height, factory))); case POLYGON: ArrayList<ADQLOperand> coords = new ArrayList<ADQLOperand>(coordinates.length * 2); for(int i = 0; i < coordinates.length; i++){ coords.add(toNumericObj(coordinates[i][0], factory)); coords.add(toNumericObj(coordinates[i][1], factory)); } return (geometry = factory.createPolygon(coordSysObj, coords)); default: return (geometry = factory.createRegion(factory.createStringConstant(toString()))); } } }catch(Exception pe){ return null; } } /** * <p>Convert a numeric value into an ADQL representation:</p> * * <ul> * <li>If negative: NegativeOperand(NumericConstant(val))</li> * <li>Otherwise: NumericConstant(val)</li> * </ul> * * @param val The value to embed in an ADQL object. * @param factory The factory to use to created ADQL objects. * * @return The representing ADQL representation. * * @throws Exception If an error occurs while creating the ADQL object. */ private ADQLOperand toNumericObj(final double val, final ADQLQueryFactory factory) throws Exception{ if (val >= 0) return factory.createNumericConstant("" + val); else return factory.createNegativeOperand(factory.createNumericConstant("" + (val * -1))); } } /** * Parse the given STC-S expression representing a geometrical region. * * @param stcsRegion STC-S expression of a region. <i>Note: MUST be different from NULL.</i> * * @return The object representation of the specified geometrical region. * * @throws ParseException If the given expression is NULL, empty string or if the STC-S syntax is wrong. */ public static Region parseRegion(final String stcsRegion) throws ParseException{ if (stcsRegion == null || stcsRegion.trim().length() == 0) throw new ParseException("Missing STC-S expression to parse!"); return (new STCSParser().parseRegion(stcsRegion)); } /** * Convert into STC-S the given object representation of a geometrical region. * * @param region Region to convert into STC-S. * * @return The corresponding STC-S expression. */ public static String toSTCS(final Region region){ if (region == null) throw new NullPointerException("Missing region to serialize into STC-S!"); return region.toSTCS(); } /** * <p>Convert into STC-S the given ADQL representation of a geometrical function.</p> * * <p><i><b>Important note:</b> * Only {@link PointFunction}, {@link CircleFunction}, {@link BoxFunction}, {@link PolygonFunction} * and {@link RegionFunction} are accepted here. Other extensions of {@link GeometryFunction} will * throw an {@link IllegalArgumentException}. * </i></p> * * @param region ADQL representation of the region to convert into STC-S. * * @return The corresponding STC-S expression. * * @throws ParseException If the given object is NULL or not of the good type. */ public static String toSTCS(final GeometryFunction region) throws ParseException{ if (region == null) throw new NullPointerException("Missing region to serialize into STC-S!"); return (new Region(region)).toSTCS(); } /* *************************** */ /* PARSER OF STC-S EXPRESSIONS */ /* *************************** */ /** * Let parse any STC-S expression. * * @author Grégory Mantelet (ARI) * @version 1.3 (11/2014) * @since 1.3 */ private static class STCSParser { /** Regular expression of a numerical value. */ private final static String numericRegExp = "(\\+|-)?(\\d+(\\.\\d*)?|\\.\\d+)([Ee](\\+|-)?\\d+)?"; /** Position of the next characters to read in the STC-S expression to parse. */ private int pos; /** Full STC-S expression to parse. */ private String stcs; /** Last read token (can be a numeric, a string, a region type, ...). */ private String token; /** Buffer used to read tokens. */ private StringBuffer buffer; /** * Exception sent when the end of the expression * (EOE = End Of Expression) is reached. * * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * @since 1.3 */ private static class EOEException extends ParseException { private static final long serialVersionUID = 1L; /** Build a simple EOEException. */ public EOEException(){ super("Unexpected End Of Expression!"); } } /** * Build the STC-S parser. */ public STCSParser(){} /** * Parse the given STC-S expression, expected as a coordinate system. * * @param stcs The STC-S expression to parse. * * @return The corresponding object representation of the specified coordinate system. * * @throws ParseException If the syntax of the given STC-S expression is wrong or if it is not a coordinate system. */ public CoordSys parseCoordSys(final String stcs) throws ParseException{ init(stcs); CoordSys coordsys = null; try{ coordsys = coordSys(); end(COORD_SYS_SYNTAX); return coordsys; }catch(EOEException ex){ ex.printStackTrace(); return new CoordSys(); } } /** * Parse the given STC-S expression, expected as a geometrical region. * * @param stcs The STC-S expression to parse. * * @return The corresponding object representation of the specified geometrical region. * * @throws ParseException If the syntax of the given STC-S expression is wrong or if it is not a geometrical region. */ public Region parseRegion(final String stcs) throws ParseException{ init(stcs); Region region = region(); end("\"POSITION <coordsys> <coordPair>\", \"CIRCLE <coordSys> <coordPair> <numeric>\", \"BOX <coordSys> <coordPair> <coordPair>\", \"POLYGON <coordSys> <coordPair> <coordPair> <coordPair> [<coordPair> ...]\", \"UNION <coordSys> ( <region> <region> [<region> ...] )\", \"INTERSECTION [<coordSys>] ( <region> <region> [<region> ...] )\" or \"NOT ( <region> )\""); return region; } /** * Prepare the parser in order to read the given STC-S expression. * * @param newStcs New STC-S expression to parse from now. */ private void init(final String newStcs){ stcs = (newStcs == null) ? "" : newStcs; token = null; buffer = new StringBuffer(); pos = 0; } /** * Finalize the parsing. * No more characters (except eventually some space characters) should remain in the STC-S expression to parse. * * @param expectedSyntax Description of the good syntax expected. This description is used only to write the * {@link ParseException} in case other non-space characters are found among the remaining characters. * * @throws ParseException If other non-space characters remains. */ private void end(final String expectedSyntax) throws ParseException{ // Skip all spaces: skipSpaces(); // If there is still some characters, they are not expected, and so throw an exception: if (stcs.length() > 0 && pos < stcs.length()) throw new ParseException("Incorrect syntax: \"" + stcs.substring(pos) + "\" was unexpected! Expected syntax: " + expectedSyntax + ".", new TextPosition(1, pos, 1, stcs.length())); // Reset the buffer, token and the STC-S expression to parse: buffer = null; stcs = null; token = null; } /** * Tool function which skip all next space characters until the next meaningful characters. */ private void skipSpaces(){ while(pos < stcs.length() && Character.isWhitespace(stcs.charAt(pos))) pos++; } /** * <p>Get the next meaningful word. This word can be a numeric, any string constant or a region type.</p> * * <p> * In case the end of the expression is reached before getting any meaningful character, an {@link EOEException} is thrown. * </p> * * @return The full read word/token. * * @throws EOEException If the end of the STC-S expression is reached before getting any meaningful character. */ private String nextToken() throws EOEException{ // Skip all spaces: skipSpaces(); // Fetch all characters until word separator (a space or a open/close parenthesis): while(pos < stcs.length() && !Character.isWhitespace(stcs.charAt(pos)) && stcs.charAt(pos) != '(' && stcs.charAt(pos) != ')') buffer.append(stcs.charAt(pos++)); // If no character has been fetched while at least one was expected, throw an exception: if (buffer.length() == 0) throw new EOEException(); // Save the read token and reset the buffer: token = buffer.toString(); buffer.delete(0, token.length()); return token; } /** * Read the next token as a numeric. * If not a numeric, a {@link ParseException} is thrown. * * @return The read numerical value. * * @throws ParseException If the next token is not a numerical expression. */ private double numeric() throws ParseException{ if (nextToken().matches(numericRegExp)) return Double.parseDouble(token); else throw new ParseException("a numeric was expected!", new TextPosition(1, pos - token.length(), 1, pos)); } /** * Read the next 2 tokens as a coordinate pairs (so as 2 numerical values). * If not 2 numeric, a {@link ParseException} is thrown. * * @return The read coordinate pairs. * * @throws ParseException If the next 2 tokens are not 2 numerical expressions. */ private double[] coordPair() throws ParseException{ skipSpaces(); int startPos = pos; try{ return new double[]{numeric(),numeric()}; }catch(ParseException pe){ if (pe instanceof EOEException) throw pe; else throw new ParseException("a coordinates pair (2 numerics separated by one or more spaces) was expected!", new TextPosition(1, startPos, 1, pos)); } } /** * Read and parse the next tokens as a coordinate system expression. * If they do not match, a {@link ParseException} is thrown. * * @return The object representation of the read coordinate system. * * @throws ParseException If the next tokens are not representing a valid coordinate system. */ private CoordSys coordSys() throws ParseException{ // Skip all spaces: skipSpaces(); // Backup the current position: /* (because every parts of a coordinate system are optional ; * like this, it will be possible to go back in the expression * to parse if optional parts are not written) */ String oldToken = token; int startPos = pos; Frame fr = null; RefPos rp = null; Flavor fl = null; try{ // Read the token: nextToken(); // Try to parse it as a frame: if ((fr = frame()) != null){ // if success, go the next token: startPos = pos; oldToken = token; nextToken(); } // Try to parse the last read token as a reference position: if ((rp = refpos()) != null){ // if success, go the next token: startPos = pos; oldToken = token; nextToken(); } // Try to parse the last read token as a flavor: if ((fl = flavor()) == null){ // if NOT a success, go back "in time" (go back to the position before reading the token): pos = startPos; token = oldToken; } }catch(EOEException ex){ /* End Of Expression may happen here since all parts of a coordinate system are optional. * So, there is no need to treat the error. */ } // Build the object representation of the read coordinate system: /* Note: if nothing has been read for one or all parts of the coordinate system, * the NULL value will be replaced automatically in the constructor * by the default value of the corresponding part(s). */ try{ return new CoordSys(fr, rp, fl); }catch(IllegalArgumentException iae){ throw new ParseException(iae.getMessage(), new TextPosition(1, startPos, 1, pos)); } } /** * Parse the last read token as FRAME. * * @return The corresponding enumeration item, or NULL if the last token is not a valid FRAME item. */ private Frame frame(){ try{ return Frame.valueOf(token.toUpperCase()); }catch(IllegalArgumentException iae){ return null; } } /** * Parse the last read token as REFERENCE POSITION. * * @return The corresponding enumeration item, or NULL if the last token is not a valid REFERENCE POSITION item. */ private RefPos refpos(){ try{ return RefPos.valueOf(token.toUpperCase()); }catch(IllegalArgumentException iae){ return null; } } /** * Parse the last read token as FLAVOR. * * @return The corresponding enumeration item, or NULL if the last token is not a valid FLAVOR item. */ private Flavor flavor(){ try{ return Flavor.valueOf(token.toUpperCase()); }catch(IllegalArgumentException iae){ return null; } } /** * Read and parse the next tokens as a geometrical region. * If they do not match, a {@link ParseException} is thrown. * * @return The object representation of the read geometrical region. * * @throws ParseException If the next tokens are not representing a valid geometrical region. */ private Region region() throws ParseException{ // Skip all spaces: skipSpaces(); // Read the next token (it should be the region type): int startPos = pos; token = nextToken().toUpperCase(); /* Identify the region type, next the expected parameters and finally build the corresponding object representation */ // POSITION case: if (token.equals("POSITION")){ try{ CoordSys coordSys = coordSys(); double[] coords = coordPair(); return new Region(coordSys, coords); }catch(Exception e){ throw buildException(e, "\"POSITION <coordSys> <coordPair>\", where coordPair=\"<numeric> <numeric>\" and coordSys=" + COORD_SYS_SYNTAX, startPos); } } // CIRCLE case: else if (token.equals("CIRCLE")){ try{ CoordSys coordSys = coordSys(); double[] coords = coordPair(); double radius = numeric(); return new Region(coordSys, coords, radius); }catch(Exception e){ throw buildException(e, "\"CIRCLE <coordSys> <coordPair> <radius>\", where coordPair=\"<numeric> <numeric>\", radius=\"<numeric>\" and coordSys=" + COORD_SYS_SYNTAX, startPos); } } // BOX case: else if (token.equals("BOX")){ try{ CoordSys coordSys = coordSys(); double[] coords = coordPair(); double width = numeric(), height = numeric(); return new Region(coordSys, coords, width, height); }catch(Exception e){ throw buildException(e, "\"BOX <coordSys> <coordPair> <width> <height>\", where coordPair=\"<numeric> <numeric>\", width and height=\"<numeric>\" and coordSys=" + COORD_SYS_SYNTAX, startPos); } } // POLYGON case: else if (token.equals("POLYGON")){ try{ CoordSys coordSys = coordSys(); ArrayList<Double> coordinates = new ArrayList<Double>(6); double[] coords; for(int i = 0; i < 3; i++){ coords = coordPair(); coordinates.add(coords[0]); coordinates.add(coords[1]); } boolean moreCoord = true; int posBackup; do{ posBackup = pos; try{ coords = coordPair(); coordinates.add(coords[0]); coordinates.add(coords[1]); }catch(ParseException pe){ moreCoord = false; pos = posBackup; } }while(moreCoord); double[][] allCoords = new double[coordinates.size() / 2][2]; for(int i = 0; i < coordinates.size() && i + 1 < coordinates.size(); i += 2) allCoords[i / 2] = new double[]{coordinates.get(i),coordinates.get(i + 1)}; return new Region(coordSys, allCoords); }catch(Exception e){ throw buildException(e, "\"POLYGON <coordSys> <coordPair> <coordPair> <coordPair> [<coordPair> ...]\", where coordPair=\"<numeric> <numeric>\" and coordSys=" + COORD_SYS_SYNTAX, startPos); } } // UNION & INTERSECTION cases: else if (token.equals("UNION") || token.equals("INTERSECTION")){ RegionType type = (token.equals("UNION") ? RegionType.UNION : RegionType.INTERSECTION); try{ CoordSys coordSys = coordSys(); ArrayList<Region> regions = new ArrayList<Region>(2); skipSpaces(); if (stcs.charAt(pos) != '(') throw buildException(new ParseException("a opening parenthesis - ( - was expected!", new TextPosition(1, pos, 1, pos + 1)), "\"" + type + " <coordSys> ( <region> <region> [<region> ...] )\", where coordSys=" + COORD_SYS_SYNTAX, startPos); else pos++; // parse and add the FIRST region: regions.add(region()); // parse and add the SECOND region: regions.add(region()); skipSpaces(); while(stcs.charAt(pos) != ')'){ regions.add(region()); skipSpaces(); } pos++; return new Region(type, coordSys, regions.toArray(new Region[regions.size()])); }catch(Exception e){ if (e instanceof ParseException && e.getMessage().startsWith("Incorrect syntax: \"")) throw (ParseException)e; else throw buildException(e, "\"" + type + " <coordSys> ( <region> <region> [<region> ...] )\", where coordSys=" + COORD_SYS_SYNTAX, startPos); } } // NOT case: else if (token.equals("NOT")){ try{ skipSpaces(); if (stcs.charAt(pos) != '(') throw buildException(new ParseException("an opening parenthesis - ( - was expected!", new TextPosition(1, pos, 1, pos + 1)), "\"NOT ( <region> )\"", startPos); else pos++; Region region = region(); skipSpaces(); if (stcs.charAt(pos) != ')') throw buildException(new ParseException("a closing parenthesis - ) - was expected!", new TextPosition(1, pos, 1, pos + 1)), "\"NOT ( <region> )\"", startPos); else pos++; return new Region(region); }catch(Exception e){ if (e instanceof ParseException && e.getMessage().startsWith("Incorrect syntax: ")) throw (ParseException)e; else throw buildException(e, "\"NOT ( <region> )\"", startPos); } } // Otherwise, the region type is not known and so a ParseException is thrown: else throw new ParseException("Unknown STC region type: \"" + token + "\"!", new TextPosition(1, startPos, 1, pos)); } /** * Build a {@link ParseException} based on the given one and by adding the human description of what was expected, if needed. * * @param ex Root exception. * @param expectedSyntax Human description of what was expected. * @param startPos Position of the first character of the wrong part of expression. * * @return The build exception. */ private ParseException buildException(final Exception ex, final String expectedSyntax, int startPos){ if (ex instanceof EOEException) return new ParseException("Unexpected End Of Expression! Expected syntax: " + expectedSyntax + ".", new TextPosition(1, startPos, 1, pos)); else if (ex instanceof ParseException) return new ParseException("Incorrect syntax: " + ex.getMessage() + " Expected syntax: " + expectedSyntax + ".", (((ParseException)ex).getPosition() != null ? ((ParseException)ex).getPosition() : new TextPosition(1, startPos, 1, pos))); else return new ParseException(ex.getMessage(), new TextPosition(1, startPos, 1, pos)); } } }