/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.image.io.metadata;
import java.awt.image.RenderedImage;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import org.geotools.resources.UnmodifiableArrayList;
import org.opengis.coverage.SampleDimension;
import org.opengis.geometry.Envelope;
import org.opengis.metadata.spatial.PixelOrientation;
import org.opengis.parameter.ParameterValue;
import org.opengis.referencing.crs.CompoundCRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.datum.Datum;
import org.opengis.referencing.datum.Ellipsoid;
import org.opengis.referencing.datum.PrimeMeridian;
import org.opengis.referencing.operation.Projection;
/**
* Describes the structure of {@linkplain GeographicMetadata geographic metadata}.
* The following formatting rules apply:
* <p>
* <ul>
* <li>Numbers must be formatted as in the {@linkplain Locale#US US locale}, i.e.
* as {@link Integer#toString(int)} or {@link Double#toString(double)}.</li>
* <li>Dates must be formatted with the {@code "yyyy-MM-dd HH:mm:ss"}
* {@linkplain SimpleDateFormat pattern} in UTC {@linkplain TimeZone timezone}.</li>
* </ul>
* <p>
* This format tries to match approximatively the
* <a href="http://www.opengeospatial.org/standards/gmljp2">GML in JPEG 2000</a> standard.
* See the {@linkplain org.geotools.image.io.metadata package javadoc} for a list of departures
* from the standard.
*
* @since 2.4
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
* @author Cédric Briançon
*/
public class GeographicMetadataFormat extends IIOMetadataFormatImpl {
/**
* The metadata format name.
*/
public static final String FORMAT_NAME = "geotools_coverage_1.0";
/**
* The maximum number of dimension allowed for the image coordinate system. Images
* must be at least two-dimensional. Some plugins consider the set of bands as the
* third dimension (for example slices at different depths). An additional "1 pixel
* large" temporal dimension is sometime used for storing the image timestamp.
*/
private static final int MAXIMUM_DIMENSIONS = 4;
/**
* The maximum number of bands allowed. This is a somewhat arbitrary value since
* there is no reason (except memory or disk space constraints) to restrict the
* number of bands in the image stream. The number of bands actually read is
* usually much smaller.
*/
private static final int MAXIMUM_BANDS = Short.MAX_VALUE;
/**
* The maximum number of parameters for the projection.
*/
private static final int MAXIMUM_PARAMETERS = 10;
/**
* The geographic {@linkplain CoordinateReferenceSystem coordinate reference system}
* type. This is often used together with the {@linkplain #ELLIPSOIDAL ellipsoidal}
* coordinate system type.
*/
public static final String GEOGRAPHIC = "geographic";
/**
* The geographic {@linkplain CoordinateReferenceSystem coordinate reference system}
* type with a vertical axis. This is often used together with a three-dimensional
* {@linkplain #ELLIPSOIDAL ellipsoidal} coordinate system type.
* <p>
* If the coordinate reference system has no vertical axis, or has additional axis
* of other kind than vertical (for example only a temporal axis), then the type
* should be the plain {@value #GEOGRAPHIC}. This is because such CRS are usually
* constructed as {@linkplain CompoundCRS compound CRS} rather than a CRS with a
* three-dimensional coordinate system.
* <p>
* To be strict, a 3D CRS should be allowed only if the vertical axis is of the kind
* "height above the ellipsoid" (as opposed to "height above the geoid" for example),
* otherwise we have a compound CRS. But many datafile don't make this distinction.
*/
public static final String GEOGRAPHIC_3D = "geographic3D";
/**
* The projected {@linkplain CoordinateReferenceSystem coordinate reference system}
* type. This is often used together with the {@linkplain #CARTESIAN cartesian}
* coordinate system type.
*/
public static final String PROJECTED = "projected";
/**
* The projected {@linkplain CoordinateReferenceSystem coordinate reference system}
* type with a vertical axis. This is often used together with a three-dimensional
* {@linkplain #CARTESIAN cartesian} coordinate system type.
* <p>
* If the coordinate reference system has no vertical axis, or has additional axis
* of other kind than vertical (for example only a temporal axis), then the type
* should be the plain {@value #PROJECTED}. This is because such CRS are usually
* constructed as {@linkplain CompoundCRS compound CRS} rather than a CRS with a
* three-dimensional coordinate system.
* <p>
* To be strict, a 3D CRS should be allowed only if the vertical axis is of the kind
* "height above the ellipsoid" (as opposed to "height above the geoid" for example),
* otherwise we have a compound CRS. But many datafile don't make this distinction.
*/
public static final String PROJECTED_3D = "projected3D";
/**
* The ellipsoidal {@linkplain CoordinateSystem coordinate system} type.
*/
public static final String ELLIPSOIDAL = "ellipsoidal";
/**
* The cartesian {@linkplain CoordinateSystem coordinate system} type.
*/
public static final String CARTESIAN = "cartesian";
/**
* The geophysics {@linkplain SampleDimension sample dimension} type.
* Pixels in the {@linkplain RenderedImage rendered image} produced by the image
* reader contain directly geophysics values like temperature or elevation.
* Sample type is typically {@code float} or {@code double} and missing value, if
* any, <strong>must</strong> be one of {@linkplain Float#isNaN NaN values}.
*/
public static final String GEOPHYSICS = "geophysics";
/**
* The packed {@linkplain SampleDimension sample dimension} type.
* Pixels in the {@linkplain RenderedImage rendered image} produced by the image
* reader contain packed data, typically as {@code byte} or {@code short}
* integer type. Conversions to geophysics values are performed by the application
* of a scale and offset. Some special values are typically used for missing values.
*/
public static final String PACKED = "packed";
/**
* The engineering {@linkplain Datum datum} type.
*/
public static final String ENGINEERING = "engineering";
/**
* The geodetic {@linkplain Datum datum} type.
*/
public static final String GEODETIC = "geodetic";
/**
* The image {@linkplain Datum datum} type.
*/
public static final String IMAGE = "image";
/**
* The temporal {@linkplain Datum datum} type.
*/
public static final String TEMPORAL = "temporal";
/**
* The vertical {@linkplain Datum datum} type.
*/
public static final String VERTICAL = "vertical";
/**
* Enumeration of valid coordinate reference system types.
*/
static final List<String> CRS_TYPES = UnmodifiableArrayList.wrap(new String[] {
GEOGRAPHIC, PROJECTED
});
/**
* Enumeration of valid coordinate system types.
*/
static final List<String> CS_TYPES = UnmodifiableArrayList.wrap(new String[] {
ELLIPSOIDAL, CARTESIAN
});
/**
* Enumeration of valid datum types.
*/
static final List<String> DATUM_TYPES = UnmodifiableArrayList.wrap(new String[] {
ENGINEERING, GEODETIC, IMAGE, TEMPORAL, VERTICAL
});
/**
* Enumeration of valid axis directions. We do not declare {@link String} constants
* for them since they are already available as {@linkplain AxisDirection
* axis direction} code list.
*/
static final List<String> DIRECTIONS = UnmodifiableArrayList.wrap(new String[] {
"north", "east", "south", "west", "up", "down"
});
/**
* Enumeration of valid pixel orientation. We do not declare {@link String} constants
* for them since they are already available as {@linkplain PixelOrientation pixel orientation} code list.
*/
static final List<String> PIXEL_ORIENTATIONS = UnmodifiableArrayList.wrap(new String[] {
"center", "lower left", "lower right", "upper right", "upper left"
});
/**
* Enumeration of valid sample dimention types.
*/
static final List<String> SAMPLE_TYPES = UnmodifiableArrayList.wrap(new String[] {
GEOPHYSICS, PACKED
});
/**
* The default instance. Will be created only when first needed.
*
* @see #getInstance
*/
private static GeographicMetadataFormat DEFAULT;
/**
* Creates a default metadata format.
*/
private GeographicMetadataFormat() {
this(FORMAT_NAME, MAXIMUM_DIMENSIONS, MAXIMUM_BANDS);
}
/**
* Creates a metadata format of the given name. Subclasses should invoke the various
* {@link #addElement(String,String,int) addElement} or {@link #addAttribute
* addAttribute} methods for adding new elements compared to the {@linkplain
* #getInstance default instance}.
*
* @param rootName the name of the root element.
* @param maximumDimensions The maximum number of dimensions allowed for coordinate
* systems.
* @param maximumBands The maximum number of sample dimensions allowed for images.
*/
protected GeographicMetadataFormat(final String rootName,
final int maximumDimensions, final int maximumBands)
{
/*
* The schemas illustrated in the following comments use the this syntax:
*
* +-- element (attribute) : ClassName
*
* Legend: (*) Mandatory element or attribute.
* (!) Element or attribute that is not from the OGC specification.
*
* In this tree, the index {@code n} is the number of dimensions.
*/
super(rootName, CHILD_POLICY_SOME);
/*
* root
* +-- boundedBy : Envelope
* +-- lowerCorner (*)
* +-- upperCorner (*)
*/
addElement ("boundedBy", rootName, CHILD_POLICY_ALL);
addElement ("lowerCorner", "boundedBy", CHILD_POLICY_EMPTY);
addElement ("upperCorner", "boundedBy", CHILD_POLICY_EMPTY);
addObjectValue("lowerCorner", Double.TYPE, 1, MAXIMUM_DIMENSIONS);
addObjectValue("upperCorner", Double.TYPE, 1, MAXIMUM_DIMENSIONS);
/*
* root : RectifiedGridCoverage
* +-- rectifiedGridDomain (dimension, srsName) : RectifiedGrid (*)
* +-- crs (name, type) : CoordinateReferenceSystem (!)
* | +-- datum (name, type) : Datum (!)
* | | +-- ellipsoid (name, unit) : Ellipsoid (!)
* | | | +-- semiMajorAxis : double (!)
* | | | +-- secondDefiningParameter (!)
* | | | +-- semiMinorAxis : double (!)
* | | | +-- inverseFlattening : double (!)
* | | +-- primeMeridian (name, greenwichLongitude) : PrimeMeridian (!)
* | +-- cs (name, type) : CoordinateSystem (!)
* | | +-- axis[0] (name, direction, units, origin) : Axis (!)
* | | +-- ... (!)
* | | +-- axis[n-1] (name, direction, units, origin) : Axis (!)
* | +-- projection (name) : Projection (!)
* | +-- parameter[0] (name, value) : ParameterValue (!)
* | +-- parameter[1] (name, value) : ParameterValue (!)
* | +-- ... (!)
* +-- limits : GridEnvelope (*)
* | +-- low : int[] (*)
* | +-- high : int[] (*)
* +-- origin : Point (*)
* | +-- coordinates (*)
* +-- cells (!)
* | +-- offsetVector[0] (!)
* | +-- ... (!)
* | +-- offsetVector[n-1] (!)
* +-- localizationGrid (!)
* | +-- ordinates[0] : double[] (!)
* | +-- ... (!)
* | +-- ordinates[n-1] : double[] (!)
* +-- pixelOrientation (!)
* +-- rangeSet : File (*)
* +-- rangeParameters
* | +-- TBD
* +-- fileName (*)
* +-- fileStructure (*)
* +-- fileDate
* +-- fileFormat
* +-- spatialResolution (uom)
* +-- spectrum
* +-- bandRange (uom)
* +-- bands (type) (!)
* | +-- band[0] (name, scale, offset, minValue, maxValue, fillValues) : Band
* | +-- band[1] (name, scale, offset, minValue, maxValue, fillValues) : Band
* | +-- ...
* +-- mimeType
* +-- compression
*/
addElement ("rectifiedGridDomain", rootName, CHILD_POLICY_SOME);
addAttribute ("rectifiedGridDomain", "dimension", DATATYPE_INTEGER, true, null);
addAttribute ("rectifiedGridDomain", "srsName", DATATYPE_STRING);
addElement ("crs", "rectifiedGridDomain", CHILD_POLICY_SOME);
addAttribute ("crs", "name", DATATYPE_STRING);
addAttribute ("crs", "type", DATATYPE_STRING, false, null, CRS_TYPES);
addElement ("datum", "crs", CHILD_POLICY_SOME);
addAttribute ("datum", "name", DATATYPE_STRING);
addAttribute ("datum", "type", DATATYPE_STRING, false, null, DATUM_TYPES);
addElement ("ellipsoid", "datum", CHILD_POLICY_ALL);
addAttribute ("ellipsoid", "name", DATATYPE_STRING);
addAttribute ("ellipsoid", "unit", DATATYPE_STRING);
addElement ("primeMeridian", "datum", CHILD_POLICY_EMPTY);
addAttribute ("primeMeridian", "name", DATATYPE_STRING);
addAttribute ("primeMeridian", "greenwichLongitude", DATATYPE_DOUBLE);
addElement ("semiMajorAxis", "ellipsoid", CHILD_POLICY_EMPTY);
addObjectValue("semiMajorAxis", Double.class);
addElement ("secondDefiningParameter", "ellipsoid", CHILD_POLICY_CHOICE);
addElement ("semiMinorAxis", "secondDefiningParameter", CHILD_POLICY_EMPTY);
addObjectValue("semiMinorAxis", Double.class);
addElement ("inverseFlattening", "secondDefiningParameter", CHILD_POLICY_EMPTY);
addObjectValue("inverseFlattening", Double.class);
addElement ("cs", "crs", 2, maximumDimensions);
addAttribute ("cs", "name", DATATYPE_STRING);
addAttribute ("cs", "type", DATATYPE_STRING, false, null, CS_TYPES);
addElement ("axis", "cs", CHILD_POLICY_EMPTY);
addAttribute ("axis", "name", DATATYPE_STRING);
addAttribute ("axis", "direction", DATATYPE_STRING, true, null, DIRECTIONS);
addAttribute ("axis", "units", DATATYPE_STRING);
addAttribute ("axis", "origin", DATATYPE_STRING);
addElement ("projection", "crs", 0, MAXIMUM_PARAMETERS);
addAttribute ("projection", "name", DATATYPE_STRING);
addElement ("parameter", "projection", CHILD_POLICY_EMPTY);
addAttribute ("parameter", "name", DATATYPE_STRING);
addAttribute ("parameter", "value", DATATYPE_DOUBLE);
addElement ("limits", "rectifiedGridDomain", CHILD_POLICY_ALL);
addElement ("low", "limits", CHILD_POLICY_EMPTY);
addElement ("high", "limits", CHILD_POLICY_EMPTY);
addObjectValue("low", Integer.class);
addObjectValue("high", Integer.class);
addElement ("origin", "rectifiedGridDomain", CHILD_POLICY_ALL);
addElement ("coordinates", "origin", CHILD_POLICY_EMPTY);
addElement ("cells", "rectifiedGridDomain", 1, MAXIMUM_DIMENSIONS);
addElement ("offsetVector", "cells", CHILD_POLICY_EMPTY);
addElement ("localizationGrid", "rectifiedGridDomain", 1, MAXIMUM_DIMENSIONS);
addElement ("ordinates", "localizationGrid", CHILD_POLICY_EMPTY);
addElement ("pixelOrientation", "rectifiedGridDomain", CHILD_POLICY_EMPTY);
addObjectValue("pixelOrientation", String.class, 1, PIXEL_ORIENTATIONS.size());
addElement ("rangeSet", "rectifiedGridDomain", CHILD_POLICY_SOME );
addElement ("rangeParameters", "rangeSet", CHILD_POLICY_EMPTY);
// todo: handle rangeParameters' children; for the moment it is considered
//as a leaf of the tree.
addElement ("fileName", "rangeSet", CHILD_POLICY_EMPTY);
addElement ("fileStructure", "rangeSet", CHILD_POLICY_EMPTY);
addElement ("fileDate", "rangeSet", CHILD_POLICY_EMPTY);
addElement ("fileFormat", "rangeSet", CHILD_POLICY_EMPTY);
addElement ("spatialResolution", "rangeSet", CHILD_POLICY_EMPTY);
addAttribute ("spatialResolution", "uom", DATATYPE_STRING);
addElement ("spectrum", "rangeSet", CHILD_POLICY_EMPTY);
addElement ("bandRange", "rangeSet", 0, MAXIMUM_BANDS);
addAttribute ("bandRange", "uom", DATATYPE_STRING);
addElement ("bands", "rangeSet", 0, MAXIMUM_BANDS);
addAttribute ("bands", "type", DATATYPE_STRING, false, null, SAMPLE_TYPES);
addElement ("band", "bands", CHILD_POLICY_EMPTY);
addElement ("mimeType", "rangeSet", CHILD_POLICY_EMPTY);
addElement ("compression", "rangeSet", CHILD_POLICY_EMPTY);
/*
* root : RectifiedGridCoverage
* +-- rectifiedGridDomain (dimension, srsName) : RectifiedGrid (*)
* +-- rangeSet : File (*)
* +-- bands (!)
* +-- band[0] (name, scale, offset, minValue, maxValue, fillValues)
* +-- band[1] (name, scale, offset, minValue, maxValue, fillValues)
* +-- ...
*/
addAttribute("band", "name", DATATYPE_STRING);
addAttribute("band", "scale", DATATYPE_DOUBLE);
addAttribute("band", "offset", DATATYPE_DOUBLE);
addAttribute("band", "minValue", DATATYPE_DOUBLE);
addAttribute("band", "maxValue", DATATYPE_DOUBLE);
addAttribute("band", "fillValues", DATATYPE_DOUBLE, false, 0, Short.MAX_VALUE);
/*
* Allow users to specify fully-constructed GeoAPI objects.
*/
addObjectValue("crs", CoordinateReferenceSystem.class);
addObjectValue("datum", Datum.class);
addObjectValue("ellipsoid", Ellipsoid.class);
addObjectValue("cs", CoordinateSystem.class);
addObjectValue("axis", CoordinateSystemAxis.class);
addObjectValue("projection", Projection.class);
addObjectValue("parameter", ParameterValue.class);
addObjectValue("boundedBy", Envelope.class);
addObjectValue("primeMeridian", PrimeMeridian.class);
}
/**
* Adds an optional attribute of the specified data type.
*/
private void addAttribute(final String elementName, final String attrName,
final int dataType)
{
addAttribute(elementName, attrName, dataType, false, null);
}
/**
* Adds an optional object value of the specified class.
*/
private void addObjectValue(final String elementName, final Class<?> classType) {
addObjectValue(elementName, classType, false, null);
}
/**
* Returns {@code true} if the element (and the subtree below it) is allowed to appear
* in a metadata document for an image of the given type. The default implementation
* always returns {@code true}.
*/
public boolean canNodeAppear(final String elementName,
final ImageTypeSpecifier imageType)
{
return true;
}
/**
* Returns the default geographic metadata format instance.
*/
public static synchronized GeographicMetadataFormat getInstance() {
if (DEFAULT == null) {
DEFAULT = new GeographicMetadataFormat();
}
return DEFAULT;
}
}