/* * 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.test.referencing; import java.io.Flushable; import java.io.IOException; import java.io.PrintStream; import java.util.Random; import org.opengis.geometry.Envelope; import org.opengis.metadata.extent.GeographicBoundingBox; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.crs.ProjectedCRS; import org.opengis.referencing.operation.*; import org.opengis.util.FactoryException; import org.apache.sis.geometry.Envelopes; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.metadata.iso.citation.Citations; import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox; import org.apache.sis.metadata.iso.extent.Extents; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.IdentifiedObjects; import org.apache.sis.referencing.crs.AbstractCRS; import org.apache.sis.referencing.cs.AxesConvention; import org.apache.sis.metadata.internal.ReferencingServices; import org.apache.sis.util.internal.StandardDateFormat; import org.apache.sis.referencing.util.Formulas; import org.apache.sis.io.TableAppender; import org.apache.sis.math.Statistics; /** * Compares coordinate operations performed with Apache SIS and {@literal Proj.4}. * This class is designed for being run from the command line, potentially with output redirected to a file. * Example: * *
{@code java org.apache.sis.test.referencing.CoordinateOperationComparator 1 --tabs > result.txt}* * The {@code --tabs} option is for using tabulations as column separator instead than formatted outputs. * This make importation in spreadsheets easier. * * @author Martin Desruisseaux (Geomatys) * @version 0.8 * @since 0.8 * @module */ public class CoordinateOperationComparator { /** * Creates one of the predefined tests identified by the given sequential number. All pre-defined tests known * to the {@link #main(String[]) method} are listed here. This list may be expanded in any future version. * * @param n sequential number of the test to create. * @return the pre-defined test identified by the given number, or {@code null} if none. */ private static CoordinateOperationComparator create(final int n) throws FactoryException, TransformException, IOException { switch (n) { case 1: return new CoordinateOperationComparator("Cylindrical Equal Area (Spherical)", 4053, 3410); case 2: return new CoordinateOperationComparator("Cylindrical Equal Area", 4326, 6933); case 3: return new CoordinateOperationComparator("Pseudo-Mercator", 4326, 3857); case 4: return new CoordinateOperationComparator("Mercator", 4326, 3395); case 5: return new CoordinateOperationComparator("Lambert Conic Conformal", 4269, 3978); case 6: return new CoordinateOperationComparator("Polar stereographic", 4326, 3031); case 7: return new CoordinateOperationComparator("Albert Equal Area", 4269, 5070); case 8: return new CoordinateOperationComparator("Mercator 41 to Mercator", 3994, 3395); case 9: return new CoordinateOperationComparator("Tokyo to JGD2000", 4301, 4612); case 10: return new CoordinateOperationComparator("Tokyo to JGD2000 in UTM zone 54", 3095, 3100); case 11: return new CoordinateOperationComparator("OSGB 1936 to ED50 (UKOOA)", 4277, 4230); case 12: return new CoordinateOperationComparator("Martinique 1938 to RGAF09", 4625, 5489); case 13: return new CoordinateOperationComparator("Stereographic to stereographic", 2986, 7082); case 14: { /* * Use the same eccentricity than Jupiter, but scaled to Earth size in order to * have distance measurement in the same order of magnitude than other tests. * The inverse flattening factor is about 15.4144 (compared to 298.26 for Earth). * Such planet would have a polar radius of 5964 km instead of 6357 km. * * Reference: https://nssdc.gsfc.nasa.gov/planetary/factsheet/jupiterfact.html */ final ProjectedCRS targetCRS = (ProjectedCRS) CRS.fromWKT( "ProjectedCRS[“Lambert Equal Area on high eccentricity planet”,\n" + " BaseGeodCRS[“High eccentricity planet”,\n" + " Datum[“High eccentricity datum”,\n" + " Ellipsoid[“Jupiter eccentricity scaled to Earth size”, 6378100.0, 15.414402759810256]],\n" + " Unit[“degree”, 0.017453292519943295]],\n" + " Conversion[“High eccentricity planet to Lambert Equal Area”,\n" + " Method[“Lambert Cylindrical Equal Area”],\n" + " Parameter[“Latitude of 1st standard parallel”, 30]],\n" + " CS[Cartesian, 2],\n" + " Axis[“Easting (E)”, east],\n" + " Axis[“Northing (N)”, north],\n" + " Unit[“metre”, 1],\n" + " Scope[“Test only.”],\n" + " BBox[-86, -180, 86, 180]]"); final GeographicCRS sourceCRS = (GeographicCRS) AbstractCRS.castOrCopy(targetCRS.getBaseCRS()) .forConvention(AxesConvention.NORMALIZED); return new CoordinateOperationComparator("Jupiter eccentricity scaled to Earth size", new String[] {"Apache SIS", "Proj.4"}, targetCRS.getConversionFromBase(), Proj4.createOperation(sourceCRS, targetCRS, true)); } default: return null; } } /** * Number of dimensions in all coordinates to be used for the tests. */ private static final int DIM = 2; /** * Number of test coordinates along x and y axes (must be a power of 2). * The total number of coordinates used in each iteration will the the square of this value. */ private static final int NUM_PTS = 256; /** * Number of "transform projection - inverse projection" cycles to do. * GIGS (Geospatial Integrity of Geoscience Applications) recommends 100 iterations. */ private static final int NUM_LOOPS = 100; /** * Number of cycles to skip (for HotSpot warmup) before to collect statistics about performance. * Note that statistics about accuracy are collected for all iterations, regardless this value. */ private static final int WARMUP_LOOPS = 10; /** * The coordinates to transform as (x₀,y₀), (x₁,y₁), (x₂,y₂), (x₃,y₃), etc. tupples. * This array will be initialized by the constructor, then never modified. */ private final double[] inputs; /** * The array where to store results of coordinate operations. * This array will be overwritten at each iteration. */ private final double[] outputs; /** * {@code true} if the CRS is geographic, or {@code false} if the CRS is projected. * If {@code true}, then distances will be estimated using the nautical mile as an approximation. * * @see #distance(boolean, double[], double[], int) */ private final boolean isSourceGeographic, isTargetGeographic; /** * The coordinate operation defined by EPSG as provided by Apache SIS. This is the most authoritative * source of information that we have, since Apache SIS follows more closely EPSG definitions than many * other map projection libraries. However, axis order may be different than the order expected by other * implementations like Proj.4. */ private final CoordinateOperation authoritative; /** * The coordinate operations to compare. The first operation should be the same than {@link #authoritative}, * but with (longitude, latitude) axis order. We use that axis order for avoiding * complications with the use of Proj.4 or similar libraries. */ private final CoordinateOperation[] normalized; /** * A title for the test to be run, for information purpose only. */ private final String title; /** * Names of the implementation being tested, for information purpose only. */ private final String[] implementationNames; /** * Where to write benchmark results. */ @SuppressWarnings("UseOfSystemOutOrSystemErr") private static final PrintStream out = System.out; /** * {@code true} for using tabulations instead than formatting in tables. * This is enabled by the {@code --tabs}} flag on the command-line. */ private static boolean useTabulations; /** * {@code true} for flushing the output stream after each line when writing a table. * This allows more immediate feedback to the user, but sometime break the table layout. * We disable the immediate mode when writing to a file since the user would not see it anyway. */ private static boolean immediate; /** * Creates a comparator for coordinate operations between the given pair of EPSG codes. * Current implementations creates operations for Apache SIS and Proj.4 libraries, * but this list may be expanded in any future version. * * @param title a title for this benchmark. * @param source EPSG code of source CRS. * @param target EPSG code of target CRS. * @throws FactoryException if an error occurred while instantiating CRS or coordinate operation. * @throws TransformException if an error occurred while transforming coordinates. * @throws IOException if an error occurred while writing results. */ public CoordinateOperationComparator(final String title, final int source, final int target) throws FactoryException, TransformException, IOException { this(title, new String[] {"Apache SIS", "Proj.4"}, CRS.findOperation( CRS.forCode( "EPSG:" + source), CRS.forCode( "EPSG:" + target), null), Proj4.createOperation(Proj4.createCRS("+init=epsg:" + source + " +over", 2), Proj4.createCRS("+init=epsg:" + target + " +over", 2), true)); } /** * Creates a comparator for all the given coordinate operations. * The first coordinate operation shall be the authoritative one as defined by EPSG, * including domain of validity and positional accuracy metadata. * This is usually the operation created by Apache SIS. All other operations are implemented by other libraries. * All operations except the first (authoritative) one shall use the (longitude, latitude) * axis order, for simpler comparisons with Proj.4 and similar libraries. * * @param operations the operations to compare. The first operation shall be the authoritative one. * @param implementationNames names of the implementation being tested, for information purpose only. * @throws FactoryException if an error occurred while creating a coordinate operation. * @throws TransformException if an error occurred while computing the domain of validity. */ private CoordinateOperationComparator(final String title, final String[] implementationNames, CoordinateOperation... operations) throws FactoryException, TransformException, IOException { this.title = title; this.implementationNames = implementationNames.clone(); operations = operations.clone(); authoritative = operations[0]; final AbstractCRS sourceCRS = AbstractCRS.castOrCopy(authoritative.getSourceCRS()); final AbstractCRS targetCRS = AbstractCRS.castOrCopy(authoritative.getTargetCRS()); final AbstractCRS normalizedSource = sourceCRS.forConvention(AxesConvention.NORMALIZED); final AbstractCRS normalizedTarget = targetCRS.forConvention(AxesConvention.NORMALIZED); operations[0] = CRS.findOperation(normalizedSource, normalizedTarget, null); isSourceGeographic = (normalizedSource instanceof GeographicCRS); isTargetGeographic = (normalizedTarget instanceof GeographicCRS); normalized = operations; /* * Get the operation domain of validity in the units of source CRS, * then print a summary about the comparison we are about to do. */ GeographicBoundingBox bbox = CRS.getGeographicBoundingBox(authoritative); bbox = Extents.intersection(bbox, new DefaultGeographicBoundingBox(-179, 179, -89, 89)); final Envelope domain = Envelopes.transform(new GeneralEnvelope(bbox), normalizedSource); final Appendable table = newTable(); printIdentification(table, "Source CRS", sourceCRS); printIdentification(table, "Target CRS", targetCRS); printIdentification(table, "Operation", authoritative); printIdentification(table, "Method", method(authoritative)); table.append(String.format("Domain:\t\t%s%n", domain)); flush(table); /* * Fills the input array with random coordinates. * This array shall not be modified after construction. */ final Random random = new Random(); inputs = new double[NUM_PTS * NUM_PTS * DIM]; final double minX = domain.getMinimum(0); final double minY = domain.getMinimum(1); final double scaleX = domain.getSpan(0) / NUM_PTS; final double scaleY = domain.getSpan(1) / NUM_PTS; int n = 0; for (int j=0; j
NOTE: this method assumes that {@link #DIM} is equal to 2. * If this is no longer the case, replace {@link Math#hypot(double, double)} call * by {@link org.apache.sis.math.MathFunctions#magnitude(double...)}.
* * @param expected array of expected coordinates. * @param actual array of actual coordinates. * @param i index of the first ordinal value in {@code expected} and {@code actual} arrays. */ private static double distance(final boolean isGeographic, final double[] expected, final double[] actual, int i) { double d = Math.hypot(actual[i] - expected[i], actual[++i] - expected[i]); if (isGeographic) { d *= ReferencingServices.NAUTICAL_MILE * 60; } return d; } /** * Returns the destination where to write next tabular data. */ private static Appendable newTable() { if (useTabulations) return out; TableAppender table = new TableAppender(out); table.appendHorizontalSeparator(); return table; } /** * Flushes the output created by {@link #newTable()} and append a new line. * After this method call, the table should not be used anymore. */ private static void flush(final Appendable table) throws IOException { if (table instanceof TableAppender) { ((TableAppender) table).appendHorizontalSeparator(); } ((Flushable) table).flush(); out.println(); } /** * Prints the name of the given identified objects. * * @param label label to write before the identified object. * @param object object for which to write the name. */ private static void printIdentification(final Appendable table, final String label, final IdentifiedObject object) throws IOException { String id = IdentifiedObjects.toString(IdentifiedObjects.getIdentifier(object, Citations.EPSG)); table.append(String.format("%s:\t%s\t%s%n", label, (id != null) ? id : "", object.getName().getCode())); } /** * Runs the benchmark identified by the given number. Each number identifies a particular pair of source * and target CRS. For example the test #1 applies the "Cylindrical Equal Area (Spherical)" projection. * See source code of this class for the list of available tests. * * @param args number of the test to execute. * @throws FactoryException if an error occurred while instantiating CRS or coordinate operation. * @throws TransformException if an error occurred while transforming coordinates. * @throws IOException if an error occurred while writing results. */ public static void main(final String[] args) throws FactoryException, TransformException, IOException { /* * Where to write error messages or information. While this stream is called "the error stream", it is actually * also used for printing information (for example progress) that we don't want to include in the result file. * Note that this is also the stream used by {@code java.util.logging}. */ @SuppressWarnings("UseOfSystemOutOrSystemErr") final PrintStream info = System.err; immediate = (System.console() != null); String error = null; int sourceCRS = 0, targetCRS = 0; for (final String p : args) { if (p.equals("--tabs")) { useTabulations = true; } else if (p.startsWith("--")) { error = String.format("Unrecognized option: %s", p); break; } else { final int n; try { n = Integer.parseInt(args[0]); } catch (NumberFormatException e) { error = String.format("Invalid test number or EPSG code: %s", e.getLocalizedMessage()); break; } if (n <= 0) { error = String.format("Invalid test number or EPSG code: %d", n); break; } if (sourceCRS == 0) { sourceCRS = n; } else if (targetCRS == 0) { targetCRS = n; } else { error = "Too many EPSG codes (expected 2)."; break; } } } /* * If there is two numbers, they are interpreted as EPSG codes of source and target CRS respectively. * But if there is only one number (targetCRS == 0), then the source CRS is interpreted as the number * of a pre-defined test. */ CoordinateOperationComparator c = null; if (error == null) { if (sourceCRS == 0) { error = String.format("Usage: one or two numbers%n" + " [sequential number of pre-defined test]%n" + " [EPSG code of source CRS] [EPSG code of targetCRS]"); } else if (targetCRS == 0) { info.printf("Initializing pre-defined test #%d...%n", sourceCRS); c = create(sourceCRS); if (c == null) { error = "Error: no such pre-defined test."; } } else { String name = String.format("EPSG:%d to EPSG:%d", sourceCRS, targetCRS); info.printf("Initializing test for %s...%n", name); c = new CoordinateOperationComparator(name, sourceCRS, targetCRS); } } if (c == null) { info.println(error); System.exit(1); } c.run(); } }