| /* |
| * 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} < {@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> > <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))); |
| } |
| } |