blob: ce1e8fb809790af0cbd6ac0c767cdd91384c34fa [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.storage.gdal;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Collections;
import javax.measure.Unit;
import javax.measure.format.ParserException;
import org.opengis.util.FactoryException;
import org.opengis.util.NoSuchIdentifierException;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.citation.Citation;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.cs.*;
import org.opengis.referencing.crs.*;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.operation.*;
import org.opengis.referencing.IdentifiedObject;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.ImmutableIdentifier;
import org.apache.sis.referencing.operation.DefaultConversion;
import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
import org.apache.sis.referencing.factory.UnavailableFactoryException;
import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.internal.util.Constants;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.metadata.iso.citation.DefaultCitation;
import org.apache.sis.internal.referencing.LazySet;
import org.apache.sis.internal.referencing.AxisDirections;
import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.internal.system.Modules;
import org.apache.sis.internal.util.CollectionsExt;
import org.apache.sis.util.iso.SimpleInternationalString;
import org.apache.sis.util.collection.WeakValueHashMap;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.Classes;
import org.apache.sis.measure.Latitude;
import org.apache.sis.measure.Units;
/**
* A factory for Coordinate Reference Systems created from {@literal Proj.4} definitions.
* This authority factory recognizes codes in the {@code "Proj4"} name space.
* The main methods in this class are:
* <ul>
* <li>{@link #getAuthority()}</li>
* <li>{@link #createCoordinateReferenceSystem(String)}</li>
* <li>{@link #createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, boolean)}</li>
* <li>{@link #createParameterizedTransform(ParameterValueGroup)}</li>
* </ul>
*
* Other methods delegate to one of above-cited methods if possible, or throw a {@link FactoryException} otherwise.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 0.8
* @module
*/
public class Proj4Factory extends GeodeticAuthorityFactory implements CRSAuthorityFactory {
/*
* NOTE: Proj4Factory could implement CoordinateOperationFactory or MathTransformFactory.
* But we don't do that yet because we are not sure about exposing large amount of
* methods that are not really supported by Proj.4 wrappers. However this approach
* is experimented in the MTFactory class in the test directory. MTFactory methods
* could be refactored here if experience shows us that it would be useful.
*/
/**
* The {@literal Proj.4} parameter used for projection name.
*/
static final String PROJ_PARAM = '+' + Proj4Parser.PROJ + '=';
/**
* Options to be added in any {@literal Proj.4} definition strings.
* <ul>
* <li>The {@code "+over"} option is for disabling the default wrapping of output longitudes in the -180 to 180 range.
* We do that for having the same behavior between Proj.4 and Apache SIS. No wrapping reduce discontinuity problems
* with geometries that cross the anti-meridian.</li>
* <li>The {@code "+no_defs"} option is for ensuring that no defaults are read from {@code "/usr/share/proj/proj_def.dat"} file.
* That file contains default values for various map projections, for example {@code "+lat_1=29.5"} and {@code "+lat_2=45.5"}
* for the {@code "aea"} projection. Those defaults are assuming that users want Conterminous U.S. map.
* This may cause surprising behavior for users outside USA.</li>
* </ul>
*/
static String STANDARD_OPTIONS = " +over +no_defs";
/**
* The {@literal Proj.4} parameter used for declaration of axis order. Proj.4 expects the axis parameter
* to be exactly 3 characters long, but Apache SIS accepts 2 characters as well. We relax the Proj.4 rule
* because we use the number of characters for determining the number of dimensions.
* This is okay since 1 character = 1 axis.
*/
static final String AXIS_ORDER_PARAM = "+axis=";
/**
* The name to use when no specific name were found for an object.
*/
static final String UNNAMED = "Unnamed";
/**
* The default factory instance.
*/
static final Proj4Factory INSTANCE = new Proj4Factory();
/**
* The {@literal Proj.4} authority completed by the version string. Created when first needed.
*
* @see #getAuthority()
*/
private volatile Citation authority;
/**
* The default properties, or an empty map if none. This map shall not change after construction in
* order to allow usage without synchronization in multi-thread context. But we do not need to wrap
* in a unmodifiable map since {@code Proj4Factory} does not provide public access to it.
*/
private final Map<String,?> defaultProperties;
/**
* The factory for coordinate reference system objects.
*/
private final CRSFactory crsFactory;
/**
* The factory for coordinate system objects.
*/
private final CSFactory csFactory;
/**
* The factory for datum objects.
*/
private final DatumFactory datumFactory;
/**
* The {@code MathTransform} factory on which to delegate operations that are not supported by {@code Proj4Factory}.
*/
private final MathTransformFactory mtFactory;
/**
* The factory for coordinate operation objects, created when first needed.
* Currently restricted to Apache SIS implementation because we use a method not yet available in GeoAPI and
* because we configure it for using the {@link MathTransformFactory} provided by this {@code Proj4Factory}.
*
* @see #opFactory()
*/
private volatile DefaultCoordinateOperationFactory opFactory;
/**
* Poll of identifiers created by this factory.
*/
private final Map<String,Identifier> identifiers = new HashMap<>();
/**
* Pool of {@literal Proj.4} objects created so far. The keys are the Proj.4 definition strings.
* The same {@link PJ} instance may appear more than once if various definition strings resulted
* in the same Proj.4 object.
*/
private final WeakValueHashMap<String,PJ> pool = new WeakValueHashMap<>(String.class);
/**
* Creates a default factory.
*/
public Proj4Factory() {
this(null);
}
/**
* Creates a new {@literal Proj.4} factory. The {@code properties} argument is an optional map
* for specifying common properties shared by the objects to create. Some available properties are
* {@linkplain org.apache.sis.referencing.AbstractIdentifiedObject#AbstractIdentifiedObject(Map) listed there}.
* Unknown properties, or properties that do not apply, or properties for which {@code Proj4Factory} supplies
* itself a value, are ignored.
*
* @param properties common properties for the objects to create, or {@code null} if none.
*/
public Proj4Factory(Map<String,?> properties) {
properties = new HashMap<>(properties != null ? properties : Collections.emptyMap());
crsFactory = factory(CRSFactory.class, properties, ReferencingFactoryContainer.CRS_FACTORY);
csFactory = factory(CSFactory.class, properties, ReferencingFactoryContainer.CS_FACTORY);
datumFactory = factory(DatumFactory.class, properties, ReferencingFactoryContainer.DATUM_FACTORY);
mtFactory = factory(MathTransformFactory.class, properties, ReferencingFactoryContainer.MT_FACTORY);
defaultProperties = CollectionsExt.compact(properties);
}
/**
* Returns the factory to use, using the instance specified in the properties map if it exists,
* or the system-wide default instance otherwise.
*/
@SuppressWarnings("unchecked")
private static <T> T factory(final Class<T> type, final Map<String,?> properties, final String key) {
final Object value = properties.remove(key);
if (value == null) {
return DefaultFactories.forBuildin(type);
}
if (type.isInstance(value)) {
return (T) value;
}
throw new IllegalArgumentException(Errors.getResources(properties)
.getString(Errors.Keys.IllegalPropertyValueClass_2, key, Classes.getClass(value)));
}
/**
* Returns the factory for coordinate operation objects.
* The factory is backed by this {@code Proj4Factory} as the {@code MathTransformFactory} implementation.
*/
final DefaultCoordinateOperationFactory opFactory() {
DefaultCoordinateOperationFactory factory = opFactory;
if (factory == null) {
final Map<String,Object> properties = new HashMap<>(defaultProperties);
properties.put(ReferencingFactoryContainer.CRS_FACTORY, crsFactory);
properties.put(ReferencingFactoryContainer.CS_FACTORY, csFactory);
properties.put(ReferencingFactoryContainer.DATUM_FACTORY, datumFactory);
factory = new DefaultCoordinateOperationFactory(properties, mtFactory);
opFactory = factory;
}
return factory;
}
/**
* Returns the project that defines the codes recognized by this factory.
* The authority determines the {@linkplain #getCodeSpaces() code space}.
*
* @return {@link Citations#PROJ4}.
*/
@Override
public Citation getAuthority() {
Citation c = authority;
if (c == null) {
c = Citations.PROJ4;
final String release = Proj4.version();
if (release != null) {
final DefaultCitation df = new DefaultCitation(c);
df.setEdition(new SimpleInternationalString(release));
df.transitionTo(DefaultCitation.State.FINAL);
c = df;
}
authority = c;
}
return c;
}
/**
* Returns the code space of the authority. The code space is the prefix that may appear before codes.
* It allows to differentiate Proj.4 definitions from EPSG codes or other authorities. The code space is
* removed by {@link #createCoordinateReferenceSystem(String)} before the definition string is passed to Proj.4
*
* <div class="note"><b>Example</b>
* a complete identifier can be {@code "Proj4:+init=epsg:4326"}.
* Note that this is <strong>not</strong> equivalent to the standard {@code "EPSG:4326"} definition since the
* axis order is not the same. The {@code "Proj4:"} prefix specifies that the remaining part of the string is
* a Proj.4 definition; the presence of an {@code "epsg"} word in the definition does not change that fact.
* </div>
*
* @return {@code "Proj4"}.
*/
@Override
public Set<String> getCodeSpaces() {
return Collections.singleton(Constants.PROJ4);
}
/**
* Returns the set of authority codes for objects of the given type.
* Current implementation can not return complete Proj.4 definition strings.
* Instead, this method currently returns only fragments (e.g. {@code "+proj=lcc"}).
*
* @param type the spatial reference objects type.
* @return fragments of definition strings for spatial reference objects of the given type.
* @throws FactoryException if access to the underlying database failed.
*/
@Override
public Set<String> getAuthorityCodes(Class<? extends IdentifiedObject> type) throws FactoryException {
final Set<String> codes = new LinkedHashSet<>(10);
if (type.isAssignableFrom(GeographicCRS.class)) {
codes.add("latlon");
}
if (type.isAssignableFrom(GeocentricCRS.class)) {
codes.add("geocent");
}
if (type.isAssignableFrom(ProjectedCRS.class)) {
codes.addAll(Arrays.asList("lcc", "merc", "tmerc", "stere")); // Only a subset of supported projections.
}
final String[] methods = codes.toArray(new String[codes.size()]);
codes.clear();
for (final String method : methods) {
codes.add(PROJ_PARAM.concat(method));
}
return codes;
}
/**
* Returns some map projection methods supported by {@literal Proj.4}.
* Current implementation can not return the complete list of Proj.4 method, but returns the main ones.
* For each operation method in the returned set, the Proj.4 projection name can be obtained as below:
*
* {@preformat java
* String proj = IdentifiedObjects.getName(method, Citations.PROJ4);
* }
*
* The {@code proj} names obtained as above can be given in argument to the
* {@link #getOperationMethod(String)} and {@link #getDefaultParameters(String)} methods.
*
* @param type <code>{@linkplain SingleOperation}.class</code> for fetching all operation methods, or
* <code>{@linkplain Projection}.class</code> for fetching only map projection methods.
* @return methods available in this factory for coordinate operations of the given type.
*
* @see org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory#getAvailableMethods(Class)
*/
public Set<OperationMethod> getAvailableMethods(final Class<? extends SingleOperation> type) {
return new LazySet<>(CollectionsExt.filter(mtFactory.getAvailableMethods(type).iterator(), Proj4Factory::isSupported));
}
/**
* Returns the operation method of the given name. The given argument can be a Proj.4 projection name,
* but other authorities (OGC, EPSG…) are also accepted. A partial list of supported projection names
* can be obtained by {@link #getAvailableMethods(Class)}. Some examples of Proj.4 projection names
* are given below (not all of them are supported by this Proj.4 wrapper).
*
* <table class="sis">
* <caption>Some Proj.4 projection names</caption>
* <tr><th>Name</th> <th>Meaning</th></tr>
* <tr><td>{@code aea}</td> <td>Albers Equal-Area Conic</td></tr>
* <tr><td>{@code aeqd}</td> <td>Azimuthal Equidistant</td></tr>
* <tr><td>{@code cass}</td> <td>Cassini-Soldner</td></tr>
* <tr><td>{@code cea}</td> <td>Cylindrical Equal Area</td></tr>
* <tr><td>{@code eck4}</td> <td>Eckert IV</td></tr>
* <tr><td>{@code eck6}</td> <td>Eckert VI</td></tr>
* <tr><td>{@code eqdc}</td> <td>Equidistant Conic</td></tr>
* <tr><td>{@code gall}</td> <td>Gall Stereograpic</td></tr>
* <tr><td>{@code geos}</td> <td>Geostationary Satellite View</td></tr>
* <tr><td>{@code gnom}</td> <td>Gnomonic</td></tr>
* <tr><td>{@code krovak}</td> <td>Krovak Oblique Conic Conformal</td></tr>
* <tr><td>{@code laea}</td> <td>Lambert Azimuthal Equal Area</td></tr>
* <tr><td>{@code lcc}</td> <td>Lambert Conic Conformal</td></tr>
* <tr><td>{@code merc}</td> <td>Mercator</td></tr>
* <tr><td>{@code mill}</td> <td>Miller Cylindrical</td></tr>
* <tr><td>{@code moll}</td> <td>Mollweide</td></tr>
* <tr><td>{@code nzmg}</td> <td>New Zealand Map Grid</td></tr>
* <tr><td>{@code omerc}</td> <td>Oblique Mercator</td></tr>
* <tr><td>{@code ortho}</td> <td>Orthographic</td></tr>
* <tr><td>{@code sterea}</td> <td>Oblique Stereographic</td></tr>
* <tr><td>{@code stere}</td> <td>Stereographic</td></tr>
* <tr><td>{@code robin}</td> <td>Robinson</td></tr>
* <tr><td>{@code sinu}</td> <td>Sinusoidal</td></tr>
* <tr><td>{@code tmerc}</td> <td>Transverse Mercator</td></tr>
* <tr><td>{@code vandg}</td> <td>VanDerGrinten</td></tr>
* </table>
*
* The default implementation delegates to a {@code DefaultCoordinateOperationFactory} instance.
* It works because the Apache SIS operation methods declare the Proj.4 projection names as
* {@linkplain org.apache.sis.referencing.AbstractIdentifiedObject#getAlias() aliases}.
*
* @param name the name of the operation method to fetch.
* @return the operation method of the given name.
* @throws FactoryException if the requested operation method can not be fetched.
*
* @see org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory#getOperationMethod(String)
*/
public OperationMethod getOperationMethod(final String name) throws FactoryException {
final OperationMethod method = opFactory().getOperationMethod(name);
if (isSupported(method)) {
return method;
}
throw new NoSuchIdentifierException(Errors.getResources(defaultProperties)
.getString(Errors.Keys.UnsupportedOperation_1, name), name);
}
/**
* Returns the default parameter values for a math transform using the given operation method.
* The given argument can be a Proj.4 projection name, but other authorities (OGC, EPSG…) are also accepted.
* A partial list of supported projection names can be obtained by {@link #getAvailableMethods(Class)}.
* The returned parameters can be given to {@link #createParameterizedTransform(ParameterValueGroup)}.
*
* @param method the case insensitive name of the coordinate operation method to search for.
* @return a new group of parameter values for the {@code OperationMethod} identified by the given name.
* @throws NoSuchIdentifierException if there is no method registered for the given name or identifier.
*/
public ParameterValueGroup getDefaultParameters(final String method) throws NoSuchIdentifierException {
final ParameterValueGroup parameters = mtFactory.getDefaultParameters(method);
if (isSupported(parameters.getDescriptor())) {
return parameters;
}
throw new NoSuchIdentifierException(Errors.getResources(defaultProperties)
.getString(Errors.Keys.UnsupportedOperation_1, method), method);
}
/**
* Creates a transform from a group of parameters. The {@link OperationMethod} name is inferred from
* the {@linkplain org.opengis.parameter.ParameterDescriptorGroup#getName() parameter group name}.
* Each parameter value is formatted as a Proj.4 parameter in a definition string.
*
* <div class="note"><b>Example:</b>
* {@preformat java
* ParameterValueGroup p = factory.getDefaultParameters("Mercator");
* p.parameter("semi_major").setValue(6378137.000);
* p.parameter("semi_minor").setValue(6356752.314);
* MathTransform mt = factory.createParameterizedTransform(p);
* }
*
* The corresponding Proj.4 definition string is:
*
* {@preformat text
* +proj=merc +a=6378137.0 +b=6356752.314
* }
* </div>
*
* @param parameters the parameter values.
* @return the parameterized transform.
* @throws FactoryException if the object creation failed. This exception is thrown
* if some required parameter has not been supplied, or has illegal value.
*
* @see #getDefaultParameters(String)
* @see #getAvailableMethods(Class)
*/
public MathTransform createParameterizedTransform(final ParameterValueGroup parameters) throws FactoryException {
final String proj = name(parameters.getDescriptor(), Errors.Keys.UnsupportedOperation_1);
final StringBuilder buffer = new StringBuilder(100).append(PROJ_PARAM).append(proj).append(STANDARD_OPTIONS);
/*
* Proj.4 requires some parameters that are not defined in the EPSG geodetic dataset for some projections.
* Those parameters are unnecessary since their values are implied by the other parameters. However Proj.4
* does not seem to have any "intelligence" for such inference; we have to specify explicitly those values
* in the 'switch' statements below. The Objects listed below are parameters needed for those special cases.
*/
Object latitudeOfOrigin = null;
Object latitudeTrueScale = null;
Object standardParallel1 = null;
for (final GeneralParameterValue p : parameters.values()) {
/*
* Unconditionally ask the parameter name in order to throw an exception
* with better error message in case of unrecognized parameter.
*/
final String name = name(p.getDescriptor(), Errors.Keys.UnexpectedParameter_1);
if (p instanceof ParameterValue) {
final Object value = ((ParameterValue) p).getValue();
if (value != null) {
append(buffer, name, value);
switch (name) {
case "lat_0": latitudeOfOrigin = value; break;
case "lat_1": standardParallel1 = value; break;
case "lat_ts": latitudeTrueScale = value; break;
}
}
}
}
/*
* See above comment about parameter inference in Proj4. To verify if those special cases
* are still necessary, one can try to disable them and run TransformTest. If those tests
* work with a future Proj4 version, then the special cases below should be deleted.
*/
switch (proj) {
/*
* In "Lambert Conic Conformal (1SP)" case, there is no standard parallel (lat_1) since a scale factor (k_0)
* is used instead. That scale is defined as the "Scale factor at natural origin", i.e. at lat_0. But Proj4
* does not seem to know that definition, so we have to explicitly tell it that lat_0 is the latitude of
* true scale.
*/
case "lcc": {
if (standardParallel1 == null && latitudeOfOrigin != null) {
append(buffer, "lat_1", latitudeOfOrigin);
}
break;
}
/*
* In "Polar Stereographic (variant B)", the latitude of natural origin is always a pole (90°N or S).
* Whether it is the North or South pole is determined by the sign of the latitude of true scale.
*/
case "stere": {
if (latitudeOfOrigin == null && latitudeTrueScale instanceof Number) {
append(buffer, "lat_0", ((Number) latitudeTrueScale).doubleValue() < 0 ? Latitude.MIN_VALUE : Latitude.MAX_VALUE);
}
break;
}
}
final String definition = buffer.toString();
try {
final PJ pj = unique(new PJ(definition));
final PJ base = unique(new PJ(pj));
return new Transform(base, false, pj, false);
} catch (UnsatisfiedLinkError | NoClassDefFoundError e) {
throw new UnavailableFactoryException(Proj4.unavailable(e), e);
}
}
/**
* Appends a Proj4 parameter in the given string buffer.
*/
private static void append(final StringBuilder buffer, final String param, final Object value) {
buffer.append(" +").append(param).append('=').append(value);
}
/**
* Creates a new geodetic object from the given {@literal Proj.4} definition.
* The default implementation delegates to {@link #createCoordinateReferenceSystem(String)}.
*
* @param code the Proj.4 definition of the geodetic object to create.
* @return a geodetic created from the given definition.
* @throws FactoryException if the geodetic object can not be created for the given definition.
*/
@Override
public IdentifiedObject createObject(String code) throws FactoryException {
return createCoordinateReferenceSystem(code);
}
/**
* Creates a new CRS from the given {@literal Proj.4} definition.
* The {@code "Proj4:"} prefix (ignoring case), if present, is ignored.
*
* <div class="section">Apache SIS extension</div>
* Proj.4 unconditionally requires 3 letters for the {@code "+axis="} parameter — for example {@code "neu"} for
* <cite>North</cite>, <cite>East</cite> and <cite>Up</cite> respectively — regardless the number of dimensions
* in the CRS to create. Apache SIS makes the vertical direction optional:
*
* <ul>
* <li>If the vertical direction is present (as in {@code "neu"}), a three-dimensional CRS is created.</li>
* <li>If the vertical direction is absent (as in {@code "ne"}), a two-dimensional CRS is created.</li>
* </ul>
*
* <div class="note"><b>Examples:</b>
* <ul>
* <li>{@code "+init=epsg:4326"} (<strong>not</strong> equivalent to the standard EPSG::4326 definition)</li>
* <li>{@code "+proj=latlong +datum=WGS84 +ellps=WGS84 +towgs84=0,0,0"} (default to two-dimensional CRS)</li>
* <li>{@code "+proj=latlon +a=6378137.0 +b=6356752.314245179 +pm=0.0 +axis=ne"} (explicitly two-dimensional)</li>
* <li>{@code "+proj=latlon +a=6378137.0 +b=6356752.314245179 +pm=0.0 +axis=neu"} (three-dimensional)</li>
* </ul>
* </div>
*
* @param code the Proj.4 definition of the CRS object to create.
* @return a CRS created from the given definition.
* @throws FactoryException if the CRS object can not be created for the given definition.
*
* @see Proj4#createCRS(String, int)
*/
@Override
public CoordinateReferenceSystem createCoordinateReferenceSystem(String code) throws FactoryException {
code = trimNamespace(code);
boolean hasHeight = false;
/*
* Count the number of axes declared in the "+axis" parameter.
* If there is only two axes, add a 'u' (up) direction even in the two-dimensional case.
* We make this addition because Proj.4 seems to require the 3-letters code in all case.
*/
int offset = code.indexOf(AXIS_ORDER_PARAM);
if (offset >= 0) {
offset += AXIS_ORDER_PARAM.length();
final CharSequence orientation = CharSequences.token(code, offset);
for (int i=orientation.length(); --i >= 0;) {
final char c = orientation.charAt(i);
hasHeight = (c == 'u' || c == 'd');
if (hasHeight) break;
}
if (!hasHeight && orientation.length() < 3) {
offset = code.indexOf(orientation.toString(), offset);
if (offset >= 0) { // Should never be -1, but we are paranoiac.
offset += orientation.length();
code = new StringBuilder(code).insert(offset, 'u').toString();
}
}
}
try {
return createCRS(code, hasHeight);
} catch (IllegalArgumentException | ParserException e) {
throw new InvalidGeodeticParameterException(Proj4.canNotParse(code), e);
} catch (UnsatisfiedLinkError | NoClassDefFoundError e) {
throw new UnavailableFactoryException(Proj4.unavailable(e), e);
}
}
/**
* Returns the identifier for the given code in {@literal Proj.4} namespace.
*/
private Map<String,Object> identifier(final String code) {
Identifier id = identifiers.computeIfAbsent(code, (k) -> {
short i18n = 0;
if (k.equalsIgnoreCase( UNNAMED )) i18n = Vocabulary.Keys.Unnamed;
if (k.equalsIgnoreCase("Unknown")) i18n = Vocabulary.Keys.Unknown;
return new ImmutableIdentifier(Citations.PROJ4, Constants.PROJ4, k, null,
(i18n != 0) ? Vocabulary.formatInternational(i18n) : null);
});
final Map<String,Object> properties = new HashMap<>(defaultProperties);
properties.put(IdentifiedObject.NAME_KEY, id);
return properties;
}
/**
* Creates a geodetic datum from the given {@literal Proj.4} wrapper.
*
* @param pj the Proj.4 object to wrap.
* @param parser the parameter values of the Proj.4 object to wrap.
* @throws NumberFormatException if a Proj.4 parameter value can not be parsed.
*/
private GeodeticDatum createDatum(final PJ pj, final Proj4Parser parser) throws FactoryException {
final PrimeMeridian pm;
final double greenwichLongitude = Double.parseDouble(parser.value("pm", "0"));
if (greenwichLongitude == 0) {
pm = CommonCRS.WGS84.datum().getPrimeMeridian();
} else {
pm = datumFactory.createPrimeMeridian(identifier(UNNAMED), greenwichLongitude, Units.DEGREE);
}
final double[] def = pj.getEllipsoidDefinition();
return datumFactory.createGeodeticDatum(identifier(parser.value("datum", UNNAMED)),
datumFactory.createEllipsoid (identifier(parser.value("ellps", UNNAMED)),
def[0], // Semi-major axis length
def[0] * Math.sqrt(1 - def[1]), // Semi-minor axis length
Units.METRE), pm);
}
/**
* Creates a coordinate reference system from the given {@literal Proj.4} wrapper.
* The given {@code pj} will be stored as the CRS identifier.
*
* @param pj the Proj.4 object to wrap.
* @param withHeight whether to include a height axis.
* @throws IllegalArgumentException if a Proj.4 parameter value can not be parsed or assigned.
* @throws ParserException if a unit symbol can not be parsed.
*/
private CoordinateReferenceSystem createCRS(final PJ pj, final boolean withHeight) throws FactoryException {
final PJ.Type type = pj.getType();
final boolean geographic = PJ.Type.GEOGRAPHIC.equals(type);
final boolean geocentric = PJ.Type.GEOCENTRIC.equals(type);
final Proj4Parser parser = new Proj4Parser(pj.getCode());
final String dir = parser.value("axis", "enu");
final CoordinateSystemAxis[] axes = new CoordinateSystemAxis[geocentric | withHeight ? dir.length() : 2];
for (int i=0; i<axes.length; i++) {
final char d = Character.toLowerCase(dir.charAt(i));
char abbreviation = Character.toUpperCase(d);
boolean vertical = false;
final AxisDirection c;
final String name;
if (geocentric) switch (d) {
case 'e': c = AxisDirection.GEOCENTRIC_X; name = "Geocentric X"; break;
case 'n': c = AxisDirection.GEOCENTRIC_Y; name = "Geocentric Y"; break;
case 'u': c = AxisDirection.GEOCENTRIC_Z; name = "Geocentric Z"; break;
default: c = AxisDirection.OTHER; name = "Unknown"; break;
} else switch (d) {
case 'e': c = AxisDirection.EAST; name = geographic ? "Geodetic longitude" : "Easting"; break;
case 'w': c = AxisDirection.WEST; name = geographic ? "Geodetic longitude" : "Westing"; break;
case 'n': c = AxisDirection.NORTH; name = geographic ? "Geodetic latitude" : "Northing"; break;
case 's': c = AxisDirection.SOUTH; name = geographic ? "Geodetic latitude" : "Southing"; break;
case 'u': c = AxisDirection.UP; name = "Height"; vertical = true; abbreviation = 'h'; break;
case 'd': c = AxisDirection.DOWN; name = "Depth"; vertical = true; break;
default: c = AxisDirection.OTHER; name = "Unknown"; break;
}
if (geographic && AxisDirections.isCardinal(c)) {
abbreviation = (d == 'e' || d == 'w') ? 'λ' : 'φ';
}
final Unit<?> unit = (vertical || !geographic) ? parser.unit(vertical) : Units.DEGREE;
axes[i] = csFactory.createCoordinateSystemAxis(identifier(name), String.valueOf(abbreviation).intern(), c, unit);
}
/*
* At this point we got the coordinate system axes. Now create the CRS. The given Proj.4 object
* will be stored as the CRS identifier for allowing OperationFactory to get it back before to
* attempt to create a new one for a given CRS.
*/
final Map<String,Object> csName = identifier(UNNAMED);
final Map<String,Object> name = new HashMap<>(identifier(parser.name(type == PJ.Type.PROJECTED)));
name.put(CoordinateReferenceSystem.IDENTIFIERS_KEY, pj);
switch (type) {
case GEOGRAPHIC: {
return crsFactory.createGeographicCRS(name, createDatum(pj, parser), withHeight ?
csFactory.createEllipsoidalCS(csName, axes[0], axes[1], axes[2]) :
csFactory.createEllipsoidalCS(csName, axes[0], axes[1]));
}
case GEOCENTRIC: {
return crsFactory.createGeocentricCRS(name, createDatum(pj, parser),
csFactory.createCartesianCS(csName, axes[0], axes[1], axes[2]));
}
case PROJECTED: {
final PJ base = unique(new PJ(pj));
final CoordinateReferenceSystem baseCRS = createCRS(base, withHeight);
final Transform tr = new Transform(pj, withHeight, base, withHeight);
/*
* Try to convert the Proj.4 parameters into OGC parameters in order to have a less opaque structure.
* Failure to perform this conversion will not cause a failure to create the ProjectedCRS. After all,
* maybe the user invokes this method for using a map projection not yet supported by Apache SIS.
* Instead, fallback on the more opaque Transform.METHOD description. Apache SIS will not be able to
* perform analysis on those parameters, but it will not prevent the Proj.4 transformation to work.
*/
OperationMethod method;
ParameterValueGroup parameters;
try {
method = parser.method(opFactory());
parameters = parser.parameters();
} catch (IllegalArgumentException | FactoryException e) {
Logging.recoverableException(Logging.getLogger(Modules.GDAL), Proj4Factory.class, "createProjectedCRS", e);
method = Transform.METHOD;
parameters = null; // Will let Apache SIS infers the parameters from the Transform instance.
}
final Conversion fromBase = new DefaultConversion(name, method, tr, parameters);
return crsFactory.createProjectedCRS(name, (GeographicCRS) baseCRS, fromBase, withHeight ?
csFactory.createCartesianCS(csName, axes[0], axes[1], axes[2]) :
csFactory.createCartesianCS(csName, axes[0], axes[1]));
}
default: {
throw new FactoryException(Errors.getResources(defaultProperties)
.getString(Errors.Keys.UnknownEnumValue_2, type, PJ.Type.class));
}
}
}
/**
* Gets the {@literal Proj.4} object from the given coordinate reference system. If an existing {@code PJ}
* instance is found, returns it. Otherwise if {@code force} is {@code true}, creates a new {@code PJ}
* instance from a Proj.4 definition inferred from the given CRS.
* This method is the converse of {@link #createCRS(PJ, boolean)}.
*/
private PJ unwrapOrCreate(final CoordinateReferenceSystem crs, final boolean force) throws FactoryException {
for (final Identifier id : crs.getIdentifiers()) {
if (id instanceof PJ) {
return (PJ) id;
}
}
return force ? unique(new PJ(Proj4.definition(crs))) : null;
}
/**
* Returns a unique instance of the given {@literal Proj.4} object.
*/
@SuppressWarnings("FinalizeCalledExplicitly")
private PJ unique(PJ pj) {
final PJ existing = pool.putIfAbsent(pj.getCode(), pj);
if (existing != null) {
pj.finalize(); // Release Proj.4 resources.
return existing;
}
return pj;
}
/**
* Creates a coordinate reference system from the given {@literal Proj.4} definition string.
* The {@code Proj4} suffix shall have been removed before to invoke this method.
*
* @param definition the Proj.4 definition.
* @param withHeight whether to include a height axis.
*
* @see Proj4#createCRS(String, int)
* @see #createCoordinateReferenceSystem(String)
*/
final CoordinateReferenceSystem createCRS(final String definition, final boolean withHeight) throws FactoryException {
PJ pj = pool.get(definition);
if (pj == null) {
pj = unique(new PJ(definition));
pool.putIfAbsent(definition, pj);
}
return createCRS(pj, withHeight);
}
/**
* Creates an operation for conversion or transformation between two coordinate reference systems.
* The given CRSs should be instances {@linkplain #createCoordinateReferenceSystem created by this factory}.
* If not, then there is a choice:
*
* <ul>
* <li>If {@code force} is {@code false}, then this method returns {@code null}.</li>
* <li>Otherwise this method always uses Proj.4 for performing the coordinate operations,
* regardless if the given CRS were created from Proj.4 definition strings or not.
* This method fails if it can not map the given CRS to Proj.4 data structures.</li>
* </ul>
*
* @param sourceCRS the source coordinate reference system.
* @param targetCRS the target coordinate reference system.
* @param force whether to force the creation of a Proj.4 transform
* even if the given CRS are not wrappers around Proj.4 data structures.
* @return a coordinate operation for transforming coordinates from the given source CRS to the given target CRS, or
* {@code null} if the given CRS are not wrappers around Proj.4 data structures and {@code force} is false.
* @throws FactoryException if {@code force} is {@code true} and this method can not create Proj.4 transform
* for the given pair of coordinate reference systems.
*
* @see Proj4#createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, boolean)
* @see DefaultCoordinateOperationFactory#createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem)
*/
public CoordinateOperation createOperation(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS,
final boolean force)
throws FactoryException
{
final PJ source, target;
try {
if ((source = unwrapOrCreate(sourceCRS, force)) == null ||
(target = unwrapOrCreate(targetCRS, force)) == null)
{
return null; // At least one CRS is not a Proj.4 wrapper and 'force' is false.
}
} catch (UnsatisfiedLinkError | NoClassDefFoundError e) {
throw new UnavailableFactoryException(Proj4.unavailable(e), e);
}
/*
* Before to create a transform, verify if the target CRS already contains a suitable transform.
* In such case, returning the existing operation is preferable since it usually contains better
* parameter description than what this method build.
*/
if (targetCRS instanceof GeneralDerivedCRS) {
final CoordinateOperation op = ((GeneralDerivedCRS) targetCRS).getConversionFromBase();
final MathTransform tr = op.getMathTransform();
if (tr instanceof Transform && ((Transform) tr).isFor(sourceCRS, source, targetCRS, target)) {
return op;
}
}
/*
* The 'Transform' construction implies parameter validation, so we do it first before to
* construct other objects.
*/
final Transform tr = new Transform(source, is3D("sourceCRS", sourceCRS),
target, is3D("targetCRS", targetCRS));
Identifier id;
String src = null, tgt = null, name = UNNAMED;
if ((id = sourceCRS.getName()) != null) src = id.getCode();
if ((id = targetCRS.getName()) != null) tgt = id.getCode();
if (src != null || tgt != null) {
final StringBuilder buffer = new StringBuilder();
if (src != null) buffer.append("From ").append(src);
if (tgt != null) buffer.append(buffer.length() == 0 ? "To " : " to ").append(tgt);
name = buffer.toString();
}
return opFactory().createSingleOperation(identifier(name), sourceCRS, targetCRS, null, Transform.METHOD, tr);
}
/**
* Returns whether the given CRS is three-dimensional.
* Thrown an exception if the number of dimension is unsupported.
*/
private boolean is3D(final String arg, final CoordinateReferenceSystem crs) throws FactoryException {
final int dim = crs.getCoordinateSystem().getDimension();
final boolean is3D = (dim >= 3);
if (dim < 2 || dim > 3) {
throw new FactoryException(Errors.getResources(defaultProperties)
.getString(Errors.Keys.MismatchedDimension_3, arg, is3D ? 3 : 2, dim));
}
return is3D;
}
/**
* Returns {@code true} if the given coordinate operation method or parameter group is supported.
*/
private static boolean isSupported(final IdentifiedObject method) {
return IdentifiedObjects.getName(method, Citations.PROJ4) != null;
}
/**
* Returns the {@literal Proj.4} name of the given parameter value or parameter group.
*
* @param param the parameter value or parameter group for which to get the Proj.4 name.
* @param errorKey the resource key of the error message to produce if no Proj.4 name has been found.
* The message shall expect exactly one argument. This error key can be
* {@link Errors.Keys#UnsupportedOperation_1} or {@link Errors.Keys#UnexpectedParameter_1}.
* @return the Proj.4 name of the given object (never null).
* @throws FactoryException if the Proj.4 name has not been found.
*/
private String name(final IdentifiedObject param, final short errorKey) throws FactoryException {
String name = IdentifiedObjects.getName(param, Citations.PROJ4);
if (name == null) {
name = param.getName().getCode();
final String message = Errors.getResources(defaultProperties).getString(errorKey, name);
if (errorKey == Errors.Keys.UnsupportedOperation_1) {
throw new NoSuchIdentifierException(message, name);
} else {
throw new InvalidGeodeticParameterException(message);
}
}
return name;
}
}