blob: 45337503bf6b401f98289cf24b532d52078411f2 [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.io.wkt;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import java.util.Date;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.FieldPosition;
import java.io.IOException;
import java.lang.reflect.Array;
import java.math.RoundingMode;
import javax.measure.Unit;
import javax.measure.Quantity;
import org.opengis.util.InternationalString;
import org.opengis.util.ControlledVocabulary;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.citation.Citation;
import org.opengis.metadata.extent.Extent;
import org.opengis.metadata.extent.VerticalExtent;
import org.opengis.metadata.extent.TemporalExtent;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.ReferenceSystem;
import org.opengis.referencing.datum.Datum;
import org.opengis.referencing.crs.CompoundCRS;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.ConcatenatedOperation;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.geometry.coordinate.Position;
import org.opengis.geometry.Envelope;
import org.apache.sis.measure.Units;
import org.apache.sis.math.DecimalFunctions;
import org.apache.sis.util.iso.Types;
import org.apache.sis.util.Classes;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.Localized;
import org.apache.sis.util.Exceptions;
import org.apache.sis.util.Characters;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.collection.IntegerList;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.internal.util.X364;
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.internal.util.Constants;
import org.apache.sis.internal.util.StandardDateFormat;
import org.apache.sis.internal.simple.SimpleExtent;
import org.apache.sis.internal.metadata.Resources;
import org.apache.sis.internal.referencing.WKTKeywords;
import org.apache.sis.internal.referencing.WKTUtilities;
import org.apache.sis.referencing.AbstractIdentifiedObject;
import org.apache.sis.geometry.AbstractDirectPosition;
import org.apache.sis.geometry.AbstractEnvelope;
import org.apache.sis.measure.UnitFormat;
import org.apache.sis.measure.Range;
import org.apache.sis.measure.MeasurementRange;
import org.apache.sis.referencing.ImmutableIdentifier;
import org.apache.sis.metadata.iso.extent.Extents;
import org.apache.sis.math.Vector;
/**
* Provides support methods for formatting a <cite>Well Known Text</cite> (WKT).
*
* <p>{@code Formatter} instances are created by {@link WKTFormat} and given to the
* {@link FormattableObject#formatTo(Formatter)} method of the object to format.
* {@code Formatter} provides the following services:</p>
*
* <ul>
* <li>A series of {@code append(…)} methods to be invoked by the {@code formatTo(Formatter)} implementations.</li>
* <li>Contextual information. In particular, the {@linkplain #toContextualUnit(Unit) contextual units} depend on
* the {@linkplain #getEnclosingElement(int) enclosing WKT element}.</li>
* <li>A flag for declaring the object unformattable.</li>
* </ul>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.0
*
* @see <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html">WKT 2 specification</a>
* @see <a href="http://www.geoapi.org/3.0/javadoc/org/opengis/referencing/doc-files/WKT.html">Legacy WKT 1</a>
*
* @since 0.4
* @module
*/
public class Formatter implements Localized {
/**
* Accuracy of geographic bounding boxes, in number of fraction digits.
* We use the accuracy recommended by ISO 19162.
*/
static final int BBOX_ACCURACY = 2;
/**
* Maximal accuracy of vertical extents, in number of fraction digits.
* The value used here is arbitrary and may change in any future SIS version.
*/
private static final int VERTICAL_ACCURACY = 9;
/**
* The value of {@code X364.FOREGROUND_DEFAULT.sequence()}, hard-coded for avoiding
* {@link org.apache.sis.internal.util.X364} class loading.
*/
static final String FOREGROUND_DEFAULT = "\u001B[39m";
/**
* The value of {@code X364.BACKGROUND_DEFAULT.sequence()}, hard-coded for avoiding
* {@link org.apache.sis.internal.util.X364} class loading.
*/
static final String BACKGROUND_DEFAULT = "\u001B[49m";
/**
* The locale for the localization of international strings.
* This is not the same than {@link Symbols#getLocale()}.
*/
private final Locale locale;
/**
* The symbols to use for this formatter.
*
* @see WKTFormat#getSymbols()
* @see WKTFormat#setSymbols(Symbols)
*/
private final Symbols symbols;
/**
* The value of {@link Symbols#getSeparator()} without trailing spaces, followed by the system line separator.
* Computed by {@link Symbols#separatorNewLine()} and stored for reuse.
*/
private final String separatorNewLine;
/**
* The colors to use for this formatter, or {@code null} for no syntax coloring.
* If non-null, the terminal must be ANSI X3.64 compatible.
* The default value is {@code null}.
*
* @see #configure(Convention, Citation, Colors, byte, byte, byte, int)
*/
private Colors colors;
/**
* The preferred convention for objects or parameter names.
* This field should never be {@code null}.
*
* @see #configure(Convention, Citation, Colors, byte, byte, byte, int)
*/
private Convention convention;
/**
* The preferred authority for objects or parameter names.
*
* @see #configure(Convention, Citation, Colors, byte, byte, byte, int)
*/
private Citation authority;
/**
* {@link Transliterator#IDENTITY} for preserving non-ASCII characters. The default value is
* {@link Transliterator#DEFAULT}, which causes replacements like "é" → "e" in all elements
* except {@code REMARKS["…"]}. May also be a user-supplied transliterator.
*
* @see #getTransliterator()
*/
Transliterator transliterator;
/**
* {@code true} if this {@code Formatter} should verify the validity of characters in quoted texts.
* ISO 19162 restricts quoted texts to ASCII characters with addition of degree symbol (°).
*/
boolean verifyCharacterValidity = true;
/**
* The enclosing WKT element being formatted.
*
* @see #getEnclosingElement(int)
*/
private final List<FormattableObject> enclosingElements = new ArrayList<>();
/**
* The contextual units for writing lengths, angles or other type of measurements.
* A unit not present in this map means that the "natural" unit of the WKT element shall be used.
* This value is set for example by {@code "GEOGCS"}, which force its enclosing {@code "PRIMEM"}
* to take the same units than itself.
*
* @see #addContextualUnit(Unit)
* @see #toContextualUnit(Unit)
*/
private final Map<Unit<?>, Unit<?>> units = new HashMap<>(4);
/**
* A bits mask of elements which defined a contextual units.
* The rightmost bit is for the current element. The bit before the rightmost
* is for the parent of current element, etc.
*
* @see #hasContextualUnit(int)
*/
private long hasContextualUnit;
/**
* The object to use for formatting numbers.
*/
private final NumberFormat numberFormat;
/**
* The object to use for formatting dates.
*/
private final DateFormat dateFormat;
/**
* The object to use for formatting unit symbols.
*/
private final UnitFormat unitFormat;
/**
* Dummy field position.
*/
private final FieldPosition dummy = new FieldPosition(0);
/**
* The buffer in which to format. Consider this field as final. The only method to change
* (indirectly) the value of this field is {@link WKTFormat#format(Object, Appendable)}.
*
* @see #setBuffer(StringBuffer)
*/
private StringBuffer buffer;
/**
* Index of the first character in the buffer where the element content will be formatted.
* This is set after the opening bracket and is used for determining if a separator needs
* to be appended.
*
* @see #setBuffer(StringBuffer)
*/
private int elementStart;
/**
* {@code 1} if keywords shall be converted to upper cases, or {@code -1} for lower cases.
*
* @see #configure(Convention, Citation, Colors, byte, byte, byte, int)
*/
private byte toUpperCase;
/**
* {@code -1} for short keywords, {@code +1} for long keywords or 0 for the default.
*/
private byte longKeywords;
/**
* Maximum number of elements to show in lists, or {@link Integer#MAX_VALUE} if unlimited.
* If a list is longer than this length, only the first and the last elements will be shown.
* This limit applies in particular to {@link MathTransform} parameter values of {@code double[]}
* type, since those parameters may be large interpolation tables.
*
* @see #configure(Convention, Citation, Colors, byte, byte, byte, int)
*/
private int listSizeLimit;
/**
* Incremented when {@link #setColor(ElementKind)} is invoked, and decremented when {@link #resetColor()}
* is invoked. Used in order to prevent child elements to overwrite the colors decided by enclosing elements.
*/
private int colorApplied;
/**
* The amount of spaces to use in indentation, or {@value org.apache.sis.io.wkt.WKTFormat#SINGLE_LINE}
* if indentation is disabled.
*
* @see #configure(Convention, Citation, Colors, byte, byte, byte, int)
*/
private byte indentation;
/**
* The amount of space to write on the left side of each line. This amount is increased
* by {@code indentation} every time a {@link FormattableObject} is appended in a new
* indentation level.
*/
private int margin;
/**
* Indices where to insert additional margin, or {@code null} if none. The margin to insert will be
* the the width of the keyword (e.g. {@code "BOX"}), which is usually unknown to {@code Formatter}
* until {@link FormattableObject} finished to write the element. This field is usually {@code null},
* unless formatting geometries.
*/
private IntegerList keywordSpaceAt;
/**
* {@code true} if a new line were requested during the execution of {@link #append(FormattableObject)}.
* This is used to determine if the next {@code UNIT} and {@code ID} elements shall appear on a new line.
*/
private boolean requestNewLine;
/**
* {@code true} if we are in the process of formatting the optional complementary attributes.
* Those attributes are {@code SCOPE}, {@code AREA}, {@code BBOX}, {@code VERTICALEXTENT}, {@code TIMEEXTENT},
* {@code ID} (previously known as {@code AUTHORITY}) and {@code REMARKS}, and have a special treatment: they
* are written by {@link #append(FormattableObject)} after the {@code formatTo(Formatter)} method returned.
*
* @see #appendComplement(IdentifiedObject, FormattableObject, FormattableObject)
*/
private boolean isComplement;
/**
* {@code true} if the last formatted element was invalid WKT and shall be highlighted with syntactic coloration.
* This field has no effect if {@link #colors} is null. This field is reset to {@code false} after the invalid
* part has been processed by {@link #append(FormattableObject)}, in order to highlight only the first erroneous
* element without clearing the {@link #warnings} value.
*/
private boolean highlightError;
/**
* The warnings that occurred during WKT formatting, or {@code null} if none.
*
* @see #isInvalidWKT()
* @see #getWarnings()
*/
private Warnings warnings;
/**
* Creates a new formatter instance with the default configuration.
*/
public Formatter() {
this(Convention.DEFAULT, Symbols.getDefault(), Constants.DEFAULT_INDENTATION);
}
/**
* Creates a new formatter instance with the specified convention, symbols and indentation.
*
* @param convention the convention to use.
* @param symbols the symbols.
* @param indentation the amount of spaces to use in indentation for WKT formatting,
* or {@link WKTFormat#SINGLE_LINE} for formatting the whole WKT on a single line.
*/
public Formatter(final Convention convention, final Symbols symbols, final int indentation) {
ArgumentChecks.ensureNonNull("convention", convention);
ArgumentChecks.ensureNonNull("symbols", symbols);
ArgumentChecks.ensureBetween("indentation", WKTFormat.SINGLE_LINE, Byte.MAX_VALUE, indentation);
this.locale = Locale.getDefault(Locale.Category.DISPLAY);
this.convention = convention;
this.authority = convention.getNameAuthority();
this.symbols = symbols.immutable();
this.separatorNewLine = this.symbols.separatorNewLine();
this.indentation = (byte) indentation;
this.numberFormat = symbols.createNumberFormat();
this.dateFormat = new StandardDateFormat(symbols.getLocale());
this.unitFormat = new UnitFormat(symbols.getLocale());
this.buffer = new StringBuffer();
unitFormat.setStyle(UnitFormat.Style.NAME);
if (convention.usesCommonUnits) {
unitFormat.setLocale(Locale.US);
}
}
/**
* Constructor for private use by {@link WKTFormat} only. This allows to use the number format
* created by {@link WKTFormat#createFormat(Class)}, which may be overridden by the user.
*/
Formatter(final Locale locale, final Symbols symbols, final NumberFormat numberFormat,
final DateFormat dateFormat, final UnitFormat unitFormat)
{
this.locale = locale;
this.convention = Convention.DEFAULT;
this.authority = Convention.DEFAULT.getNameAuthority();
this.symbols = symbols;
this.separatorNewLine = symbols.separatorNewLine();
this.indentation = Constants.DEFAULT_INDENTATION;
this.numberFormat = numberFormat; // No clone needed.
this.dateFormat = dateFormat;
this.unitFormat = unitFormat;
// Do not set the buffer. It will be set by WKTFormat.format(…).
}
/**
* Sets the destination buffer. Used by {@link WKTFormat#format(Object, Appendable)} only.
*/
final void setBuffer(final StringBuffer buffer) {
this.buffer = buffer;
elementStart = (buffer != null) ? buffer.length() : 0;
}
/**
* Sets the convention, authority, colors and indentation to use for formatting WKT elements.
* This method does not validate the argument — validation must be done by the caller.
*
* @param convention the convention, or {@code null} for the default value.
* @param authority the authority, or {@code null} for inferring it from the convention.
* @param colors the syntax coloring, or {@code null} if none.
* @param toUpperCase whether keywords shall be converted to upper cases.
* @param longKeywords {@code -1} for short keywords, {@code +1} for long keywords or 0 for the default.
* @param indentation the amount of spaces to use in indentation for WKT formatting, or {@link WKTFormat#SINGLE_LINE}.
* @param listSizeLimit maximum number of elements to show in lists, or {@link Integer#MAX_VALUE} if unlimited.
*/
final void configure(Convention convention, final Citation authority, final Colors colors,
final byte toUpperCase, final byte longKeywords, final byte indentation, final int listSizeLimit)
{
this.convention = convention;
this.authority = (authority != null) ? authority : convention.getNameAuthority();
this.colors = colors;
this.toUpperCase = toUpperCase;
this.longKeywords = longKeywords;
this.indentation = indentation;
this.listSizeLimit = listSizeLimit;
this.transliterator = (convention == Convention.INTERNAL) ? Transliterator.IDENTITY : Transliterator.DEFAULT;
unitFormat.setLocale(convention.usesCommonUnits ? Locale.US : Locale.ROOT);
}
/**
* Returns the convention to use for formatting the WKT. The default is {@link Convention#WKT2}.
*
* @return the convention (never {@code null}).
*
* @see WKTFormat#setConvention(Convention)
* @see FormattableObject#toString(Convention)
*/
public final Convention getConvention() {
return convention;
}
/**
* Returns a mapper between Java character sequences and the characters to write in WKT.
* The intent is to specify how to write characters that are not allowed in WKT strings
* according ISO 19162 specification. Return values can be:
*
* <ul>
* <li>{@link Transliterator#DEFAULT} for performing replacements like "é" → "e"
* in all WKT elements except {@code REMARKS["…"]}.</li>
* <li>{@link Transliterator#IDENTITY} for preserving non-ASCII characters.</li>
* <li>Any other user-supplied mapping.</li>
* </ul>
*
* @return the mapper between Java character sequences and the characters to write in WKT.
*
* @see WKTFormat#setTransliterator(Transliterator)
*
* @since 0.6
*/
public final Transliterator getTransliterator() {
return transliterator;
}
/**
* Returns the preferred authority for choosing the projection and parameter names.
*
* <p>The preferred authority can be set by the {@link WKTFormat#setNameAuthority(Citation)} method.
* This is not necessarily the authority who created the object to format.</p>
*
* <div class="note"><b>Example:</b>
* The EPSG name of the {@code EPSG:6326} datum is <cite>"World Geodetic System 1984"</cite>.
* However if the preferred authority is OGC, then the formatted datum name will rather look like
* <cite>"WGS84"</cite> (the exact string depends on the object aliases).</div>
*
* @return the authority for projection and parameter names.
*
* @see WKTFormat#getNameAuthority()
* @see org.apache.sis.referencing.IdentifiedObjects#getName(IdentifiedObject, Citation)
*/
public final Citation getNameAuthority() {
return authority;
}
/**
* Returns the locale to use for localizing {@link InternationalString} instances.
* This is <em>not</em> the locale for formatting dates and numbers.
*
* @return the locale to use for localizing international strings.
*/
@Override
public final Locale getLocale() {
return locale;
}
/**
* Appends in the {@linkplain #buffer} the ANSI escape sequence for the given kind of element.
* This method does nothing unless syntax coloring has been explicitly enabled.
*/
private void setColor(final ElementKind type) {
if (colors != null) {
if (colorApplied == 0) {
final String color = colors.getAnsiSequence(type);
if (color == null) {
// Do not increment 'colorApplied' for giving a chance to children to apply their colors.
return;
}
final boolean isStart = (buffer.length() == elementStart);
buffer.append(color);
if (isStart) {
elementStart = buffer.length();
}
}
colorApplied++;
}
}
/**
* Appends in the {@linkplain #buffer} the ANSI escape sequence for reseting the color to the default.
* This method does nothing unless syntax coloring has been explicitly enabled.
*/
private void resetColor() {
if (colors != null && --colorApplied <= 0) {
colorApplied = 0;
buffer.append(FOREGROUND_DEFAULT);
}
}
/**
* Request a line separator before the next element to format. Invoking this method before any
* {@code append(…)} method call will cause the next element to appear on the next line.
*
* <p>This method has no effect in any of the following cases:</p>
* <ul>
* <li>This method has already been invoked before the next {@code append(…)}.</li>
* <li>The indentation is {@link WKTFormat#SINGLE_LINE}.</li>
* </ul>
*/
public void newLine() {
if (indentation > WKTFormat.SINGLE_LINE) {
requestNewLine = true;
}
}
/**
* Increases or decreases the indentation. A value of {@code +1} increases
* the indentation by the amount of spaces specified at construction time,
* and a value of {@code -1} reduces it by the same amount.
*
* @param amount +1 for increasing the indentation, or -1 for decreasing it, or 0 for no-op.
*/
public void indent(final int amount) {
margin = Math.max(0, margin + indentation*amount);
}
/**
* Selects a short or long keyword depending on the {@link KeywordStyle} value.
* This method can be used by {@link FormattableObject#formatTo(Formatter)}
* implementations for choosing the return value.
*
* @param shortKeyword the keyword to return if the style is {@link KeywordStyle#SHORT}.
* @param longKeyword the keyword to return if the style is {@link KeywordStyle#LONG}.
* @return the short or long keyword depending on the keyword style setting.
*
* @see WKTFormat#setKeywordStyle(KeywordStyle)
*
* @since 0.6
*/
public String shortOrLong(final String shortKeyword, final String longKeyword) {
return (longKeywords != 0
? longKeywords < 0 // If keyword style was explicitly specified, use the setting.
: convention.toUpperCase) // Otherwise use the default value determined by the convention.
? shortKeyword : longKeyword;
}
/**
* Conditionally appends a separator to the {@linkplain #buffer}, if needed.
* This method does nothing if there is currently no element at the buffer end.
*/
private void appendSeparator() {
if (buffer.length() != elementStart) {
if (requestNewLine) {
buffer.append(separatorNewLine).append(CharSequences.spaces(margin));
} else {
buffer.append(symbols.getSeparator());
}
} else if (requestNewLine) {
buffer.append(System.lineSeparator()).append(CharSequences.spaces(margin));
}
requestNewLine = false;
}
/**
* Appends a separator if needed, then opens a new element.
*
* @param newLine {@code true} for invoking {@link #newLine()} first.
* @param keyword the element keyword (e.g. {@code "DATUM"}, {@code "AXIS"}, <i>etc</i>).
*/
private void openElement(final boolean newLine, String keyword) {
if (newLine && buffer.length() != elementStart) {
newLine();
}
appendSeparator();
if (toUpperCase != 0) {
final Locale locale = symbols.getLocale();
keyword = (toUpperCase >= 0) ? keyword.toUpperCase(locale) : keyword.toLowerCase(locale);
}
elementStart = buffer.append(keyword).appendCodePoint(symbols.getOpeningBracket(0)).length();
}
/**
* Closes the element opened by {@link #openElement(boolean, String)}.
*
* @param newLine {@code true} for invoking {@link #newLine()} last.
*/
private void closeElement(final boolean newLine) {
buffer.appendCodePoint(symbols.getClosingBracket(0));
if (newLine) {
newLine();
}
}
/**
* Appends the given {@code FormattableObject}.
* This method performs the following steps:
*
* <ul>
* <li>Invoke <code>object.{@linkplain FormattableObject#formatTo(Formatter) formatTo}(this)</code>.</li>
* <li>Prepend the keyword returned by the above method call (e.g. {@code "GEOCS"}).</li>
* <li>If the given object is an instance of {@link IdentifiedObject}, then append complementary information:</li>
* </ul>
*
* <blockquote><table class="sis">
* <caption>Complementary WKT elements</caption>
* <tr><th>WKT 2 element</th><th>WKT 1 element</th><th>For types</th></tr>
* <tr><td>{@code Anchor[…]}</td> <td></td> <td>{@link Datum}</td></tr>
* <tr><td>{@code Scope[…]}</td> <td></td> <td>{@link ReferenceSystem}, {@link Datum}, {@link CoordinateOperation}</td></tr>
* <tr><td>{@code Area[…]}</td> <td></td> <td>{@link ReferenceSystem}, {@link Datum}, {@link CoordinateOperation}</td></tr>
* <tr><td>{@code BBox[…]}</td> <td></td> <td>{@link ReferenceSystem}, {@link Datum}, {@link CoordinateOperation}</td></tr>
* <tr><td>{@code VerticalExtent[…]}</td><td></td> <td>{@link ReferenceSystem}, {@link Datum}, {@link CoordinateOperation}</td></tr>
* <tr><td>{@code TimeExtent[…]}</td> <td></td> <td>{@link ReferenceSystem}, {@link Datum}, {@link CoordinateOperation}</td></tr>
* <tr><td>{@code Id[…]}</td><td>{@code Authority[…]}</td><td>{@link IdentifiedObject}</td></tr>
* <tr><td>{@code Remarks[…]}</td> <td></td> <td>{@link ReferenceSystem}, {@link CoordinateOperation}</td></tr>
* </table></blockquote>
*
* @param object the formattable object to append to the WKT, or {@code null} if none.
*/
public void append(final FormattableObject object) {
if (object == null) {
return;
}
/*
* Safety check: ensure that we do not have circular dependencies (e.g. a ProjectedCRS contains
* a Conversion which may contain the ProjectedCRS as its target CRS). Without this protection,
* a circular dependency would cause an OutOfMemoryError.
*/
final int stackDepth = enclosingElements.size();
for (int i=stackDepth; --i >= 0;) {
if (enclosingElements.get(i) == object) {
throw new IllegalStateException(Errors.getResources(locale).getString(Errors.Keys.CircularReference));
}
}
enclosingElements.add(object);
if (hasContextualUnit < 0) { // Test if leftmost bit is set to 1.
throw new IllegalStateException(Errors.getResources(locale).getString(Errors.Keys.TreeDepthExceedsMaximum));
}
hasContextualUnit <<= 1;
/*
* Add a new line if it was requested, open the bracket and increase indentation in case the
* element to format contains other FormattableObject elements.
*/
appendSeparator();
int base = buffer.length();
elementStart = buffer.appendCodePoint(symbols.getOpeningBracket(0)).length();
indent(+1);
/*
* Formats the inner part, then prepend the WKT keyword.
* The result looks like the following:
*
* <previous text>,
* PROJCS["NAD27 / Idaho Central",
* GEOGCS[...etc...],
* ...etc...
*/
IdentifiedObject info = (object instanceof IdentifiedObject) ? (IdentifiedObject) object : null;
String keyword = object.formatTo(this);
if (keyword == null) {
if (info != null) {
setInvalidWKT(info, null);
} else {
setInvalidWKT(object.getClass(), null);
}
keyword = getName(object.getClass());
} else if (toUpperCase != 0) {
final Locale locale = symbols.getLocale();
keyword = (toUpperCase >= 0) ? keyword.toUpperCase(locale) : keyword.toLowerCase(locale);
}
if (highlightError && colors != null) {
final String color = colors.getAnsiSequence(ElementKind.ERROR);
if (color != null) {
buffer.insert(base, color + BACKGROUND_DEFAULT);
base += color.length();
}
}
highlightError = false;
buffer.insert(base, keyword);
/*
* When formatting geometry coordinates, we may need to shift all numbers by the width
* of the keyword inserted above in order to keep numbers properly aligned. Exemple:
*
* BOX[ 4.000 -10.000
* 50.000 2.000]
*/
if (keywordSpaceAt != null) {
final int length = keyword.length();
final CharSequence additionalMargin = CharSequences.spaces(keyword.codePointCount(0, length));
final int n = keywordSpaceAt.size();
for (int i=0; i<n;) {
int p = keywordSpaceAt.getInt(i);
p += (++i * length); // Take in account spaces added previously.
buffer.insert(p, additionalMargin);
}
keywordSpaceAt.clear();
}
/*
* Format the SCOPE["…"], AREA["…"] and other elements. Some of those information
* are available only for Datum, CoordinateOperation and ReferenceSystem objects.
*/
if (info == null && convention.majorVersion() != 1 && object instanceof GeneralParameterValue) {
info = ((GeneralParameterValue) object).getDescriptor();
}
if (info != null) {
appendComplement(info, (stackDepth >= 1) ? enclosingElements.get(stackDepth - 1) : null,
(stackDepth >= 2) ? enclosingElements.get(stackDepth - 2) : null);
}
/*
* Close the bracket, then update the queue of enclosed elements by removing this element.
*/
buffer.appendCodePoint(symbols.getClosingBracket(0));
indent(-1);
enclosingElements.remove(stackDepth);
hasContextualUnit >>>= 1;
}
/**
* Appends the optional complementary attributes common to many {@link IdentifiedObject} subtypes.
* Those attributes are {@code ANCHOR}, {@code SCOPE}, {@code AREA}, {@code BBOX}, {@code VERTICALEXTENT},
* {@code TIMEEXTENT}, {@code ID} (previously known as {@code AUTHORITY}) and {@code REMARKS},
* and have a special treatment: they are written by {@link #append(FormattableObject)}
* after the {@code formatTo(Formatter)} method returned.
*
* <p>The {@code ID[<name>,<code>,…]} element is normally written only for the root element
* (unless the convention is {@code INTERNAL}), but there is various exceptions to this rule.
* If formatted, the {@code ID} element will be by default on the same line than the enclosing
* element (e.g. {@code SPHEROID["Clarke 1866", …, ID["EPSG", 7008]]}). Other example:</p>
*
* {@preformat text
* PROJCS["NAD27 / Idaho Central",
* GEOGCS[...etc...],
* ...etc...
* ID["EPSG", 26769]]
* }
*
* For non-internal conventions, all elements other than {@code ID[…]} are formatted
* only for {@link CoordinateOperation} and root {@link ReferenceSystem} instances,
* with an exception for remarks of {@code ReferenceSystem} embedded inside {@code CoordinateOperation}.
* Those restrictions are our interpretation of the following ISO 19162 requirement:
*
* <blockquote>(…snip…) {@code <scope extent identifier remark>} is a collection of four optional attributes
* which may be applied to a coordinate reference system, a coordinate operation or a boundCRS. (…snip…)
* Identifier (…snip…) may also be utilised for components of these objects although this is not recommended
* except for coordinate operation methods (including map projections) and parameters. (…snip…)
* A {@code <remark>} can be included within the descriptions of source and target CRS embedded within
* a coordinate transformation as well as within the coordinate transformation itself.</blockquote>
*/
@SuppressWarnings("null")
private void appendComplement(final IdentifiedObject object, final FormattableObject parent, final FormattableObject gp) {
isComplement = true;
final boolean showIDs; // Whether to format ID[…] elements.
final boolean filterID; // Whether we shall limit to a single ID[…] element.
final boolean showOthers; // Whether to format any element other than ID[…] and Remarks[…].
final boolean showRemarks; // Whether to format Remarks[…].
if (convention == Convention.INTERNAL) {
showIDs = true;
filterID = false;
showOthers = true;
showRemarks = true;
} else {
/*
* Except for the special cases of OperationMethod and Parameters, ISO 19162 recommends to format the
* ID only for the root element. But Apache SIS adds an other exception to this rule by handling the
* components of CompoundCRS as if they were root elements. The reason is that users often create their
* own CompoundCRS from standard components, for example by adding a time axis to some standard CRS like
* "WGS84". The resulting CompoundCRS usually have no identifier. Then the users often need to extract a
* particular component of a CompoundCRS, most often the horizontal part, and will need its identifier
* for example in a Web Map Service (WMS). Those ID are lost if we do not format them here.
*/
if (parent == null || parent instanceof CompoundCRS) {
showIDs = true;
} else if (gp instanceof CoordinateOperation && !(parent instanceof IdentifiedObject)) {
// "SourceCRS[…]" and "TargetCRS[…]" sub-elements in CoordinateOperation.
showIDs = true;
} else if (convention == Convention.WKT2_SIMPLIFIED) {
showIDs = false;
} else {
showIDs = (object instanceof OperationMethod) || (object instanceof GeneralParameterDescriptor);
}
if (convention.majorVersion() == 1) {
filterID = true;
showOthers = false;
showRemarks = false;
} else {
filterID = (parent != null);
if (object instanceof CoordinateOperation) {
showOthers = !(parent instanceof ConcatenatedOperation);
showRemarks = showOthers;
} else if (object instanceof ReferenceSystem) {
showOthers = (parent == null);
showRemarks = (parent == null) || (gp instanceof CoordinateOperation);
} else {
showOthers = false; // Mandated by ISO 19162.
showRemarks = false;
}
}
}
if (showOthers) {
appendForSubtypes(object);
}
if (showIDs) {
Collection<? extends Identifier> identifiers = object.getIdentifiers();
if (identifiers != null) { // Paranoiac check
if (filterID) {
for (final Identifier id : identifiers) {
if (Citations.identifierMatches(authority, id.getAuthority())) {
identifiers = Collections.singleton(id);
break;
}
}
}
for (Identifier id : identifiers) {
if (!(id instanceof FormattableObject)) {
id = ImmutableIdentifier.castOrCopy(id);
}
append((FormattableObject) id);
if (filterID) break;
}
}
}
if (showRemarks) {
appendOnNewLine(WKTKeywords.Remark, object.getRemarks(), ElementKind.REMARKS);
}
isComplement = false;
}
/**
* Appends the anchor, scope and domain of validity of the given object. Those information are available
* only for {@link ReferenceSystem}, {@link Datum} and {@link CoordinateOperation} objects.
*/
private void appendForSubtypes(final IdentifiedObject object) {
final InternationalString anchor, scope;
final Extent area;
if (object instanceof ReferenceSystem) {
anchor = null;
scope = ((ReferenceSystem) object).getScope();
area = ((ReferenceSystem) object).getDomainOfValidity();
} else if (object instanceof Datum) {
anchor = ((Datum) object).getAnchorPoint();
scope = ((Datum) object).getScope();
area = ((Datum) object).getDomainOfValidity();
} else if (object instanceof CoordinateOperation) {
anchor = null;
scope = ((CoordinateOperation) object).getScope();
area = ((CoordinateOperation) object).getDomainOfValidity();
} else {
return;
}
appendOnNewLine(WKTKeywords.Anchor, anchor, null);
appendOnNewLine(WKTKeywords.Scope, scope, ElementKind.SCOPE);
if (area != null) {
appendOnNewLine(WKTKeywords.Area, area.getDescription(), ElementKind.EXTENT);
append(Extents.getGeographicBoundingBox(area), BBOX_ACCURACY);
appendVerticalExtent(Extents.getVerticalRange(area));
appendTemporalExtent(Extents.getTimeRange(area));
}
}
/**
* Appends the given geographic bounding box in a {@code BBOX[…]} element.
* Longitude and latitude values will be formatted in decimal degrees.
* Longitudes are relative to the Greenwich meridian, with values increasing toward East.
* Latitudes values are increasing toward North.
*
* <div class="section">Numerical precision</div>
* The ISO 19162 standards recommends to format those values with only 2 decimal digits.
* This is because {@code GeographicBoundingBox} does not specify the datum, so this box
* is an approximated information only.
*
* @param bbox the geographic bounding box to append to the WKT, or {@code null}.
* @param fractionDigits the number of fraction digits to use. The recommended value is 2.
*/
public void append(final GeographicBoundingBox bbox, final int fractionDigits) {
if (bbox != null) {
openElement(isComplement, WKTKeywords.BBox);
setColor(ElementKind.EXTENT);
numberFormat.setMinimumFractionDigits(fractionDigits);
numberFormat.setMaximumFractionDigits(fractionDigits);
numberFormat.setRoundingMode(RoundingMode.FLOOR);
appendPreset(bbox.getSouthBoundLatitude());
appendPreset(bbox.getWestBoundLongitude());
numberFormat.setRoundingMode(RoundingMode.CEILING);
appendPreset(bbox.getNorthBoundLatitude());
appendPreset(bbox.getEastBoundLongitude());
resetColor();
closeElement(isComplement);
}
}
/**
* Appends the given vertical extent, if non-null.
* This method chooses an accuracy from the vertical span.
* Examples:
*
* <ul>
* <li>“{@code VerticalExtent[102, 108, LengthUnit["m", 1]]}” (Δz = 6)</li>
* <li>“{@code VerticalExtent[100.2, 100.8, LengthUnit["m", 1]]}” (Δz = 0.6)</li>
* </ul>
*
* Note that according ISO 19162, heights are positive toward up and relative to an unspecified mean sea level.
* It is caller's responsibility to ensure that the given range complies with that specification as much as possible.
*/
private void appendVerticalExtent(final MeasurementRange<Double> range) {
if (range != null) {
final double min = range.getMinDouble();
final double max = range.getMaxDouble();
int minimumFractionDigits = Numerics.fractionDigitsForDelta(max - min);
int maximumFractionDigits = Math.min(Math.min(
Numerics.suggestFractionDigits(min, max),
minimumFractionDigits + 2), VERTICAL_ACCURACY); // Arbitrarily limit to 2 more digits.
openElement(true, WKTKeywords.VerticalExtent);
setColor(ElementKind.EXTENT);
numberFormat.setMinimumFractionDigits(minimumFractionDigits);
numberFormat.setMaximumFractionDigits(maximumFractionDigits);
numberFormat.setRoundingMode(RoundingMode.FLOOR); appendPreset(min);
numberFormat.setRoundingMode(RoundingMode.CEILING); appendPreset(max);
final Unit<?> unit = range.unit();
if (!convention.isSimplified() || !Units.METRE.equals(unit)) {
append(unit); // Unit are optional if they are metres.
}
resetColor();
closeElement(true);
}
}
/**
* Appends the given temporal extent, if non-null.
* Examples:
*
* <ul>
* <li>“{@code TemporalExtent[1980-04-12, 1980-04-18]}”</li>
* <li>“{@code TemporalExtent[1980-04-12T18:00:00.0Z, 1980-04-12T21:00:00.0Z]}”</li>
* </ul>
*/
private void appendTemporalExtent(final Range<Date> range) {
if (range != null) {
final Date min = range.getMinValue();
final Date max = range.getMaxValue();
if (min != null && max != null) {
openElement(true, WKTKeywords.TimeExtent);
setColor(ElementKind.EXTENT);
append(min);
append(max);
resetColor();
closeElement(true);
}
}
}
/**
* Appends the given math transform, typically (but not necessarily) in a {@code PARAM_MT[…]} element.
*
* @param transform the transform object to append to the WKT, or {@code null} if none.
*/
public void append(final MathTransform transform) {
if (transform != null) {
if (transform instanceof FormattableObject) {
append((FormattableObject) transform);
} else {
final FormattableObject object = WKTUtilities.toFormattable(transform, convention == Convention.INTERNAL);
if (object != null) {
append(object);
} else {
throw new UnformattableObjectException(Errors.format(
Errors.Keys.IllegalClass_2, FormattableObject.class, transform.getClass()));
}
}
}
}
/**
* Appends an international text in an element having the given keyword. Since this method
* is typically invoked for long descriptions, the element will be written on its own line.
*
* <div class="note"><b>Example:</b>
* <ul>
* <li>{@code Scope["Large scale topographic mapping and cadastre."]}</li>
* <li>{@code Area["Netherlands offshore."]}</li>
* </ul>
* </div>
*
* @param keyword the {@linkplain KeywordCase#CAMEL_CASE camel-case} keyword.
* Example: {@code "Scope"}, {@code "Area"} or {@code "Remarks"}.
* @param text the text, or {@code null} if none.
* @param type the key of the colors to apply if syntax coloring is enabled.
*/
private void appendOnNewLine(final String keyword, final InternationalString text, final ElementKind type) {
ArgumentChecks.ensureNonNull("keyword", keyword);
if (text != null) {
final String localized = CharSequences.trimWhitespaces(text.toString(locale));
if (localized != null && !localized.isEmpty()) {
openElement(true, keyword);
quote(localized, type);
closeElement(true);
}
}
}
/**
* Appends a character string between quotes.
* The {@linkplain Symbols#getSeparator() element separator} will be written before the text if needed.
*
* @param text the string to format to the WKT, or {@code null} if none.
* @param type the key of the colors to apply if syntax coloring is enabled, or {@code null} if none.
*/
public void append(final String text, final ElementKind type) {
if (text != null) {
appendSeparator();
if (type != ElementKind.CODE_LIST) {
quote(text, type);
} else {
/*
* Code lists have no quotes. They are normally formatted by the append(ControlledVocabulary) method,
* but an important exception is the CS[type] element in which the type is defined by the interface
* implemented by the CoordinateSystem rather than a CodeList instance.
*/
setColor(type);
buffer.append(text);
resetColor();
}
}
}
/**
* Appends the given string as a quoted text. If the given string contains the closing quote character,
* that character will be doubled (WKT 2) or deleted (WKT 1). We check for the closing quote only because
* it is the character that the parser will look for determining the text end.
*/
private void quote(String text, final ElementKind type) {
setColor(type);
final int base = buffer.appendCodePoint(symbols.getOpeningQuote(0)).length();
if (type != ElementKind.REMARKS) {
text = transliterator.filter(text);
if (verifyCharacterValidity) {
int startAt = 0; // Index of the last space character.
final int length = text.length();
for (int i = 0; i < length;) {
int c = text.codePointAt(i);
int n = Character.charCount(c);
if (!Characters.isValidWKT(c)) {
final String illegal = text.substring(i, i+n);
while ((i += n) < length) {
c = text.codePointAt(i);
n = Character.charCount(c);
if (c == ' ' || c == '_') break;
}
warnings().add(Errors.formatInternational(Errors.Keys.IllegalCharacterForFormat_3,
"Well-Known Text", text.substring(startAt, i), illegal), null, null);
break;
}
i += n;
if (c == ' ' || c == '_') {
startAt = i;
}
}
}
}
buffer.append(text);
closeQuote(base);
resetColor();
}
/**
* Double or delete any closing quote character that may appear at or after the given index,
* then append the closing quote character. The action taken for the quote character depends
* on the WKT version:
*
* <ul>
* <li>For WKT 2, double the quote as specified in the standard.</li>
* <li>For WKT 1, conservatively delete the quote because the standard does not said what to do.</li>
* </ul>
*/
private void closeQuote(int fromIndex) {
final String quote = symbols.getQuote();
while ((fromIndex = buffer.indexOf(quote, fromIndex)) >= 0) {
final int n = quote.length();
if (convention.majorVersion() == 1) {
buffer.delete(fromIndex, fromIndex + n);
} else {
buffer.insert(fromIndex += n, quote);
fromIndex += n;
}
}
buffer.append(quote);
}
/**
* Appends an enumeration or code list value.
* The {@linkplain Symbols#getSeparator() element separator} will be written before the code list if needed.
*
* <p>For the WKT 2 format, this method uses the {@linkplain Types#getCodeName ISO name if available}
* (for example {@code "northEast"}).
* For the WKT 1 format, this method uses the programmatic name instead (for example {@code "NORTH_EAST"}).</p>
*
* @param code the code list to append to the WKT, or {@code null} if none.
*/
public void append(final ControlledVocabulary code) {
if (code != null) {
appendSeparator();
final String name = convention.majorVersion() == 1 ? code.name() : Types.getCodeName(code);
if (CharSequences.isUnicodeIdentifier(name)) {
setColor(ElementKind.CODE_LIST);
buffer.append(name);
resetColor();
} else {
quote(name, ElementKind.CODE_LIST);
setInvalidWKT(code.getClass(), null);
}
}
}
/**
* Appends a date.
* The {@linkplain Symbols#getSeparator() element separator} will be written before the date if needed.
*
* @param date the date to append to the WKT, or {@code null} if none.
*/
public void append(final Date date) {
if (date != null) {
appendSeparator();
dateFormat.format(date, buffer, dummy);
}
}
/**
* Appends a boolean value.
* The {@linkplain Symbols#getSeparator() element separator} will be written before the boolean if needed.
*
* @param value the boolean to append to the WKT.
*/
public void append(final boolean value) {
appendSeparator();
buffer.append(value ? "TRUE" : "FALSE");
}
/**
* Appends an integer value.
* The {@linkplain Symbols#getSeparator() element separator} will be written before the number if needed.
*
* @param number the integer to append to the WKT.
*/
public void append(final long number) {
appendSeparator();
/*
* The check for 'isComplement' is a hack for ImmutableIdentifier.formatTo(Formatter).
* We do not have a public API for controlling the integer colors (it may not be desirable).
*/
setColor(isComplement ? ElementKind.IDENTIFIER : ElementKind.INTEGER);
numberFormat.setMaximumFractionDigits(0);
numberFormat.format(number, buffer, dummy);
resetColor();
}
/**
* Appends an floating point value.
* The {@linkplain Symbols#getSeparator() element separator} will be written before the number if needed.
*
* @param number the floating point value to append to the WKT.
*/
public void append(final double number) {
appendSeparator();
setColor(ElementKind.NUMBER);
/*
* Use scientific notation if the number magnitude is too high or too low. The threshold values used here
* may be different than the threshold values used in the standard 'StringBuilder.append(double)' method.
* In particular, we use a higher threshold for large numbers because ellipsoid axis lengths are above the
* JDK threshold when the axis length is given in feet (about 2.1E+7) while we still want to format them
* as usual numbers.
*
* Note that we perform this special formatting only if the 'NumberFormat' is not localized
* (which is the usual case).
*/
if (symbols.useScientificNotation(Math.abs(number))) {
buffer.append(number);
} else {
/*
* The 2 below is for using two less fraction digits than the expected number accuracy.
* The intent is to give to DecimalFormat a chance to hide rounding errors, keeping in
* mind that the number value is not necessarily the original one (we may have applied
* a unit conversion). In the case of WGS84 semi-major axis in metres, we still have a
* maximum of 8 fraction digits, which is more than enough.
*/
numberFormat.setMaximumFractionDigits(DecimalFunctions.fractionDigitsForValue(number, 2));
numberFormat.setMinimumFractionDigits(1); // Must be after setMaximumFractionDigits(…).
numberFormat.setRoundingMode(RoundingMode.HALF_EVEN);
numberFormat.format(number, buffer, dummy);
}
resetColor();
}
/**
* Appends rows of numbers. Each number is separated by a space, and each row is separated by a comma.
* Rows usually have all the same length, but this is not mandatory.
* This method can be used for formatting geometries or matrix.
*
* @param rows the rows to append, or {@code null} if none.
* @param fractionDigits the number of fraction digits for each column in a row, or {@code null} for default.
* A precision can be specified for each column because those columns are often different dimensions of
* a Coordinate Reference System (CRS), each with their own units of measurement.
* If a row contains more numbers than {@code fractionDigits.length},
* then the last value in this array is repeated for all remaining row numbers.
*
* @since 1.0
*/
public void append(final Vector[] rows, int... fractionDigits) {
if (rows == null || rows.length == 0) {
return;
}
if (fractionDigits == null || fractionDigits.length == 0) {
fractionDigits = Numerics.suggestFractionDigits(rows);
}
numberFormat.setRoundingMode(RoundingMode.HALF_EVEN);
/*
* If the rows are going to be formatted on many lines, then we will need to put some margin before each row.
* If the first row starts on its own line, then the margin will be the usual indentation. But if the first
* row starts on the same line than previous elements (or the keyword of this element, e.g. "BOX["), then we
* will need a different amount of spaces if we want to have the numbers properly aligned.
*/
final int numRows = rows.length;
final boolean isMultiLines = (indentation > WKTFormat.SINGLE_LINE) && (numRows > 1);
final boolean needsAlignment = !requestNewLine;
final CharSequence marginBeforeRow;
if (isMultiLines) {
int currentLineLength = margin;
if (needsAlignment) {
final int length = buffer.length();
int i = length;
while (i > 0) { // Locate beginning of current line.
final int c = buffer.codePointBefore(i);
if (Characters.isLineOrParagraphSeparator(c)) break;
i -= Character.charCount(c);
}
currentLineLength = buffer.codePointCount(i, length);
}
marginBeforeRow = CharSequences.spaces(currentLineLength);
} else {
marginBeforeRow = "";
}
/*
* 'formattedNumberMarks' contains, for each number in each row, positions in the 'buffer' where
* the number starts and position where it ends. Those positions are stored as (start,end) pairs.
* We compute those marks unconditionally for simplicity, but will ignore them if formatting on
* a single line.
*/
final int[][] formattedNumberMarks = new int[numRows][];
int numColumns = 0;
for (int j=0; j<numRows; j++) {
if (j == 0) {
appendSeparator(); // It is up to the caller to decide if we begin with a new line.
} else {
buffer.append(separatorNewLine).append(marginBeforeRow);
}
final Vector numbers = rows[j];
final int numCols = numbers.size();
numColumns = Math.max(numColumns, numCols); // Store the length of longest row.
final int[] marks = new int[numCols << 1]; // Positions where numbers are formatted.
formattedNumberMarks[j] = marks;
for (int i=0; i<numCols; i++) {
if (i != 0) buffer.append(Symbols.NUMBER_SEPARATOR);
if (i < fractionDigits.length) { // Otherwise, same than previous number.
final int f = fractionDigits[i];
numberFormat.setMaximumFractionDigits(f);
numberFormat.setMinimumFractionDigits(f);
}
marks[i << 1] = buffer.length(); // Store the start position where number is formatted.
setColor(ElementKind.NUMBER);
final Number n = numbers.get(i);
if (n != null) {
numberFormat.format(n, buffer, dummy);
} else {
buffer.append('…');
}
resetColor();
marks[(i << 1) | 1] = buffer.length(); // Store the end position where number is formatted.
}
}
/*
* If formatting on more than one line, insert the amount of spaces required for aligning numbers.
* This is possible because we wrote the coordinate values with fixed number of fraction digits.
*/
if (isMultiLines) {
final int base = elementStart;
final String toWrite = buffer.substring(base); // Save what we formatted in above loop.
buffer.setLength(base); // Discard what we formatted - we will rewrite.
final int[] columnWidths = new int[numColumns];
for (final int[] marks : formattedNumberMarks) { // Compute the maximal width of each column.
for (int i=0; i<marks.length; i += 2) {
final int k = i >> 1;
final int w = toWrite.codePointCount(marks[i ] -= base,
marks[i+1] -= base);
if (w > columnWidths[k]) columnWidths[k] = w;
}
}
if (needsAlignment && keywordSpaceAt == null) {
keywordSpaceAt = new IntegerList(formattedNumberMarks.length, Integer.MAX_VALUE);
}
boolean requestAlignment = false;
int lastPosition = 0;
for (int[] marks : formattedNumberMarks) { // Recopy the formatted text, with more spaces.
for (int i = 0; i<marks.length;) {
final int w = columnWidths[i >> 1];
final int s = marks[i++];
final int e = marks[i++];
buffer.append(toWrite, lastPosition, s)
.append(CharSequences.spaces(w - toWrite.codePointCount(s, e)));
if (requestAlignment) {
requestAlignment = false;
keywordSpaceAt.add(buffer.length());
}
buffer.append(toWrite, s, e);
lastPosition = e;
}
requestAlignment = needsAlignment;
}
}
}
/**
* Appends the given number without any change to the {@link NumberFormat} setting.
* Caller shall ensure that the following method has been invoked prior this method call:
*
* <ul>
* <li>{@link NumberFormat#setMinimumFractionDigits(int)}</li>
* <li>{@link NumberFormat#setMaximumFractionDigits(int)}</li>
* <li>{@link NumberFormat#setRoundingMode(RoundingMode)}</li>
* </ul>
*/
private void appendPreset(final double number) {
appendSeparator();
setColor(ElementKind.NUMBER);
numberFormat.format(number, buffer, dummy);
resetColor();
}
/**
* Appends a number which is assumed to have no rounding error greater than the limit of IEEE 754 accuracy.
* This method is invoked for formatting the unit conversion factors, which are defined by the Unit library
* rather than specified by the user. The given number is formatted by {@link Double#toString(double)} both
* for accuracy and for automatic usage of scientific notation. If the given number is an integer, then it
* formatted without the trailing ".0".
*/
private void appendExact(final double number) {
if (Locale.ROOT.equals(symbols.getLocale())) {
appendSeparator();
setColor(highlightError ? ElementKind.ERROR : ElementKind.NUMBER);
final int i = (int) number;
if (i == number) {
buffer.append(i);
} else {
buffer.append(number);
}
resetColor();
} else {
append(number);
}
highlightError = false;
}
/**
* Appends a unit in a {@code Unit[…]} element or one of the specialized elements. Specialized elements are
* {@code AngleUnit}, {@code LengthUnit}, {@code ScaleUnit}, {@code ParametricUnit} and {@code TimeUnit}.
* By {@linkplain KeywordStyle#DEFAULT default}, specialized unit keywords are used with the
* {@linkplain Convention#WKT2 WKT 2 convention}.
*
* <div class="note"><b>Example:</b>
* {@code append(Units.KILOMETRE)} will append "{@code LengthUnit["km", 1000]}" to the WKT.</div>
*
* @param unit the unit to append to the WKT, or {@code null} if none.
*
* @see <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#35">WKT 2 specification §7.4</a>
*/
public void append(final Unit<?> unit) {
if (unit != null) {
final boolean isSimplified = (longKeywords == 0) ? convention.isSimplified() : (longKeywords < 0);
final boolean isWKT1 = convention.majorVersion() == 1;
final Unit<?> base = unit.getSystemUnit();
final String keyword;
if (base.equals(Units.METRE)) {
keyword = isSimplified ? WKTKeywords.Unit : WKTKeywords.LengthUnit;
} else if (base.equals(Units.RADIAN)) {
keyword = isSimplified ? WKTKeywords.Unit : WKTKeywords.AngleUnit;
} else if (base.equals(Units.UNITY)) {
keyword = isSimplified ? WKTKeywords.Unit : WKTKeywords.ScaleUnit;
} else if (base.equals(Units.SECOND)) {
keyword = WKTKeywords.TimeUnit; // "Unit" alone is not allowed for time units according ISO 19162.
} else {
keyword = WKTKeywords.ParametricUnit;
}
openElement(false, keyword);
setColor(ElementKind.UNIT);
final int fromIndex = buffer.appendCodePoint(symbols.getOpeningQuote(0)).length();
unitFormat.format(unit, buffer, dummy);
closeQuote(fromIndex);
resetColor();
final double conversion = Units.toStandardUnit(unit);
if (Double.isNaN(conversion) && Units.isAngular(unit)) {
appendExact(Math.PI / 180); // Presume that we have sexagesimal degrees (see below).
} else {
appendExact(conversion);
}
/*
* The EPSG code in UNIT elements is generally not recommended. But we make an exception for sexagesimal
* units (EPSG:9108, 9110 and 9111) because they can not be represented by a simple scale factor in WKT.
* Those units are identified by a conversion factor set to NaN since the conversion is non-linear.
*/
if (convention == Convention.INTERNAL || Double.isNaN(conversion)) {
final Integer code = Units.getEpsgCode(unit, getEnclosingElement(1) instanceof CoordinateSystemAxis);
if (code != null) {
openElement(false, isWKT1 ? WKTKeywords.Authority : WKTKeywords.Id);
append(Constants.EPSG, null);
if (isWKT1) {
append(code.toString(), null);
} else {
append(code);
}
closeElement(false);
}
}
closeElement(false);
/*
* ISO 19162 requires the conversion factor to be positive.
* In addition, keywords other than "Unit" are not valid in WKt 1.
*/
if (!(conversion > 0) || (keyword != WKTKeywords.Unit && isWKT1)) {
setInvalidWKT(Unit.class, null);
}
}
}
/**
* Appends an object or an array of objects.
* This method performs the following choices:
*
* <ul>
* <li>If the given value is {@code null}, then this method appends the "{@code null}" string (without quotes).</li>
* <li>Otherwise if the given value is an array, then this method appends the opening sequence symbol, formats all
* elements by invoking this method recursively, then appends the closing sequence symbol.</li>
* <li>Otherwise if the value type is assignable to the argument type of one of the {@code append(…)} methods
* in this class, then the formatting will be delegated to that method.</li>
* <li>Otherwise the given value is appended as a quoted text with its {@code toString()} representation.</li>
* </ul>
*
* @param value the value to append to the WKT, or {@code null}.
*/
public void appendAny(final Object value) {
if (value == null) {
appendSeparator();
buffer.append("null");
} else if (!appendValue(value) && !appendElement(value)) {
append(value.toString(), null);
}
}
/**
* Tries to append a small unit of information like number, date, boolean, code list, character string
* or an array of those. The key difference between this method and {@link #appendElement(Object)} is
* that the values formatted by this {@code appendValue(Object)} method do not have keyword.
*
* @return {@code true} on success, or {@code false} if the given type is not recognized.
*/
final boolean appendValue(final Object value) {
if (value instanceof Number) {
final Number number = (Number) value;
if (Numbers.isInteger(number.getClass())) {
append(number.longValue());
} else {
append(number.doubleValue());
}
} else if (value instanceof ControlledVocabulary) {
append((ControlledVocabulary) value);
} else if (value instanceof Date) {
append((Date) value);
} else if (value instanceof Boolean) {
append((Boolean) value);
} else if (value instanceof CharSequence) {
append((value instanceof InternationalString) ?
((InternationalString) value).toString(locale) : value.toString(), null);
} else if (value.getClass().isArray()) {
/*
* All above cases delegated to another method which invoke 'appendSeparator()'.
* Since the following block is writing itself a new element, we need to invoke
* 'appendSeparator()' here. This block invokes (indirectly) this 'appendValue'
* method recursively for some or all elements in the list.
*/
appendSeparator();
elementStart = buffer.appendCodePoint(symbols.getOpenSequence()).length();
final int length = Array.getLength(value);
final int cut = (length <= listSizeLimit) ? length : Math.max(listSizeLimit/2 - 1, 1);
for (int i=0; i<length; i++) {
if (i == cut) {
/*
* Skip elements in the middle if the list is too long. The 'cut' index has been computed
* in such a way that the number of elements to skip should be greater than 1, otherwise
* formatting the single missing element would often have been shorter.
*/
final int skip = length - Math.min(2*cut, listSizeLimit);
buffer.append(symbols.getSeparator());
setColor(ElementKind.REMARKS);
buffer.append(Resources.forLocale(locale).getString(Resources.Keys.ElementsOmitted_1, skip));
resetColor();
i += skip;
setInvalidWKT(value.getClass().getSimpleName(), null);
}
appendAny(Array.get(value, i));
}
buffer.appendCodePoint(symbols.getCloseSequence());
} else {
return false;
}
return true;
}
/**
* Tries to append an object of the {@code KEYWORD[something]} form. The given value is typically,
* but not necessarily, a {@link FormattableObject} object or an instance of an interface that can
* be converted to {@code FormattableObject}.
*
* @return {@code true} on success, or {@code false} if the given type is not recognized.
*/
final boolean appendElement(final Object value) {
if (value instanceof FormattableObject) {
append((FormattableObject) value);
} else if (value instanceof IdentifiedObject) {
append(AbstractIdentifiedObject.castOrCopy((IdentifiedObject) value));
} else if (value instanceof MathTransform) {
append((MathTransform) value);
} else if (value instanceof Unit<?>) {
append((Unit<?>) value);
} else if (value instanceof GeographicBoundingBox) {
append((GeographicBoundingBox) value, BBOX_ACCURACY);
} else if (value instanceof VerticalExtent) {
appendVerticalExtent(Extents.getVerticalRange(new SimpleExtent(null, (VerticalExtent) value, null)));
} else if (value instanceof TemporalExtent) {
appendTemporalExtent(Extents.getTimeRange(new SimpleExtent(null, null, (TemporalExtent) value)));
} else if (value instanceof Position) {
append(AbstractDirectPosition.castOrCopy(((Position) value).getDirectPosition()));
} else if (value instanceof Envelope) {
append(AbstractEnvelope.castOrCopy((Envelope) value)); // Non-standard
} else {
return false;
}
return true;
}
/**
* Delegates the formatting to another {@link FormattableObject} implementation.
* Invoking this method is equivalent to first verifying the {@code other} class,
* then delegating as below:
*
* {@preformat java
* return other.formatTo(this);
* }
*
* This method is useful for {@code FormattableObject} which are wrapper around another object.
* It allows to delegate the WKT formatting to the wrapped object.
*
* @param other the object to format with this formatter.
* @return the value returned by {@link FormattableObject#formatTo(Formatter)}.
*
* @since 0.5
*/
public String delegateTo(final Object other) throws UnformattableObjectException {
ArgumentChecks.ensureNonNull("other", other);
if (other instanceof FormattableObject) {
return ((FormattableObject) other).formatTo(this);
}
throw new UnformattableObjectException(Errors.format(
Errors.Keys.IllegalClass_2, FormattableObject.class, other.getClass()));
}
/**
* Returns the enclosing WKT element, or {@code null} if element being formatted is the root.
* This method can be invoked by child elements having some aspects that depend on the enclosing element.
*
* @param depth 1 for the immediate parent, 2 for the parent of the parent, <i>etc.</i>
* @return the parent element at the given depth, or {@code null}.
*/
public FormattableObject getEnclosingElement(int depth) {
ArgumentChecks.ensurePositive("depth", depth);
depth = (enclosingElements.size() - 1) - depth;
return (depth >= 0) ? enclosingElements.get(depth) : null;
}
/**
* Returns {@code true} if the element at the given depth specified a contextual unit.
* This method returns {@code true} if the formattable object given by {@code getEnclosingElement(depth)}
* has invoked {@link #addContextualUnit(Unit)} with a non-null unit at least once.
*
* <div class="note"><b>Note:</b>
* The main purpose of this method is to allow {@code AXIS[…]} elements to determine if they should
* inherit the unit specified by the enclosing CRS, or if they should specify their unit explicitly.</div>
*
* @param depth 1 for the immediate parent, 2 for the parent of the parent, <i>etc.</i>
* @return whether the parent element at the given depth has invoked {@code addContextualUnit(…)} at least once.
*/
public boolean hasContextualUnit(final int depth) {
ArgumentChecks.ensurePositive("depth", depth);
return (hasContextualUnit & Numerics.bitmask(depth)) != 0;
}
/**
* Adds a unit to use for the next measurements of the quantity {@code Q}. The given unit will apply to
* all WKT elements containing a value of quantity {@code Q} without their own {@code UNIT[…]} element,
* until the {@link #restoreContextualUnit(Unit, Unit)} method is invoked.
*
* <p>If the given unit is null, then this method does nothing and returns {@code null}.</p>
*
* <div class="section">Special case</div>
* If the WKT conventions are {@code WKT1_COMMON_UNITS}, then this method ignores the given unit
* and returns {@code null}. See {@link Convention#WKT1_COMMON_UNITS} javadoc for more information.
*
* @param <Q> the unit quantity.
* @param unit the contextual unit to add, or {@code null} if none.
* @return the previous contextual unit for quantity {@code Q}, or {@code null} if none.
*/
@SuppressWarnings("unchecked")
public <Q extends Quantity<Q>> Unit<Q> addContextualUnit(final Unit<Q> unit) {
if (unit == null || convention.usesCommonUnits) {
return null;
}
hasContextualUnit |= 1;
return (Unit<Q>) units.put(unit.getSystemUnit(), unit);
}
/**
* Restores the contextual unit to its previous state before the call to {@link #addContextualUnit(Unit)}.
* This method is used in the following pattern:
*
* {@preformat java
* final Unit<?> previous = formatter.addContextualUnit(unit);
* // ... format some WKT elements here.
* formatter.restoreContextualUnit(unit, previous);
* }
*
* @param unit the value given in argument to {@code addContextualUnit(unit)} (can be {@code null}).
* @param previous the value returned by {@code addContextualUnit(unit)} (can be {@code null}).
* @throws IllegalStateException if this method has not been invoked in the pattern documented above.
*
* @since 0.6
*/
public void restoreContextualUnit(final Unit<?> unit, final Unit<?> previous) {
if (previous == null) {
if (unit != null && units.remove(unit.getSystemUnit()) != unit) {
/*
* The unit that we removed was not the expected one. Probably the user has invoked
* addContextualUnit(…) again without a matching call to restoreContextualUnit(…).
* However this check does not work in Convention.WKT1_COMMON_UNITS mode, since the
* map is always empty in that mode.
*/
if (!convention.usesCommonUnits) {
throw new IllegalStateException();
}
}
hasContextualUnit &= ~1;
} else if (units.put(previous.getSystemUnit(), previous) != unit) {
/*
* The unit that we replaced was not the expected one. Probably the user has invoked
* addContextualUnit(…) again without a matching call to restoreContextualUnit(…).
* Note that this case should never happen in Convention.WKT1_COMMON_UNITS mode,
* since 'previous' should never be non-null in that mode (if the user followed
* the documented pattern).
*/
throw new IllegalStateException();
}
}
/**
* Returns the unit to use instead than the given one, or {@code unit} if there is no replacement.
* This method searches for a unit specified by {@link #addContextualUnit(Unit)}
* which {@linkplain Unit#isCompatible(Unit) is compatible} with the given unit.
*
* @param <Q> the quantity of the unit.
* @param unit the unit to replace by the contextual unit, or {@code null}.
* @return a contextual unit compatible with the given unit, or {@code unit}
* (which may be null) if no contextual unit has been found.
*/
public <Q extends Quantity<Q>> Unit<Q> toContextualUnit(final Unit<Q> unit) {
if (unit != null) {
@SuppressWarnings("unchecked")
final Unit<Q> candidate = (Unit<Q>) units.get(unit.getSystemUnit());
if (candidate != null) {
return candidate;
}
}
return unit;
}
/**
* Returns {@code true} if the WKT written by this formatter is not strictly compliant to the WKT specification.
* This method returns {@code true} if {@link #setInvalidWKT(IdentifiedObject, Exception)} has been invoked at
* least once. The action to take regarding invalid WKT is caller-dependent.
* For example {@link FormattableObject#toString()} will accepts loose WKT formatting and ignore
* this flag, while {@link FormattableObject#toWKT()} requires strict WKT formatting and will
* thrown an exception if this flag is set.
*
* @return {@code true} if the WKT is invalid.
*/
public boolean isInvalidWKT() {
return (warnings != null) || (buffer != null && buffer.length() == 0);
/*
* Note: we really use a "and" condition (not an other "or") for the buffer test because
* the buffer is reset to 'null' by WKTFormat after a successfull formatting.
*/
}
/**
* Returns the object where to store warnings.
*/
private Warnings warnings() {
if (warnings == null) {
warnings = new Warnings(locale, false, Collections.emptyMap());
}
return warnings;
}
/**
* Marks the current WKT representation of the given object as not strictly compliant with the WKT specification.
* This method can be invoked by implementations of {@link FormattableObject#formatTo(Formatter)} when the object
* to format is more complex than what the WKT specification allows.
* Applications can test {@link #isInvalidWKT()} later for checking WKT validity.
*
* @param unformattable the object that can not be formatted,
* @param cause the cause for the failure to format, or {@code null} if the cause is not an exception.
*/
public void setInvalidWKT(final IdentifiedObject unformattable, final Exception cause) {
ArgumentChecks.ensureNonNull("unformattable", unformattable);
String name;
final Identifier id = unformattable.getName();
if (id == null || (name = id.getCode()) == null) {
name = getName(unformattable.getClass());
}
setInvalidWKT(name, cause);
}
/**
* Marks the current WKT representation of the given class as not strictly compliant with the WKT specification.
* This method can be used as an alternative to {@link #setInvalidWKT(IdentifiedObject, Exception)} when the
* problematic object is not an instance of {@code IdentifiedObject}.
*
* @param unformattable the class of the object that can not be formatted,
* @param cause the cause for the failure to format, or {@code null} if the cause is not an exception.
*/
public void setInvalidWKT(final Class<?> unformattable, final Exception cause) {
ArgumentChecks.ensureNonNull("unformattable", unformattable);
setInvalidWKT(getName(unformattable), cause);
}
/**
* Implementation of public {@code setInvalidWKT(…)} methods.
*
* <div class="note"><b>Note:</b> the message is stored as an {@link InternationalString}
* in order to defer the actual message formatting until needed.</div>
*/
private void setInvalidWKT(final String invalidElement, final Exception cause) {
warnings().add(Errors.formatInternational(Errors.Keys.CanNotRepresentInFormat_2, "WKT", invalidElement), cause, null);
highlightError = true;
}
/**
* Returns the name of the GeoAPI interface implemented by the given class.
* If no GeoAPI interface is found, fallback on the class name.
*/
private static String getName(Class<?> unformattable) {
if (!unformattable.isInterface()) {
for (final Class<?> candidate : unformattable.getInterfaces()) {
if (candidate.getName().startsWith("org.opengis.")) {
unformattable = candidate;
break;
}
}
}
return Classes.getShortName(unformattable);
}
/**
* Returns the warnings, or {@code null} if none.
*/
final Warnings getWarnings() {
return warnings;
}
/**
* Appends the warnings after the WKT string. If there is no warnings, then this method does nothing.
* If this method is invoked, then it shall be the last method before {@link #toWKT()}.
*/
final void appendWarnings() throws IOException {
final Warnings warnings = this.warnings; // Protect against accidental changes.
if (warnings != null) {
final StringBuffer buffer = this.buffer;
final String ln = System.lineSeparator();
buffer.append(ln).append(ln);
if (colors != null) {
buffer.append(X364.BACKGROUND_RED.sequence()).append(X364.BOLD.sequence()).append(' ');
}
Vocabulary.getResources(locale).appendLabel(Vocabulary.Keys.Warnings, buffer);
if (colors != null) {
buffer.append(' ').append(X364.RESET.sequence()).append(X364.FOREGROUND_RED.sequence());
}
buffer.append(ln);
final int n = warnings.getNumMessages();
final Set<String> done = new HashSet<>();
for (int i=0; i<n; i++) {
String message = Exceptions.getLocalizedMessage(warnings.getException(i), locale);
if (message == null) {
message = warnings.getMessage(i);
}
if (done.add(message)) {
buffer.append(" • ").append(message).append(ln);
}
}
}
}
/**
* Returns the WKT formatted by this object.
*
* @return the WKT formatted by this formatter.
*/
public String toWKT() {
return buffer.toString();
}
/**
* Returns a string representation of this formatter for debugging purpose.
*
* @return a string representation of this formatter.
*/
@Override
public String toString() {
final StringBuilder b = new StringBuilder(Classes.getShortClassName(this));
String separator = " of ";
for (int i=enclosingElements.size(); --i >= 0;) {
b.append(separator).append(Classes.getShortClassName(enclosingElements.get(i)));
separator = " inside ";
}
return b.toString();
}
/**
* Clears this formatter before formatting a new object.
* This method clears also the {@linkplain #isInvalidWKT() WKT validity flag}.
*/
final void clear() {
/*
* Configuration options (indentation, colors, conventions) are left unchanged.
* We do not mention that fact in the Javadoc because those options do not appear
* in the Formatter public API (they are in the WKTFormat API instead).
*/
if (buffer != null) {
buffer.setLength(0);
}
enclosingElements.clear();
units.clear();
hasContextualUnit = 0;
elementStart = 0;
colorApplied = 0;
margin = 0;
keywordSpaceAt = null;
requestNewLine = false;
isComplement = false;
highlightError = false;
warnings = null;
}
}