Changeset 9609 in josm for trunk/test/unit/org


Ignore:
Timestamp:
2016-01-24T11:25:55+01:00 (8 years ago)
Author:
bastiK
Message:

rewrite of ProjectionRefTest - now covers all projections (see #12186)

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/test/unit/org/openstreetmap/josm/data/projection/ProjectionRefTest.java

    r9130 r9609  
    33
    44import java.io.BufferedReader;
     5import java.io.BufferedWriter;
     6import java.io.File;
    57import java.io.FileInputStream;
    68import java.io.FileNotFoundException;
     9import java.io.FileOutputStream;
    710import java.io.IOException;
     11import java.io.InputStream;
    812import java.io.InputStreamReader;
     13import java.io.OutputStream;
     14import java.io.OutputStreamWriter;
    915import java.nio.charset.StandardCharsets;
     16import java.util.ArrayList;
     17import java.util.Arrays;
    1018import java.util.Collection;
    1119import java.util.HashMap;
     20import java.util.HashSet;
     21import java.util.LinkedHashSet;
     22import java.util.List;
    1223import java.util.Map;
    13 import java.util.Map.Entry;
    14 
     24import java.util.Objects;
     25import java.util.Random;
     26import java.util.Set;
     27import java.util.TreeMap;
     28import java.util.TreeSet;
     29import java.util.regex.Matcher;
     30import java.util.regex.Pattern;
     31
     32import org.junit.Assert;
    1533import org.junit.Test;
     34import org.openstreetmap.josm.data.Bounds;
    1635import org.openstreetmap.josm.data.coor.EastNorth;
    1736import org.openstreetmap.josm.data.coor.LatLon;
    18 import org.openstreetmap.josm.gui.preferences.projection.ProjectionChoice;
    19 import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
     37import org.openstreetmap.josm.gui.preferences.projection.CodeProjectionChoice;
     38import org.openstreetmap.josm.tools.Pair;
     39import org.openstreetmap.josm.tools.Utils;
    2040
    2141/**
    22  * Test projections using reference data. (Currently provided by proj.4)
     42 * Test projections using reference data from external program.
    2343 *
    24  * The data file data_nodist/projection/projection-reference-data.csv can be created like this:
    25  *      Fist run this file's main method to collect epsg codes and bounds data.
    26  *      Then pipe the result into test/generate-proj-data.pl.
     44 * To update the reference data file <code>data_nodist/projection/projection-reference-data</code>,
     45 * run the main method of this class. For this, you need to have the cs2cs
     46 * program from the proj.4 library in path (or use <code>CS2CS_EXE</code> to set
     47 * the full path of the executable). Make sure the required *.gsb grid files
     48 * can be accessed, i.e. copy them from <code>data/projection</code> to <code>/usr/share/proj</code> or
     49 * wherever cs2cs expects them to be placed.
     50 *
     51 * The input parameter for the external library is <em>not</em> the projection code
     52 * (e.g. "EPSG:25828"), but the entire definition, (e.g. "+proj=utm +zone=28 +ellps=GRS80 +nadgrids=null").
     53 * This means the test does not verify our definitions, but the correctness
     54 * of the algorithm, given a certain definition.
    2755 */
    2856public class ProjectionRefTest {
    2957
    30     /**
    31      * create a list of epsg codes and bounds to be used by the perl script
    32      * @param args program main arguments
    33      */
    34     public static void main(String[] args) {
    35         Map<String, Projection> allCodes = new HashMap<>();
    36         for (ProjectionChoice pc : ProjectionPreference.getProjectionChoices()) {
    37             for (String code : pc.allCodes()) {
    38                 Collection<String> pref = pc.getPreferencesFromCode(code);
    39                 pc.setPreferences(pref);
    40                 Projection p = pc.getProjection();
    41                 allCodes.put(code, p);
    42             }
    43         }
    44         for (Entry<String, Projection> e : allCodes.entrySet()) {
    45             System.out.println(String.format("%s %s", e.getKey(), e.getValue().getWorldBoundsLatLon()));
     58    private static final String CS2CS_EXE = "cs2cs";
     59
     60    private static final String REFERENCE_DATA_FILE = "data_nodist/projection/projection-reference-data";
     61
     62    private static class RefEntry {
     63        String code;
     64        String def;
     65        List<Pair<LatLon, EastNorth>> data;
     66
     67        public RefEntry(String code, String def) {
     68            this.code = code;
     69            this.def = def;
     70            this.data = new ArrayList<>();
     71        }
     72    }
     73
     74    static Random rand = new Random();
     75
     76    public static void main(String[] args) throws FileNotFoundException, IOException {
     77        Collection<RefEntry> refs = readData();
     78        refs = updateData(refs);
     79        writeData(refs);
     80    }
     81
     82    /**
     83     * Reads data from the reference file.
     84     * @return the data
     85     * @throws IOException
     86     * @throws FileNotFoundException
     87     */
     88    private static Collection<RefEntry> readData() throws IOException, FileNotFoundException {
     89        Collection<RefEntry> result = new ArrayList<>();
     90        if (!new File(REFERENCE_DATA_FILE).exists()) {
     91            System.err.println("Warning: refrence file does not exist.");
     92            return result;
     93        }
     94        try (BufferedReader in = new BufferedReader(new InputStreamReader(
     95                new FileInputStream(REFERENCE_DATA_FILE), StandardCharsets.UTF_8))) {
     96            String line;
     97            Pattern projPattern = Pattern.compile("<(.+?)>(.*)<>");
     98            RefEntry curEntry = null;
     99            while ((line = in.readLine()) != null) {
     100                if (line.startsWith("#") || line.trim().isEmpty()) {
     101                    continue;
     102                }
     103                if (line.startsWith("<")) {
     104                    Matcher m = projPattern.matcher(line);
     105                    if (!m.matches()) {
     106                        Assert.fail("unable to parse line: " + line);
     107                    }
     108                    String code = m.group(1);
     109                    String def = m.group(2).trim();
     110                    curEntry = new RefEntry(code, def);
     111                    result.add(curEntry);
     112                } else if (curEntry != null) {
     113                    String[] f = line.trim().split(",");
     114                    double lon = Double.parseDouble(f[0]);
     115                    double lat = Double.parseDouble(f[1]);
     116                    double east = Double.parseDouble(f[2]);
     117                    double north = Double.parseDouble(f[3]);
     118                    curEntry.data.add(Pair.create(new LatLon(lat, lon), new EastNorth(east, north)));
     119                }
     120            }
     121        }
     122        return result;
     123    }
     124
     125    /**
     126     * Generates new reference data by calling external program cs2cs.
     127     *
     128     * Old data is kept, as long as the projection definition is still the same.
     129     *
     130     * @param refs old data
     131     * @return updated data
     132     */
     133    private static Collection<RefEntry> updateData(Collection<RefEntry> refs) {
     134        Set<String> failed = new LinkedHashSet<>();
     135        final int N_POINTS = 20;
     136
     137        Map<String, RefEntry> refsMap = new HashMap<>();
     138        for (RefEntry ref : refs) {
     139            refsMap.put(ref.code, ref);
     140        }
     141
     142        List<RefEntry> refsNew = new ArrayList<>();
     143
     144        Set<String> codes = new TreeSet<>(new CodeProjectionChoice.CodeComparator());
     145        codes.addAll(Projections.getAllProjectionCodes());
     146        for (String code : codes) {
     147            String def = Projections.getInit(code);
     148
     149            RefEntry ref = new RefEntry(code, def);
     150            RefEntry oldRef = refsMap.get(code);
     151
     152            if (oldRef != null && Objects.equals(def, oldRef.def)) {
     153                for (int i = 0; i < N_POINTS && i < oldRef.data.size(); i++) {
     154                    ref.data.add(oldRef.data.get(i));
     155                }
     156            }
     157            if (ref.data.size() < N_POINTS) {
     158                System.out.print(code);
     159                System.out.flush();
     160                Projection proj = Projections.getProjectionByCode(code);
     161                Bounds b = proj.getWorldBoundsLatLon();
     162                for (int i = ref.data.size(); i < N_POINTS; i++) {
     163                    System.out.print(".");
     164                    System.out.flush();
     165                    LatLon ll = getRandom(b);
     166                    EastNorth en = latlon2eastNorthProj4(def, ll);
     167                    if (en != null) {
     168                        ref.data.add(Pair.create(ll, en));
     169                    } else {
     170                        System.err.println("Warning: cannot convert "+code+" at "+ll);
     171                        failed.add(code);
     172                    }
     173                }
     174                System.out.println();
     175            }
     176            refsNew.add(ref);
     177        }
     178        if (!failed.isEmpty()) {
     179            System.err.println("Error: the following " + failed.size() + " entries had errors: " + failed);
     180        }
     181        return refsNew;
     182    }
     183
     184    /**
     185     * Get random LatLon value within the bounds.
     186     * @param b the bounds
     187     * @return random LatLon value within the bounds
     188     */
     189    private static LatLon getRandom(Bounds b) {
     190        double lat, lon;
     191        lat = b.getMin().lat() + rand.nextDouble() * (b.getMax().lat() - b.getMin().lat());
     192        double minlon = b.getMinLon();
     193        double maxlon = b.getMaxLon();
     194        if (b.crosses180thMeridian()) {
     195            maxlon += 360;
     196        }
     197        lon = minlon + rand.nextDouble() * (maxlon - minlon);
     198        lon = LatLon.toIntervalLon(lon);
     199        return new LatLon(lat, lon);
     200    }
     201
     202    /**
     203     * Run external cs2cs command from the PROJ.4 library to convert lat/lon to
     204     * east/north value.
     205     * @param def the proj.4 projection definition string
     206     * @param ll the LatLon
     207     * @return projected EastNorth or null in case of error
     208     */
     209    private static EastNorth latlon2eastNorthProj4(String def, LatLon ll) {
     210        List<String> args = new ArrayList<>();
     211        args.add(CS2CS_EXE);
     212        args.addAll(Arrays.asList("-f %.9f +proj=longlat +datum=WGS84 +to".split(" ")));
     213        // proj.4 cannot read our ntf_r93_b.gsb file
     214        // possibly because it is big endian. Use equivalent
     215        // little endian file shipped with proj.4.
     216        // see http://geodesie.ign.fr/contenu/fichiers/documentation/algorithmes/notice/NT111_V1_HARMEL_TransfoNTF-RGF93_FormatGrilleNTV2.pdf
     217        def = def.replace("ntf_r93_b.gsb", "ntf_r93.gsb");
     218        args.addAll(Arrays.asList(def.split(" ")));
     219        ProcessBuilder pb = new ProcessBuilder(args);
     220
     221        String output;
     222        try {
     223            Process process = pb.start();
     224            OutputStream stdin = process.getOutputStream();
     225            final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin));
     226            InputStream stdout = process.getInputStream();
     227            final BufferedReader reader = new BufferedReader(new InputStreamReader(stdout));
     228            String input = String.format("%.9f %.9f\n", ll.lon(), ll.lat());
     229            writer.write(input);
     230            writer.close();
     231            output = reader.readLine();
     232            reader.close();
     233        } catch (IOException e) {
     234            System.err.println("Error: Running external command failed: " + e + "\nCommand was: "+Utils.join(" ", args));
     235            return null;
     236        }
     237        Pattern p = Pattern.compile("(\\S+)\\s+(\\S+)\\s.*");
     238        Matcher m = p.matcher(output);
     239        if (!m.matches()) {
     240            System.err.println("Error: Cannot parse cs2cs output: '" + output + "'");
     241            return null;
     242        }
     243        String es = m.group(1);
     244        String ns = m.group(2);
     245        if ("*".equals(es) || "*".equals(ns)) {
     246            System.err.println("Error: cs2cs is unable to convert coordinates.");
     247            return null;
     248        }
     249        try {
     250            return new EastNorth(Double.parseDouble(es), Double.parseDouble(ns));
     251        } catch (NumberFormatException nfe) {
     252            System.err.println("Error: Cannot parse cs2cs output: '" + es + "', '" + ns + "'" + "\nCommand was: "+Utils.join(" ", args));
     253            return null;
     254        }
     255    }
     256
     257    /**
     258     * Writes data to file.
     259     * @param refs the data
     260     * @throws FileNotFoundException
     261     * @throws IOException
     262     */
     263    private static void writeData(Collection<RefEntry> refs) throws FileNotFoundException, IOException {
     264        Map<String, RefEntry> refsMap = new TreeMap<>(new CodeProjectionChoice.CodeComparator());
     265        for (RefEntry ref : refs) {
     266            refsMap.put(ref.code, ref);
     267        }
     268        try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
     269                new FileOutputStream(REFERENCE_DATA_FILE), StandardCharsets.UTF_8))) {
     270            for (Map.Entry<String, RefEntry> e : refsMap.entrySet()) {
     271                RefEntry ref = e.getValue();
     272                out.write("<" + ref.code + "> " + ref.def + "  <>\n");
     273                for (Pair<LatLon, EastNorth> p : ref.data) {
     274                    LatLon ll = p.a;
     275                    EastNorth en = p.b;
     276                    out.write("    " + ll.lon() + "," + ll.lat() + "," + en.east() + "," + en.north() + "\n");
     277                }
     278            }
    46279        }
    47280    }
    48281
    49282    @Test
    50     public void test() throws IOException, FileNotFoundException {
    51         try (BufferedReader in = new BufferedReader(new InputStreamReader(
    52                 new FileInputStream("data_nodist/projection/projection-reference-data.csv"), StandardCharsets.UTF_8))) {
    53             StringBuilder fail = new StringBuilder();
    54             String line;
    55             while ((line = in.readLine()) != null) {
    56                 if (line.startsWith("#")) {
    57                     continue;
    58                 }
    59                 String[] f = line.split(",");
    60                 String code = f[0];
    61                 double lat = Double.parseDouble(f[1]);
    62                 double lon = Double.parseDouble(f[2]);
    63                 double east = Double.parseDouble(f[3]);
    64                 double north = Double.parseDouble(f[4]);
    65                 Projection p = Projections.getProjectionByCode(code);
    66                 EastNorth en = p.latlon2eastNorth(new LatLon(lat, lon));
    67                 String errorEN = String.format("%s (%s): Projecting latlon(%s,%s):%n" +
    68                         "        expected: eastnorth(%s,%s),%n" +
    69                         "        but got:  eastnorth(%s,%s)!%n",
    70                         p.toString(), code, lat, lon, east, north, en.east(), en.north());
    71                 final double EPSILON_EN = SwissGridTest.SWISS_EPSG_CODE.equals(code)
    72                         ? SwissGridTest.EPSILON_APPROX
    73                         : 1e-3; // 1 mm accuracy
    74                 if (Math.abs(east - en.east()) > EPSILON_EN || Math.abs(north - en.north()) > EPSILON_EN) {
     283    public void test() throws IOException {
     284        StringBuilder fail = new StringBuilder();
     285        Set<String> allCodes = new HashSet<>(Projections.getAllProjectionCodes());
     286        Collection<RefEntry> refs = readData();
     287
     288        for (RefEntry ref : refs) {
     289            String def0 = Projections.getInit(ref.code);
     290            if (def0 == null) {
     291                Assert.fail("unkown code: "+ref.code);
     292            }
     293            if (!ref.def.equals(def0)) {
     294                Assert.fail("definitions for " + ref.code + " do not match");
     295            }
     296            Projection proj = Projections.getProjectionByCode(ref.code);
     297            double scale = proj.getMetersPerUnit();
     298            for (Pair<LatLon, EastNorth> p : ref.data) {
     299                LatLon ll = p.a;
     300                EastNorth enRef = p.b;
     301                enRef = new EastNorth(enRef.east() * scale, enRef.north() * scale); // convert to meter
     302
     303                EastNorth en = proj.latlon2eastNorth(ll);
     304                if (proj.switchXY()) {
     305                    en = new EastNorth(en.north(), en.east());
     306                }
     307                final double EPSILON_EN = 1e-2; // 1cm
     308                if (!isEqual(enRef, en, EPSILON_EN, true)) {
     309                    String errorEN = String.format("%s (%s): Projecting latlon(%s,%s):%n" +
     310                            "        expected: eastnorth(%s,%s),%n" +
     311                            "        but got:  eastnorth(%s,%s)!%n",
     312                            proj.toString(), proj.toCode(), ll.lat(), ll.lon(), enRef.east(), enRef.north(), en.east(), en.north());
    75313                    fail.append(errorEN);
    76314                }
    77                 LatLon ll = p.eastNorth2latlon(new EastNorth(east, north));
    78                 String errorLL = String.format("%s (%s): Inverse projecting eastnorth(%s,%s):%n" +
    79                         "        expected: latlon(%s,%s),%n" +
    80                         "        but got:  latlon(%s,%s)!%n",
    81                         p.toString(), code, east, north, lat, lon, ll.lat(), ll.lon());
    82                 final double EPSILON_LL = Math.toDegrees(EPSILON_EN / 6378137); // 1 mm accuracy (or better)
    83                 if (Math.abs(lat - ll.lat()) > EPSILON_LL || Math.abs(lon - ll.lon()) > EPSILON_LL) {
    84                     if (!("yes".equals(System.getProperty("suppressPermanentFailure")) && code.equals("EPSG:21781"))) {
    85                         fail.append(errorLL);
    86                     }
    87                 }
    88             }
    89             if (fail.length() > 0) {
    90                 System.err.println(fail.toString());
    91                 throw new AssertionError(fail.toString());
    92             }
    93         }
     315            }
     316            allCodes.remove(ref.code);
     317        }
     318        if (!allCodes.isEmpty()) {
     319            Assert.fail("no reference data for following projections: "+allCodes);
     320        }
     321        if (fail.length() > 0) {
     322            System.err.println(fail.toString());
     323            throw new AssertionError(fail.toString());
     324        }
     325    }
     326
     327    /**
     328     * Check if two EastNorth objects are equal.
     329     * @param en1 first value
     330     * @param en2 second value
     331     * @param epsilon allowed tolerance
     332     * @param abs true if absolute value is compared; this is done as long as
     333     * advanced axis configuration is not supported in JOSM
     334     * @return true if both are considered equal
     335     */
     336    private static boolean isEqual(EastNorth en1, EastNorth en2, double epsilon, boolean abs) {
     337        double east1 = en1.east();
     338        double north1 = en1.north();
     339        double east2 = en2.east();
     340        double north2 = en2.north();
     341        if (abs) {
     342            east1 = Math.abs(east1);
     343            north1 = Math.abs(north1);
     344            east2 = Math.abs(east2);
     345            north2 = Math.abs(north2);
     346        }
     347        return Math.abs(east1 - east2) < epsilon && Math.abs(north1 - north2) < epsilon;
    94348    }
    95349}
Note: See TracChangeset for help on using the changeset viewer.