| /* |
| * 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.referencing.report; |
| |
| import java.util.Map; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.io.IOException; |
| import org.opengis.util.FactoryException; |
| import org.opengis.util.GenericName; |
| import org.opengis.metadata.extent.GeographicBoundingBox; |
| import org.opengis.parameter.*; |
| import org.opengis.referencing.operation.*; |
| import org.opengis.referencing.crs.CRSAuthorityFactory; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.crs.GeneralDerivedCRS; |
| import org.apache.sis.internal.system.DefaultFactories; |
| import org.apache.sis.internal.util.Constants; |
| import org.apache.sis.internal.util.PatchedUnitFormat; |
| import org.apache.sis.internal.referencing.provider.Affine; |
| import org.apache.sis.internal.referencing.provider.LambertConformal2SP; |
| import org.apache.sis.measure.Range; |
| import org.apache.sis.measure.Latitude; |
| import org.apache.sis.measure.Longitude; |
| import org.apache.sis.measure.RangeFormat; |
| import org.apache.sis.parameter.Parameters; |
| import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox; |
| import org.apache.sis.referencing.operation.DefaultOperationMethod; |
| import org.apache.sis.referencing.CRS; |
| import org.apache.sis.util.ArraysExt; |
| import org.apache.sis.util.Characters; |
| import org.apache.sis.util.Numbers; |
| |
| // Branch-dependent imports |
| import org.opengis.metadata.Identifier; |
| |
| |
| /** |
| * Generates a list of projection parameters in a HTML page. This class is used for updating the |
| * <a href="http://sis.apache.org/tables/CoordinateOperationMethods.html">CoordinateOperationMethods.html</a> page. |
| * The {@linkplain #main(String[])} method creates the "{@code CoordinateOperationMethods.html}" file in the current |
| * default directory if it does not already exists. Users is responsible for moving the generated file to the Apache |
| * SIS {@code "content/"} site directory. |
| * |
| * <p><b>This class is designed for Apache SIS operation methods only</b> - this is not a general purpose generator |
| * for arbitrary operation methods. The reason is that we make some assumptions in various place (e.g. EPSG name is |
| * first, no HTML characters to escape in non-EPSG identifiers, etc.).</p> |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @since 0.6 |
| * @version 0.8 |
| * @module |
| */ |
| public strictfp class CoordinateOperationMethods extends HTMLGenerator { |
| /** |
| * Generates the HTML report. |
| * |
| * @param args No argument expected. |
| * @throws IOException If an error occurred while writing the HTML file. |
| */ |
| public static void main(final String[] args) throws IOException { |
| final MathTransformFactory factory = DefaultFactories.forBuildin(MathTransformFactory.class); |
| final List<OperationMethod> methods = new ArrayList<>(factory.getAvailableMethods(SingleOperation.class)); |
| methods.removeIf((method) -> method.getClass().getName().endsWith("Mock")); |
| Collections.sort(methods, (final OperationMethod o1, final OperationMethod o2) -> { |
| int c = category(o1) - category(o2); |
| if (c == 0) { // If the two methods are in the same category, sort by name. |
| final String n1 = o1.getName().getCode().replace('(',' ').replace(')',' ').replace('_',' '); |
| final String n2 = o2.getName().getCode().replace('(',' ').replace(')',' ').replace('_',' '); |
| c = n1.compareTo(n2); |
| } |
| return c; |
| }); |
| try (final CoordinateOperationMethods writer = new CoordinateOperationMethods()) { |
| writer.writeIndex(methods); |
| for (final OperationMethod method : methods) { |
| writer.write(method); |
| } |
| } |
| } |
| |
| /** |
| * Values returned by {@link #category(OperationMethod)}. |
| */ |
| private static final int CYLINDRICAL_PROJECTION = 1, CONIC_PROJECTION = 2, |
| PLANAR_PROJECTION = 3, CONVERSION = 4, TRANSFORMATION = 5; |
| |
| /** |
| * Parameters to default to the latitude of origin. We can hardly detect those cases |
| * automatically, since the behavior for the default value is hard-coded in Java. |
| * |
| * @todo Not yet completed. |
| */ |
| private final GeneralParameterDescriptor defaultToLatitudeOfOrigin[] = { |
| // AlbersEqualArea .PARAMETERS.descriptor("Latitude of 1st standard parallel"), |
| LambertConformal2SP.STANDARD_PARALLEL_1 |
| }; |
| |
| /** |
| * Parameters to default to the first standard parallel. We can hardly detect those |
| * cases automatically, since the behavior for the default value is hard-coded in Java. |
| * |
| * @todo Not yet completed. |
| */ |
| private final GeneralParameterDescriptor defaultToStandardParallel1[] = { |
| // AlbersEqualArea .PARAMETERS.descriptor("Latitude of 2nd standard parallel"), |
| LambertConformal2SP.STANDARD_PARALLEL_2 |
| }; |
| |
| /** |
| * Parameters to default to the azimuth. We can hardly detect those cases automatically, |
| * since the behavior for the default value is hard-coded in Java. |
| * |
| * @todo Not yet completed. |
| */ |
| private final GeneralParameterDescriptor defaultToAzimuth[] = { |
| // ObliqueMercator .PARAMETERS.descriptor("Angle from Rectified to Skew Grid"), |
| // HotineObliqueMercator.PARAMETERS.descriptor("Angle from Rectified to Skew Grid") |
| }; |
| |
| /** |
| * The union of domain of validity of all map projections using a method of the given name. |
| * Keys are {@link OperationMethod} names, and values are the union of the domain of validity |
| * of all CRS using that {@code OperationMethod}. |
| * |
| * @see #computeUnionOfAllDomainOfValidity(CRSAuthorityFactory) |
| */ |
| private final Map<String, DefaultGeographicBoundingBox> domainOfValidity; |
| |
| /** |
| * The object to use for formatting ranges. |
| */ |
| private final RangeFormat rangeFormat; |
| |
| /** |
| * Creates a new HTML generator for parameters. |
| * |
| * @throws IOException if an error occurred while writing to the file. |
| */ |
| public CoordinateOperationMethods() throws IOException { |
| super("CoordinateOperationMethods.html", "Apache SIS Coordinate Operation Methods", "authority-codes.css"); |
| domainOfValidity = Collections.emptyMap(); // TODO: not yet available. |
| rangeFormat = new RangeFormat(LOCALE); |
| final int header = openTag("header"); |
| println("h1", "Apache SIS™ Coordinate Operation Methods"); |
| openTag("p"); |
| println("The following tables summarize the coordinate operation methods known to Apache SIS, together with the recognized parameters."); |
| println("Unless otherwise noticed, all parameters are mandatory"); |
| println("(in the sense that they should always be shown in forms, regardless of whether they have default value),"); |
| println("but two of them are handled in a special way: the <code>semi-major</code> and <code>semi-minor</code> parameters."); |
| println("Those two parameters are needed for all map projections, but usually do not need to be specified explicitely since they are inferred from the ellipsoid."); |
| println("The only exception is when <a href=\"http://sis.apache.org/apidocs/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.html\">creating parameterized transforms directly</a>."); |
| reopenTag("p"); |
| println("All map projections support also implicit <code>earth_radius</code> and <code>inverse_flattening</code> parameters (not shown below)."); |
| println("Read and write operations on those implicit parameters are delegated to the <code>semi-major</code> and <code>semi-minor</code> parameters."); |
| closeTags(header); |
| } |
| |
| /** |
| * Writes a table of content. |
| * |
| * @param methods The methods to write to the HTML file. |
| * @throws IOException if an error occurred while writing to the file. |
| */ |
| public void writeIndex(final Iterable<? extends OperationMethod> methods) throws IOException { |
| final int nav = openTag("nav"); |
| println("p", "<b>Table of content:</b>"); |
| int innerUL = openTag("ul") + 1; |
| int category = 0; |
| for (final OperationMethod method : methods) { |
| final int nc = category(method); |
| if (nc != category) { |
| closeTags(innerUL); |
| reopenTag("li"); |
| switch (nc) { |
| case CYLINDRICAL_PROJECTION: println("Cylindrical projections"); break; |
| case CONIC_PROJECTION: println("Conic projections"); break; |
| case PLANAR_PROJECTION: println("Planar projections"); break; |
| case CONVERSION: println("Conversions"); break; |
| case TRANSFORMATION: println("Tranformations"); break; |
| default: throw new AssertionError(category); |
| } |
| innerUL = openTag("ul"); |
| category = nc; |
| } |
| println("li", "<a href=\"#" + getAnchor(method) + "\">" + escape(method.getName().getCode()) + "</a>"); |
| } |
| closeTags(nav); |
| } |
| |
| /** |
| * Writes identification info and parameters for the given method. |
| * |
| * @param method The method to write to the HTML file. |
| * @throws IOException if an error occurred while writing to the file. |
| */ |
| public void write(final OperationMethod method) throws IOException { |
| final int article = openTag("article"); |
| final int header = openTag("header"); |
| println("h2 id=\"" + getAnchor(method) + '"', escape(method.getName().getCode())); |
| closeTags(header); |
| final int blockquote = openTag("blockquote"); |
| writeIdentification(method); |
| writeParameters(method.getParameters()); |
| closeTags(blockquote); |
| closeTags(article); |
| } |
| |
| /** |
| * Writes identification info about the given method. |
| * This method writes the following information: |
| * |
| * <ul> |
| * <li>EPSG codes</li> |
| * <li>Aliases</li> |
| * <li>Domain of validity</li> |
| * </ul> |
| */ |
| private void writeIdentification(final OperationMethod method) throws IOException { |
| final int table = openTag("table class=\"info\""); |
| /* |
| * ──────────────── EPSG IDENTIFIERS ──────────────────────────────────── |
| */ |
| final StringBuilder buffer = new StringBuilder(); |
| for (final Identifier id : method.getIdentifiers()) { |
| if (Constants.EPSG.equalsIgnoreCase(id.getCodeSpace())) { |
| if (buffer.length() != 0) { |
| buffer.append(", "); |
| } |
| final boolean isDeprecated = isDeprecated(id); |
| if (isDeprecated) { |
| buffer.append("<del>"); |
| } |
| buffer.append(id.getCode()); |
| if (isDeprecated) { |
| buffer.append("</del>"); |
| } |
| } |
| } |
| if (buffer.length() != 0) { |
| final int tr = openTag("tr"); |
| println("th", "EPSG code:"); |
| println("td", buffer); |
| closeTags(tr); |
| } |
| /* |
| * ──────────────── ALIASES ───────────────────────────────────────────── |
| */ |
| buffer.setLength(0); |
| for (final GenericName alias : method.getAlias()) { |
| if (buffer.length() != 0) { |
| buffer.append(", "); |
| } |
| final GenericName head = alias.head(); |
| if (head == alias || Constants.EPSG.equalsIgnoreCase(head.toString())) { |
| buffer.append(alias.tip()); |
| } else { |
| buffer.append("<span class=\"non-epsg\">").append(head).append(":</span>") |
| .append("<code>").append(alias.tip()).append("</code>"); |
| } |
| } |
| if (buffer.length() != 0) { |
| final int tr = openTag("tr"); |
| println("th", "Aliases:"); |
| println("td", buffer); |
| closeTags(tr); |
| } |
| /* |
| * ──────────────── DOMAIN OF VALIDITY ────────────────────────────────── |
| */ |
| buffer.setLength(0); |
| final DefaultGeographicBoundingBox domain = getDomainOfValidity(method); |
| if (domain != null) { |
| openTag("tr"); |
| println("th", "Domain of validity:"); |
| println("td", buffer.append(new Latitude (domain.getSouthBoundLatitude())).append(" to ") |
| .append(new Latitude (domain.getNorthBoundLatitude())).append(" and ") |
| .append(new Longitude(domain.getWestBoundLongitude())).append(" to ") |
| .append(new Longitude(domain.getEastBoundLongitude()))); |
| } |
| closeTags(table); |
| } |
| |
| /** |
| * Writes the table of parameters. |
| * Table columns will be: |
| * |
| * <ul> |
| * <li>First EPSG code</li> |
| * <li>Primary name</li> |
| * <li>Reference to remarks, if any</li> |
| * <li>Domain of values</li> |
| * <li>Default values</li> |
| * </ul> |
| */ |
| private void writeParameters(final ParameterDescriptorGroup group) throws IOException { |
| int table = openTag("table class=\"param\""); |
| println("caption", "Operation parameters:"); |
| openTag("tr"); |
| println("th", "EPSG"); |
| println("th class=\"sep\"", "Name"); |
| println("th class=\"sep\"", "Remarks"); |
| println("th class=\"sep\" colspan=\"3\"", "Value domain"); |
| println("th class=\"sep\"", "Default"); |
| final Map<String, Integer> footnotes = new LinkedHashMap<>(); |
| for (final GeneralParameterDescriptor gp : group.descriptors()) { |
| if (isDeprecated(gp)) { |
| continue; // Hide deprecated parameters. |
| } |
| final ParameterDescriptor<?> param = (ParameterDescriptor<?>) gp; |
| reopenTag("tr"); |
| println("td", escape(getFirstEpsgCode(param.getIdentifiers()))); |
| writeName(param); |
| String remarks = toLocalizedString(param.getRemarks()); |
| if (remarks != null) { |
| Integer index = footnotes.putIfAbsent(remarks, footnotes.size() + 1); |
| if (index == null) { |
| index = footnotes.size(); |
| } |
| if (param.getMinimumOccurs() == 0) { |
| remarks = "Optional "; |
| } else { |
| final Comparable<?> min = param.getMinimumValue(); |
| if ((min instanceof Number) && ((Number) min).doubleValue() == ((Number) param.getMaximumValue()).doubleValue()) { |
| remarks = "Unmodifiable "; |
| } else { |
| remarks = "See note "; |
| } |
| } |
| remarks += toSuperScript(index); |
| } |
| println("td class=\"sep\"", escape(remarks)); |
| final String domain = toLocalizedString(Parameters.getValueDomain(param)); |
| final int s; |
| if (domain != null && ((s = domain.indexOf('…')) >= 0)) { |
| println("td class=\"sep right\"", domain.substring(0, s).trim()); |
| println("td class=\"center\"", "…"); |
| println("td class=\"left\"", domain.substring(s + 1).trim()); |
| } else { |
| println("td class=\"sep center\" colspan=\"3\"", domain); |
| } |
| println("td class=\"sep\"", escape(getDefaultValue(param, getUnit(param)))); |
| } |
| closeTags(table); |
| if (!footnotes.isEmpty()) { |
| table = openTag("table class=\"footnotes\""); |
| for (final Map.Entry<String,Integer> entry : footnotes.entrySet()) { |
| reopenTag("tr"); |
| println("td", String.valueOf(toSuperScript(entry.getValue()))); |
| println("td", escape(entry.getKey())); |
| } |
| closeTags(table); |
| } |
| } |
| |
| /** |
| * Writes the primary name and aliases. |
| */ |
| private void writeName(final ParameterDescriptor<?> param) throws IOException { |
| final int td = openTag("td class=\"sep\""); |
| openTag("details"); |
| final Identifier name = param.getName(); |
| final String codeSpace = name.getCodeSpace(); |
| if (Constants.EPSG.equalsIgnoreCase(codeSpace)) { |
| println("summary", escape(name.getCode())); |
| } else { |
| println("summary", "<span class=\"non-epsg\">" + codeSpace |
| + ":</span><code>" + name.getCode() + "</code>"); |
| } |
| openTag("table class=\"aliases\""); |
| for (final GenericName alias : param.getAlias()) { |
| reopenTag("tr"); |
| println("th", escape(alias.head().toString() + ':')); |
| println("td", escape(alias.tip().toString())); |
| } |
| closeTags(td); |
| } |
| |
| /** |
| * For each {@link OperationMethod} (identified by their name), computes the union of the domain of validity |
| * of all CRS using that operation method. The result is a map where keys are {@link OperationMethod} names, |
| * and values are the union of the domain of validity of all CRS using that {@code OperationMethod}. |
| * |
| * <p>This is a costly operation.</p> |
| * |
| * @todo This method is not yet used. This is pending the implementation of {@code CRSAuthorityFactory} is SIS. |
| * |
| * @param factory The factory to use for getting CRS. |
| * @return The union of domain of validity of all map projections using a method of the given name. |
| * @throws FactoryException If an error occurred while fetching the list of CRS. |
| */ |
| public static Map<String, DefaultGeographicBoundingBox> computeUnionOfAllDomainOfValidity( |
| final CRSAuthorityFactory factory) throws FactoryException |
| { |
| final Map<String, DefaultGeographicBoundingBox> domainOfValidity = new HashMap<>(); |
| for (final String code : factory.getAuthorityCodes(GeneralDerivedCRS.class)) { |
| final CoordinateReferenceSystem crs; |
| try { |
| crs = factory.createCoordinateReferenceSystem(code); |
| } catch (FactoryException e) { |
| continue; // Ignore and inspect the next element. |
| } |
| if (crs instanceof GeneralDerivedCRS) { |
| final GeographicBoundingBox candidate = CRS.getGeographicBoundingBox(crs); |
| if (candidate != null) { |
| final String name = ((GeneralDerivedCRS) crs).getConversionFromBase().getMethod().getName().getCode(); |
| DefaultGeographicBoundingBox validity = domainOfValidity.get(name); |
| if (validity == null) { |
| validity = new DefaultGeographicBoundingBox(candidate); |
| domainOfValidity.put(name, validity); |
| } else { |
| validity.add(candidate); |
| } |
| } |
| } |
| } |
| return domainOfValidity; |
| } |
| |
| /** |
| * Returns the domain of validity for the given operation method. |
| * If no domain of validity is found, returns {@code null}. |
| */ |
| private DefaultGeographicBoundingBox getDomainOfValidity(final OperationMethod method) { |
| DefaultGeographicBoundingBox validity = null; |
| for (final GenericName name : method.getAlias()) { |
| final String tip = name.tip().toString(); |
| final DefaultGeographicBoundingBox candidate = domainOfValidity.get(tip); |
| if (candidate != null) { |
| if (validity == null) { |
| validity = new DefaultGeographicBoundingBox(candidate); |
| } else { |
| validity.add(candidate); |
| } |
| } |
| } |
| return validity; |
| } |
| |
| /** |
| * Returns the string representation of the given parameter default value, |
| * or an empty string (never {@code null}) if none. |
| */ |
| private String getDefaultValue(final ParameterDescriptor<?> param, final String unit) { |
| Object defaultValue = param.getDefaultValue(); |
| if (defaultValue != null) { |
| if (defaultValue instanceof Number) { |
| // Trim the fractional part if unnecessary (e.g. "0.0" to "0"). |
| defaultValue = Numbers.narrowestNumber((Number) defaultValue); |
| } else if (defaultValue instanceof String) { |
| return (String) defaultValue; |
| } |
| } else { |
| if (ArraysExt.contains(defaultToLatitudeOfOrigin, param)) { |
| return "Latitude of origin"; |
| } else if (ArraysExt.contains(defaultToStandardParallel1, param)) { |
| return "Standard parallel 1"; |
| } else if (ArraysExt.contains(defaultToAzimuth, param)) { |
| return "Azimuth of initial line"; |
| } else if (param.getValueClass() == Boolean.class) { |
| defaultValue = Boolean.FALSE; |
| } |
| } |
| return (defaultValue != null) ? defaultValue + unit : ""; |
| } |
| |
| /** |
| * Returns the string representation of the given parameter unit, |
| * or an empty string (never {@code null}) if none. |
| */ |
| private static String getUnit(final ParameterDescriptor<?> param) { |
| final String unit = PatchedUnitFormat.toString(param.getUnit()); |
| if (unit != null && !unit.isEmpty()) { |
| if (unit.equals("°")) { |
| return unit; |
| } |
| return " " + unit; |
| } |
| return ""; |
| } |
| |
| /** |
| * Returns the operation type of the given method. |
| */ |
| private static Class<?> getOperationType(final DefaultOperationMethod method) { |
| Class<?> type = method.getOperationType(); |
| if (type == SingleOperation.class) { |
| if (method instanceof Affine) { // EPSG:9624 - Affine parametric transformation |
| type = Transformation.class; |
| } |
| } |
| return type; |
| } |
| |
| /** |
| * Returns a code for sorting methods in categories. |
| */ |
| private static int category(final OperationMethod method) { |
| final Class<?> c = getOperationType((DefaultOperationMethod) method); |
| if (CylindricalProjection.class.isAssignableFrom(c)) return CYLINDRICAL_PROJECTION; |
| if (ConicProjection .class.isAssignableFrom(c)) return CONIC_PROJECTION; |
| if (PlanarProjection .class.isAssignableFrom(c)) return PLANAR_PROJECTION; |
| if (Conversion .class.isAssignableFrom(c)) return CONVERSION; |
| if (Transformation .class.isAssignableFrom(c)) return TRANSFORMATION; |
| return 0; |
| } |
| |
| /** |
| * Returns the first EPSG code found in the given collection, or {@code null} if none. |
| */ |
| private static String getFirstEpsgCode(final Iterable<? extends Identifier> identifiers) { |
| for (final Identifier id : identifiers) { |
| if (Constants.EPSG.equalsIgnoreCase(id.getCodeSpace())) { |
| return id.getCode(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns an identifier to use for HREF. |
| */ |
| private static String getAnchor(final OperationMethod method) { |
| String id = getFirstEpsgCode(method.getIdentifiers()); |
| if (id == null) { |
| id = method.getName().getCode(); |
| } |
| return id; |
| } |
| |
| /** |
| * Returns a string representation of the given range, or {@code null} if none. |
| */ |
| private String toLocalizedString(final Range<?> range) { |
| return (range != null) ? rangeFormat.format(range) : null; |
| } |
| |
| /** |
| * Returns the superscript character for the given number. |
| * This is used for footnotes. |
| */ |
| private static char toSuperScript(final int index) { |
| if (index >= 10) { |
| throw new IllegalArgumentException("Too many footnotes."); |
| } |
| return Characters.toSuperScript((char) (index + '0')); |
| } |
| } |