| /* |
| * 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(); |
| } |
| } |