/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 1998-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.measure;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import javax.measure.converter.UnitConverter;
import javax.measure.unit.NonSI;
import javax.measure.unit.SI;
import javax.measure.unit.Unit;
import javax.measure.unit.UnitFormat;
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.TemporalDatum;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultTemporalCRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.resources.CRSUtilities;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
/**
* Formats a {@linkplain org.geotools.geometry.GeneralDirectPosition direct position}
* in an arbitrary {@linkplain CoordinateReferenceSystem coordinate reference system}.
* The format for each ordinate is infered from the coordinate system units using the
* following rules:
* <ul>
* <li>Ordinate values in {@linkplain NonSI#DEGREE_ANGLE degrees} are formated as angles
* using {@link AngleFormat}.</li>
* <li>Ordinate values in any unit compatible with {@linkplain SI#SECOND seconds}
* are formated as dates using {@link DateFormat}.</li>
* <li>All other values are formatted as numbers using {@link NumberFormat}.</li>
* </ul>
*
* <strong>Note:</strong> parsing is not yet implemented in this version.
*
* @since 2.0
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (PMO, IRD)
*/
public class CoordinateFormat extends Format {
/**
* Serial number for interoperability with different versions.
*/
private static final long serialVersionUID = 8235685097881260737L;
/**
* The output coordinate reference system. May be {@code null}.
*/
private CoordinateReferenceSystem crs;
/**
* The separator between each coordinate values to be formatted.
*/
private String separator;
/**
* The formats to use for formatting. This array's length must be equals
* to the {@linkplain #getCoordinateReferenceSystem coordinate system}'s
* dimension. This array is never {@code null}.
*/
private Format[] formats;
/**
* Formatter for units. Will be created only when first needed.
*/
private transient UnitFormat unitFormat;
/**
* The type for each value in the {@code formats} array.
* Types are: 0=number, 1=longitude, 2=latitude, 3=other angle,
* 4=date, 5=ellapsed time. This array is never {@code null}.
*/
private byte[] types;
/**
* Constants for the {@code types} array.
*/
private static final byte LONGITUDE=1, LATITUDE=2, ANGLE=3, DATE=4, TIME=5;
/**
* The time epochs. Non-null only if at least one ordinate is a date.
*/
private long[] epochs;
/**
* Conversions from temporal axis units to milliseconds.
* Non-null only if at least one ordinate is a date.
*/
private UnitConverter[] toMillis;
/**
* Dummy field position.
*/
private final FieldPosition dummy = new FieldPosition(0);
/**
* The locale for formatting coordinates and numbers.
*/
private final Locale locale;
/**
* Constructs a new coordinate format with default locale and a two-dimensional
* {@linkplain DefaultGeographicCRS#WGS84 geographic (WGS 1984)} coordinate reference system.
*/
public CoordinateFormat() {
this(Locale.getDefault());
}
/**
* Construct a new coordinate format for the specified locale and a two-dimensional
* {@linkplain DefaultGeographicCRS#WGS84 geographic (WGS 1984)} coordinate reference system.
*
* @param locale The locale for formatting coordinates and numbers.
*/
public CoordinateFormat(final Locale locale) {
this(locale, DefaultGeographicCRS.WGS84);
}
/**
* Constructs a new coordinate format for the specified locale and coordinate reference system.
*
* @param locale The locale for formatting coordinates and numbers.
* @param crs The output coordinate reference system.
*/
public CoordinateFormat(final Locale locale, final CoordinateReferenceSystem crs) {
this.locale = locale;
this.separator = " ";
setCoordinateReferenceSystem(crs);
}
/**
* Returns the coordinate reference system for points to be formatted.
*
* @return The output coordinate reference system.
*/
public CoordinateReferenceSystem getCoordinateReferenceSystem() {
return crs;
}
/**
* Set the coordinate reference system for points to be formatted. The number
* of dimensions must matched the dimension of points to be formatted.
*
* @param crs The new coordinate system.
*/
public void setCoordinateReferenceSystem(final CoordinateReferenceSystem crs) {
if (CRS.equalsIgnoreMetadata(this.crs, (this.crs = crs))) {
return;
}
Format numberFormat = null;
Format angleFormat = null;
Format dateFormat = null;
/*
* Reuses existing formats. It is necessary in order to avoid
* overwritting any setting done with 'setNumberPattern(...)'
* or 'setAnglePattern(...)'
*/
if (formats != null) {
for (int i=formats.length; --i>=0;) {
final Format format = formats[i];
if (format instanceof NumberFormat) {
numberFormat = format;
} else if (format instanceof AngleFormat) {
angleFormat = format;
} else if (format instanceof DateFormat) {
dateFormat = format;
}
}
}
/*
* If no CRS were specified, formats everything as numbers. Working with null CRS is
* sometime useful because null CRS are allowed in DirectPosition according ISO 19107.
*/
if (crs == null) {
if (numberFormat == null) {
numberFormat = NumberFormat.getNumberInstance(locale);
}
types = new byte[1];
formats = new Format[] {numberFormat};
return;
}
/*
* Creates a new array of 'Format' objects, one for each dimension.
* The format subclasses are infered from coordinate system axis.
*/
final CoordinateSystem cs = crs.getCoordinateSystem();
epochs = null;
toMillis = null;
formats = new Format[cs.getDimension()];
types = new byte[formats.length];
for (int i=0; i<formats.length; i++) {
final Unit<?> unit = cs.getAxis(i).getUnit();
/////////////////
//// Angle ////
/////////////////
if (NonSI.DEGREE_ANGLE.equals(unit)) {
if (angleFormat == null) {
angleFormat = new AngleFormat("DD°MM.m'", locale);
}
formats[i] = angleFormat;
final AxisDirection axis = cs.getAxis(i).getDirection().absolute();
if (AxisDirection.EAST.equals(axis)) {
types[i] = LONGITUDE;
} else if (AxisDirection.NORTH.equals(axis)) {
types[i] = LATITUDE;
} else {
types[i] = ANGLE;
}
continue;
}
////////////////
//// Date ////
////////////////
if (SI.SECOND.isCompatible(unit)) {
final Datum datum = CRSUtilities.getDatum(CRSUtilities.getSubCRS(crs, i, i+1));
if (datum instanceof TemporalDatum) {
if (toMillis == null) {
toMillis = new UnitConverter[formats.length];
epochs = new long[formats.length];
}
toMillis[i] = unit.getConverterTo(DefaultTemporalCRS.MILLISECOND);
epochs [i] = ((TemporalDatum) datum).getOrigin().getTime();
if (dateFormat == null) {
dateFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, locale);
}
formats[i] = dateFormat;
types [i] = DATE;
continue;
}
types[i] = TIME;
// Fallthrough: formatted as number for now.
// TODO: Provide ellapsed time formatting later.
}
//////////////////
//// Number ////
//////////////////
if (numberFormat == null) {
numberFormat = NumberFormat.getNumberInstance(locale);
}
formats[i] = numberFormat;
// types[i] default to 0.
}
}
/**
* Returns the separator between each coordinate (number, angle or date).
*
* @return The current coordinate separator.
*
* @since 2.2
*/
public String getSeparator() {
return separator;
}
/**
* Set the separator between each coordinate.
*
* @param separator The new coordinate separator.
*
* @since 2.2
*/
public void setSeparator(final String separator) {
this.separator = separator;
}
/**
* Set the pattern for numbers fields. If some ordinates are formatted as plain number
* (for example in {@linkplain org.geotools.referencing.cs.DefaultCartesianCS cartesian
* coordinate system}), then those numbers will be formatted using this pattern.
*
* @param pattern The number pattern as specified in {@link DecimalFormat}.
*/
public void setNumberPattern(final String pattern) {
Format lastFormat = null;
for (int i=0; i<formats.length; i++) {
final Format format = formats[i];
if (format!=lastFormat && (format instanceof DecimalFormat)) {
((DecimalFormat) format).applyPattern(pattern);
lastFormat = format;
}
}
}
/**
* Set the pattern for angles fields. If some ordinates are formatted as angle
* (for example in {@linkplain org.geotools.referencing.cs.DefaultEllipsoidalCS
* ellipsoidal coordinate system}), then those angles will be formatted using
* this pattern.
*
* @param pattern The angle pattern as specified in {@link AngleFormat}.
*/
public void setAnglePattern(final String pattern) {
Format lastFormat = null;
for (int i=0; i<formats.length; i++) {
final Format format = formats[i];
if (format!=lastFormat && (format instanceof AngleFormat)) {
((AngleFormat) format).applyPattern(pattern);
lastFormat = format;
}
}
}
/**
* Set the pattern for dates fields. If some ordinates are formatted as date (for example in
* {@linkplain org.geotools.referencing.cs.DefaultTimeCS time coordinate system}), then
* those dates will be formatted using this pattern.
*
* @param pattern The date pattern as specified in {@link SimpleDateFormat}.
*/
public void setDatePattern(final String pattern) {
Format lastFormat = null;
for (int i=0; i<formats.length; i++) {
final Format format = formats[i];
if (format!=lastFormat && (format instanceof SimpleDateFormat)) {
((SimpleDateFormat) format).applyPattern(pattern);
lastFormat = format;
}
}
}
/**
* Set the time zone for dates fields. If some ordinates are formatted as date (for example in
* {@linkplain org.geotools.referencing.cs.DefaultTimeCS time coordinate system}), then
* those dates will be formatted using the specified time zone.
*
* @param timezone The time zone for dates.
*/
public void setTimeZone(final TimeZone timezone) {
Format lastFormat = null;
for (int i=0; i<formats.length; i++) {
final Format format = formats[i];
if (format!=lastFormat && (format instanceof DateFormat)) {
((DateFormat) format).setTimeZone(timezone);
lastFormat = format;
}
}
}
/**
* Returns the format to use for formatting an ordinate at the given dimension.
* The dimension parameter range from 0 inclusive to the
* {@linkplain #getCoordinateReferenceSystem coordinate reference system}'s dimension,
* exclusive. This method returns a direct reference to the internal format; any change
* to the returned {@link Format} object will change the formatting for this
* {@code CoordinateFormat} object.
*
* @param dimension The dimension for the ordinate to format.
* @return The format for the given dimension.
* @throws IndexOutOfBoundsException if {@code dimension} is out of range.
*/
public Format getFormat(final int dimension) throws IndexOutOfBoundsException {
return formats[dimension];
}
/**
* Formats a direct position. The position's dimension must matches the
* {@linkplain #getCoordinateReferenceSystem coordinate reference system} dimension.
*
* @param point The position to format.
* @return The formatted position.
* @throws IllegalArgumentException if this {@code CoordinateFormat}
* cannot format the given object.
*/
public String format(final DirectPosition point) {
return format(point, new StringBuffer(), null).toString();
}
/**
* Formats a direct position and appends the resulting text to a given string buffer.
* The position's dimension must matches the {@linkplain #getCoordinateReferenceSystem
* coordinate reference system} dimension.
*
* @param point The position to format.
* @param toAppendTo Where the text is to be appended.
* @param position A {@code FieldPosition} identifying a field in the formatted text,
* or {@code null} if none.
* @return The string buffer passed in as {@code toAppendTo}, with formatted text appended.
* @throws IllegalArgumentException if this {@code CoordinateFormat}
* cannot format the given object.
*/
public StringBuffer format(final DirectPosition point,
final StringBuffer toAppendTo,
final FieldPosition position)
throws IllegalArgumentException
{
final int dimension = point.getDimension();
final CoordinateSystem cs;
if (crs != null) {
if (dimension != formats.length) {
throw new MismatchedDimensionException(Errors.format(
ErrorKeys.MISMATCHED_DIMENSION_$3, "point", dimension, formats.length));
}
cs = crs.getCoordinateSystem();
} else {
cs = null;
}
for (int i=0; i<dimension; i++) {
final double value = point.getOrdinate(i);
final int fi = Math.min(i, formats.length-1);
final Object object;
final byte type = types[fi];
switch (type) {
default: object=Double.valueOf(value); break;
case LONGITUDE: object=new Longitude (value); break;
case LATITUDE: object=new Latitude (value); break;
case ANGLE: object=new Angle (value); break;
case DATE: {
final CoordinateSystemAxis axis = cs.getAxis(i);
long offset = Math.round(toMillis[fi].convert(value));
if (AxisDirection.PAST.equals(axis.getDirection())) {
offset = -offset;
}
object = new Date(epochs[fi] + offset);
break;
}
}
if (i != 0) {
toAppendTo.append(separator);
}
formats[fi].format(object, toAppendTo, dummy);
/*
* If the formatted value is a number, append the units.
*/
if (type==0 && cs!=null) {
final Unit<?> unit = cs.getAxis(i).getUnit();
if (unit != null) {
if (unitFormat == null) {
unitFormat = UnitFormat.getInstance();
}
final String asText = unitFormat.format(unit);
if (asText.length() != 0) {
toAppendTo.append('\u00A0'); // No break space
toAppendTo.append(unit);
}
}
}
}
return toAppendTo;
}
/**
* Formats a direct position and appends the resulting text to a given string buffer.
* The position's dimension must matches the {@linkplain #getCoordinateReferenceSystem
* coordinate reference system} dimension.
*
* @param object The {@link DirectPosition} to format.
* @param toAppendTo Where the text is to be appended.
* @param position A {@code FieldPosition} identifying a field in the formatted text,
* or {@code null} if none.
* @return The string buffer passed in as {@code toAppendTo}, with formatted text appended.
* @throws NullPointerException if {@code toAppendTo} is null.
* @throws IllegalArgumentException if this {@code CoordinateFormat}
* cannot format the given object.
*/
public StringBuffer format(final Object object,
final StringBuffer toAppendTo,
final FieldPosition position)
throws IllegalArgumentException
{
if (object instanceof DirectPosition) {
return format((DirectPosition) object, toAppendTo, position);
} else {
throw new IllegalArgumentException(String.valueOf(object));
}
}
/**
* Not yet implemented.
*
* @param source The string to parse.
* @param position The position of the first character to parse.
*/
public DirectPosition parseObject(final String source, final ParsePosition position) {
throw new UnsupportedOperationException("DirectPosition parsing not yet implemented.");
}
}