private void format()

in endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/ParameterFormat.java [450:774]


    private void format(final String name, final ParameterDescriptorGroup group,
            final ParameterValueGroup values, final Appendable out) throws IOException
    {
        @SuppressWarnings("LocalVariableHidesMemberVariable")
        final String  lineSeparator  = this.lineSeparator;
        final boolean isBrief        = (contentLevel == ContentLevel.BRIEF);
        final boolean showObligation = !isBrief || (values == null);
        final boolean hasColors      = (colors != null);
        final var     remarks        = new LinkedHashMap<String,Integer>();
        final var     header         = new ParameterTableRow(group, displayLocale, preferredCodespaces, remarks, isBrief);
        final String  groupCodespace = header.getCodeSpace();
        /*
         * Prepares the information 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 another collection for deferred formatting after the simple parameters.
         */
        int codespaceWidth = 0;
        final Collection<?> elements = (values != null) ? values.values() : group.descriptors();
        final var descriptorValues = JDK19.<GeneralParameterDescriptor, ParameterTableRow>newLinkedHashMap(elements.size());
        List<Object> deferredGroups = null;                 // To be created only if needed (it is usually not).
        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 var p = (ParameterValue<?>) parameter;
                value = p.getValue();
                unit  = p.getUnit();
            } else if (descriptor instanceof ParameterDescriptor<?>) {
                final var 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 var buffer  = new StringBuffer();
        final var 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 as 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 cannot 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.forLocale(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<?>) {
                final var descriptor = (ParameterDescriptor<?>) generalDescriptor;
                /*
                 * Writes value type.
                 */
                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 = (valueClass != null && 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++) {
                    final 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 tuple.
                         */
                        final Format format = getFormat(value.getClass());
                        final CharSequence text;
                        if (format != null) {
                            if (format instanceof NumberFormat && value instanceof Number) {
                                configure((NumberFormat) format, Math.abs(((Number) value).doubleValue()));
                            }
                            text = format.format(value, buffer, dummyFP);
                        } else if (value instanceof CodeList<?>) {
                            text = Types.getCodeTitle((CodeList<?>) value).toString(getLocale());
                        } else if (value instanceof InternationalString) {
                            text = ((InternationalString) value).toString(getLocale());
                        } else {
                            text = value.toString();
                        }
                        table.append(text);
                        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);
            }
        }
    }