blob: cf72daddfcb9aae444b3f3d4a7d1612a6c4e9594 [file] [log] [blame]
/*
* 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.privy;
import java.util.Map;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.function.BiConsumer;
import javax.measure.Unit;
import javax.measure.quantity.Time;
import javax.measure.quantity.Length;
import org.opengis.util.GenericName;
import org.opengis.util.FactoryException;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.InvalidParameterValueException;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.TemporalCRS;
import org.opengis.referencing.crs.CompoundCRS;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CSFactory;
import org.opengis.referencing.cs.CartesianCS;
import org.opengis.referencing.cs.TimeCS;
import org.opengis.referencing.datum.Ellipsoid;
import org.opengis.referencing.datum.DatumFactory;
import org.opengis.referencing.datum.GeodeticDatum;
import org.opengis.referencing.datum.TemporalDatum;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.Conversion;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.referencing.Builder;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.operation.provider.TransverseMercator;
import org.apache.sis.referencing.operation.provider.PolarStereographicA;
import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
import org.apache.sis.metadata.iso.extent.DefaultExtent;
import org.apache.sis.measure.Latitude;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.internal.Resources;
import org.apache.sis.parameter.Parameters;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.referencing.ObjectDomain;
/**
* Helper methods for building Coordinate Reference Systems and related objects.
*
* In current version, each builder instance should be used for creating only one CRS.
* Reusing the same builder for creating many CRS has unspecified behavior.
*
* <p>For now, this class is defined in the internal package because this API needs more experimentation.
* However, this class may move in a public package later if we feel confident that its API is mature enough.</p>
*
* @author Martin Desruisseaux (Geomatys)
*/
public class GeodeticObjectBuilder extends Builder<GeodeticObjectBuilder> {
/**
* The geodetic datum, or {@code null} if none.
*/
private GeodeticDatum datum;
/**
* The name of the conversion to use for creating a {@code ProjectedCRS} or {@code DerivedCRS}.
* This name is for information purpose; its value does not impact the numerical results of coordinate operations.
*
* @see #setConversionName(String)
*/
private String conversionName;
/**
* The conversion method used by {@code ProjectedCRS} or {@code DerivedCRS}, or {@code null} if unspecified.
*
* @see #setConversionMethod(String)
*/
private OperationMethod method;
/**
* The projection parameters, or {@code null} if not applicable.
*/
private ParameterValueGroup parameters;
/**
* Group of factories used by this builder.
*/
private final ReferencingFactoryContainer factories;
/**
* The locale for error messages, or {@code null} for default locale.
*/
private final Locale locale;
/**
* Whether to use the axis order defined by {@link AxesConvention#NORMALIZED}.
*
* @see CommonCRS#normalizedGeographic()
*/
private boolean normalizedAxisOrder;
/**
* Creates a new builder with default locale and set of factories.
*/
public GeodeticObjectBuilder() {
this(null, null);
}
/**
* Creates a new builder using the given factories and locale.
*
* @param factories the factories to use for geodetic objects creation, or {@code null} for default.
* @param locale the locale for error message in exceptions, or {@code null} for default.
*/
public GeodeticObjectBuilder(final ReferencingFactoryContainer factories, final Locale locale) {
this.factories = (factories != null) ? factories : new ReferencingFactoryContainer();
this.locale = locale;
}
/**
* Creates a map of properties containing only the name of the given object.
*/
private static Map<String,Object> name(final IdentifiedObject template) {
return Map.of(IdentifiedObject.NAME_KEY, template.getName());
}
/**
* Sets whether axes should be in (longitude, latitude) order instead of (latitude, longitude).
* This flag applies to geographic CRS created by this builder.
*
* @param normalized whether axes should be in (longitude, latitude) order instead of (latitude, longitude).
* @return {@code this}, for method call chaining.
*
* @see AxesConvention#NORMALIZED
* @see CommonCRS#normalizedGeographic()
*/
public GeodeticObjectBuilder setNormalizedAxisOrder(final boolean normalized) {
normalizedAxisOrder = normalized;
return this;
}
/**
* Sets the domain of validity as a geographic bounding box set to the specified values.
* The bounding box crosses the anti-meridian if {@code eastBoundLongitude} &lt; {@code westBoundLongitude}.
* If this method has already been invoked previously, the new value overwrites the previous one.
*
* @param description a textual description of the domain of validity, or {@code null} if none.
* @param westBoundLongitude the minimal λ value.
* @param eastBoundLongitude the maximal λ value.
* @param southBoundLatitude the minimal φ value.
* @param northBoundLatitude the maximal φ value.
* @return {@code this}, for method call chaining.
* @throws IllegalArgumentException if (<var>south bound</var> &gt; <var>north bound</var>).
* Note that {@linkplain Double#NaN NaN} values are allowed.
*/
public GeodeticObjectBuilder setDomainOfValidity(final CharSequence description,
final double westBoundLongitude,
final double eastBoundLongitude,
final double southBoundLatitude,
final double northBoundLatitude)
{
DefaultGeographicBoundingBox bbox = new DefaultGeographicBoundingBox(
westBoundLongitude, eastBoundLongitude, southBoundLatitude, northBoundLatitude);
if (bbox.isEmpty()) {
bbox = null;
}
if (description != null || bbox != null) {
final DefaultExtent extent = new DefaultExtent(description, bbox, null, null);
properties.put(ObjectDomain.DOMAIN_OF_VALIDITY_KEY, extent);
}
return this;
}
/**
* Creates a geodetic datum with an ellipsoid of the given shape.
*
* @param name ellipsoid and datum name.
* @param semiMajorAxis equatorial radius in supplied linear units.
* @param inverseFlattening eccentricity of ellipsoid. An infinite value creates a sphere.
* @param units linear units of major axis.
* @return {@code this}, for method call chaining.
* @throws FactoryException if the datum cannot be created.
*/
public GeodeticObjectBuilder setFlattenedSphere(final String name, final double semiMajorAxis,
final double inverseFlattening, final Unit<Length> units) throws FactoryException
{
final DatumFactory factory = factories.getDatumFactory();
final Ellipsoid ellipsoid = factory.createFlattenedSphere(
Map.of(Ellipsoid.NAME_KEY, name), semiMajorAxis, inverseFlattening, units);
datum = factory.createGeodeticDatum(name(ellipsoid), ellipsoid, CommonCRS.WGS84.primeMeridian());
return this;
}
/**
* Sets the conversion method to use for creating a {@code ProjectedCRS} or {@code DerivedCRS}.
* The method is typically a map projection method. Examples:
*
* <ul>
* <li>Lambert Conic Conformal (1SP)</li>
* <li>Lambert Conic Conformal (2SP)</li>
* <li>Mercator (variant A)</li>
* <li>Mercator (variant B)</li>
* <li>Mercator (variant C)</li>
* <li>Popular Visualisation Pseudo Mercator</li>
* </ul>
*
* This method can be invoked only once.
*
* @param name name of the conversion method.
* @return {@code this}, for method call chaining.
* @throws FactoryException if the operation method of the given name cannot be obtained.
*/
public GeodeticObjectBuilder setConversionMethod(final String name) throws FactoryException {
if (method != null) {
throw new IllegalStateException(Errors.forLocale(locale).getString(Errors.Keys.ElementAlreadyPresent_1, "OperationMethod"));
}
method = factories.getCoordinateOperationFactory().getOperationMethod(name);
parameters = method.getParameters().createValue();
return this;
}
/**
* Sets the name of the conversion to use for creating a {@code ProjectedCRS} or {@code DerivedCRS}.
* This name is for information purpose; its value does not impact the numerical results of coordinate operations.
*
* @param name the name to give to the conversion.
* @return {@code this}, for method calls chaining.
*/
public GeodeticObjectBuilder setConversionName(final String name) {
conversionName = name;
return this;
}
/**
* Sets the conversion method together with all parameters. This method does not set the conversion name.
* If a name different than the default is desired, {@link #setConversionName(String)} should be invoked.
*
* @param parameters the map projection parameter values.
* @return {@code this}, for method calls chaining.
* @throws FactoryException if the operation method cannot be obtained.
*/
public GeodeticObjectBuilder setConversion(final ParameterValueGroup parameters) throws FactoryException {
method = factories.getCoordinateOperationFactory().getOperationMethod(parameters.getDescriptor().getName().getCode());
this.parameters = parameters; // Set only if above line succeed.
return this;
}
/**
* Ensures that {@link #setConversionMethod(String)} has been invoked.
*/
private void ensureConversionMethodSet() {
if (parameters == null) {
throw new IllegalStateException(Resources.forLocale(locale).getString(Resources.Keys.UnspecifiedParameterValues));
}
}
/**
* Sets the value of a numeric parameter. The {@link #setConversionMethod(String)} method must have been invoked
* exactly once before this method. Calls to this {@code setParameter(…)} can be repeated as many times as needed.
*
* @param name the parameter name.
* @param value the value to give to the parameter.
* @param unit unit of measurement for the given value.
* @return {@code this}, for method calls chaining.
* @throws IllegalStateException if {@link #setConversionMethod(String)} has not been invoked before this method.
* @throws ParameterNotFoundException if there is no parameter of the given name.
* @throws InvalidParameterValueException if the parameter does not accept the given value.
*/
public GeodeticObjectBuilder setParameter(final String name, final double value, final Unit<?> unit)
throws IllegalStateException, ParameterNotFoundException, InvalidParameterValueException
{
ensureConversionMethodSet();
parameters.parameter(name).setValue(value, unit);
return this;
}
/**
* Replaces the current operation method by a new one with parameter values derived form the old method.
* This method can be invoked for replacing a projection by another one with a similar set of parameters.
*
* <p>If non-null, the given {@code mapper} is used for copying parameter values from the old projection.
* The {@code accept(ParameterValue<?> source, Parameters target)} method is invoked where {@code source}
* is a parameter value of the old projection and {@code target} is the group of parameters where to set
* the values for new projection. If {@code mapper} is null, then the default implementation is as below:</p>
*
* {@snippet lang="java" :
* target.getOrCreate(source.getDescriptor()).setValue(source.getValue());
* }
*
* @param newMethod name of the new operation method, or {@code null} if no change.
* @param mapper mapper from old parameters to new parameters, or {@code null} for verbatim copy.
* @return {@code this}, for method calls chaining.
* @throws IllegalStateException if {@link #setConversionMethod(String)} has not been invoked before this method.
* @throws FactoryException if the operation method of the given name cannot be obtained.
* @throws ClassCastException if a parameter value of the old projection is not an instance of {@link ParameterValue}
* (this restriction may change in a future version).
*/
public GeodeticObjectBuilder changeConversion(final String newMethod,
BiConsumer<ParameterValue<?>, Parameters> mapper) throws FactoryException
{
ensureConversionMethodSet();
if (mapper == null) {
mapper = GeodeticObjectBuilder::copyParameterValue;
}
final ParameterValueGroup source = parameters;
if (newMethod != null) {
method = null;
setConversionMethod(newMethod);
}
final Parameters target = Parameters.castOrWrap(parameters);
for (final GeneralParameterValue param : source.values()) {
mapper.accept((ParameterValue<?>) param, target); // ClassCastException is part of current method contract.
}
return this;
}
/**
* The default {@code mapper} of {@code changeConversion(String, BiConsumer)}.
*
* @param source parameter value of the old projection.
* @param target group of parameters of the new projection where to copy source parameter value.
*/
private static void copyParameterValue(final ParameterValue<?> source, final Parameters target) {
target.getOrCreate(source.getDescriptor()).setValue(source.getValue());
}
/**
* Sets the operation method, parameters, conversion name and datum for the same projection as the given CRS.
* Metadata such as domain of validity are inherited, except identifiers.
*
* @param crs the projected CRS from which to inherit the properties.
* @return {@code this}, for method call chaining.
*/
public GeodeticObjectBuilder apply(final ProjectedCRS crs) {
final Conversion c = crs.getConversionFromBase();
conversionName = c.getName().getCode();
method = c.getMethod();
parameters = c.getParameterValues();
datum = crs.getDatum();
properties.putAll(IdentifiedObjects.getProperties(crs, ProjectedCRS.IDENTIFIERS_KEY));
return this;
}
/**
* Sets the operation method, parameters and conversion name for a Transverse Mercator projection.
* This convenience method delegates to the following methods:
*
* <ul>
* <li>{@link #setConversionName(String)} with a name like <q>Transverse Mercator</q>
* or <q>UTM zone 10N</q>, depending on the arguments given to this method.</li>
* <li>{@link #setConversionMethod(String)} with the name of the Transverse Mercator projection method.</li>
* <li>{@link #setParameter(String, double, Unit)} for each of the parameters enumerated below:</li>
* </ul>
*
* <blockquote><table class="sis">
* <caption>Transverse Mercator parameters</caption>
* <tr><th>Parameter name</th> <th>Parameter value</th></tr>
* <tr><td>Latitude of natural origin</td> <td>Given latitude, snapped to 0° in the UTM case</td></tr>
* <tr><td>Longitude of natural origin</td> <td>Given longitude, optionally snapped to a UTM zone</td></tr>
* <tr><td>Scale factor at natural origin</td> <td>0.9996 in UTM case</td></tr>
* <tr><td>False easting</td> <td>500000 metres in UTM case</td></tr>
* <tr><td>False northing</td> <td>0 (North hemisphere) or 10000000 (South hemisphere) metres</td></tr>
* </table></blockquote>
*
* Note that calculation of UTM zone contains special cases for Norway and Svalbard.
* If not desired, those exceptions can be avoided by making sure that the given latitude is below 56°N.
*
* <p>If the given {@code zoner} is {@link TransverseMercator.Zoner#ANY ANY}, then this method will use the given
* latitude and longitude verbatim (without snapping them to a zone) but will still use the UTM scale factor,
* false easting and false northing.
*
* @param zoner whether to use UTM or MTM zones, or {@code ANY} for using arbitrary central meridian.
* @param latitude the latitude in the center of the desired projection.
* @param longitude the longitude in the center of the desired projection.
* @return {@code this}, for method calls chaining.
* @throws FactoryException if the operation method for the Transverse Mercator projection cannot be obtained.
*
* @see CommonCRS#universal(double, double)
*/
public GeodeticObjectBuilder applyTransverseMercator(TransverseMercator.Zoner zoner,
double latitude, double longitude) throws FactoryException
{
ArgumentChecks.ensureBetween("latitude", Latitude.MIN_VALUE, Latitude.MAX_VALUE, latitude);
ArgumentChecks.ensureBetween("longitude", -Formulas.LONGITUDE_MAX, Formulas.LONGITUDE_MAX, longitude);
setConversionMethod(TransverseMercator.NAME);
setConversionName(zoner.setParameters(parameters, latitude, longitude));
return this;
}
/**
* Sets the operation method, parameters and conversion name for a Polar Stereographic projection.
* This convenience method delegates to the following methods:
*
* <ul>
* <li>{@link #setConversionName(String)} with a name like <q>Universal Polar Stereographic North</q>,
* depending on the argument given to this method.</li>
* <li>{@link #setConversionMethod(String)} with the name of the Polar Stereographic (variant A) projection method.</li>
* <li>{@link #setParameter(String, double, Unit)} for each of the parameters enumerated below:</li>
* </ul>
*
* <blockquote><table class="sis">
* <caption>Universal Polar Stereographic parameters</caption>
* <tr><th>Parameter name</th> <th>Parameter value</th></tr>
* <tr><td>Latitude of natural origin</td> <td>90°N or 90°S</td></tr>
* <tr><td>Longitude of natural origin</td> <td>0°</td></tr>
* <tr><td>Scale factor at natural origin</td> <td>0.994</td></tr>
* <tr><td>False easting</td> <td>2000000 metres</td></tr>
* <tr><td>False northing</td> <td>2000000 metres</td></tr>
* </table></blockquote>
*
* @param north {@code true} for North pole, or {@code false} for South pole.
* @return {@code this}, for method calls chaining.
* @throws FactoryException if the operation method for the Polar Stereographic (variant A)
* projection cannot be obtained.
*/
public GeodeticObjectBuilder applyPolarStereographic(final boolean north) throws FactoryException {
setConversionMethod(PolarStereographicA.NAME);
setConversionName(PolarStereographicA.setParameters(parameters, north));
return this;
}
/**
* Creates a projected CRS using a conversion built from the values given by the {@code setParameter(…)} calls.
*
* <h4>Example</h4>
* The following example creates a projected CRS for the <q>NTF (Paris) / Lambert zone II</q> projection,
* from a base CRS which is presumed to already exists in this example.
*
* {@snippet lang="java" :
* var builder = new GeodeticObjectBuilder();
* GeographicCRS baseCRS = ...;
* CartesianCS derivedCS = ...;
* ProjectedCRS crs = builder
* .setConversionMethod("Lambert Conic Conformal (1SP)")
* .setConversionName("Lambert zone II")
* .setParameter("Latitude of natural origin", 52, Units.GRAD)
* .setParameter("Scale factor at natural origin", 0.99987742, Units.UNITY)
* .setParameter("False easting", 600000, Units.METRE)
* .setParameter("False northing", 2200000, Units.METRE)
* .addName("NTF (Paris) / Lambert zone II")
* .createProjectedCRS(baseCRS, derivedCS);
* }
*
* @param baseCRS coordinate reference system to base the derived CRS on.
* @param derivedCS the coordinate system for the derived CRS, or {@code null} for the default.
* @return the projected CRS.
* @throws FactoryException if an error occurred while building the projected CRS.
*/
public ProjectedCRS createProjectedCRS(final GeographicCRS baseCRS, CartesianCS derivedCS) throws FactoryException {
ensureConversionMethodSet();
onCreate(false);
try {
/*
* Create a conversion with the same properties as the ProjectedCRS properties,
* except the aliases and identifiers. The name defaults to the ProjectedCRS name,
* but can optionally be different.
*/
final Object name = (conversionName != null) ? properties.put(Conversion.NAME_KEY, conversionName) : null;
final Object alias = properties.put(Conversion.ALIAS_KEY, null);
final Object identifier = properties.put(Conversion.IDENTIFIERS_KEY, null);
final Conversion conversion = factories.getCoordinateOperationFactory().createDefiningConversion(properties, method, parameters);
/*
* Restore the original properties and create the final ProjectedCRS.
*/
properties.put(Conversion.IDENTIFIERS_KEY, identifier);
properties.put(Conversion.ALIAS_KEY, alias);
if (name != null) {
properties.put(Conversion.NAME_KEY, name);
}
if (derivedCS == null) {
derivedCS = factories.getStandardProjectedCS();
}
return factories.getCRSFactory().createProjectedCRS(properties, baseCRS, conversion, derivedCS);
} finally {
onCreate(true);
}
}
/**
* Creates a projected CRS with base CRS on the datum previously specified to this builder and with default axes.
* The base CRS uses the ellipsoid specified by {@link #setFlattenedSphere(String, double, double, Unit)}.
*
* @return the projected CRS.
* @throws FactoryException if an error occurred while building the projected CRS.
*/
public ProjectedCRS createProjectedCRS() throws FactoryException {
GeographicCRS crs = getBaseCRS();
if (datum != null) {
crs = factories.getCRSFactory().createGeographicCRS(name(datum), datum, crs.getCoordinateSystem());
}
return createProjectedCRS(crs, factories.getStandardProjectedCS());
}
/**
* Returns the CRS to use as the base of a projected CRS.
*
* @todo {@code CommonCRS.WGS84} should be {@code CommonCRS.DEFAULT}, but the latter is not public.
*/
private GeographicCRS getBaseCRS() {
return normalizedAxisOrder ? CommonCRS.defaultGeographic() : CommonCRS.WGS84.geographic();
}
/**
* Creates a geographic CRS.
*
* @return the geographic coordinate reference system.
* @throws FactoryException if an error occurred while building the geographic CRS.
*/
public GeographicCRS createGeographicCRS() throws FactoryException {
final GeographicCRS crs = getBaseCRS();
if (datum != null) properties.putIfAbsent(GeographicCRS.NAME_KEY, datum.getName());
return factories.getCRSFactory().createGeographicCRS(properties, datum, crs.getCoordinateSystem());
}
/**
* Creates a temporal CRS from the given origin and temporal unit. For this method, the CRS name is optional:
* if no {@code addName(…)} method has been invoked, then a default name will be used.
*
* @param origin the epoch in milliseconds since January 1st, 1970 at midnight UTC.
* @param unit the unit of measurement.
* @return a temporal CRS using the given origin and units.
* @throws FactoryException if an error occurred while building the temporal CRS.
*/
public TemporalCRS createTemporalCRS(final Date origin, final Unit<Time> unit) throws FactoryException {
/*
* Try to use one of the predefined datum and coordinate system if possible.
* This not only saves a little bit of memory, but also provides better names.
*/
TimeCS cs = null;
TemporalDatum datum = null;
for (final CommonCRS.Temporal c : CommonCRS.Temporal.values()) {
if (datum == null) {
final TemporalDatum candidate = c.datum();
if (origin.equals(candidate.getOrigin())) {
datum = candidate;
}
}
if (cs == null) {
final TemporalCRS crs = c.crs();
final TimeCS candidate = crs.getCoordinateSystem();
if (unit.equals(candidate.getAxis(0).getUnit())) {
if (datum == candidate && properties.isEmpty()) {
return crs;
}
cs = candidate;
}
}
}
/*
* Create the datum and coordinate system before the CRS if we were not able to use a predefined object.
* In the datum case, we will use the same metadata as the CRS (domain of validity, scope, etc.) except
* the identifier and the remark.
*/
onCreate(false);
try {
if (cs == null) {
final CSFactory csFactory = factories.getCSFactory();
cs = CommonCRS.Temporal.JAVA.crs().getCoordinateSystem(); // To be used as a template, except for units.
cs = csFactory.createTimeCS(name(cs),
csFactory.createCoordinateSystemAxis(name(cs.getAxis(0)), "t", AxisDirection.FUTURE, unit));
}
if (properties.get(TemporalCRS.NAME_KEY) == null) {
properties.putAll(name(cs));
}
if (datum == null) {
final Object remarks = properties.remove(TemporalCRS.REMARKS_KEY);
final Object identifier = properties.remove(TemporalCRS.IDENTIFIERS_KEY);
datum = factories.getDatumFactory().createTemporalDatum(properties, origin);
properties.put(TemporalCRS.IDENTIFIERS_KEY, identifier);
properties.put(TemporalCRS.REMARKS_KEY, remarks);
properties.put(TemporalCRS.NAME_KEY, datum.getName()); // Share the Identifier instance.
}
return factories.getCRSFactory().createTemporalCRS(properties, datum, cs);
} finally {
onCreate(true);
}
}
/**
* Creates a compound CRS, but we special processing for (two-dimensional Geographic + ellipsoidal heights) tuples.
* If any such tuple is found, a three-dimensional geographic CRS is created instead of the compound CRS.
*
* @param components ordered array of {@code CoordinateReferenceSystem} objects.
* @return the coordinate reference system for the given properties.
* @throws FactoryException if the object creation failed.
*/
public CoordinateReferenceSystem createCompoundCRS(final CoordinateReferenceSystem... components) throws FactoryException {
return new EllipsoidalHeightCombiner(factories).createCompoundCRS(properties, components);
}
/**
* Replaces the component starting at given index by the given component. This method can be used for replacing
* e.g. the horizontal component of a CRS, or the vertical component, <i>etc.</i>. If a new compound CRS needs
* to be created and a {@linkplain #addName(GenericName) name has been specified}, that name will be used.
*
* <h4>Limitations</h4>
* Current implementation can replace exactly one component of {@link CompoundCRS}.
* If the given replacement spans more than one component, then this method will fail.
*
* @param source the coordinate reference system in which to replace a component.
* @param firstDimension index of the first dimension to replace.
* @param replacement the component to insert in place of the CRS component at given index.
* @return a CRS with the component replaced.
* @throws FactoryException if the object creation failed.
*
* @see org.apache.sis.referencing.CRS#getComponentAt(CoordinateReferenceSystem, int, int)
*/
public CoordinateReferenceSystem replaceComponent(final CoordinateReferenceSystem source,
final int firstDimension, final CoordinateReferenceSystem replacement) throws FactoryException
{
final int srcDim = ReferencingUtilities.getDimension(source);
final int repDim = ReferencingUtilities.getDimension(replacement);
if (firstDimension == 0 && srcDim == repDim) {
/*
* conceptually return the replacement. But returning the original instance if applicable
* allows the caller to detect that a compound CRS does not need to be replaced.
*/
return source.equals(replacement) ? source : replacement;
}
Objects.checkIndex(firstDimension, srcDim - repDim);
if (source instanceof CompoundCRS) {
final var components = ((CompoundCRS) source).getComponents().toArray(CoordinateReferenceSystem[]::new);
int lower = 0;
for (int i=0; i<components.length; i++) {
final CoordinateReferenceSystem c = components[i];
if (firstDimension >= lower) {
/*
* Reached the index of the CRS component to replace. Invoke this method recursively in case we have nested
* components, but without using the names and identifiers that may have been specified for the final CRS.
*/
Object name = properties.remove(IdentifiedObject.NAME_KEY);
Object alias = properties.remove(IdentifiedObject.ALIAS_KEY);
Object ids = properties.remove(IdentifiedObject.IDENTIFIERS_KEY);
final CoordinateReferenceSystem nc = replaceComponent(c, firstDimension - lower, replacement);
/*
* Restore the names and identifiers before to create the final CompoundCRS.
* If no name was specified, reuse the primary name of existing CRS but not the identifiers.
*/
if (name == null) {
name = source.getName();
}
properties.put(IdentifiedObject.NAME_KEY, name);
properties.put(IdentifiedObject.ALIAS_KEY, alias);
properties.put(IdentifiedObject.IDENTIFIERS_KEY, ids);
if (nc == c) {
return source; // No change.
}
components[i] = nc;
return createCompoundCRS(components);
}
lower += ReferencingUtilities.getDimension(c);
}
}
throw new IllegalArgumentException(Resources.forLocale(locale).getString(
Resources.Keys.CanNotSeparateCRS_1, IdentifiedObjects.getDisplayName(source, locale)));
}
}