use of org.apache.sis.io.TableAppender in project sis by apache.
the class ConsistencyTest method parseAndFormat.
/**
* Formats the given CRS using the given formatter, parses it and reformat again.
* Then the two WKT are compared.
*
* @param f the formatter to use.
* @param code the authority code, used only in case of errors.
* @param crs the CRS to test.
* @return the parsed CRS.
*/
private CoordinateReferenceSystem parseAndFormat(final WKTFormat f, final String code, final CoordinateReferenceSystem crs) {
String wkt = f.format(crs);
final Warnings warnings = f.getWarnings();
if (warnings != null && !warnings.getExceptions().isEmpty()) {
print(code, "WARNING", warnings.getException(0));
}
final CoordinateReferenceSystem parsed;
try {
parsed = (CoordinateReferenceSystem) f.parseObject(wkt);
} catch (ParseException e) {
print(code, "ERROR", "Can not parse the WKT below.");
out.println(wkt);
out.println();
e.printStackTrace(out);
fail(e.getLocalizedMessage());
return null;
}
final String again = f.format(parsed);
final CharSequence[] expectedLines = CharSequences.splitOnEOL(wkt);
final CharSequence[] actualLines = CharSequences.splitOnEOL(again);
/*
* WKT 2 contains a line like below:
*
* METHOD["Transverse Mercator", ID["EPSG", 9807, "8.9"]]
*
* But after parsing, the version number disaspear:
*
* METHOD["Transverse Mercator", ID["EPSG", 9807]]
*
* This is a side effect of the fact that operation method are hard-coded in Java code.
* This is normal for our implementation, so remove the version number from the expected lines.
*/
if (f.getConvention().majorVersion() >= 2) {
for (int i = 0; i < expectedLines.length; i++) {
final CharSequence line = expectedLines[i];
int p = line.length();
int s = CharSequences.skipLeadingWhitespaces(line, 0, p);
if (CharSequences.regionMatches(line, s, "METHOD[\"")) {
assertEquals(code, ',', line.charAt(--p));
assertEquals(code, ']', line.charAt(--p));
assertEquals(code, ']', line.charAt(--p));
if (line.charAt(--p) == '"') {
p = CharSequences.lastIndexOf(line, ',', 0, p);
expectedLines[i] = line.subSequence(0, p) + "]],";
}
}
}
}
/*
* Now compare the WKT line-by-line.
*/
final int length = StrictMath.min(expectedLines.length, actualLines.length);
try {
for (int i = 0; i < length; i++) {
assertEquals(code, expectedLines[i], actualLines[i]);
}
} catch (AssertionError e) {
print(code, "ERROR", "WKT are not equal.");
final TableAppender table = new TableAppender();
table.nextLine('═');
table.setMultiLinesCells(true);
table.append("Original WKT:");
table.nextColumn();
table.append("CRS parsed from the WKT:");
table.nextLine();
table.appendHorizontalSeparator();
table.append(wkt);
table.nextColumn();
table.append(again);
table.nextLine();
table.nextLine('═');
out.println(table);
throw e;
}
assertEquals("Unexpected number of lines.", expectedLines.length, actualLines.length);
return parsed;
}
use of org.apache.sis.io.TableAppender in project sis by apache.
the class MathTransformTestCase method printInternalWKT.
/**
* Prints the current {@linkplain #transform transform} as normal and internal WKT.
* This method is for debugging purpose only.
*/
@Debug
protected final void printInternalWKT() {
@SuppressWarnings("UseOfSystemOutOrSystemErr") final TableAppender table = new TableAppender(System.out);
table.setMultiLinesCells(true);
table.appendHorizontalSeparator();
table.append("WKT of “").append(getName()).append('”').nextColumn();
table.append("Internal WKT").appendHorizontalSeparator();
String wkt;
try {
wkt = transform.toWKT();
} catch (UnsupportedOperationException e) {
wkt = transform.toString();
}
table.append(wkt).nextColumn();
if (transform instanceof FormattableObject) {
wkt = ((FormattableObject) transform).toString(Convention.INTERNAL);
} else {
wkt = transform.toString();
}
table.append(wkt).appendHorizontalSeparator();
try {
table.flush();
} catch (IOException e) {
throw new AssertionError(e);
}
}
use of org.apache.sis.io.TableAppender in project sis by apache.
the class FeatureFormat method format.
/**
* Formats the given object to the given stream of buffer.
* The object may be an instance of any of the following types:
*
* <ul>
* <li>{@code Feature}</li>
* <li>{@code FeatureType}</li>
* </ul>
*
* @throws IOException if an error occurred while writing to the given appendable.
*/
@Override
public void format(final Object object, final Appendable toAppendTo) throws IOException {
ArgumentChecks.ensureNonNull("object", object);
ArgumentChecks.ensureNonNull("toAppendTo", toAppendTo);
/*
* Separate the Feature (optional) and the FeatureType (mandatory) instances.
*/
final DefaultFeatureType featureType;
final AbstractFeature feature;
if (object instanceof AbstractFeature) {
feature = (AbstractFeature) object;
featureType = feature.getType();
} else if (object instanceof DefaultFeatureType) {
featureType = (DefaultFeatureType) object;
feature = null;
} else {
throw new IllegalArgumentException(Errors.getResources(displayLocale).getString(Errors.Keys.UnsupportedType_1, object.getClass()));
}
/*
* Computes the columns to show. We start with the set of columns specified by setAllowedColumns(Set),
* then we check if some of those columns are empty. For example in many cases there is no attribute
* with characteritic, in which case we will ommit the whole "characteristics" column. We perform such
* check only for optional information, not for mandatory information like property names.
*/
final EnumSet<Column> visibleColumns = columns.clone();
{
boolean hasDesignation = false;
boolean hasCharacteristics = false;
boolean hasDeprecatedTypes = false;
for (final AbstractIdentifiedType propertyType : featureType.getProperties(true)) {
if (!hasDesignation) {
hasDesignation = propertyType.getDesignation() != null;
}
if (!hasCharacteristics && propertyType instanceof DefaultAttributeType<?>) {
hasCharacteristics = !((DefaultAttributeType<?>) propertyType).characteristics().isEmpty();
}
if (!hasDeprecatedTypes && propertyType instanceof Deprecable) {
hasDeprecatedTypes = ((Deprecable) propertyType).isDeprecated();
}
}
if (!hasDesignation)
visibleColumns.remove(Column.DESIGNATION);
if (!hasCharacteristics)
visibleColumns.remove(Column.CHARACTERISTICS);
if (!hasDeprecatedTypes)
visibleColumns.remove(Column.REMARKS);
}
/*
* Format the feature type name. In the case of feature type, format also the names of super-type
* after the UML symbol for inheritance (an arrow with white head). We do not use the " : " ASCII
* character for avoiding confusion with the ":" separator in namespaces. After the feature (type)
* name, format the column header: property name, type, cardinality and (default) value.
*/
toAppendTo.append(toString(featureType.getName()));
if (feature == null) {
// UML symbol for inheritance.
String separator = " ⇾ ";
for (final FeatureType parent : featureType.getSuperTypes()) {
toAppendTo.append(separator).append(toString(parent.getName()));
separator = SEPARATOR;
}
}
toAppendTo.append(getLineSeparator());
/*
* Create a table and format the header. Columns will be shown in Column enumeration order.
*/
final Vocabulary resources = Vocabulary.getResources(displayLocale);
final TableAppender table = new TableAppender(toAppendTo, columnSeparator);
table.setMultiLinesCells(true);
table.nextLine('─');
boolean isFirstColumn = true;
for (final Column column : visibleColumns) {
short key = column.resourceKey;
if (key == Vocabulary.Keys.Value && feature == null) {
key = Vocabulary.Keys.DefaultValue;
}
if (!isFirstColumn)
nextColumn(table);
table.append(resources.getString(key));
isFirstColumn = false;
}
table.nextLine();
table.nextLine('─');
/*
* Done writing the header. Now write all property rows. For each row, the first part in the loop
* extracts all information needed without formatting anything yet. If we detect in that part that
* a row has no value, it will be skipped if and only if that row is optional (minimum occurrence
* of zero).
*/
final StringBuffer buffer = new StringBuffer();
final FieldPosition dummyFP = new FieldPosition(-1);
final List<String> remarks = new ArrayList<>();
for (final AbstractIdentifiedType propertyType : featureType.getProperties(true)) {
Object value = null;
int cardinality = -1;
if (feature != null) {
if (!(propertyType instanceof DefaultAttributeType<?>) && !(propertyType instanceof DefaultAssociationRole) && !DefaultFeatureType.isParameterlessOperation(propertyType)) {
continue;
}
value = feature.getPropertyValue(propertyType.getName().toString());
if (value == null) {
if (propertyType instanceof FieldType && ((FieldType) propertyType).getMinimumOccurs() == 0) {
// If optional and no value, skip the full row.
continue;
}
cardinality = 0;
} else if (value instanceof Collection<?>) {
cardinality = ((Collection<?>) value).size();
} else {
cardinality = 1;
}
} else if (propertyType instanceof DefaultAttributeType<?>) {
value = ((DefaultAttributeType<?>) propertyType).getDefaultValue();
} else if (propertyType instanceof AbstractOperation) {
buffer.append(" = ");
try {
((AbstractOperation) propertyType).formatResultFormula(buffer);
} catch (IOException e) {
// Should never happen since we write in a StringBuffer.
throw new UncheckedIOException(e);
}
value = CharSequences.trimWhitespaces(buffer).toString();
buffer.setLength(0);
}
// The value to write in the type column.
final String valueType;
// AttributeType.getValueClass() if applicable.
final Class<?> valueClass;
// Negative values mean no cardinality.
final int minimumOccurs, maximumOccurs;
// Result of operation if applicable.
final AbstractIdentifiedType resultType;
if (propertyType instanceof AbstractOperation) {
// May be null
resultType = ((AbstractOperation) propertyType).getResult();
} else {
resultType = propertyType;
}
if (resultType instanceof DefaultAttributeType<?>) {
final DefaultAttributeType<?> pt = (DefaultAttributeType<?>) resultType;
minimumOccurs = pt.getMinimumOccurs();
maximumOccurs = pt.getMaximumOccurs();
valueClass = pt.getValueClass();
valueType = getFormat(Class.class).format(valueClass, buffer, dummyFP).toString();
buffer.setLength(0);
} else if (resultType instanceof DefaultAssociationRole) {
final DefaultAssociationRole pt = (DefaultAssociationRole) resultType;
minimumOccurs = pt.getMinimumOccurs();
maximumOccurs = pt.getMaximumOccurs();
valueType = toString(DefaultAssociationRole.getValueTypeName(pt));
valueClass = AbstractFeature.class;
} else {
valueType = (resultType != null) ? toString(resultType.getName()) : "";
valueClass = null;
minimumOccurs = -1;
maximumOccurs = -1;
}
/*
* At this point we determined that the row should not be skipped
* and we got all information to format.
*/
isFirstColumn = true;
for (final Column column : visibleColumns) {
if (!isFirstColumn)
nextColumn(table);
isFirstColumn = false;
switch(column) {
/*
* Human-readable name of the property. May contains any characters (spaces, ideographs, etc).
* In many cases, this information is not provided and the whole column is skipped.
*/
case DESIGNATION:
{
final InternationalString d = propertyType.getDesignation();
if (d != null)
table.append(d.toString(displayLocale));
break;
}
/*
* Machine-readable name of the property (identifier). This information is mandatory.
* This name is usually shorter than the designation and should contain only valid
* Unicode identifier characters (e.g. no spaces).
*/
case NAME:
{
table.append(toString(propertyType.getName()));
break;
}
/*
* The base class or interface for all values in properties of the same type.
* This is typically String, Number, Integer, Geometry or URL.
*/
case TYPE:
{
table.append(valueType);
break;
}
/*
* Minimum and maximum number of occurrences allowed for this property.
* If we are formatting a Feature instead than a FeatureType, then the
* actual number of values is also formatted. Example: 42 ∈ [0 … ∞]
*/
case CARDINALITY:
{
table.setCellAlignment(TableAppender.ALIGN_RIGHT);
if (cardinality >= 0) {
table.append(getFormat(Integer.class).format(cardinality, buffer, dummyFP));
buffer.setLength(0);
}
if (maximumOccurs >= 0) {
if (cardinality >= 0) {
table.append(' ').append((cardinality >= minimumOccurs && cardinality <= maximumOccurs) ? '∈' : '∉').append(' ');
}
final Format format = getFormat(Integer.class);
table.append('[').append(format.format(minimumOccurs, buffer, dummyFP)).append(" … ");
buffer.setLength(0);
if (maximumOccurs != Integer.MAX_VALUE) {
table.append(format.format(maximumOccurs, buffer, dummyFP));
} else {
table.append('∞');
}
buffer.setLength(0);
table.append(']');
}
break;
}
/*
* If formatting a FeatureType, the default value. If formatting a Feature, the actual value.
* A java.text.Format instance dedicated to the value class is used if possible. In addition
* to types for which a java.text.Format may be available, we also have to check for other
* special cases. If there is more than one value, they are formatted as a coma-separated list.
*/
case VALUE:
{
table.setCellAlignment(TableAppender.ALIGN_LEFT);
// Null if valueClass is null.
final Format format = getFormat(valueClass);
final Iterator<?> it = CollectionsExt.toCollection(value).iterator();
String separator = "";
int length = 0;
while (it.hasNext()) {
value = it.next();
if (value != null) {
if (propertyType instanceof DefaultAssociationRole) {
final String p = DefaultAssociationRole.getTitleProperty((DefaultAssociationRole) propertyType);
if (p != null) {
value = ((AbstractFeature) value).getPropertyValue(p);
if (value == null)
continue;
}
} else if (format != null && valueClass.isInstance(value)) {
// Null safe because of getFormat(valueClass) contract.
/*
* Convert numbers, dates, angles, etc. to character sequences before to append them in the table.
* Note that DecimalFormat writes Not-a-Number as "NaN" in some locales and as "�" in other locales
* (U+FFFD - Unicode replacement character). The "�" seems to be used mostly for historical reasons;
* as of 2017 the Unicode Common Locale Data Repository (CLDR) seems to define "NaN" for all locales.
* We could configure DecimalFormatSymbols for using "NaN", but (for now) we rather substitute "�" by
* "NaN" here for avoiding to change the DecimalFormat configuration and for distinguishing the NaNs.
*/
final StringBuffer t = format.format(value, buffer, dummyFP);
if (value instanceof Number) {
final float f = ((Number) value).floatValue();
if (Float.isNaN(f)) {
if ("�".contentEquals(t)) {
t.setLength(0);
t.append("NaN");
}
try {
final int n = MathFunctions.toNanOrdinal(f);
if (n > 0)
t.append(" #").append(n);
} catch (IllegalArgumentException e) {
// May happen if the NaN is a signaling NaN instead than a quiet NaN.
final int bits = Float.floatToRawIntBits(f);
if (bits != illegalNaN) {
illegalNaN = bits;
Logging.recoverableException(Logging.getLogger(Modules.FEATURE), FeatureFormat.class, "format", e);
}
}
}
}
value = t;
}
/*
* All values: the numbers, dates, angles, etc. formatted above, any other character sequences
* (e.g. InternationalString), or other kind of values - some of them handled in a special way.
*/
length = formatValue(value, table.append(separator), length);
buffer.setLength(0);
// Value is too long, abandon remaining iterations.
if (length < 0)
break;
separator = SEPARATOR;
length += SEPARATOR.length();
}
}
break;
}
/*
* Characteristics are optional information attached to some values. For example if a property
* value is a temperature measurement, a characteritic of that value may be the unit of measure.
* Characteristics are handled as "attributes of attributes".
*/
case CHARACTERISTICS:
{
if (propertyType instanceof DefaultAttributeType<?>) {
int length = 0;
String separator = "";
format: for (final DefaultAttributeType<?> ct : ((DefaultAttributeType<?>) propertyType).characteristics().values()) {
/*
* Format the characteristic name. We will append the value(s) later.
* We keep trace of the text length in order to stop formatting if the
* text become too long.
*/
final GenericName cn = ct.getName();
final String cs = toString(cn);
table.append(separator).append(cs);
length += separator.length() + cs.length();
Collection<?> cv = CollectionsExt.singletonOrEmpty(ct.getDefaultValue());
if (feature != null) {
/*
* Usually, the property 'cp' below is null because all features use the same
* characteristic value (for example the same unit of measurement), which is
* given by the default value 'cv'. Nevertheless we have to check if current
* feature overrides this characteristic.
*/
final Object cp = feature.getProperty(propertyType.getName().toString());
if (cp instanceof AbstractAttribute<?>) {
// Should always be true, but we are paranoiac.
AbstractAttribute<?> ca = ((AbstractAttribute<?>) cp).characteristics().get(cn.toString());
if (ca != null)
cv = ca.getValues();
}
}
/*
* Now format the value, separated from the name with " = ". Example: unit = m/s
* If the value accepts multi-occurrences, we will format the value between {…}.
* We use {…} because we may have more than one characteristic in the same cell,
* so we need a way to distinguish multi-values from multi-characteristics.
*/
final boolean multi = ct.getMaximumOccurs() > 1;
String sep = multi ? " = {" : " = ";
for (Object c : cv) {
length = formatValue(c, table.append(sep), length += sep.length());
// Value is too long, abandon remaining iterations.
if (length < 0)
break format;
sep = SEPARATOR;
}
separator = SEPARATOR;
if (multi && sep == SEPARATOR) {
table.append('}');
}
}
}
break;
}
case REMARKS:
{
if (org.apache.sis.feature.Field.isDeprecated(propertyType)) {
table.append(resources.getString(Vocabulary.Keys.Deprecated));
final InternationalString r = ((Deprecable) propertyType).getRemarks();
if (r != null) {
remarks.add(r.toString(displayLocale));
appendSuperscript(remarks.size(), table);
}
}
break;
}
}
}
table.nextLine();
}
table.nextLine('─');
table.flush();
/*
* If there is any remarks, write them below the table.
*/
final int n = remarks.size();
for (int i = 0; i < n; i++) {
appendSuperscript(i + 1, toAppendTo);
toAppendTo.append(' ').append(remarks.get(i)).append(lineSeparator);
}
}
use of org.apache.sis.io.TableAppender in project sis by apache.
the class LocationFormat method format.
/**
* Writes a textual representation of the given location in the given stream or buffer.
*
* <div class="warning"><b>Upcoming API change — generalization</b><br>
* in a future SIS version, the type of {@code location} parameter may be generalized
* to the {@code org.opengis.referencing.gazetteer.Location} interface.
* This change is pending GeoAPI revision.</div>
*
* @param location the location to format.
* @param toAppendTo where to format the location.
* @throws IOException if an error occurred while writing to the given appendable.
*/
@Override
@SuppressWarnings({ "fallthrough", "null" })
public void format(final AbstractLocation location, final Appendable toAppendTo) throws IOException {
ArgumentChecks.ensureNonNull("location", location);
final Locale locale = getLocale(Locale.Category.DISPLAY);
final Vocabulary vocabulary = Vocabulary.getResources(locale);
final TableAppender table = new TableAppender(toAppendTo, "│ ", columnSeparator, " │");
table.setMultiLinesCells(true);
/*
* Location type.
*/
table.appendHorizontalSeparator();
final AbstractLocationType type = location.type();
if (type != null) {
append(table, vocabulary, Vocabulary.Keys.LocationType, toString(type.getName(), locale));
}
/*
* Geographic identifier and alternative identifiers, if any.
*/
append(table, vocabulary, Vocabulary.Keys.GeographicIdentifier, toString(location.getGeographicIdentifier(), locale));
final Collection<? extends InternationalString> alt = location.getAlternativeGeographicIdentifiers();
if (alt != null && !alt.isEmpty()) {
boolean isFirst = true;
vocabulary.appendLabel(Vocabulary.Keys.AlternativeIdentifiers, table);
nextColumn(table);
for (final InternationalString id : alt) {
if (!isFirst) {
isFirst = false;
table.append(lineSeparator);
}
table.append(id);
}
table.nextLine();
}
/*
* Extents (temporal and geographic). If an envelope exists and the CRS is not geographic,
* then the envelope bounds will be appended on the same lines than the geographic bounds.
* But before writing the bounding box and/or the envelope, check if they are redundant.
* We may also need to change axis order (but not unit) of the envelope in order to match
* the axis order of the geographic bounding box.
*/
final Extent extent = new DefaultExtent(null, location.getGeographicExtent(), null, location.getTemporalExtent());
final Range<Date> time = Extents.getTimeRange(extent);
if (time != null) {
append(table, vocabulary, Vocabulary.Keys.StartDate, toString(time.getMinValue()));
append(table, vocabulary, Vocabulary.Keys.EndDate, toString(time.getMaxValue()));
}
GeographicBoundingBox bbox = Extents.getGeographicBoundingBox(extent);
Envelope envelope = location.getEnvelope();
DirectPosition position = position(location.getPosition());
// Position in geographic CRS.
DirectPosition geopos = null;
// Envelope Coordinate Reference System.
CoordinateReferenceSystem crs = null;
// CRS in conventional (x,y) axis order.
CoordinateReferenceSystem normCRS = null;
// If failed to transform envelope.
Exception warning = null;
try {
if (envelope != null) {
normCRS = normalize(crs = envelope.getCoordinateReferenceSystem());
if (normCRS != crs) {
// Should only change order and sign.
envelope = Envelopes.transform(envelope, normCRS);
}
}
if (position != null) {
/*
* If only one of the envelope or the position objects specify a CRS, assume that the other object
* use the same CRS. If both the envelope and the position objects specify a CRS, the envelope CRS
* will have precedence and the "representative position" will be projected to that CRS.
*/
final CoordinateReferenceSystem posCRS = position.getCoordinateReferenceSystem();
if (normCRS == null) {
normCRS = normalize(crs = posCRS);
if (normCRS != crs) {
// Should only change order and sign.
envelope = Envelopes.transform(envelope, normCRS);
}
}
if (bbox != null) {
// Compute geographic position only if there is a geographic bounding box.
GeographicCRS geogCRS = ReferencingUtilities.toNormalizedGeographicCRS(posCRS);
if (geogCRS != null) {
geopos = transform(position, posCRS, geogCRS);
}
}
position = transform(position, posCRS, normCRS);
}
} catch (FactoryException | TransformException e) {
envelope = null;
position = null;
warning = e;
}
/*
* At this point we got the final geographic bounding box and/or envelope to write.
* Since we will write the projected and geographic coordinates side-by-side in the same cells,
* we need to format them in advance so we can compute their width for internal right-alignment.
* We do the alignment ourselves instead than using TableAppender.setCellAlignment(ALIGN_RIGHT)
* because we do not want (projected geographic) tuple to appear far on the right side if other
* cells have long texts.
*/
if (bbox != null || envelope != null) {
final CoordinateSystem cs = (crs != null) ? crs.getCoordinateSystem() : null;
String[] geographic = null;
String[] projected = null;
String[] unitSymbol = null;
AngleFormat geogFormat = null;
NumberFormat projFormat = null;
UnitFormat unitFormat = null;
int maxGeogLength = 0;
int maxProjLength = 0;
int maxUnitLength = 0;
boolean showProj = false;
if (bbox != null || geopos != null) {
geogFormat = (AngleFormat) getFormat(Angle.class);
geographic = new String[BOUND_KEY.length];
Arrays.fill(geographic, "");
}
if (envelope != null || position != null) {
projFormat = (NumberFormat) getFormat(Number.class);
unitFormat = (UnitFormat) getFormat(Unit.class);
projected = new String[BOUND_KEY.length];
unitSymbol = new String[BOUND_KEY.length];
Arrays.fill(projected, "");
Arrays.fill(unitSymbol, "");
}
for (int i = 0; i < BOUND_KEY.length; i++) {
RoundingMode rounding = RoundingMode.FLOOR;
double g = Double.NaN;
double p = Double.NaN;
int dimension = 0;
switch(i) {
case 0:
if (bbox != null)
g = bbox.getWestBoundLongitude();
if (envelope != null)
p = envelope.getMinimum(0);
break;
case 2:
if (bbox != null)
g = bbox.getEastBoundLongitude();
if (envelope != null)
p = envelope.getMaximum(0);
rounding = RoundingMode.CEILING;
break;
case 3:
if (bbox != null)
g = bbox.getSouthBoundLatitude();
if (envelope != null)
p = envelope.getMinimum(1);
dimension = 1;
break;
case 5:
if (bbox != null)
g = bbox.getNorthBoundLatitude();
if (envelope != null)
p = envelope.getMaximum(1);
rounding = RoundingMode.CEILING;
dimension = 1;
break;
// Fall through
case 4:
dimension = 1;
case 1:
if (geopos != null)
g = geopos.getOrdinate(dimension);
if (position != null)
p = position.getOrdinate(dimension);
rounding = RoundingMode.HALF_EVEN;
break;
}
if (!Double.isNaN(p)) {
showProj |= (g != p);
if (cs != null) {
final Unit<?> unit = cs.getAxis(dimension).getUnit();
if (unit != null) {
final int length = (unitSymbol[i] = unitFormat.format(unit)).length();
if (length > maxUnitLength) {
maxUnitLength = length;
}
}
}
try {
projFormat.setRoundingMode(rounding);
} catch (UnsupportedOperationException e) {
// Ignore.
}
final int length = (projected[i] = projFormat.format(p)).length();
if (length > maxProjLength) {
maxProjLength = length;
}
}
if (!Double.isNaN(g)) {
geogFormat.setRoundingMode(rounding);
final Angle angle = (dimension == 0) ? new Longitude(g) : new Latitude(g);
final int length = (geographic[i] = geogFormat.format(angle)).length();
if (length > maxGeogLength) {
maxGeogLength = length;
}
}
}
if (!showProj) {
// All projected coordinates are identical to geographic ones.
projected = null;
unitSymbol = null;
maxProjLength = 0;
maxUnitLength = 0;
} else if (maxProjLength != 0) {
if (maxUnitLength != 0) {
maxUnitLength++;
}
// Arbitrary space between projected and geographic coordinates.
maxGeogLength += 4;
}
/*
* At this point all coordinates have been formatted in advance.
*/
final String separator = (projected != null && geographic != null) ? " —" : "";
for (int i = 0; i < BOUND_KEY.length; i++) {
final String p = (projected != null) ? projected[i] : "";
final String u = (unitSymbol != null) ? unitSymbol[i] : "";
final String g = (geographic != null) ? geographic[i] : "";
if (!p.isEmpty() || !g.isEmpty()) {
vocabulary.appendLabel(BOUND_KEY[i], table);
nextColumn(table);
table.append(CharSequences.spaces(maxProjLength - p.length())).append(p);
table.append(CharSequences.spaces(maxUnitLength - u.length())).append(u).append(separator);
table.append(CharSequences.spaces(maxGeogLength - g.length())).append(g);
table.nextLine();
}
}
}
if (crs != null) {
append(table, vocabulary, Vocabulary.Keys.CoordinateRefSys, IdentifiedObjects.getName(crs, null));
}
/*
* Organization responsible for defining the characteristics of the location instance.
*/
final AbstractParty administrator = location.getAdministrator();
if (administrator != null) {
append(table, vocabulary, Vocabulary.Keys.Administrator, toString(administrator.getName(), locale));
}
table.appendHorizontalSeparator();
table.flush();
if (warning != null) {
vocabulary.appendLabel(Vocabulary.Keys.Warnings, toAppendTo);
toAppendTo.append(warning.toString()).append(lineSeparator);
}
}
use of org.apache.sis.io.TableAppender in project sis by apache.
the class ParameterFormat method format.
/**
* Implementation of public {@code format(…)} methods for all content levels except {@code NAME_SUMMARY}.
*
* @param name the group name, usually {@code descriptor.getName().getCode()}.
* @param group the parameter descriptor, usually {@code values.getDescriptor()}.
* @param values the parameter values, or {@code null} if none.
* @throws IOException if an error occurred while writing to the given appendable.
*/
private void format(final String name, final ParameterDescriptorGroup group, final ParameterValueGroup values, final Appendable out) throws IOException {
final boolean isBrief = (contentLevel == ContentLevel.BRIEF);
final boolean showObligation = !isBrief || (values == null);
final boolean hasColors = (colors != null);
final String lineSeparator = this.lineSeparator;
final Map<String, Integer> remarks = new LinkedHashMap<>();
final ParameterTableRow header = new ParameterTableRow(group, displayLocale, preferredCodespaces, remarks, isBrief);
final String groupCodespace = header.getCodeSpace();
/*
* Prepares the informations to be printed later as table rows. We scan all rows before to print them
* in order to compute the width of codespaces. During this process, we split the objects to be printed
* later in two collections: simple parameters are stored as (descriptor,value) pairs, while groups are
* stored in an other collection for deferred formatting after the simple parameters.
*/
int codespaceWidth = 0;
final Collection<?> elements = (values != null) ? values.values() : group.descriptors();
final Map<GeneralParameterDescriptor, ParameterTableRow> descriptorValues = new LinkedHashMap<>(hashMapCapacity(elements.size()));
// To be created only if needed (it is usually not).
List<Object> deferredGroups = null;
for (final Object element : elements) {
final GeneralParameterValue parameter;
final GeneralParameterDescriptor descriptor;
if (values != null) {
parameter = (GeneralParameterValue) element;
descriptor = parameter.getDescriptor();
} else {
parameter = null;
descriptor = (GeneralParameterDescriptor) element;
}
if (descriptor instanceof ParameterDescriptorGroup) {
if (deferredGroups == null) {
deferredGroups = new ArrayList<>(4);
}
deferredGroups.add(element);
continue;
}
/*
* In the vast majority of cases, there is only one value for each parameter. However
* if we find more than one value, we will append all extra occurrences in a "multiple
* values" list to be formatted in the same row.
*/
Object value = null;
Unit<?> unit = null;
if (parameter instanceof ParameterValue<?>) {
final ParameterValue<?> p = (ParameterValue<?>) parameter;
value = p.getValue();
unit = p.getUnit();
} else if (descriptor instanceof ParameterDescriptor<?>) {
final ParameterDescriptor<?> p = (ParameterDescriptor<?>) descriptor;
value = p.getDefaultValue();
unit = p.getUnit();
}
ParameterTableRow row = descriptorValues.get(descriptor);
if (row == null) {
row = new ParameterTableRow(descriptor, displayLocale, preferredCodespaces, remarks, isBrief);
descriptorValues.put(descriptor, row);
if (row.codespaceWidth > codespaceWidth) {
codespaceWidth = row.codespaceWidth;
}
}
row.addValue(value, unit);
}
/*
* Finished to collect the values. Now transform the values:
*
* - Singleton value of array types (either primitive or not) are wrapped into a list.
* - Values are formatted.
* - Value domains are formatted.
* - Position of the character on which to do the alignment are remembered.
*/
int unitWidth = 0;
int valueDomainAlignment = 0;
boolean writeCodespaces = (groupCodespace == null);
final StringBuffer buffer = new StringBuffer();
final FieldPosition dummyFP = new FieldPosition(-1);
for (final Map.Entry<GeneralParameterDescriptor, ParameterTableRow> entry : descriptorValues.entrySet()) {
final GeneralParameterDescriptor descriptor = entry.getKey();
if (descriptor instanceof ParameterDescriptor<?>) {
final ParameterTableRow row = entry.getValue();
/*
* Verify if all rows use the same codespace than the header, in which case we can omit
* row codespace formatting.
*/
if (!writeCodespaces && !groupCodespace.equals(entry.getValue().getCodeSpace())) {
writeCodespaces = true;
}
/*
* Format the value domain, so we can compute the character position on which to perform alignment.
*/
final Range<?> valueDomain = Parameters.getValueDomain((ParameterDescriptor<?>) descriptor);
if (valueDomain != null) {
final int p = row.setValueDomain(valueDomain, getFormat(Range.class), buffer);
if (p > valueDomainAlignment) {
valueDomainAlignment = p;
}
}
/*
* Singleton array conversion. Because it may be an array of primitive types, we can not just
* cast to Object[]. Then formats the units, with a space before the unit if the symbol is a
* letter or digit (i.e. we do not put a space in front of ° symbol for instance).
*/
row.expandSingleton();
final int length = row.units.size();
for (int i = 0; i < length; i++) {
final Object unit = row.units.get(i);
if (unit != null) {
if (getFormat(Unit.class).format(unit, buffer, dummyFP).length() != 0) {
if (Character.isLetterOrDigit(buffer.codePointAt(0))) {
buffer.insert(0, ' ');
}
}
final String symbol = buffer.toString();
row.units.set(i, symbol);
buffer.setLength(0);
final int p = symbol.length();
if (p > unitWidth) {
unitWidth = p;
}
}
}
}
}
/*
* Finished to prepare information. Now begin the actual writing.
* First, formats the table header (i.e. the column names).
*/
final Vocabulary resources = Vocabulary.getResources(displayLocale);
header.writeIdentifiers(out, true, colors, false, lineSeparator);
out.append(lineSeparator);
final char horizontalBorder = isBrief ? '─' : '═';
final TableAppender table = (isBrief || !columnSeparator.equals(SEPARATOR)) ? new TableAppender(out, columnSeparator) : new TableAppender(out);
table.setMultiLinesCells(true);
table.nextLine(horizontalBorder);
int numColumnsBeforeValue = 0;
for (int i = 0; ; i++) {
boolean end = false;
final short key;
switch(i) {
case 0:
{
key = Vocabulary.Keys.Name;
break;
}
case 1:
{
key = Vocabulary.Keys.Type;
break;
}
case 2:
{
if (!showObligation) {
continue;
}
key = Vocabulary.Keys.Obligation;
break;
}
case 3:
{
key = Vocabulary.Keys.ValueDomain;
break;
}
case 4:
{
key = (values == null) ? Vocabulary.Keys.DefaultValue : Vocabulary.Keys.Value;
end = true;
break;
}
default:
throw new AssertionError(i);
}
if (hasColors)
table.append(X364.BOLD.sequence());
table.append(resources.getString(key));
if (hasColors)
table.append(X364.NORMAL.sequence());
if (!writeCodespaces && i == 0) {
table.append(" (").append(groupCodespace).append(')');
}
if (end)
break;
nextColumn(table);
numColumnsBeforeValue++;
}
table.nextLine();
/*
* Now process to the formatting of (descriptor,value) pairs. Each descriptor's alias
* will be formatted on its own line in a table row. If there is more than one value,
* then each value will be formatted on its own line as well. Note that the values may
* be null if there is none.
*/
char horizontalLine = horizontalBorder;
for (final Map.Entry<GeneralParameterDescriptor, ParameterTableRow> entry : descriptorValues.entrySet()) {
if (horizontalLine != 0) {
table.nextLine('─');
}
horizontalLine = isBrief ? 0 : '─';
final ParameterTableRow row = entry.getValue();
row.codespaceWidth = codespaceWidth;
row.writeIdentifiers(table, writeCodespaces, null, hasColors, lineSeparator);
nextColumn(table);
final GeneralParameterDescriptor generalDescriptor = entry.getKey();
if (generalDescriptor instanceof ParameterDescriptor<?>) {
/*
* Writes value type.
*/
final ParameterDescriptor<?> descriptor = (ParameterDescriptor<?>) generalDescriptor;
final Class<?> valueClass = descriptor.getValueClass();
if (valueClass != null) {
// Should never be null, but let be safe.
table.append(getFormat(Class.class).format(valueClass, buffer, dummyFP).toString());
}
nextColumn(table);
buffer.setLength(0);
/*
* Writes the obligation (mandatory or optional).
*/
if (showObligation) {
final int minimumOccurs = descriptor.getMinimumOccurs();
final int maximumOccurs = descriptor.getMaximumOccurs();
if (maximumOccurs == 1) {
table.append(resources.getString(minimumOccurs == 0 ? Vocabulary.Keys.Optional : Vocabulary.Keys.Mandatory));
} else {
final Format f = getFormat(Integer.class);
table.append(f.format(minimumOccurs, buffer, dummyFP).toString()).append(" … ");
buffer.setLength(0);
if (maximumOccurs == Integer.MAX_VALUE) {
table.append('∞');
} else {
table.append(f.format(maximumOccurs, buffer, dummyFP).toString());
buffer.setLength(0);
}
}
nextColumn(table);
}
/*
* Writes minimum and maximum values, together with the unit of measurement (if any).
*/
final String valueDomain = row.valueDomain;
if (valueDomain != null) {
table.append(CharSequences.spaces(valueDomainAlignment - row.valueDomainAlignment)).append(valueDomain);
}
nextColumn(table);
/*
* Writes the values, each on its own line, together with their unit of measurement.
*/
final byte alignment = Number.class.isAssignableFrom(valueClass) ? TableAppender.ALIGN_RIGHT : TableAppender.ALIGN_LEFT;
table.setCellAlignment(alignment);
final int length = row.values.size();
for (int i = 0; i < length; i++) {
Object value = row.values.get(i);
if (value != null) {
if (i != 0) {
/*
* If the same parameter is repeated more than once (not allowed by ISO 19111,
* but this extra flexibility is allowed by Apache SIS), write the ditto mark
* in all previous columns (name, type, etc.) on a new row.
*/
final String ditto = resources.getString(Vocabulary.Keys.DittoMark);
table.nextLine();
table.setCellAlignment(TableAppender.ALIGN_CENTER);
for (int j = 0; j < numColumnsBeforeValue; j++) {
table.append(ditto);
nextColumn(table);
}
table.setCellAlignment(alignment);
}
/*
* Format the value followed by the unit of measure, or followed by spaces if there is no unit
* for this value. The intent is the right align the numerical value rather than the numerical
* + unit tupple.
*/
final Format format = getFormat(value.getClass());
if (format != null) {
if (format instanceof NumberFormat && value instanceof Number) {
configure((NumberFormat) format, Math.abs(((Number) value).doubleValue()));
}
value = format.format(value, buffer, dummyFP);
}
table.append(value.toString());
buffer.setLength(0);
int pad = unitWidth;
final String unit = (String) row.units.get(i);
if (unit != null) {
table.append(unit);
pad -= unit.length();
}
table.append(CharSequences.spaces(pad));
}
}
}
table.nextLine();
table.setCellAlignment(TableAppender.ALIGN_LEFT);
}
table.nextLine(horizontalBorder);
table.flush();
/*
* Write remarks, if any.
*/
for (final Map.Entry<String, Integer> remark : remarks.entrySet()) {
ParameterTableRow.writeFootnoteNumber(out, remark.getValue());
out.append(' ').append(remark.getKey()).append(lineSeparator);
}
/*
* Now formats all groups deferred to the end of this table, with recursive calls to
* this method (recursive calls use their own TableWriter instance, so they may result
* in a different cell layout). Most of the time, there is no such additional group.
*/
if (deferredGroups != null) {
for (final Object element : deferredGroups) {
final ParameterValueGroup value;
final ParameterDescriptorGroup descriptor;
if (element instanceof ParameterValueGroup) {
value = (ParameterValueGroup) element;
descriptor = value.getDescriptor();
} else {
value = null;
descriptor = (ParameterDescriptorGroup) element;
}
out.append(lineSeparator);
format(name + '/' + descriptor.getName().getCode(), descriptor, value, out);
}
}
}
Aggregations