// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.projection; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.TreeSet; import org.junit.BeforeClass; import org.junit.Test; import org.openstreetmap.josm.JOSMFixture; import org.openstreetmap.josm.TestUtils; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.tools.Pair; /** * This test is used to monitor changes in projection code. * * It keeps a record of test data in the file data_nodist/projection/projection-regression-test-data. * This record is generated from the current Projection classes available in JOSM. It needs to * be updated, whenever a projection is added / removed or an algorithm is changed, such that * the computed values are numerically different. There is no error threshold, every change is reported. * * So when this test fails, first check if the change is intended. Then update the regression * test data, by running the main method of this class and commit the new data file. */ public class ProjectionRegressionTest { private static final String PROJECTION_DATA_FILE = "data_nodist/projection/projection-regression-test-data"; private static class TestData { public String code; public LatLon ll; public EastNorth en; public LatLon ll2; } /** * Program entry point to update reference projection file. * @param args not used * @throws IOException if any I/O errors occurs */ public static void main(String[] args) throws IOException { setUp(); Map<String, Projection> supportedCodesMap = new HashMap<>(); for (String code : Projections.getAllProjectionCodes()) { supportedCodesMap.put(code, Projections.getProjectionByCode(code)); } List<TestData> prevData = new ArrayList<>(); if (new File(PROJECTION_DATA_FILE).exists()) { prevData = readData(); } Map<String, TestData> prevCodesMap = new HashMap<>(); for (TestData data : prevData) { prevCodesMap.put(data.code, data); } Set<String> codesToWrite = new TreeSet<>(); for (TestData data : prevData) { if (supportedCodesMap.containsKey(data.code)) { codesToWrite.add(data.code); } } for (String code : supportedCodesMap.keySet()) { if (!codesToWrite.contains(code)) { codesToWrite.add(code); } } Random rand = new SecureRandom(); try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(PROJECTION_DATA_FILE), StandardCharsets.UTF_8))) { out.write("# Data for test/unit/org/openstreetmap/josm/data/projection/ProjectionRegressionTest.java\n"); out.write("# Format: 1. Projection code; 2. lat/lon; 3. lat/lon projected -> east/north; 4. east/north (3.) inverse projected\n"); for (String code : codesToWrite) { Projection proj = supportedCodesMap.get(code); Bounds b = proj.getWorldBoundsLatLon(); double lat, lon; TestData prev = prevCodesMap.get(proj.toCode()); if (prev != null) { lat = prev.ll.lat(); lon = prev.ll.lon(); } else { lat = b.getMin().lat() + rand.nextDouble() * (b.getMax().lat() - b.getMin().lat()); lon = b.getMin().lon() + rand.nextDouble() * (b.getMax().lon() - b.getMin().lon()); } EastNorth en = proj.latlon2eastNorth(new LatLon(lat, lon)); LatLon ll2 = proj.eastNorth2latlon(en); out.write(String.format( "%s%n ll %s %s%n en %s %s%n ll2 %s %s%n", proj.toCode(), lat, lon, en.east(), en.north(), ll2.lat(), ll2.lon())); } } System.out.println("Update successful."); } private static EastNorth getRoundedToOsmPrecision(double east, double north) { return new EastNorth(LatLon.roundToOsmPrecision(east), LatLon.roundToOsmPrecision(north)); } private static List<TestData> readData() throws IOException, FileNotFoundException { try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(PROJECTION_DATA_FILE), StandardCharsets.UTF_8))) { List<TestData> result = new ArrayList<>(); String line; while ((line = in.readLine()) != null) { if (line.startsWith("#")) { continue; } TestData next = new TestData(); Pair<Double, Double> ll = readLine("ll", in.readLine()); Pair<Double, Double> en = readLine("en", in.readLine()); Pair<Double, Double> ll2 = readLine("ll2", in.readLine()); next.code = line; next.ll = new LatLon(ll.a, ll.b); next.en = new EastNorth(en.a, en.b); next.ll2 = new LatLon(ll2.a, ll2.b); result.add(next); } return result; } } private static Pair<Double, Double> readLine(String expectedName, String input) { String[] fields = input.trim().split("[ ]+"); if (fields.length != 3) throw new AssertionError(); if (!fields[0].equals(expectedName)) throw new AssertionError(); double a = Double.parseDouble(fields[1]); double b = Double.parseDouble(fields[2]); return Pair.create(a, b); } /** * Setup test. */ @BeforeClass public static void setUp() { JOSMFixture.createUnitTestFixture().init(); } /** * Non-regression unit test. * @throws IOException if any I/O error occurs */ @Test public void testNonRegression() throws IOException { List<TestData> allData = readData(); Set<String> dataCodes = new HashSet<>(); for (TestData data : allData) { dataCodes.add(data.code); } StringBuilder fail = new StringBuilder(); for (String code : Projections.getAllProjectionCodes()) { if (!dataCodes.contains(code)) { fail.append("Did not find projection "+code+" in test data!\n"); } } for (TestData data : allData) { Projection proj = Projections.getProjectionByCode(data.code); if (proj == null) { fail.append("Projection "+data.code+" from test data was not found!\n"); continue; } EastNorth en = proj.latlon2eastNorth(data.ll); LatLon ll2 = proj.eastNorth2latlon(data.en); if (TestUtils.getJavaVersion() >= 9) { en = getRoundedToOsmPrecision(en.east(), en.north()); ll2 = ll2.getRoundedToOsmPrecision(); data.en = getRoundedToOsmPrecision(data.en.east(), data.en.north()); data.ll2 = data.ll2.getRoundedToOsmPrecision(); } if (!en.equals(data.en)) { String error = String.format("%s (%s): Projecting latlon(%s,%s):%n" + " expected: eastnorth(%s,%s),%n" + " but got: eastnorth(%s,%s)!%n", proj.toString(), data.code, data.ll.lat(), data.ll.lon(), data.en.east(), data.en.north(), en.east(), en.north()); fail.append(error); } if (!ll2.equals(data.ll2)) { String error = String.format("%s (%s): Inverse projecting eastnorth(%s,%s):%n" + " expected: latlon(%s,%s),%n" + " but got: latlon(%s,%s)!%n", proj.toString(), data.code, data.en.east(), data.en.north(), data.ll2.lat(), data.ll2.lon(), ll2.lat(), ll2.lon()); fail.append(error); } } if (fail.length() > 0) { System.err.println(fail.toString()); throw new AssertionError(fail.toString()); } } }