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);
}
}
}