blob: a98e17d96b0a92ffc6c70bf7c0513fd498a23a52 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.parameter;
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.Locale;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.lang.reflect.Array;
import java.io.IOException;
import java.text.Format;
import java.text.FieldPosition;
import javax.measure.Unit;
import org.opengis.util.GenericName;
import org.opengis.metadata.Identifier;
import org.opengis.referencing.IdentifiedObject;
import org.apache.sis.io.wkt.Colors;
import org.apache.sis.io.wkt.ElementKind;
import org.apache.sis.util.Characters;
import org.apache.sis.util.Deprecable;
import org.apache.sis.measure.Range;
import org.apache.sis.measure.RangeFormat;
import org.apache.sis.metadata.privy.NameToIdentifier;
import org.apache.sis.util.privy.X364;
import static org.apache.sis.util.privy.X364.*;
import static org.apache.sis.util.CharSequences.spaces;
import static org.apache.sis.util.privy.Constants.DEFAULT_SEPARATOR;
// Specific to the main and geoapi-3.1 branches:
import org.opengis.util.InternationalString;
/**
* A row in the table to be formatted by {@link ParameterFormat}.
*
* @author Martin Desruisseaux (Geomatys)
*/
final class ParameterTableRow {
/**
* The (<var>codespace(s)</var>, <var>name(s)</var>) entries for the identifier and all aliases
* declared in the constructor. The codespace key may be null, but the name values shall never be null.
*
* <p>Values can be of two kinds:</p>
* <ul>
* <li>{@link String} for names or aliases.</li>
* <li>{@link Identifier} for identifiers.</li>
* </ul>
*
* @see #addIdentifier(String, Object)
*/
private final Map<String,Set<Object>> identifiers;
/**
* The largest codespace width, in number of Unicode code points.
*
* @see #addIdentifier(String, Object)
*/
int codespaceWidth;
/**
* The string representation of the domain of values, or {@code null} if none.
*
* @see #setValueDomain(Range, Format, StringBuffer)
*/
String valueDomain;
/**
* The position to use for alignment of {@link #valueDomain}.
* This is usually after the '…' separator.
*
* @see #setValueDomain(Range, Format, StringBuffer)
*/
int valueDomainAlignment;
/**
* The values. Some elements in this list may be null.
*
* @see #addValue(Object, Unit)
*/
final List<Object> values;
/**
* The units of measurement. The size of this list shall be the same as {@link #values}.
* The list may contain null elements.
*
* <p>This list is initially filled with {@link Unit} instance. Later in the formatting process,
* {@code Unit} instances will be replaced by their symbol.</p>
*
* @see #addValue(Object, Unit)
*/
final List<Object> units;
/**
* Reference to a remark, or {@code 0} if none.
*/
private int remarks;
/**
* Creates a new row in a table to be formatted by {@link ParameterFormat}.
*
* @param object the object for which to get the (<var>codespace(s)</var>, <var>name(s)</var>).
* @param locale the locale for formatting the names and the remarks.
* @param remarks an initially empty map, to be filled with any remarks we may found.
*/
ParameterTableRow(final IdentifiedObject object, final Locale locale, final Set<String> preferredCodespaces,
final Map<String,Integer> remarks, final boolean isBrief)
{
values = new ArrayList<>(2); // In the vast majority of cases, we will have only one value.
units = new ArrayList<>(2);
identifiers = new LinkedHashMap<>();
Identifier name = object.getName();
if (name != null) { // Paranoiac check.
final String codespace = name.getCodeSpace();
if (preferredCodespaces == null || preferredCodespaces.contains(codespace)) {
addIdentifier(codespace, name.getCode()); // Value needs to be a String here.
name = null;
}
}
/*
* For detailed content, add aliases.
* For brief content, add the first alias if we have not been able to add the name.
*/
if (!isBrief || identifiers.isEmpty()) {
final Collection<GenericName> aliases = object.getAlias();
if (aliases != null) { // Paranoiac check.
for (GenericName alias : aliases) {
if (!isDeprecated(alias)) {
final String codespace = NameToIdentifier.getCodeSpace(alias, locale);
if (codespace != null) {
alias = alias.tip();
}
if (preferredCodespaces == null || preferredCodespaces.contains(codespace)) {
addIdentifier(codespace, NameToIdentifier.toString(alias, locale));
name = null;
if (isBrief) {
break;
}
}
}
}
}
}
/*
* If we found no name and no alias in the codespaces requested by the user,
* unconditionally add the name regardless its namespace.
*/
if (name != null) {
addIdentifier(name.getCodeSpace(), name.getCode()); // Value needs to be a String here.
}
/*
* Add identifiers (detailed mode only).
*/
if (!isBrief) {
final Collection<? extends Identifier> ids = object.getIdentifiers();
if (ids != null) { // Paranoiac check.
for (final Identifier id : ids) {
if (!isDeprecated(id)) {
final String codespace = id.getCodeSpace();
if (preferredCodespaces == null || preferredCodespaces.contains(codespace)) {
addIdentifier(codespace, id); // No .getCode() here.
}
}
}
}
}
/*
* Take the remarks, if any.
*/
final InternationalString r = object.getRemarks();
if (r != null) {
final int n = remarks.size() + 1;
final Integer p = remarks.putIfAbsent(r.toString(locale), n);
this.remarks = (p != null) ? p : n;
}
}
/**
* Returns {@code true} if the given name or identifier is deprecated.
*/
private static boolean isDeprecated(final Object object) {
return (object instanceof Deprecable) && ((Deprecable) object).isDeprecated();
}
/**
* Helper method for the constructor only, adding an identifier for the given code space.
* As a side effect, this method remembers the length of the widest code space.
*/
private void addIdentifier(final String codespace, final Object identifier) {
if (codespace != null) {
final int width = codespace.codePointCount(0, codespace.length());
if (width > codespaceWidth) {
codespaceWidth = width;
}
}
Set<Object> ids = identifiers.get(codespace);
if (ids == null) {
ids = new LinkedHashSet<>(8);
identifiers.put(codespace, ids);
}
ids.add(identifier);
}
/**
* If this row has exactly one codespace, returns that codespace.
* Otherwise returns {@code null}.
*/
final String getCodeSpace() {
final Iterator<Map.Entry<String,Set<Object>>> it = identifiers.entrySet().iterator();
if (it.hasNext()) {
final Map.Entry<String,Set<Object>> entry = it.next();
if (!it.hasNext()) {
return entry.getKey();
}
}
return null;
}
/**
* Sets the value domain to the string representation of the given range.
*
* @param range the range to format.
* @param format the format to use for formatting the {@code range}.
* @param buffer a temporary buffer to use for formatting the range.
* @return the position of a character on which to align the text in the cell.
*/
final int setValueDomain(final Range<?> range, final Format format, final StringBuffer buffer) {
final FieldPosition fieldPosition = new FieldPosition(RangeFormat.Field.MAX_VALUE);
valueDomain = format.format(range, buffer, fieldPosition).toString();
buffer.setLength(0);
return valueDomainAlignment = fieldPosition.getBeginIndex();
}
/**
* Adds a value and its unit of measurement.
*
* @param value the value, or {@code null}.
* @param unit the unit of measurement, or {@code null}.
*/
final void addValue(final Object value, final Unit<?> unit) {
values.add(value);
units .add(unit);
}
/**
* If the list has only one element and this element is an array or a collection, expands it.
* This method shall be invoked only after the caller finished to add all elements in the
* {@link #values} and {@link #units} lists.
*/
final void expandSingleton() {
assert values.size() == units.size();
if (values.size() == 1) {
Object value = values.get(0);
if (value != null) {
if (value instanceof Collection<?>) {
value = ((Collection<?>) value).toArray();
}
if (value.getClass().isArray()) {
final int length = Array.getLength(value);
final Object unit = units.get(0);
values.clear();
units.clear();
for (int i=0; i<length; i++) {
values.add(Array.get(value, i));
units.add(unit);
}
}
}
}
}
/**
* Writes the color for the given type if {@code colors} is non-null.
*/
private static void writeColor(final Appendable out, final Colors colors, final ElementKind type)
throws IOException
{
if (colors != null) {
final String name = colors.getName(type);
if (name != null) {
out.append(X364.forColorName(name).sequence());
}
}
}
/**
* Writes the given color if {@code colorEnabled} is {@code true}.
*/
private static void writeColor(final Appendable out, final X364 color, final boolean colorEnabled)
throws IOException
{
if (colorEnabled) {
out.append(color.sequence());
}
}
/**
* Writes the identifiers. At most one of {@code colors != null} and {@code colorsForRows}
* can be {@code true}.
*
* <p><b>This method can be invoked only once per {@code ParameterTableRow} instance</b>,
* as its implementation destroys the internal list of identifiers.</p>
*
* @param out where to write.
* @param writeCodespaces {@code true} for writing codespaces, or {@code false} for omitting them.
* @param colors non-null if syntax coloring should be applied for table title.
* @param colorsForRows {@code true} if syntax coloring should be applied for table rows.
* @param lineSeparator the system-dependent line separator.
* @throws IOException if an exception occurred while writing.
*/
final void writeIdentifiers(final Appendable out, final boolean writeCodespaces,
final Colors colors, final boolean colorsForRows, final String lineSeparator) throws IOException
{
if (codespaceWidth != 0) {
codespaceWidth += 2; // Add a colon and space between codespace and code in e.g. "OGC: Mercator".
}
boolean isNewLine = false;
for (final Map.Entry<String,Set<Object>> entry : identifiers.entrySet()) {
final String codespace = entry.getKey();
final Set<Object> identifiers = entry.getValue();
Iterator<Object> it = identifiers.iterator();
while (it.hasNext()) {
if (isNewLine) {
out.append(lineSeparator);
}
isNewLine = true;
/*
* Write the codespace. More than one name may exist for the same codespace,
* in which case the code space will be repeated on a new line each time.
*/
writeColor(out, colors, ElementKind.NAME);
if (writeCodespaces) {
int pad = codespaceWidth;
if (codespace != null) {
writeColor(out, FAINT, colorsForRows);
out.append(codespace).append(DEFAULT_SEPARATOR);
writeColor(out, NORMAL, colorsForRows);
pad -= (codespace.length() + 1);
}
out.append(spaces(pad));
}
/*
* Write the name or alias after the codespace. We remove what we wrote,
* because we may iterate over the 'identifiers' set more than once.
*/
writeColor(out, BOLD, colors != null);
out.append(toString(it.next()));
writeColor(out, RESET, colors != null);
it.remove();
/*
* Write the footnote number if there is a remark associated to this parameter.
* We write the remark only for the first name or identifier.
*/
if (remarks != 0) {
writeFootnoteNumber(out, remarks);
remarks = 0;
}
/*
* Write all identifiers between parenthesis after the firt name only.
* Aliases (to be written in a new iteration) will not have identifier.
*/
boolean hasAliases = false;
boolean hasIdentifiers = false;
while (it.hasNext()) {
final Object id = it.next();
if (id instanceof Identifier) {
out.append(hasIdentifiers ? ", " : " (");
writeColor(out, colors, ElementKind.IDENTIFIER);
out.append(toString(id));
writeColor(out, FOREGROUND_DEFAULT, colors != null);
hasIdentifiers = true;
it.remove();
} else {
hasAliases = true;
}
}
if (hasIdentifiers) {
out.append(')');
}
if (hasAliases) {
it = identifiers.iterator();
}
}
}
}
/**
* Writes the footnote number to the given appendable.
* The number is written in superscript if possible.
*/
static void writeFootnoteNumber(final Appendable out, final int n) throws IOException {
if (n >= 0 && n < 10) {
out.append(Characters.toSuperScript((char) ('0' + n)));
} else {
out.append('(').append(Integer.toString(n)).append(')');
}
}
/**
* Returns the string representation of the given parameter name.
*/
private static String toString(Object parameter) {
if (parameter instanceof Identifier) {
parameter = ((Identifier) parameter).getCode();
}
return parameter.toString();
}
}