Search in sources :

Example 6 with TableAppender

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;
}
Also used : TableAppender(org.apache.sis.io.TableAppender) CoordinateReferenceSystem(org.opengis.referencing.crs.CoordinateReferenceSystem) ParseException(java.text.ParseException) Warnings(org.apache.sis.io.wkt.Warnings)

Example 7 with TableAppender

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);
    }
}
Also used : TableAppender(org.apache.sis.io.TableAppender) IOException(java.io.IOException) FormattableObject(org.apache.sis.io.wkt.FormattableObject) Debug(org.apache.sis.util.Debug)

Example 8 with TableAppender

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);
    }
}
Also used : Vocabulary(org.apache.sis.util.resources.Vocabulary) TableAppender(org.apache.sis.io.TableAppender) ArrayList(java.util.ArrayList) UncheckedIOException(java.io.UncheckedIOException) InternationalString(org.opengis.util.InternationalString) GenericName(org.opengis.util.GenericName) Format(java.text.Format) TabularFormat(org.apache.sis.io.TabularFormat) Iterator(java.util.Iterator) Deprecable(org.apache.sis.util.Deprecable) IOException(java.io.IOException) UncheckedIOException(java.io.UncheckedIOException) FieldPosition(java.text.FieldPosition) InternationalString(org.opengis.util.InternationalString) Collection(java.util.Collection) IdentifiedObject(org.opengis.referencing.IdentifiedObject)

Example 9 with TableAppender

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);
    }
}
Also used : Locale(java.util.Locale) Vocabulary(org.apache.sis.util.resources.Vocabulary) DirectPosition(org.opengis.geometry.DirectPosition) DefaultExtent(org.apache.sis.metadata.iso.extent.DefaultExtent) Extent(org.opengis.metadata.extent.Extent) FactoryException(org.opengis.util.FactoryException) CoordinateSystem(org.opengis.referencing.cs.CoordinateSystem) UnitFormat(org.apache.sis.measure.UnitFormat) RoundingMode(java.math.RoundingMode) TableAppender(org.apache.sis.io.TableAppender) Latitude(org.apache.sis.measure.Latitude) GeographicBoundingBox(org.opengis.metadata.extent.GeographicBoundingBox) InternationalString(org.opengis.util.InternationalString) AngleFormat(org.apache.sis.measure.AngleFormat) Envelope(org.opengis.geometry.Envelope) DefaultExtent(org.apache.sis.metadata.iso.extent.DefaultExtent) CoordinateReferenceSystem(org.opengis.referencing.crs.CoordinateReferenceSystem) GeographicCRS(org.opengis.referencing.crs.GeographicCRS) AbstractParty(org.apache.sis.metadata.iso.citation.AbstractParty) TransformException(org.opengis.referencing.operation.TransformException) Longitude(org.apache.sis.measure.Longitude) Date(java.util.Date) ParseException(java.text.ParseException) TransformException(org.opengis.referencing.operation.TransformException) IOException(java.io.IOException) FactoryException(org.opengis.util.FactoryException) Angle(org.apache.sis.measure.Angle) InternationalString(org.opengis.util.InternationalString) NumberFormat(java.text.NumberFormat)

Example 10 with TableAppender

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);
        }
    }
}
Also used : Vocabulary(org.apache.sis.util.resources.Vocabulary) TableAppender(org.apache.sis.io.TableAppender) Unit(javax.measure.Unit) LinkedHashMap(java.util.LinkedHashMap) Format(java.text.Format) NumberFormat(java.text.NumberFormat) TabularFormat(org.apache.sis.io.TabularFormat) FieldPosition(java.text.FieldPosition) Range(org.apache.sis.measure.Range) IdentifiedObject(org.opengis.referencing.IdentifiedObject) LinkedHashMap(java.util.LinkedHashMap) Map(java.util.Map) NumberFormat(java.text.NumberFormat)

Aggregations

TableAppender (org.apache.sis.io.TableAppender)15 IOException (java.io.IOException)7 Vocabulary (org.apache.sis.util.resources.Vocabulary)6 IdentifiedObject (org.opengis.referencing.IdentifiedObject)4 InternationalString (org.opengis.util.InternationalString)4 Format (java.text.Format)3 NumberFormat (java.text.NumberFormat)3 TabularFormat (org.apache.sis.io.TabularFormat)3 Debug (org.apache.sis.util.Debug)3 CoordinateReferenceSystem (org.opengis.referencing.crs.CoordinateReferenceSystem)3 FieldPosition (java.text.FieldPosition)2 ParseException (java.text.ParseException)2 ArrayList (java.util.ArrayList)2 LinkedHashMap (java.util.LinkedHashMap)2 Map (java.util.Map)2 GeographicBoundingBox (org.opengis.metadata.extent.GeographicBoundingBox)2 GeographicCRS (org.opengis.referencing.crs.GeographicCRS)2 FileInputStream (java.io.FileInputStream)1 InputStreamReader (java.io.InputStreamReader)1 LineNumberReader (java.io.LineNumberReader)1