| /* |
| * 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.geotiff.writer; |
| |
| import java.util.List; |
| import java.util.EnumMap; |
| import java.util.logging.Level; |
| import static javax.imageio.plugins.tiff.GeoTIFFTagSet.TAG_GEO_ASCII_PARAMS; |
| import static javax.imageio.plugins.tiff.GeoTIFFTagSet.TAG_GEO_DOUBLE_PARAMS; |
| import javax.measure.Unit; |
| import javax.measure.UnitConverter; |
| import javax.measure.IncommensurableException; |
| import javax.measure.quantity.Angle; |
| import javax.measure.quantity.Length; |
| import org.opengis.util.FactoryException; |
| import org.opengis.metadata.Identifier; |
| import org.opengis.metadata.spatial.CellGeometry; |
| import org.opengis.referencing.IdentifiedObject; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.crs.GeodeticCRS; |
| import org.opengis.referencing.crs.ProjectedCRS; |
| import org.opengis.referencing.crs.VerticalCRS; |
| import org.opengis.referencing.cs.AxisDirection; |
| import org.opengis.referencing.cs.CoordinateSystem; |
| import org.opengis.referencing.cs.CartesianCS; |
| import org.opengis.referencing.cs.EllipsoidalCS; |
| import org.opengis.referencing.datum.Ellipsoid; |
| import org.opengis.referencing.datum.PrimeMeridian; |
| import org.opengis.referencing.datum.GeodeticDatum; |
| import org.opengis.referencing.datum.VerticalDatum; |
| import org.opengis.referencing.datum.PixelInCell; |
| import org.opengis.referencing.operation.Conversion; |
| import org.opengis.referencing.operation.Matrix; |
| import org.opengis.parameter.GeneralParameterValue; |
| import org.opengis.parameter.ParameterValue; |
| import org.apache.sis.measure.Units; |
| import org.apache.sis.util.ArraysExt; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.privy.Strings; |
| import org.apache.sis.util.privy.CollectionsExt; |
| import org.apache.sis.referencing.CRS; |
| import org.apache.sis.referencing.IdentifiedObjects; |
| import org.apache.sis.referencing.cs.CoordinateSystems; |
| import org.apache.sis.referencing.operation.matrix.Matrices; |
| import org.apache.sis.referencing.operation.transform.MathTransforms; |
| import org.apache.sis.referencing.factory.UnavailableFactoryException; |
| import org.apache.sis.referencing.privy.ReferencingUtilities; |
| import org.apache.sis.referencing.privy.WKTKeywords; |
| import org.apache.sis.coverage.grid.GridGeometry; |
| import org.apache.sis.coverage.grid.IncompleteGridGeometryException; |
| import org.apache.sis.storage.base.MetadataFetcher; |
| import org.apache.sis.storage.geotiff.base.UnitKey; |
| import org.apache.sis.storage.geotiff.base.GeoKeys; |
| import org.apache.sis.storage.geotiff.base.GeoCodes; |
| import org.apache.sis.storage.geotiff.base.Resources; |
| import org.apache.sis.storage.event.StoreListeners; |
| import org.apache.sis.metadata.iso.citation.Citations; |
| import org.apache.sis.pending.jdk.JDK15; |
| |
| |
| /** |
| * Helper class for writing GeoKeys. |
| * This class decomposes a CRS into entries written by calls to {@code writeShort(…)}, {@code writeDouble(…)} |
| * or {@code writeString(…)} methods. The order in which those methods are invoked matter, because the GeoTIFF |
| * specification requires that keys are sorted in increasing order. We do not sort them after writing. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| public final class GeoEncoder { |
| /** |
| * Size of the model transformation matrix, in number of rows and columns. |
| * This size is fixed by the GeoTIFF specification. |
| */ |
| private static final int MATRIX_SIZE = 4; |
| |
| /** |
| * The listeners where to report warnings. |
| */ |
| private final StoreListeners listeners; |
| |
| /** |
| * Overall configuration of the GeoTIFF file, or {@code null} if none. |
| * This is the value to store in {@link GeoKeys#Citation}. |
| */ |
| private String citation; |
| |
| /** |
| * The coordinate reference system of the grid geometry, or {@code null} if none. |
| * This CRS may contain more dimensions than the 3 dimensions allowed by GeoTIFF. |
| * Axis order and axis directions may be different than the (east, north, up) directions mandated by GeoTIFF. |
| */ |
| private CoordinateReferenceSystem fullCRS; |
| |
| /** |
| * Whether the coordinate system has a vertical component. |
| */ |
| private boolean hasVerticalAxis; |
| |
| /** |
| * The conversion from grid coordinates to full CRS, which determines the model transformation. |
| * This conversion may operate on more dimensions than the three dimensions mandated by GeoTIFF. |
| * Furthermore the output may need to be reordered for the (east, north, up) axis order mandated by GeoTIFF. |
| * |
| * @see #modelTransformation() |
| */ |
| private Matrix gridToCRS; |
| |
| /** |
| * Whether the raster model is "point" or "area". |
| * The default is area ({@code false}). |
| */ |
| private boolean isPoint; |
| |
| /** |
| * Units of measurement found by the analysis of coordinate system axes. |
| * Should be filled as soon as possible because it determines also the |
| * units of measurement to use for encoding map projection parameters. |
| */ |
| private final EnumMap<UnitKey, Unit<?>> units; |
| |
| /** |
| * The key directory, including the header. |
| * Each entry is a record of {@value GeoCodes#ENTRY_LENGTH} values. |
| * The first record is a header of the same length. |
| * |
| * @see #keyCount |
| * @see #keyDirectory() |
| */ |
| private final short[] keyDirectory; |
| |
| /** |
| * Number of valid elements in {@link #keyDirectory}, not counting the header. |
| */ |
| private int keyCount; |
| |
| /** |
| * Parameters to encode as IEEE-754 floating point values. |
| * |
| * @see #doubleCount |
| * @see #doubleParams() |
| */ |
| private final double[] doubleParams; |
| |
| /** |
| * Number of valid elements in {@link #doubleParams}. |
| */ |
| private int doubleCount; |
| |
| /** |
| * Parameters to encode as ASCII character strings. |
| * Strings are separated by the {@value GeoCodes#STRING_SEPARATOR} character. |
| * |
| * @see #asciiParams() |
| */ |
| private final StringBuilder asciiParams; |
| |
| /** |
| * If multiple names are packed in a single citation GeoKey, the citation key of the main object. |
| * This is a sub-encoding applied inside {@link #asciiParams} for the citation. Example: |
| * |
| * <pre>GCS Name=Moon 2000|Datum=D_Moon_2000|Ellipsoid=Moon_2000_IAU_IAG|Primem=Reference_Meridian|AUnits=Decimal_Degree|</pre> |
| * |
| * Above sub-encoding is applied only if necessary. In such case, this field is the first key to prepend. |
| * Currently the only accepted value is: "GCS Name". |
| */ |
| private String citationMainKey; |
| |
| /** |
| * Index in the {@link #keyDirectory} array where the length (in number of characters) of current citation is stored. |
| * The {@code keyDirectory[citationLengthIndex]} value is the number of characters, excluding the trailing separator. |
| * The {@code keyDirectory[citationLengthIndex+1]} value is the offset where the citation starts. |
| * This information is used for modifying in-place the ASCII entry of a citation for inserting more names. |
| */ |
| private int citationLengthIndex; |
| |
| /** |
| * Whether to disable attempts to write EPSG codes. This is set to {@code true} on the first attempt to use the |
| * EPSG database if it appears to be unavailable. This is used for avoiding many retries which will continue to |
| * fail. |
| */ |
| private boolean disableEPSG; |
| |
| /** |
| * Prepares information for writing GeoTIFF tags for the given grid geometry. |
| * Caller shall invoke {@link #write(GridGeometry, MetadataFetcher)} exactly once after construction. |
| * |
| * @param listeners the listeners where to report warnings. |
| */ |
| public GeoEncoder(final StoreListeners listeners) { |
| this.listeners = listeners; |
| units = new EnumMap<>(UnitKey.class); |
| asciiParams = new StringBuilder(100); |
| doubleParams = new double[GeoCodes.NUM_DOUBLE_GEOKEYS]; |
| keyDirectory = new short[(GeoCodes.NUM_GEOKEYS + 1) * GeoCodes.ENTRY_LENGTH]; |
| keyDirectory[0] = 1; // Directory version. |
| keyDirectory[1] = 1; // Revision major number. We implement GeoTIFF 1.1. |
| keyDirectory[2] = 1; // Revision minor number. We implement GeoTIFF 1.1. |
| } |
| |
| /** |
| * Writes GeoTIFF keys for the given grid geometry. |
| * This method should be invoked exactly once. |
| * |
| * @param store the store for which to write GeoTIFF keys. |
| * @param grid grid geometry of the image to write. |
| * @param metadata overall configuration information. |
| * @throws FactoryException if an error occurred while fetching the EPSG code. |
| * @throws ArithmeticException if a short value cannot be stored as an unsigned 16 bits integer. |
| * @throws IncommensurableException if a measure uses an unexpected unit of measurement. |
| * @throws IncompleteGridGeometryException if the grid geometry is incomplete. |
| */ |
| public void write(final GridGeometry grid, final MetadataFetcher<?> metadata) |
| throws FactoryException, IncommensurableException |
| { |
| citation = CollectionsExt.first(metadata.transformationDimension); |
| isPoint = CollectionsExt.first(metadata.cellGeometry) == CellGeometry.POINT; |
| gridToCRS = MathTransforms.getMatrix(grid.getGridToCRS(isPoint ? PixelInCell.CELL_CENTER : PixelInCell.CELL_CORNER)); |
| if (gridToCRS == null) { |
| warning(resources().getString(Resources.Keys.CanNotEncodeNonLinearModel), null); |
| } |
| if (grid.isDefined(GridGeometry.CRS)) { |
| fullCRS = grid.getCoordinateReferenceSystem(); |
| final CoordinateReferenceSystem crs = CRS.getHorizontalComponent(fullCRS); |
| if ((crs instanceof ProjectedCRS && writeCRS((ProjectedCRS) crs)) || |
| (crs instanceof GeodeticCRS && writeCRS((GeodeticCRS) crs, false))) |
| { |
| writeCRS(CRS.getVerticalComponent(fullCRS, true)); |
| } else { |
| unsupportedType(fullCRS); |
| writeModelType(GeoCodes.userDefined); |
| } |
| } else { |
| writeModelType(GeoCodes.undefined); |
| } |
| } |
| |
| /** |
| * Writes the first keys (model type, raster type, citation). |
| * This method shall be the first write operation, before to write any other keys. |
| * |
| * @param type value of {@link GeoKeys#ModelType}. |
| */ |
| private void writeModelType(final short type) { |
| writeShort(GeoKeys.ModelType, type); |
| writeShort(GeoKeys.RasterType, isPoint ? GeoCodes.RasterPixelIsPoint : GeoCodes.RasterPixelIsArea); |
| if (citation != null) { |
| writeString(GeoKeys.Citation, citation); |
| citation = null; |
| } |
| } |
| |
| /** |
| * Writes the vertical component of the CRS. |
| * The horizontal component must have been written before this method is invoked. |
| * |
| * @param crs the CRS to write, or {@code null} if none. |
| * @throws FactoryException if an error occurred while fetching an EPSG code. |
| */ |
| private void writeCRS(final VerticalCRS crs) throws FactoryException { |
| if (crs != null) { |
| hasVerticalAxis = true; |
| if (writeEPSG(GeoKeys.Vertical, crs)) { |
| writeName(GeoKeys.VerticalCitation, null, crs); |
| addUnits(UnitKey.VERTICAL, crs.getCoordinateSystem()); |
| final VerticalDatum datum = crs.getDatum(); |
| if (writeEPSG(GeoKeys.VerticalDatum, datum)) { |
| /* |
| * OGC requirement 25.5 said "VerticalCitationGeoKey SHALL be populated." |
| * But how? Using the same multiple-names convention as for geodetic CRS? |
| * |
| * https://github.com/opengeospatial/geotiff/issues/59 |
| */ |
| } |
| writeUnit(UnitKey.VERTICAL); |
| } |
| } |
| } |
| |
| /** |
| * Writes entries for a geographic or geocentric CRS. |
| * The CRS type is inferred from the coordinate system type. |
| * This method may be invoked for writing the base CRS of a projected CRS. |
| * |
| * @param crs the CRS to write. |
| * @param isBaseCRS whether to write the base CRS of a projected CRS. |
| * @return whether this method has been able to write the CRS. |
| * @throws FactoryException if an error occurred while fetching an EPSG code. |
| * @throws IncommensurableException if a measure uses an unexpected unit of measurement. |
| */ |
| private boolean writeCRS(final GeodeticCRS crs, final boolean isBaseCRS) throws FactoryException, IncommensurableException { |
| final short type; |
| final CoordinateSystem cs = crs.getCoordinateSystem(); |
| addUnits(UnitKey.ANGULAR, cs); |
| if (cs instanceof EllipsoidalCS) { |
| type = GeoCodes.ModelTypeGeographic; |
| } else if (isBaseCRS) { |
| warning(resources().getString(Resources.Keys.CanNotEncodeNonGeographicBase), null); |
| return false; |
| } else if (cs instanceof CartesianCS) { |
| type = GeoCodes.ModelTypeGeocentric; |
| } else { |
| unsupportedType(cs); |
| return false; |
| } |
| /* |
| * Start writing GeoTIFF keys for the geodetic CRS, potentially followed by datum, prime meridian and ellipsoid |
| * in that order. The order matter because GeoTIFF specification requires keys to be sorted in increasing order. |
| * A difficulty is that units of measurement are between prime meridian and ellipsoid, and the angular unit is |
| * needed for projected CRS too. |
| */ |
| writeModelType(isBaseCRS ? GeoCodes.ModelTypeProjected : type); |
| if (writeEPSG(GeoKeys.GeodeticCRS, crs)) { |
| writeName(GeoKeys.GeodeticCitation, "GCS Name", crs); |
| final GeodeticDatum datum = crs.getDatum(); |
| if (writeEPSG(GeoKeys.GeodeticDatum, datum)) { |
| appendName(WKTKeywords.Datum, datum); |
| final PrimeMeridian primem = datum.getPrimeMeridian(); |
| final double longitude; |
| if (writeEPSG(GeoKeys.PrimeMeridian, primem)) { |
| appendName(WKTKeywords.PrimeM, datum); |
| longitude = primem.getGreenwichLongitude(); |
| } else { |
| longitude = 0; // Means "do not write prime meridian". |
| } |
| final Ellipsoid ellipsoid = datum.getEllipsoid(); |
| final Unit<Length> axisUnit = ellipsoid.getAxisUnit(); |
| final Unit<?> linearUnit = units.putIfAbsent(UnitKey.LINEAR, axisUnit); |
| final UnitConverter toLinear = axisUnit.getConverterToAny(linearUnit != null ? linearUnit : axisUnit); |
| writeUnit(UnitKey.LINEAR); // Must be after the `units` map have been updated. |
| writeUnit(UnitKey.ANGULAR); |
| if (writeEPSG(GeoKeys.Ellipsoid, ellipsoid)) { |
| appendName(WKTKeywords.Ellipsoid, ellipsoid); |
| writeDouble(GeoKeys.SemiMajorAxis, toLinear.convert(ellipsoid.getSemiMajorAxis())); |
| if (ellipsoid.isSphere() || !ellipsoid.isIvfDefinitive()) { |
| writeDouble(GeoKeys.SemiMinorAxis, toLinear.convert(ellipsoid.getSemiMinorAxis())); |
| } else { |
| writeDouble(GeoKeys.InvFlattening, ellipsoid.getInverseFlattening()); |
| } |
| } |
| if (longitude != 0) { |
| Unit<Angle> unit = primem.getAngularUnit(); |
| UnitConverter c = unit.getConverterToAny(units.getOrDefault(UnitKey.ANGULAR, Units.DEGREE)); |
| writeDouble(GeoKeys.PrimeMeridianLongitude, c.convert(longitude)); |
| } |
| } |
| } else if (isBaseCRS) { |
| writeUnit(UnitKey.ANGULAR); // Map projection parameters may need this unit. |
| } |
| return true; |
| } |
| |
| /** |
| * Writes entries for a projected CRS. |
| * If the CRS is user-specified, then this method writes the geodetic CRS first. |
| * |
| * @return whether this method has been able to write the CRS. |
| * @throws FactoryException if an error occurred while fetching an EPSG or GeoTIFF code. |
| * @throws IncommensurableException if a measure uses an unexpected unit of measurement. |
| */ |
| private boolean writeCRS(final ProjectedCRS crs) throws FactoryException, IncommensurableException { |
| if (!writeCRS(crs.getBaseCRS(), true)) { |
| return false; |
| } |
| if (writeEPSG(GeoKeys.ProjectedCRS, crs)) { |
| writeName(GeoKeys.ProjectedCitation, null, crs); |
| addUnits(UnitKey.PROJECTED, crs.getCoordinateSystem()); |
| final Conversion projection = crs.getConversionFromBase(); |
| if (writeEPSG(GeoKeys.Projection, projection)) { |
| final var method = projection.getMethod(); |
| final short projCode = getGeoCode(method); |
| writeShort(GeoKeys.ProjMethod, projCode); |
| writeUnit(UnitKey.PROJECTED); |
| switch (projCode) { |
| case GeoCodes.undefined: missingValue(GeoKeys.ProjMethod); return true; |
| case GeoCodes.userDefined: cannotEncode(0, name(method), null); break; |
| /* |
| * TODO: GeoTIFF requirement 27.4 said that ProjectedCitationGeoKey shall be provided, |
| * But how? Using the same multiple-names convention ("GCS Name") as for geodetic CRS? |
| * |
| * https://github.com/opengeospatial/geotiff/issues/59 |
| */ |
| } |
| } |
| for (final GeneralParameterValue p : projection.getParameterValues().values()) { |
| RuntimeException cause = null; |
| final var descriptor = p.getDescriptor(); |
| if (p instanceof ParameterValue<?>) { |
| final short key = getGeoCode(descriptor); |
| if (key != GeoCodes.undefined && key != GeoCodes.userDefined) { |
| final var pv = (ParameterValue<?>) p; |
| final UnitKey type = UnitKey.ofProjectionParameter(key); |
| if (type == UnitKey.LINEAR) { |
| continue; // Skip the "cannot encode" warning. |
| } |
| if (type != UnitKey.NULL) try { |
| final Unit<?> unit = units.getOrDefault(type, type.defaultUnit()); |
| writeDouble(key, (unit != null) ? pv.doubleValue(unit) : pv.doubleValue()); |
| continue; |
| } catch (IllegalArgumentException | IllegalStateException e) { |
| cause = e; |
| } |
| } |
| } |
| cannotEncode(1, name(descriptor), cause); |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Remembers the units of measurement found in all coordinate system axes. |
| * The units are stored in the {@link #units} map. |
| * |
| * @param main the main kind of units expected in the coordinate system. |
| * @param cs the coordinate system to analyze. |
| */ |
| private void addUnits(final UnitKey main, final CoordinateSystem cs) { |
| for (int i = cs.getDimension(); --i >= 0;) { |
| final Unit<?> unit = cs.getAxis(i).getUnit(); |
| final UnitKey type = main.validate(unit); |
| if (type != null) { |
| final Unit<?> previous = units.putIfAbsent(type, unit); |
| if (previous != null && !previous.equals(unit)) { |
| warning(errors().getString(Errors.Keys.HeterogynousUnitsIn_1, name(cs)), null); |
| } |
| } else { |
| cannotEncode(2, unit.toString(), null); |
| } |
| } |
| } |
| |
| /** |
| * Writes the entries for the specified unit of measurement. |
| * This method should be invoked only once per unit key. |
| * |
| * @param key identification of the unit to write. |
| */ |
| private void writeUnit(final UnitKey key) { |
| final Unit<?> unit = units.get(key); |
| if (unit != null) { |
| final short epsg = toShortEPSG(Units.getEpsgCode(unit, key.isAxis)); |
| if (epsg != GeoCodes.userDefined) { |
| writeShort(key.codeKey, epsg); |
| } else if (key.scaleKey != 0) { |
| writeShort(key.codeKey, epsg); |
| writeDouble(key.scaleKey, Units.toStandardUnit(unit)); |
| } else { |
| cannotEncode(2, unit.toString(), null); |
| } |
| } |
| } |
| |
| /** |
| * Writes the name of the specified object. |
| * |
| * @param key the numeric identifier of the GeoTIFF key. |
| * @param type type of object for which to write the name, or {@code null} for no multiple-names citation. |
| * @param object the object for which to write the name. |
| */ |
| private void writeName(final short key, final String type, final IdentifiedObject object) { |
| String name = IdentifiedObjects.getName(object, null); |
| if (name == null) { |
| name = "Unnamed"; |
| } |
| writeString(key, name); |
| citationMainKey = type; |
| citationLengthIndex = keyCount * GeoCodes.ENTRY_LENGTH + 2; // Length is the field #2. |
| } |
| |
| /** |
| * Writes the name of the specified object using the "multi-names in single citation" convention. |
| * The {@link #writeName(short, String, IdentifiedObject)} method must have been invoked for the |
| * main object before this method call. |
| * |
| * @param type type of object for which to write the name. |
| * @param object the object for which to write the name. |
| */ |
| private void appendName(final String type, final IdentifiedObject object) { |
| final String name = IdentifiedObjects.getName(object, null); |
| if (name != null) { |
| int i = citationLengthIndex; |
| int offset = Short.toUnsignedInt(keyDirectory[i+1]); |
| int length = Short.toUnsignedInt(keyDirectory[i]); |
| int start = length; |
| if (citationMainKey != null) { |
| final String value = citationMainKey + '='; |
| asciiParams.insert(offset, value); |
| length += value.length(); |
| citationMainKey = null; |
| } |
| final String value = GeoCodes.STRING_SEPARATOR + type + '=' + name; |
| asciiParams.insert(offset + length, value); |
| keyDirectory[i] = toShort(length += value.length()); |
| /* |
| * After we inserted the name, adjust the offsets of all ASCII entries written after the citation. |
| * Note that in the following loop, (i < limit) must be tested before increment because the limit |
| * is inclusive. This loop will do nothing with GeoTIFF 1.1 because there is no other ASCII entry |
| * after citation, but we keep it in case a future GeoTIFF version adds more ASCII entries. |
| */ |
| final int shift = length - start; |
| final int limit = keyCount * GeoCodes.ENTRY_LENGTH; // Inclusive. |
| i++; // Offset is the field after length. |
| while (i < limit) { |
| i += GeoCodes.ENTRY_LENGTH; // Really after (i < limit) test. |
| if (keyDirectory[i-2] == (short) TAG_GEO_ASCII_PARAMS) { |
| offset = Short.toUnsignedInt(keyDirectory[i]); |
| keyDirectory[i] = toShort(offset + shift); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Fetches the GeoTIFF code of the given object. If {@code null}, returns {@link GeoCodes#undefined}. |
| * If the object has no GeTIFF identifier, returns {@value GeoCodes#userDefined}. |
| * |
| * @param object the object for which to get the GeoTIFF code. |
| * @return the GeoTIFF code, or {@link GeoCodes#undefined} or {@link GeoCodes#userDefined} if none. |
| * @throws FactoryException if an error occurred while fetching the GeoTIFF code. |
| */ |
| private short getGeoCode(final IdentifiedObject object) throws FactoryException { |
| if (object == null) { |
| return GeoCodes.undefined; |
| } |
| final Identifier id = IdentifiedObjects.getIdentifier(object, Citations.GEOTIFF); |
| if (id != null) try { |
| return Short.parseShort(id.getCode()); |
| } catch (NumberFormatException e) { |
| warning(errors().getString(Errors.Keys.CanNotParse_1, IdentifiedObjects.toString(id)), e); |
| } |
| return GeoCodes.userDefined; |
| } |
| |
| /** |
| * Writes the EPSG code of the given object, or {@value GeoCodes#userDefined} if none. |
| * Returns whether the caller should write user-defined object in replacement or in addition to EPSG code. |
| * |
| * @param key the numeric identifier of the GeoTIFF key. |
| * @param object the object for which to get the EPSG code. |
| * @return whether the caller should write user-defined object. |
| * @throws FactoryException if an error occurred while fetching the EPSG code. |
| */ |
| private boolean writeEPSG(final short key, final IdentifiedObject object) throws FactoryException { |
| if (object == null) { |
| writeShort(key, GeoCodes.undefined); |
| missingValue(key); |
| return false; |
| } |
| /* |
| * Note `lookupEPSG(…)` will return a value only if the axes have the same order and units. |
| * We could ignore axis order because GeoTIFF specification fixes it to (east, north, up), |
| * but we shall not ignore axis units. The `IdentifiedObjectFinder` API does not currently |
| * allow ignoring only axis order, so we fallback on strict equality (ignoring metadata). |
| * This is not necessarily a bad thing, because there is a possibility that future GeoTIFF |
| * specifications become stricter, so we are already "strict" regarding usages of EPSG codes. |
| */ |
| short epsg = GeoCodes.userDefined; |
| if (!disableEPSG) try { |
| epsg = toShortEPSG(IdentifiedObjects.lookupEPSG(object)); |
| } catch (UnavailableFactoryException e) { |
| listeners.warning(Level.FINE, null, e); |
| disableEPSG = true; |
| } |
| writeShort(key, epsg); |
| return (epsg == GeoCodes.userDefined); |
| } |
| |
| /** |
| * Returns an optional EPSG code as a short code that can be stored in a GeoTIFF key. |
| * |
| * @param epsg the optional EPSG code. |
| * @return the code as a short integer, or {@link GeoCodes#userDefined} if none. |
| * |
| * @see #toShort(int) |
| */ |
| private static short toShortEPSG(final Integer epsg) { |
| if (epsg != null) { |
| final int c = epsg; |
| if (c >= 1024 && c <= 32766) { // This range is defined by the GeoTIFF specification. |
| return (short) c; |
| } |
| } |
| return GeoCodes.userDefined; |
| } |
| |
| /** |
| * Appends an entry for a 16 bits integer value. This method uses a TIFF tag location of 0, |
| * which implies that value is {@code SHORT}, and is contained in the "ValueOffset" entry |
| * |
| * @param key the numeric identifier of the GeoTIFF key. |
| * @param value the value to store. |
| */ |
| private void writeShort(final short key, final short value) { |
| int i = ++keyCount * GeoCodes.ENTRY_LENGTH; |
| keyDirectory[i++] = key; // Key identifier. |
| keyDirectory[++i] = 1; // Number of values in this key. |
| keyDirectory[++i] = value; // Value offset. In this particular case, contains directly the value. |
| } |
| |
| /** |
| * Appends an entry for a floating point value. |
| * |
| * @param key the numeric identifier of the Key. |
| * @param value the value to store. |
| */ |
| private void writeDouble(final short key, final double value) { |
| int i = ++keyCount * GeoCodes.ENTRY_LENGTH; |
| keyDirectory[i++] = key; // Key identifier. |
| keyDirectory[i++] = (short) TAG_GEO_DOUBLE_PARAMS; // TIFF tag location. |
| keyDirectory[i++] = 1; // Number of values in this key. |
| keyDirectory[i ] = toShort(doubleCount); |
| doubleParams[doubleCount++] = value; |
| } |
| |
| /** |
| * Appends an entry for a character string. |
| * |
| * @param key the numeric identifier of the GeoTIFF key. |
| * @param value the value to store. |
| */ |
| private void writeString(final short key, final String value) { |
| int i = ++keyCount * GeoCodes.ENTRY_LENGTH; |
| keyDirectory[i++] = key; // Key identifier. |
| keyDirectory[i++] = (short) TAG_GEO_ASCII_PARAMS; // TIFF tag location. |
| keyDirectory[i++] = toShort(value.length()); // Number of values in this key. |
| keyDirectory[i ] = toShort(asciiParams.length()); // Offset of the first character. |
| asciiParams.append(value).append(GeoCodes.STRING_SEPARATOR); |
| } |
| |
| /** |
| * Ensures that the given value can be represented as an unsigned 16 bits integer. |
| * |
| * @param value the value to cast to an unsigned short. |
| * @return the value as an unsigned short. |
| * @throws ArithmeticException if the given value cannot be stored as an unsigned 16 bits integer. |
| * |
| * @see #toShortEPSG(Integer) |
| */ |
| private static short toShort(final int value) { |
| if ((value & ~0xFFFF) == 0) { |
| return (short) value; |
| } |
| throw new ArithmeticException(Errors.format(Errors.Keys.IntegerOverflow_1, Short.SIZE)); |
| } |
| |
| /** |
| * {@return the values to write in the "GeoTIFF keys directory" tag}. |
| */ |
| public short[] keyDirectory() { |
| if (keyCount == 0) return null; |
| keyDirectory[GeoCodes.ENTRY_LENGTH - 1] = (short) keyCount; |
| return ArraysExt.resize(keyDirectory, (keyCount + 1) * GeoCodes.ENTRY_LENGTH); |
| } |
| |
| /** |
| * {@return the values to write in the "GeoTIFF double-precision parameters" tag}. |
| */ |
| public double[] doubleParams() { |
| if (doubleCount == 0) return null; |
| return ArraysExt.resize(doubleParams, doubleCount); |
| } |
| |
| /** |
| * {@return the values to write in the "GeoTIFF ASCII strings" tag}. |
| */ |
| public List<String> asciiParams() { |
| return JDK15.isEmpty(asciiParams) ? null : List.of(asciiParams.toString()); |
| } |
| |
| /** |
| * Returns the coefficients of the affine transform, or {@code null} if none. |
| * Array length is fixed to 16 elements, for a 4×4 matrix in row-major order. |
| * Axis order is fixed to (longitude, latitude, height). |
| */ |
| public double[] modelTransformation() { |
| if (gridToCRS == null) { |
| return null; |
| } |
| /* |
| * The CRS stored in GeoTIFF files have axis directions fixed to (east, north, up). |
| * If the CRS of the grid geometry has different axis order, we need to adjust the |
| * "grid to CRS" transform. |
| */ |
| if (fullCRS != null) { |
| final AxisDirection[] source = CoordinateSystems.getAxisDirections(fullCRS.getCoordinateSystem()); |
| final AxisDirection[] target = new AxisDirection[hasVerticalAxis ? 3 : 2]; |
| target[0] = AxisDirection.EAST; |
| target[1] = AxisDirection.NORTH; |
| if (hasVerticalAxis) { |
| target[2] = AxisDirection.UP; |
| } |
| gridToCRS = Matrices.createTransform(source, target).multiply(gridToCRS); |
| fullCRS = null; // For avoiding to do the multiplication again. |
| } |
| /* |
| * Copy matrix coefficients. This matrix size is always 4×4, no matter the size of the `gridToCRS` matrix. |
| * So we cannot invoke `MatrixSIS.getElements()`. |
| */ |
| final double[] cf = new double[MATRIX_SIZE * MATRIX_SIZE]; |
| final int lastRow = gridToCRS.getNumRow() - 1; |
| final int lastCol = gridToCRS.getNumCol() - 1; |
| final int maxRow = Math.min(lastRow, MATRIX_SIZE-1); |
| int offset = 0; |
| for (int row = 0; row < maxRow; row++) { |
| copyRow(gridToCRS, row, lastCol, cf, offset); |
| offset += MATRIX_SIZE; |
| } |
| copyRow(gridToCRS, lastRow, lastCol, cf, MATRIX_SIZE * (MATRIX_SIZE - 1)); |
| return cf; |
| } |
| |
| /** |
| * Copies a matrix row into the model transformation array. |
| * |
| * @param gridToCRS the source of model transformation coefficients. |
| * @param row row of the matrix to copy. |
| * @param lastCol value of {@code gridToCRS.getNumCol() - 1}. |
| * @param target where to write the coefficients. |
| * @param offset index of the first element to write in the destination array. |
| */ |
| private static void copyRow(final Matrix gridToCRS, final int row, final int lastCol, final double[] target, final int offset) { |
| target[offset + (MATRIX_SIZE - 1)] = gridToCRS.getElement(row, lastCol); |
| for (int i = Math.min(lastCol, MATRIX_SIZE-1); --i >= 0;) { |
| target[offset + i] = gridToCRS.getElement(row, i); |
| } |
| } |
| |
| /** |
| * Returns the name of the given object. Used for formatting error messages. |
| * |
| * @param object the object for which to get a name to insert in error message. |
| * @return the object name. |
| */ |
| private String name(final IdentifiedObject object) { |
| return IdentifiedObjects.getDisplayName(object, listeners.getLocale()); |
| } |
| |
| /** |
| * {@return the resources for error messages in the current locale}. |
| */ |
| private Errors errors() { |
| return Errors.forLocale(listeners.getLocale()); |
| } |
| |
| /** |
| * {@return the resources in the current locale}. |
| */ |
| private Resources resources() { |
| return Resources.forLocale(listeners.getLocale()); |
| } |
| |
| /** |
| * Logs a warning saying that no value is associated to the given key. |
| * |
| * @param key the GeoKey for which we found no value. |
| */ |
| private void missingValue(final short key) { |
| warning(resources().getString(Resources.Keys.MissingGeoValue_1, GeoKeys.name(key)), null); |
| } |
| |
| /** |
| * Logs a warning saying that the given object cannot be encoded becasuse of its type. |
| * |
| * @param object object that cannot be encoded. |
| */ |
| private void unsupportedType(final IdentifiedObject object) { |
| warning(resources().getString(Resources.Keys.CanNotEncodeObjectType_1, ReferencingUtilities.getInterface(object)), null); |
| } |
| |
| /** |
| * Logs a warning saying that an object of the given name cannot be encoded. |
| * |
| * @param type object type: 0 = operation method, 1 = parameter, 2 = unit of measurement. |
| * @param name name of the object that cannot be encoded. |
| * @param cause the reason why a warning occurred, or {@code null} if none. |
| */ |
| private void cannotEncode(final int type, final String name, final Exception cause) { |
| warning(resources().getString(Resources.Keys.CanNotEncodeNamedObject_2, type, name), cause); |
| } |
| |
| /** |
| * Reports a warning that occurred while analyzing the CRS. |
| * This warning may prevent readers to reconstruct the CRS correctly. |
| * |
| * @param message the warning message. |
| * @param cause the reason why a warning occurred, or {@code null} if none. |
| */ |
| private void warning(final String message, final Exception cause) { |
| listeners.warning(message, cause); |
| } |
| |
| /** |
| * Returns a string representation for debugging purpose. |
| * |
| * @return a string representation of this keys writer. |
| */ |
| @Override |
| public String toString() { |
| return Strings.toString(getClass(), "citation", citation); |
| } |
| } |