| /* |
| * 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.referencing.operation.provider; |
| |
| import java.util.Map; |
| import java.util.HashMap; |
| import java.util.NoSuchElementException; |
| import jakarta.xml.bind.annotation.XmlTransient; |
| import javax.measure.Unit; |
| import org.opengis.util.GenericName; |
| import org.opengis.util.FactoryException; |
| import org.opengis.util.InternationalString; |
| import org.opengis.metadata.Identifier; |
| import org.opengis.metadata.citation.Citation; |
| import org.opengis.parameter.ParameterDescriptor; |
| import org.opengis.parameter.GeneralParameterDescriptor; |
| import org.opengis.parameter.ParameterDescriptorGroup; |
| import org.opengis.parameter.ParameterNotFoundException; |
| import org.opengis.referencing.cs.CartesianCS; |
| import org.opengis.referencing.cs.EllipsoidalCS; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.opengis.referencing.operation.Conversion; |
| import static org.opengis.metadata.Identifier.AUTHORITY_KEY; |
| import org.apache.sis.referencing.NamedIdentifier; |
| import org.apache.sis.referencing.IdentifiedObjects; |
| import org.apache.sis.referencing.ImmutableIdentifier; |
| import org.apache.sis.referencing.internal.Resources; |
| import org.apache.sis.util.privy.Constants; |
| import org.apache.sis.measure.MeasurementRange; |
| import org.apache.sis.measure.Units; |
| import org.apache.sis.referencing.operation.projection.NormalizedProjection; |
| import org.apache.sis.metadata.iso.citation.Citations; |
| import org.apache.sis.parameter.DefaultParameterDescriptor; |
| import org.apache.sis.parameter.ParameterBuilder; |
| import org.apache.sis.parameter.Parameters; |
| import org.apache.sis.util.resources.Errors; |
| |
| |
| /** |
| * Base class for most two-dimensional map projection providers defined in this package. |
| * This base class defines some descriptors for the most commonly used parameters. |
| * Subclasses will declare additional parameters and group them in a |
| * {@linkplain ParameterDescriptorGroup descriptor group} named {@code PARAMETERS}. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| @XmlTransient |
| public abstract class MapProjection extends AbstractProvider { |
| /** |
| * Serial number for inter-operability with different versions. |
| */ |
| private static final long serialVersionUID = -601413094691431910L; |
| |
| /** |
| * All names known to Apache SIS for the <i>semi-major</i> parameter. |
| * This parameter is mandatory and has no default value. The range of valid values is (0 … ∞). |
| * |
| * <p>Some names for this parameter are {@code "semi_major"}, {@code "SemiMajorAxis"} and {@code "a"}.</p> |
| * |
| * <!-- Generated by ParameterNameTableGenerator --> |
| * <table class="sis"> |
| * <caption>Parameter names</caption> |
| * <tr><td> OGC: </td><td> semi_major </td></tr> |
| * <tr><td> ESRI: </td><td> Semi_Major </td></tr> |
| * <tr><td> NetCDF: </td><td> semi_major_axis </td></tr> |
| * <tr><td> GeoTIFF: </td><td> SemiMajorAxis </td></tr> |
| * <tr><td> Proj4: </td><td> a </td></tr> |
| * </table> |
| * <b>Notes:</b> |
| * <ul> |
| * <li>Value domain: (0.0 … ∞) m</li> |
| * <li>No default value</li> |
| * </ul> |
| */ |
| public static final DefaultParameterDescriptor<Double> SEMI_MAJOR; |
| |
| /** |
| * All names known to Apache SIS for the <i>semi-minor</i> parameter. |
| * This parameter is mandatory and has no default value. The range of valid values is (0 … ∞). |
| * |
| * <p>Some names for this parameter are {@code "semi_minor"}, {@code "SemiMinorAxis"} and {@code "b"}.</p> |
| * |
| * <!-- Generated by ParameterNameTableGenerator --> |
| * <table class="sis"> |
| * <caption>Parameter names</caption> |
| * <tr><td> OGC: </td><td> semi_minor </td></tr> |
| * <tr><td> ESRI: </td><td> Semi_Minor </td></tr> |
| * <tr><td> NetCDF: </td><td> semi_minor_axis </td></tr> |
| * <tr><td> GeoTIFF: </td><td> SemiMinorAxis </td></tr> |
| * <tr><td> Proj4: </td><td> b </td></tr> |
| * </table> |
| * <b>Notes:</b> |
| * <ul> |
| * <li>Value domain: (0.0 … ∞) m</li> |
| * <li>No default value</li> |
| * </ul> |
| */ |
| public static final DefaultParameterDescriptor<Double> SEMI_MINOR; |
| |
| /** |
| * The ellipsoid eccentricity, computed from the semi-major and semi-minor axis lengths. |
| * This a SIS-specific parameter. |
| * |
| * <!-- Generated by ParameterNameTableGenerator --> |
| * <table class="sis"> |
| * <caption>Parameter names</caption> |
| * <tr><td> SIS: </td><td> eccentricity </td></tr> |
| * </table> |
| * <b>Notes:</b> |
| * <ul> |
| * <li>Value domain: [0.0 … 1.0]</li> |
| * <li>No default value</li> |
| * </ul> |
| */ |
| public static final DefaultParameterDescriptor<Double> ECCENTRICITY; |
| |
| /* |
| * Initializes all static fields. |
| */ |
| static { |
| final MeasurementRange<Double> valueDomain = MeasurementRange.createGreaterThan(0, Units.METRE); |
| final GenericName[] aliases = { |
| new NamedIdentifier(Citations.ESRI, "Semi_Major"), |
| new NamedIdentifier(Citations.NETCDF, "semi_major_axis"), |
| new NamedIdentifier(Citations.GEOTIFF, "SemiMajorAxis"), |
| new NamedIdentifier(Citations.PROJ4, "a") |
| }; |
| final Map<String,Object> properties = new HashMap<>(4); |
| properties.put(AUTHORITY_KEY, Citations.OGC); |
| properties.put(NAME_KEY, Constants.SEMI_MAJOR); |
| properties.put(ALIAS_KEY, aliases); |
| properties.put(IDENTIFIERS_KEY, new ImmutableIdentifier(Citations.GEOTIFF, null, "2057")); |
| SEMI_MAJOR = new DefaultParameterDescriptor<>(properties, 1, 1, Double.class, valueDomain, null, null); |
| /* |
| * Change in-place the name and aliases (we do not need to create new objects) |
| * before to create the SEMI_MINOR descriptor. |
| */ |
| properties.put(NAME_KEY, Constants.SEMI_MINOR); |
| aliases[0] = new NamedIdentifier(Citations.ESRI, "Semi_Minor"); |
| aliases[1] = new NamedIdentifier(Citations.NETCDF, "semi_minor_axis"); |
| aliases[2] = new NamedIdentifier(Citations.GEOTIFF, "SemiMinorAxis"); |
| aliases[3] = new NamedIdentifier(Citations.PROJ4, "b"); |
| properties.put(IDENTIFIERS_KEY, new ImmutableIdentifier(Citations.GEOTIFF, null, "2058")); |
| SEMI_MINOR = new DefaultParameterDescriptor<>(properties, 1, 1, Double.class, valueDomain, null, null); |
| /* |
| * SIS-specific parameter for debugging purpose only. |
| */ |
| properties.clear(); |
| properties.put(AUTHORITY_KEY, Citations.SIS); |
| properties.put(NAME_KEY, "eccentricity"); |
| ECCENTRICITY = new DefaultParameterDescriptor<>(properties, 1, 1, Double.class, |
| MeasurementRange.create(0d, true, 1d, true, null), null, null); |
| } |
| |
| /** |
| * Constructs a math transform provider from a set of parameters. The provider |
| * {@linkplain #getIdentifiers() identifiers} will be the same as the parameter ones. |
| * |
| * @param parameters the set of parameters (never {@code null}). |
| */ |
| protected MapProjection(final ParameterDescriptorGroup parameters) { |
| super(Conversion.class, parameters, |
| EllipsoidalCS.class, true, |
| CartesianCS.class, false, |
| (byte) 2); |
| } |
| |
| /** |
| * Validates the given parameter value. This method duplicates the verification already |
| * done by {@link org.apache.sis.parameter.DefaultParameterValue#setValue(Object, Unit)}. |
| * But we check again because we have no guarantee that the parameters given by the user |
| * were instances of {@code DefaultParameterValue}, or that the descriptor associated to |
| * the user-specified {@code ParameterValue} has sufficient information. |
| * |
| * @param descriptor the descriptor that specify the parameter to validate. |
| * @param value the parameter value in the units given by the descriptor. |
| * @throws IllegalArgumentException if the given value is out of bounds. |
| * |
| * @see #createZeroConstant(ParameterBuilder) |
| */ |
| public static void validate(final ParameterDescriptor<? extends Number> descriptor, final double value) |
| throws IllegalArgumentException |
| { |
| if (!Double.isFinite(value)) { |
| throw new IllegalArgumentException(Resources.format(Resources.Keys.IllegalParameterValue_2, |
| descriptor.getName(), value)); |
| } |
| final Comparable<? extends Number> min = descriptor.getMinimumValue(); |
| final Comparable<? extends Number> max = descriptor.getMaximumValue(); |
| final double minValue = (min instanceof Number) ? ((Number) min).doubleValue() : Double.NaN; |
| final double maxValue = (max instanceof Number) ? ((Number) max).doubleValue() : Double.NaN; |
| if (value < minValue || value > maxValue) { |
| /* |
| * RATIONAL: why we do not check the bounds if (min == max): |
| * The only case when our descriptor have (min == max) is when a parameter can only be zero, |
| * because of the way the map projection is defined (see e.g. Mercator1SP.LATITUDE_OF_ORIGIN). |
| * But in some cases, it would be possible to deal with non-zero values, even if in principle |
| * we should not. In such case we let the caller decides. |
| * |
| * Above check should be revisited if createZeroConstant(ParameterBuilder) is modified. |
| */ |
| if (minValue != maxValue) { // Compare as 'double' because we want (-0 == +0) to be true. |
| throw new IllegalArgumentException(Errors.format(Errors.Keys.ValueOutOfRange_4, |
| descriptor.getName(), min, max, value)); |
| } |
| } |
| } |
| |
| /** |
| * Creates a map projection from the specified group of parameter values. |
| * |
| * @param context the parameter values together with its context. |
| * @return the map projection created from the given parameter values. |
| * @throws ParameterNotFoundException if a required parameter was not found. |
| * @throws FactoryException if the map projection cannot be created. |
| */ |
| @Override |
| public final MathTransform createMathTransform(final Context context) throws FactoryException { |
| return maybe3D(context, createProjection(Parameters.castOrWrap(context.getCompletedParameters())) |
| .createMapProjection(context)); |
| } |
| |
| /** |
| * Eventually adds a third dimensions to the given transform for ellipsoidal height. |
| * Any dimension other than 2 or 3 is invalid, but those values are not verified by this method: |
| * it is caller's responsibility to handle discrepancy between its request and legal transforms. |
| * We do not verify source dimensions for the same reason. |
| * |
| * @param context the parameter values together with its context. |
| * @param mt the map projection created by the provider. |
| * @return the given map projection, potentially with ellipsoidal height added. |
| * @throws FactoryException if the map projection cannot be created. |
| */ |
| static MathTransform maybe3D(final Context context, MathTransform mt) throws FactoryException { |
| if (context.getTargetDimensions().orElse(2) >= 3) { |
| mt = context.getFactory().createPassThroughTransform(0, mt, 1); |
| } |
| return mt; |
| } |
| |
| /** |
| * Creates a map projection on an ellipsoid having a semi-major axis length of 1. |
| * |
| * @param parameters the group of parameter values. |
| * @return the map projection created from the given parameter values. |
| * @throws ParameterNotFoundException if a required parameter was not found. |
| */ |
| protected abstract NormalizedProjection createProjection(final Parameters parameters); |
| |
| |
| |
| |
| ////////////////////////////////////////////////////////////////////////////////////////// |
| //////// //////// |
| //////// HELPER METHODS FOR SUBCLASSES //////// |
| //////// //////// |
| //////// Following methods are defined for sharing the same GenericName or //////// |
| //////// Identifier instances when possible. //////// |
| ////////////////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * Returns the name of the given authority declared in the given parameter descriptor. |
| * This method is used only as a way to avoid creating many instances of the same name. |
| * |
| * @param authority the authority for which to get the name. |
| * @param parameters where to get name for the given authority. |
| * @throws NoSuchElementException if the given authority has not been found. |
| */ |
| private static GenericName sameNameAs(final Citation authority, final GeneralParameterDescriptor parameters) { |
| for (final GenericName candidate : parameters.getAlias()) { |
| if (candidate instanceof Identifier && ((Identifier) candidate).getAuthority() == authority) { |
| return candidate; |
| } |
| } |
| throw new NoSuchElementException(); |
| } |
| |
| /** |
| * Copies name, aliases and identifiers of the given {@code template}, except the alias and identifiers of the |
| * given authority which are replaced by the alias and identifiers of the same authority in {@code replacement}. |
| * |
| * @param builder an initially clean builder where to add the names and identifiers. |
| * @param template the parameter from which to copy names and identifiers. |
| * @param toRename authority of the alias to rename. |
| * @param replacement the parameter from which to get the new name for the alias to rename. |
| * @return the given {@code builder}, for method call chaining. |
| */ |
| static ParameterBuilder renameAlias(final ParameterBuilder builder, final ParameterDescriptor<Double> template, |
| final Citation toRename, final ParameterDescriptor<Double> replacement) |
| { |
| renameAliases(builder, template, new Citation[] {toRename}, new ParameterDescriptor<?>[] {replacement}); |
| return builder; |
| } |
| |
| /** |
| * Same as above {@link #renameAlias(ParameterBuilder, ParameterDescriptor, Citation, ParameterDescriptor) |
| * renameAlias(…)} but with two aliases to rename. |
| * |
| * @param builder an initially clean builder where to add the names and identifiers. |
| * @param template the parameter from which to copy names and identifiers. |
| * @param s1 authority of the first alias to rename. |
| * @param r1 the parameter from which to get the new name for the first alias to rename. |
| * @param s2 authority of the second alias to rename. |
| * @param r2 the parameter from which to get the new name for the second alias to rename. |
| * @return the given {@code builder}, for method call chaining. |
| */ |
| static ParameterBuilder renameAlias(final ParameterBuilder builder, final ParameterDescriptor<Double> template, |
| final Citation s1, final ParameterDescriptor<Double> r1, |
| final Citation s2, final ParameterDescriptor<Double> r2) |
| { |
| renameAliases(builder, template, new Citation[] {s1, s2}, new ParameterDescriptor<?>[] {r1, r2}); |
| return builder; |
| } |
| |
| /** |
| * Implementation of {@code renameAlias(…)} methods. |
| */ |
| private static void renameAliases(final ParameterBuilder builder, final ParameterDescriptor<Double> template, |
| final Citation[] toRename, final ParameterDescriptor<?>[] replacement) |
| { |
| builder.addName(template.getName()); |
| final GenericName[] newNames = new GenericName[toRename.length]; |
| final Identifier[] newCodes = new Identifier [toRename.length]; |
| for (int i=0; i<toRename.length; i++) { |
| newNames[i] = sameNameAs(toRename[i], replacement[i]); |
| newCodes[i] = IdentifiedObjects.getIdentifier(replacement[i], toRename[i]); |
| } |
| for (GenericName alias : template.getAlias()) { |
| final Citation authority = ((Identifier) alias).getAuthority(); |
| for (int i=0; i<toRename.length; i++) { |
| if (authority == toRename[i]) { |
| if (newNames[i] == null) continue; |
| alias = newNames[i]; |
| newNames[i] = null; |
| break; |
| } |
| } |
| builder.addName(alias); |
| } |
| for (Identifier id : template.getIdentifiers()) { |
| final Citation authority = id.getAuthority(); |
| for (int i=0; i<toRename.length; i++) { |
| if (authority == toRename[i]) { |
| if (newCodes[i] == null) continue; |
| id = newCodes[i]; |
| newCodes[i] = null; |
| break; |
| } |
| } |
| builder.addIdentifier(id); |
| } |
| } |
| |
| /** |
| * Creates a remarks for parameters that are not formally EPSG parameter. |
| * |
| * @param origin the name of the projection for where the parameter is formally used. |
| * @return a remarks saying that the parameter is actually defined in {@code origin}. |
| */ |
| static InternationalString notFormalParameter(final String origin) { |
| return Resources.formatInternational(Resources.Keys.NotFormalProjectionParameter_1, origin); |
| } |
| } |