001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, GeoTools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
006: * (C) 1998, Pêches et Océans Canada
007: *
008: * This library is free software; you can redistribute it and/or
009: * modify it under the terms of the GNU Lesser General Public
010: * License as published by the Free Software Foundation;
011: * version 2.1 of the License.
012: *
013: * This library is distributed in the hope that it will be useful,
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
016: * Lesser General Public License for more details.
017: */
018: package org.geotools.measure;
019:
020: // J2SE dependencies
021: import java.text.DateFormat;
022: import java.text.DecimalFormat;
023: import java.text.FieldPosition;
024: import java.text.Format;
025: import java.text.NumberFormat;
026: import java.text.ParsePosition;
027: import java.text.SimpleDateFormat;
028: import java.util.Date;
029: import java.util.Locale;
030: import java.util.TimeZone;
031:
032: // Units dependencies
033: import javax.units.Converter;
034: import javax.units.NonSI;
035: import javax.units.SI;
036: import javax.units.Unit;
037: import javax.units.UnitFormat;
038:
039: // OpenGIS dependencies
040: import org.opengis.referencing.crs.CoordinateReferenceSystem;
041: import org.opengis.referencing.cs.AxisDirection;
042: import org.opengis.referencing.cs.CoordinateSystem;
043: import org.opengis.referencing.cs.CoordinateSystemAxis;
044: import org.opengis.referencing.datum.Datum;
045: import org.opengis.referencing.datum.TemporalDatum;
046: import org.opengis.geometry.DirectPosition;
047: import org.opengis.geometry.MismatchedDimensionException;
048:
049: // Geotools dependencies
050: import org.geotools.referencing.CRS;
051: import org.geotools.referencing.crs.DefaultTemporalCRS;
052: import org.geotools.referencing.crs.DefaultGeographicCRS;
053: import org.geotools.resources.CRSUtilities;
054: import org.geotools.resources.i18n.ErrorKeys;
055: import org.geotools.resources.i18n.Errors;
056:
057: /**
058: * Formats a {@linkplain org.geotools.geometry.GeneralDirectPosition direct position}
059: * in an arbitrary {@linkplain CoordinateReferenceSystem coordinate reference system}.
060: * The format for each ordinate is infered from the coordinate system units using the
061: * following rules:
062: * <ul>
063: * <li>Ordinate values in {@linkplain NonSI#DEGREE_ANGLE degrees} are formated as angles
064: * using {@link AngleFormat}.</li>
065: * <li>Ordinate values in any unit compatible with {@linkplain SI#SECOND seconds}
066: * are formated as dates using {@link DateFormat}.</li>
067: * <li>All other values are formatted as numbers using {@link NumberFormat}.</li>
068: * </ul>
069: *
070: * <strong>Note:</strong> parsing is not yet implemented in this version.
071: *
072: * @since 2.0
073: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/measure/CoordinateFormat.java $
074: * @version $Id: CoordinateFormat.java 24925 2007-03-27 20:12:08Z jgarnett $
075: * @author Martin Desruisseaux
076: */
077: public class CoordinateFormat extends Format {
078: /**
079: * Serial number for interoperability with different versions.
080: */
081: private static final long serialVersionUID = 8235685097881260737L;
082:
083: /**
084: * The output coordinate reference system. May be {@code null}.
085: */
086: private CoordinateReferenceSystem crs;
087:
088: /**
089: * The separator between each coordinate values to be formatted.
090: */
091: private String separator;
092:
093: /**
094: * The formats to use for formatting. This array's length must be equals
095: * to the {@linkplain #getCoordinateReferenceSystem coordinate system}'s
096: * dimension. This array is never {@code null}.
097: */
098: private Format[] formats;
099:
100: /**
101: * Formatter for units. Will be created only when first needed.
102: */
103: private transient UnitFormat unitFormat;
104:
105: /**
106: * The type for each value in the {@code formats} array.
107: * Types are: 0=number, 1=longitude, 2=latitude, 3=other angle,
108: * 4=date, 5=ellapsed time. This array is never {@code null}.
109: */
110: private byte[] types;
111:
112: /**
113: * Constants for the {@code types} array.
114: */
115: private static final byte LONGITUDE = 1, LATITUDE = 2, ANGLE = 3,
116: DATE = 4, TIME = 5;
117:
118: /**
119: * The time epochs. Non-null only if at least one ordinate is a date.
120: */
121: private long[] epochs;
122:
123: /**
124: * Conversions from temporal axis units to milliseconds.
125: * Non-null only if at least one ordinate is a date.
126: */
127: private Converter[] toMillis;
128:
129: /**
130: * Dummy field position.
131: */
132: private final FieldPosition dummy = new FieldPosition(0);
133:
134: /**
135: * The locale for formatting coordinates and numbers.
136: */
137: private final Locale locale;
138:
139: /**
140: * Constructs a new coordinate format with default locale and a two-dimensional
141: * {@linkplain DefaultGeographicCRS#WGS84 geographic (WGS 1984)} coordinate reference system.
142: */
143: public CoordinateFormat() {
144: this (Locale.getDefault());
145: }
146:
147: /**
148: * Construct a new coordinate format for the specified locale and a two-dimensional
149: * {@linkplain DefaultGeographicCRS#WGS84 geographic (WGS 1984)} coordinate reference system.
150: *
151: * @param locale The locale for formatting coordinates and numbers.
152: */
153: public CoordinateFormat(final Locale locale) {
154: this (locale, DefaultGeographicCRS.WGS84);
155: }
156:
157: /**
158: * Constructs a new coordinate format for the specified locale and coordinate reference system.
159: *
160: * @param locale The locale for formatting coordinates and numbers.
161: * @param crs The output coordinate reference system.
162: */
163: public CoordinateFormat(final Locale locale,
164: final CoordinateReferenceSystem crs) {
165: this .locale = locale;
166: this .separator = " ";
167: setCoordinateReferenceSystem(crs);
168: }
169:
170: /**
171: * Returns the coordinate reference system for points to be formatted.
172: *
173: * @return The output coordinate reference system.
174: */
175: public CoordinateReferenceSystem getCoordinateReferenceSystem() {
176: return crs;
177: }
178:
179: /**
180: * Set the coordinate reference system for points to be formatted. The number
181: * of dimensions must matched the dimension of points to be formatted.
182: *
183: * @param crs The new coordinate system.
184: */
185: public void setCoordinateReferenceSystem(
186: final CoordinateReferenceSystem crs) {
187: if (CRS.equalsIgnoreMetadata(this .crs, (this .crs = crs))) {
188: return;
189: }
190: Format numberFormat = null;
191: Format angleFormat = null;
192: Format dateFormat = null;
193: /*
194: * Reuses existing formats. It is necessary in order to avoid
195: * overwritting any setting done with 'setNumberPattern(...)'
196: * or 'setAnglePattern(...)'
197: */
198: if (formats != null) {
199: for (int i = formats.length; --i >= 0;) {
200: final Format format = formats[i];
201: if (format instanceof NumberFormat) {
202: numberFormat = format;
203: } else if (format instanceof AngleFormat) {
204: angleFormat = format;
205: } else if (format instanceof DateFormat) {
206: dateFormat = format;
207: }
208: }
209: }
210: /*
211: * If no CRS were specified, formats everything as numbers. Working with null CRS is
212: * sometime useful because null CRS are allowed in DirectPosition according ISO 19107.
213: */
214: if (crs == null) {
215: if (numberFormat == null) {
216: numberFormat = NumberFormat.getNumberInstance(locale);
217: }
218: types = new byte[1];
219: formats = new Format[] { numberFormat };
220: return;
221: }
222: /*
223: * Creates a new array of 'Format' objects, one for each dimension.
224: * The format subclasses are infered from coordinate system axis.
225: */
226: final CoordinateSystem cs = crs.getCoordinateSystem();
227: epochs = null;
228: toMillis = null;
229: formats = new Format[cs.getDimension()];
230: types = new byte[formats.length];
231: for (int i = 0; i < formats.length; i++) {
232: final Unit unit = cs.getAxis(i).getUnit();
233: /////////////////
234: //// Angle ////
235: /////////////////
236: if (NonSI.DEGREE_ANGLE.equals(unit)) {
237: if (angleFormat == null) {
238: angleFormat = new AngleFormat("DD°MM.m'", locale);
239: }
240: formats[i] = angleFormat;
241: final AxisDirection axis = cs.getAxis(i).getDirection()
242: .absolute();
243: if (AxisDirection.EAST.equals(axis)) {
244: types[i] = LONGITUDE;
245: } else if (AxisDirection.NORTH.equals(axis)) {
246: types[i] = LATITUDE;
247: } else {
248: types[i] = ANGLE;
249: }
250: continue;
251: }
252: ////////////////
253: //// Date ////
254: ////////////////
255: if (SI.SECOND.isCompatible(unit)) {
256: final Datum datum = CRSUtilities.getDatum(CRSUtilities
257: .getSubCRS(crs, i, i + 1));
258: if (datum instanceof TemporalDatum) {
259: if (toMillis == null) {
260: toMillis = new Converter[formats.length];
261: epochs = new long[formats.length];
262: }
263: toMillis[i] = unit
264: .getConverterTo(DefaultTemporalCRS.MILLISECOND);
265: epochs[i] = ((TemporalDatum) datum).getOrigin()
266: .getTime();
267: if (dateFormat == null) {
268: dateFormat = DateFormat.getDateInstance(
269: DateFormat.DEFAULT, locale);
270: }
271: formats[i] = dateFormat;
272: types[i] = DATE;
273: continue;
274: }
275: types[i] = TIME;
276: // Fallthrough: formatted as number for now.
277: // TODO: Provide ellapsed time formatting later.
278: }
279: //////////////////
280: //// Number ////
281: //////////////////
282: if (numberFormat == null) {
283: numberFormat = NumberFormat.getNumberInstance(locale);
284: }
285: formats[i] = numberFormat;
286: // types[i] default to 0.
287: }
288: }
289:
290: /**
291: * Returns the separator between each coordinate (number, angle or date).
292: *
293: * @since 2.2
294: */
295: public String getSeparator() {
296: return separator;
297: }
298:
299: /**
300: * Set the separator between each coordinate.
301: *
302: * @since 2.2
303: */
304: public void setSeparator(final String separator) {
305: this .separator = separator;
306: }
307:
308: /**
309: * Set the pattern for numbers fields. If some ordinates are formatted as plain number
310: * (for example in {@linkplain org.geotools.referencing.cs.DefaultCartesianCS cartesian
311: * coordinate system}), then those numbers will be formatted using this pattern.
312: *
313: * @param pattern The number pattern as specified in {@link DecimalFormat}.
314: */
315: public void setNumberPattern(final String pattern) {
316: Format lastFormat = null;
317: for (int i = 0; i < formats.length; i++) {
318: final Format format = formats[i];
319: if (format != lastFormat
320: && (format instanceof DecimalFormat)) {
321: ((DecimalFormat) format).applyPattern(pattern);
322: lastFormat = format;
323: }
324: }
325: }
326:
327: /**
328: * Set the pattern for angles fields. If some ordinates are formatted as angle
329: * (for example in {@linkplain org.geotools.referencing.cs.DefaultEllipsoidalCS
330: * ellipsoidal coordinate system}), then those angles will be formatted using
331: * this pattern.
332: *
333: * @param pattern The angle pattern as specified in {@link AngleFormat}.
334: */
335: public void setAnglePattern(final String pattern) {
336: Format lastFormat = null;
337: for (int i = 0; i < formats.length; i++) {
338: final Format format = formats[i];
339: if (format != lastFormat && (format instanceof AngleFormat)) {
340: ((AngleFormat) format).applyPattern(pattern);
341: lastFormat = format;
342: }
343: }
344: }
345:
346: /**
347: * Set the pattern for dates fields. If some ordinates are formatted as date (for example in
348: * {@linkplain org.geotools.referencing.cs.DefaultTimeCS time coordinate system}), then
349: * those dates will be formatted using this pattern.
350: *
351: * @param pattern The date pattern as specified in {@link SimpleDateFormat}.
352: */
353: public void setDatePattern(final String pattern) {
354: Format lastFormat = null;
355: for (int i = 0; i < formats.length; i++) {
356: final Format format = formats[i];
357: if (format != lastFormat
358: && (format instanceof SimpleDateFormat)) {
359: ((SimpleDateFormat) format).applyPattern(pattern);
360: lastFormat = format;
361: }
362: }
363: }
364:
365: /**
366: * Set the time zone for dates fields. If some ordinates are formatted as date (for example in
367: * {@linkplain org.geotools.referencing.cs.DefaultTimeCS time coordinate system}), then
368: * those dates will be formatted using the specified time zone.
369: *
370: * @param timezone The time zone for dates.
371: */
372: public void setTimeZone(final TimeZone timezone) {
373: Format lastFormat = null;
374: for (int i = 0; i < formats.length; i++) {
375: final Format format = formats[i];
376: if (format != lastFormat && (format instanceof DateFormat)) {
377: ((DateFormat) format).setTimeZone(timezone);
378: lastFormat = format;
379: }
380: }
381: }
382:
383: /**
384: * Returns the format to use for formatting an ordinate at the given dimension.
385: * The dimension parameter range from 0 inclusive to the
386: * {@linkplain #getCoordinateReferenceSystem coordinate reference system}'s dimension,
387: * exclusive. This method returns a direct reference to the internal format; any change
388: * to the returned {@link Format} object will change the formatting for this
389: * {@code CoordinateFormat} object.
390: *
391: * @param dimension The dimension for the ordinate to format.
392: * @return The format for the given dimension.
393: * @throws IndexOutOfBoundsException if {@code dimension} is out of range.
394: */
395: public Format getFormat(final int dimension)
396: throws IndexOutOfBoundsException {
397: return formats[dimension];
398: }
399:
400: /**
401: * Formats a direct position. The position's dimension must matches the
402: * {@linkplain #getCoordinateReferenceSystem coordinate reference system} dimension.
403: *
404: * @param point The position to format.
405: * @return The formatted position.
406: * @throws IllegalArgumentException if this {@code CoordinateFormat}
407: * cannot format the given object.
408: */
409: public String format(final DirectPosition point) {
410: return format(point, new StringBuffer(), null).toString();
411: }
412:
413: /**
414: * Formats a direct position and appends the resulting text to a given string buffer.
415: * The position's dimension must matches the {@linkplain #getCoordinateReferenceSystem
416: * coordinate reference system} dimension.
417: *
418: * @param point The position to format.
419: * @param toAppendTo Where the text is to be appended.
420: * @param position A {@code FieldPosition} identifying a field in the formatted text,
421: * or {@code null} if none.
422: * @return The string buffer passed in as {@code toAppendTo}, with formatted text appended.
423: * @throws IllegalArgumentException if this {@code CoordinateFormat}
424: * cannot format the given object.
425: */
426: public StringBuffer format(final DirectPosition point,
427: final StringBuffer toAppendTo, final FieldPosition position)
428: throws IllegalArgumentException {
429: final int dimension = point.getDimension();
430: final CoordinateSystem cs;
431: if (crs != null) {
432: if (dimension != formats.length) {
433: throw new MismatchedDimensionException(Errors.format(
434: ErrorKeys.MISMATCHED_DIMENSION_$3, "point",
435: new Integer(dimension), new Integer(
436: formats.length)));
437: }
438: cs = crs.getCoordinateSystem();
439: } else {
440: cs = null;
441: }
442: for (int i = 0; i < dimension; i++) {
443: final double value = point.getOrdinate(i);
444: final int fi = Math.min(i, formats.length - 1);
445: final Object object;
446: final byte type = types[fi];
447: switch (type) {
448: default:
449: object = new Double(value);
450: break;
451: case LONGITUDE:
452: object = new Longitude(value);
453: break;
454: case LATITUDE:
455: object = new Latitude(value);
456: break;
457: case ANGLE:
458: object = new Angle(value);
459: break;
460: case DATE: {
461: final CoordinateSystemAxis axis = cs.getAxis(i);
462: long offset = Math.round(toMillis[fi].convert(value));
463: if (AxisDirection.PAST.equals(axis.getDirection())) {
464: offset = -offset;
465: }
466: object = new Date(epochs[fi] + offset);
467: break;
468: }
469: }
470: if (i != 0) {
471: toAppendTo.append(separator);
472: }
473: formats[fi].format(object, toAppendTo, dummy);
474: /*
475: * If the formatted value is a number, append the units.
476: */
477: if (type == 0 && cs != null) {
478: final Unit unit = cs.getAxis(i).getUnit();
479: if (unit != null) {
480: if (unitFormat == null) {
481: unitFormat = UnitFormat.getInstance();
482: }
483: final String asText = unitFormat.format(unit);
484: if (asText.length() != 0) {
485: toAppendTo.append('\u00A0'); // No break space
486: toAppendTo.append(unit);
487: }
488: }
489: }
490: }
491: return toAppendTo;
492: }
493:
494: /**
495: * Formats a direct position and appends the resulting text to a given string buffer.
496: * The position's dimension must matches the {@linkplain #getCoordinateReferenceSystem
497: * coordinate reference system} dimension.
498: *
499: * @param object The {@link DirectPosition} to format.
500: * @param toAppendTo Where the text is to be appended.
501: * @param position A {@code FieldPosition} identifying a field in the formatted text,
502: * or {@code null} if none.
503: * @return The string buffer passed in as {@code toAppendTo}, with formatted text appended.
504: * @throws NullPointerException if {@code toAppendTo} is null.
505: * @throws IllegalArgumentException if this {@code CoordinateFormat}
506: * cannot format the given object.
507: */
508: public StringBuffer format(final Object object,
509: final StringBuffer toAppendTo, final FieldPosition position)
510: throws IllegalArgumentException {
511: if (object instanceof DirectPosition) {
512: return format((DirectPosition) object, toAppendTo, position);
513: } else {
514: throw new IllegalArgumentException(String.valueOf(object));
515: }
516: }
517:
518: /**
519: * Not yet implemented.
520: */
521: public Object parseObject(final String source,
522: final ParsePosition position) {
523: throw new UnsupportedOperationException(
524: "DirectPosition parsing not yet implemented.");
525: }
526: }
|