| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.internal.referencing; |
| |
| import java.lang.reflect.Array; |
| import java.util.function.Function; |
| import javax.measure.Unit; |
| import javax.measure.Quantity; |
| import javax.measure.quantity.Angle; |
| import org.opengis.parameter.ParameterValue; |
| import org.opengis.parameter.ParameterValueGroup; |
| import org.opengis.parameter.GeneralParameterValue; |
| import org.opengis.parameter.GeneralParameterDescriptor; |
| import org.opengis.referencing.IdentifiedObject; |
| import org.opengis.referencing.ReferenceIdentifier; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.cs.CoordinateSystem; |
| import org.opengis.referencing.cs.CoordinateSystemAxis; |
| import org.opengis.referencing.datum.Datum; |
| import org.opengis.referencing.datum.GeodeticDatum; |
| import org.opengis.referencing.datum.PrimeMeridian; |
| import org.opengis.referencing.datum.Ellipsoid; |
| import org.opengis.referencing.operation.Matrix; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.apache.sis.referencing.IdentifiedObjects; |
| import org.apache.sis.referencing.crs.AbstractCRS; |
| import org.apache.sis.referencing.cs.AbstractCS; |
| import org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis; |
| import org.apache.sis.referencing.datum.AbstractDatum; |
| import org.apache.sis.referencing.datum.DefaultGeodeticDatum; |
| import org.apache.sis.referencing.datum.DefaultPrimeMeridian; |
| import org.apache.sis.referencing.datum.DefaultEllipsoid; |
| import org.apache.sis.referencing.operation.transform.MathTransforms; |
| import org.apache.sis.internal.referencing.provider.Affine; |
| import org.apache.sis.parameter.DefaultParameterValue; |
| import org.apache.sis.parameter.Parameterized; |
| import org.apache.sis.io.wkt.ElementKind; |
| import org.apache.sis.io.wkt.FormattableObject; |
| import org.apache.sis.io.wkt.Formatter; |
| import org.apache.sis.measure.Units; |
| import org.apache.sis.util.Static; |
| import org.apache.sis.util.CharSequences; |
| import org.apache.sis.util.resources.Vocabulary; |
| import org.apache.sis.internal.util.Constants; |
| import org.apache.sis.internal.util.Numerics; |
| import org.apache.sis.math.DecimalFunctions; |
| import org.apache.sis.math.Vector; |
| |
| |
| /** |
| * Utility methods for referencing WKT formatting. |
| * |
| * This class provides a set of {@code toFormattable(…)} for various {@link IdentifiedObject} subtypes. |
| * It is important to <strong>not</strong> provide a generic {@code toFormattable(IdentifiedObject)} |
| * method, because the user may choose to implement more than one GeoAPI interface for the same object. |
| * We need to be specific in order to select the right "aspect" of the given object. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.0 |
| * @since 0.4 |
| * @module |
| */ |
| public final class WKTUtilities extends Static { |
| /** |
| * Do not allow instantiation of this class. |
| */ |
| private WKTUtilities() { |
| } |
| |
| /** |
| * Returns the WKT type of the given interface. |
| * |
| * For {@link CoordinateSystem} base type, the returned value shall be one of |
| * {@code affine}, {@code Cartesian}, {@code cylindrical}, {@code ellipsoidal}, {@code linear}, |
| * {@code parametric}, {@code polar}, {@code spherical}, {@code temporal} or {@code vertical}. |
| * |
| * @param base the abstract base interface. |
| * @param type the interface or classes for which to get the WKT type. |
| * @return the WKT type for the given class or interface, or {@code null} if none. |
| * |
| * @see ReferencingUtilities#toPropertyName(Class, Class) |
| */ |
| public static String toType(final Class<?> base, final Class<?> type) { |
| if (type != base) { |
| final StringBuilder name = ReferencingUtilities.toPropertyName(base, type); |
| if (name != null) { |
| int end = name.length() - 2; |
| if (CharSequences.regionMatches(name, end, "CS")) { |
| name.setLength(end); |
| if ("time".contentEquals(name)) { |
| return "temporal"; |
| } |
| if (CharSequences.regionMatches(name, 0, "cartesian")) { |
| name.setCharAt(0, 'C'); // "Cartesian" |
| } |
| return name.toString(); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the given coordinate reference system as a formattable object. |
| * |
| * @param object the coordinate reference system, or {@code null}. |
| * @return the given coordinate reference system as a formattable object, or {@code null}. |
| */ |
| public static FormattableObject toFormattable(final CoordinateReferenceSystem object) { |
| if (object instanceof FormattableObject) { |
| return (FormattableObject) object; |
| } else { |
| return AbstractCRS.castOrCopy(object); |
| } |
| } |
| |
| /** |
| * Returns the given coordinate system as a formattable object. |
| * |
| * @param object the coordinate system, or {@code null}. |
| * @return the given coordinate system as a formattable object, or {@code null}. |
| */ |
| public static FormattableObject toFormattable(final CoordinateSystem object) { |
| if (object instanceof FormattableObject) { |
| return (FormattableObject) object; |
| } else { |
| return AbstractCS.castOrCopy(object); |
| } |
| } |
| |
| /** |
| * Returns the given coordinate system axis as a formattable object. |
| * |
| * @param object the coordinate system axis, or {@code null}. |
| * @return the given coordinate system axis as a formattable object, or {@code null}. |
| */ |
| public static FormattableObject toFormattable(final CoordinateSystemAxis object) { |
| if (object instanceof FormattableObject) { |
| return (FormattableObject) object; |
| } else { |
| return DefaultCoordinateSystemAxis.castOrCopy(object); |
| } |
| } |
| |
| /** |
| * Returns the given datum as a formattable object. |
| * |
| * @param object the datum, or {@code null}. |
| * @return the given datum as a formattable object, or {@code null}. |
| */ |
| public static FormattableObject toFormattable(final Datum object) { |
| if (object instanceof FormattableObject) { |
| return (FormattableObject) object; |
| } else { |
| return AbstractDatum.castOrCopy(object); |
| } |
| } |
| |
| /** |
| * Returns the given geodetic datum as a formattable object. |
| * |
| * @param object the datum, or {@code null}. |
| * @return the given datum as a formattable object, or {@code null}. |
| */ |
| public static FormattableObject toFormattable(final GeodeticDatum object) { |
| if (object instanceof FormattableObject) { |
| return (FormattableObject) object; |
| } else { |
| return DefaultGeodeticDatum.castOrCopy(object); |
| } |
| } |
| |
| /** |
| * Returns the ellipsoid as a formattable object. |
| * |
| * @param object the ellipsoid, or {@code null}. |
| * @return the given ellipsoid as a formattable object, or {@code null}. |
| */ |
| public static FormattableObject toFormattable(final Ellipsoid object) { |
| if (object instanceof FormattableObject) { |
| return (FormattableObject) object; |
| } else { |
| return DefaultEllipsoid.castOrCopy(object); |
| } |
| } |
| |
| /** |
| * Returns the given prime meridian as a formattable object. |
| * |
| * @param object the prime meridian, or {@code null}. |
| * @return the given prime meridian as a formattable object, or {@code null}. |
| */ |
| public static FormattableObject toFormattable(final PrimeMeridian object) { |
| if (object instanceof FormattableObject) { |
| return (FormattableObject) object; |
| } else { |
| return DefaultPrimeMeridian.castOrCopy(object); |
| } |
| } |
| |
| /** |
| * Converts the given object in a {@code FormattableObject} instance. Callers should verify that the |
| * given object is not already an instance of {@code FormattableObject} before to invoke this method. |
| * This method returns {@code null} if it can not convert the object. |
| * |
| * @param object the object to wrap. |
| * @param internal {@code true} if the formatting convention is {@code Convention.INTERNAL}. |
| * @return the given object converted to a {@code FormattableObject} instance, or {@code null}. |
| */ |
| public static FormattableObject toFormattable(final MathTransform object, boolean internal) { |
| Matrix matrix; |
| final ParameterValueGroup parameters; |
| if (internal && (matrix = MathTransforms.getMatrix(object)) != null) { |
| parameters = Affine.parameters(matrix); |
| } else if (object instanceof Parameterized) { |
| parameters = ((Parameterized) object).getParameterValues(); |
| } else { |
| matrix = MathTransforms.getMatrix(object); |
| if (matrix == null) { |
| return null; |
| } |
| parameters = Affine.parameters(matrix); |
| } |
| return new FormattableObject() { |
| @Override protected String formatTo(final Formatter formatter) { |
| WKTUtilities.appendParamMT(parameters, formatter); |
| return WKTKeywords.Param_MT; |
| } |
| }; |
| } |
| |
| /** |
| * If the given unit is one of the unit that can not be formatted without ambiguity in WKT format, |
| * return a proposed replacement. Otherwise returns {@code unit} unchanged. |
| * |
| * @param <Q> the unit dimension. |
| * @param unit the unit to test. |
| * @return the replacement to format, or {@code unit} if not needed. |
| * |
| * @since 0.8 |
| */ |
| @SuppressWarnings("unchecked") |
| public static <Q extends Quantity<Q>> Unit<Q> toFormattable(Unit<Q> unit) { |
| if (Units.isAngular(unit)) { |
| if (!((Unit<Angle>) unit).getConverterTo(Units.RADIAN).isLinear()) { |
| unit = (Unit<Q>) Units.DEGREE; |
| } |
| } |
| return unit; |
| } |
| |
| /** |
| * Appends the name of the given object to the formatter. |
| * |
| * @param object the object from which to get the name. |
| * @param formatter the formatter where to append the name. |
| * @param type the key of colors to apply if syntax colors are enabled. |
| */ |
| public static void appendName(final IdentifiedObject object, final Formatter formatter, final ElementKind type) { |
| String name = IdentifiedObjects.getName(object, formatter.getNameAuthority()); |
| if (name == null) { |
| name = IdentifiedObjects.getName(object, null); |
| if (name == null) { |
| name = Vocabulary.getResources(formatter.getLocale()).getString(Vocabulary.Keys.Unnamed); |
| } |
| } |
| formatter.append(name, (type != null) ? type : ElementKind.NAME); |
| } |
| |
| /** |
| * Appends a {@linkplain ParameterValueGroup group of parameters} in a {@code Param_MT[…]} element. |
| * |
| * @param parameters the parameter to append to the WKT, or {@code null} if none. |
| * @param formatter the formatter where to append the parameter. |
| */ |
| public static void appendParamMT(final ParameterValueGroup parameters, final Formatter formatter) { |
| if (parameters != null) { |
| appendName(parameters.getDescriptor(), formatter, ElementKind.PARAMETER); |
| append(parameters, formatter); |
| } |
| } |
| |
| /** |
| * Appends a {@linkplain ParameterValue parameter} in a {@code PARAMETER[…]} element. |
| * If the supplied parameter is actually a {@linkplain ParameterValueGroup parameter group}, |
| * all contained parameters will be flattened in a single list. |
| * |
| * @param parameter the parameter to append to the WKT, or {@code null} if none. |
| * @param formatter the formatter where to append the parameter. |
| */ |
| public static void append(GeneralParameterValue parameter, final Formatter formatter) { |
| if (parameter instanceof ParameterValueGroup) { |
| boolean first = true; |
| for (final GeneralParameterValue param : ((ParameterValueGroup) parameter).values()) { |
| if (first) { |
| formatter.newLine(); |
| first = false; |
| } |
| append(param, formatter); |
| } |
| } |
| if (parameter instanceof ParameterValue<?>) { |
| if (!(parameter instanceof FormattableObject)) { |
| parameter = new DefaultParameterValue<>((ParameterValue<?>) parameter); |
| } |
| formatter.append((FormattableObject) parameter); |
| formatter.newLine(); |
| } |
| } |
| |
| /** |
| * Returns {@code true} if the given parameter is defined in the EPSG code space. We handle EPSG |
| * parameters in a special way because Apache SIS uses the EPSG geodetic dataset as the primary |
| * source of coordinate operation definitions. |
| * |
| * <p>We intentionally don't define {@code isEPSG(OperationMethod)} method because the operation |
| * method may be the inverse of an EPSG method (for example "Inverse of Mercator (variant A)") |
| * which would not be recognized. Instead, {@code isEPSG(method.getParameters())} should work.</p> |
| * |
| * @param descriptor the parameter or group of parameters to inspect. |
| * @param ifUndefined the value to return if the code space is undefined. |
| * @return whether the given parameter is an EPSG parameter. |
| */ |
| public static boolean isEPSG(final GeneralParameterDescriptor descriptor, final boolean ifUndefined) { |
| if (descriptor != null) { |
| final ReferenceIdentifier id = descriptor.getName(); |
| if (id != null) { |
| final String cs = id.getCodeSpace(); |
| if (cs != null) { |
| return Constants.EPSG.equalsIgnoreCase(cs); |
| } |
| } |
| } |
| return ifUndefined; |
| } |
| |
| /** |
| * Suggests an amount of fraction digits to use for formatting numbers in each column of the given sequence |
| * of points. The number of fraction digits may be negative if we could round the numbers to 10, <i>etc</i>. |
| * |
| * @param crs the coordinate reference system for each points, or {@code null} if unknown. |
| * @param points the sequence of points. It is not required that each point has the same dimension. |
| * @return suggested amount of fraction digits as an array as long as the longest row. |
| */ |
| public static int[] suggestFractionDigits(final CoordinateReferenceSystem crs, final Vector[] points) { |
| final int[] fractionDigits = Numerics.suggestFractionDigits(points); |
| final Ellipsoid ellipsoid = ReferencingUtilities.getEllipsoid(crs); |
| if (ellipsoid != null) { |
| /* |
| * Use heuristic precisions for geodetic or projected CRS. We do not apply those heuristics |
| * for other kind of CRS (e.g. engineering) because we do not know what could be the size |
| * of the object attached to the CRS. |
| */ |
| final CoordinateSystem cs = crs.getCoordinateSystem(); |
| final int dimension = Math.min(cs.getDimension(), fractionDigits.length); |
| final double scale = Formulas.scaleComparedToEarth(ellipsoid); |
| for (int i=0; i<dimension; i++) { |
| final Unit<?> unit = cs.getAxis(i).getUnit(); |
| double precision; |
| if (Units.isLinear(unit)) { |
| precision = Formulas.LINEAR_TOLERANCE * scale; // In metres |
| } else if (Units.isAngular(unit)) { |
| precision = Formulas.ANGULAR_TOLERANCE * (Math.PI / 180) * scale; // In radians |
| } else if (Units.isTemporal(unit)) { |
| precision = Formulas.TEMPORAL_TOLERANCE; // In seconds |
| } else { |
| continue; |
| } |
| precision /= Units.toStandardUnit(unit); // In units used by the coordinates. |
| final int f = DecimalFunctions.fractionDigitsForDelta(precision, false); |
| if (f > fractionDigits[i]) { |
| fractionDigits[i] = f; // Use at least the heuristic precision. |
| } |
| } |
| } |
| return fractionDigits; |
| } |
| |
| /** |
| * Returns the values in the corners and in the center of the given tensor. The values are returned in a |
| * <var>n</var>-dimensional array of {@link Number} where <var>n</var> is the length of {@code size}. |
| * If some values have been skipped, {@code null} values are inserted in the rows or columns where the |
| * skipping occurs. |
| * |
| * @param tensor function providing values of the tensor. Inputs are indices of the desired value with |
| * index in each dimension ranging from 0 inclusive to {@code size[dimension]} exclusive. |
| * @param size size of the tensor. The length of this array is the tensor dimension. |
| * @param cornerSize number of values to keep in each corner. |
| * @return <var>n</var>-dimensional array of {@link Number} containing corners and center of the given tensor. |
| * |
| * @since 1.0 |
| */ |
| public static Object[] cornersAndCenter(final Function<int[],Number> tensor, final int[] size, final int cornerSize) { |
| /* |
| * The 'source' array will contain indices of values to fetch in the tensor, and the 'target' array will contain |
| * indices where to store those values in the returned data structure. Other arrays contain threshold indices of |
| * points of interest in the target data structure. |
| */ |
| final int sizeLimit = cornerSize*2 + 1; |
| final int[] shown = size.clone(); |
| final int[] empty = size.clone(); // Target index of row/column to leave empty, or an unreachable value if none. |
| for (int d=0; d<shown.length; d++) { |
| if (shown[d] > sizeLimit) { |
| shown[d] = sizeLimit; |
| empty[d] = cornerSize; |
| } |
| } |
| final int[] source = new int[shown.length]; |
| final int[] target = new int[shown.length]; |
| final Object[] numbers = (Object[]) Array.newInstance(Number.class, shown); |
| /* |
| * The loops below are used for simulating GOTO statements. This is usually a deprecated practice, |
| * but in this case we can hardly use normal loops because the number of nested loops is dynamic. |
| * We want something equivalent to the code below where 'n' - the number of nested loops - is not |
| * known at compile-time: |
| * |
| * for (int i0=0; i0<size[0]; i0++) { |
| * for (int i1=0; i1<size[1]; i1++) { |
| * for (int i2=0; i2<size[2]; i2++) { |
| * // ... etc ... |
| * for (int in=0; in<size[n]; in++) { |
| * } |
| * } |
| * } |
| * } |
| * |
| * Since we can not have a varying number of nested loops in the code, we achieve the same effect with |
| * GOTO-like statements. It would be possible to achieve the same effect with recursive method calls, |
| * but the GOTO-like approach is a little bit more compact. |
| */ |
| Number[] row = null; |
| fill: for (;;) { |
| if (row == null) { |
| Object[] walk = numbers; |
| for (int d=shown.length; --d >= 1;) { |
| walk = (Object[]) walk[target[d]]; |
| } |
| row = (Number[]) walk; |
| } |
| row[target[0]] = tensor.apply(source); |
| for (int d=0;;) { |
| source[d]++; |
| final int p = ++target[d]; |
| if (p == shown[d]) { // End of row (or higher dimension). This check must be first. |
| row = null; |
| source[d] = 0; |
| target[d] = 0; |
| if (++d >= shown.length) { |
| break fill; |
| } |
| // Continue loop for incrementing the higher dimension. |
| } else { |
| switch (p - empty[d]) { |
| case 0: continue; // Column/row to leave null. Continue the loop for moving to next column/row. |
| case 1: source[d] = size[d] - cornerSize; // Skip source columns/rows (or higher dimensions). |
| } |
| continue fill; // Stop incrementing indices and fetch the value at current location. |
| } |
| } |
| } |
| /* |
| * Add the center value in the empty location (in the middle). |
| */ |
| Object walk = numbers; |
| Object[] previous = null; |
| for (int d=size.length; --d >= 0;) { |
| previous = (Object[]) walk; |
| walk = previous[empty[d]]; |
| source[d] = size[d] / 2; |
| } |
| if (walk == null) { |
| previous[empty[0]] = tensor.apply(source); |
| } |
| return numbers; |
| } |
| } |