source: josm/trunk/test/unit/org/openstreetmap/josm/data/projection/ProjectionRegressionTest.java@ 19050

Last change on this file since 19050 was 19050, checked in by taylor.smock, 15 months ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

  • Property svn:eol-style set to native
File size: 9.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.projection;
3
4import static org.junit.jupiter.api.Assertions.fail;
5
6import java.io.BufferedReader;
7import java.io.BufferedWriter;
8import java.io.File;
9import java.io.IOException;
10import java.io.OutputStreamWriter;
11import java.nio.charset.StandardCharsets;
12import java.nio.file.Files;
13import java.nio.file.Paths;
14import java.security.SecureRandom;
15import java.util.ArrayList;
16import java.util.List;
17import java.util.Map;
18import java.util.Random;
19import java.util.Set;
20import java.util.TreeSet;
21import java.util.stream.Collectors;
22
23import org.junit.jupiter.api.Assumptions;
24import org.junit.jupiter.api.Test;
25import org.openstreetmap.josm.JOSMFixture;
26import org.openstreetmap.josm.data.Bounds;
27import org.openstreetmap.josm.data.coor.EastNorth;
28import org.openstreetmap.josm.data.coor.LatLon;
29import org.openstreetmap.josm.testutils.annotations.ProjectionNadGrids;
30import org.openstreetmap.josm.tools.Pair;
31import org.openstreetmap.josm.tools.Platform;
32import org.openstreetmap.josm.tools.Utils;
33
34/**
35 * This test is used to monitor changes in projection code.
36 * <p>
37 * It keeps a record of test data in the file nodist/data/projection/projection-regression-test-data.
38 * This record is generated from the current Projection classes available in JOSM. It needs to
39 * be updated, whenever a projection is added / removed or an algorithm is changed, such that
40 * the computed values are numerically different. There is no error threshold, every change is reported.
41 * <p>
42 * So when this test fails, first check if the change is intended. Then update the regression
43 * test data, by running the main method of this class and commit the new data file.
44 */
45class ProjectionRegressionTest {
46
47 private static final String PROJECTION_DATA_FILE = "nodist/data/projection/projection-regression-test-data";
48
49 private static final class TestData {
50 public String code;
51 public LatLon ll;
52 public EastNorth en;
53 public LatLon ll2;
54 }
55
56 /**
57 * Program entry point to update reference projection file.
58 * @param args not used
59 * @throws IOException if any I/O errors occurs
60 */
61 public static void main(String[] args) throws IOException {
62 JOSMFixture.createUnitTestFixture().init();
63
64 Map<String, Projection> supportedCodesMap = Projections.getAllProjectionCodes().stream()
65 .collect(Collectors.toMap(code -> code, Projections::getProjectionByCode));
66
67 List<TestData> prevData = new ArrayList<>();
68 if (new File(PROJECTION_DATA_FILE).exists()) {
69 prevData = readData();
70 }
71 Map<String, TestData> prevCodesMap = prevData.stream()
72 .collect(Collectors.toMap(data -> data.code, data -> data));
73
74 Set<String> codesToWrite = new TreeSet<>(supportedCodesMap.keySet());
75 prevData.stream()
76 .filter(data -> supportedCodesMap.containsKey(data.code)).map(data -> data.code)
77 .forEach(codesToWrite::add);
78
79 Random rand = new SecureRandom();
80 try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
81 Files.newOutputStream(Paths.get(PROJECTION_DATA_FILE)), StandardCharsets.UTF_8))) {
82 out.write("# Data for test/unit/org/openstreetmap/josm/data/projection/ProjectionRegressionTest.java\n");
83 out.write("# Format: 1. Projection code; 2. lat/lon; 3. lat/lon projected -> east/north; 4. east/north (3.) inverse projected\n");
84 for (String code : codesToWrite) {
85 Projection proj = supportedCodesMap.get(code);
86 Bounds b = proj.getWorldBoundsLatLon();
87 double lat, lon;
88 TestData prev = prevCodesMap.get(proj.toCode());
89 if (prev != null) {
90 lat = prev.ll.lat();
91 lon = prev.ll.lon();
92 } else {
93 lat = b.getMin().lat() + rand.nextDouble() * (b.getMax().lat() - b.getMin().lat());
94 lon = b.getMin().lon() + rand.nextDouble() * (b.getMax().lon() - b.getMin().lon());
95 }
96 EastNorth en = proj.latlon2eastNorth(new LatLon(lat, lon));
97 LatLon ll2 = proj.eastNorth2latlon(en);
98 out.write(String.format(
99 "%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()));
100 }
101 }
102 System.out.println("Update successful.");
103 }
104
105 private static List<TestData> readData() throws IOException {
106 try (BufferedReader in = Files.newBufferedReader(Paths.get(PROJECTION_DATA_FILE), StandardCharsets.UTF_8)) {
107 List<TestData> result = new ArrayList<>();
108 String line;
109 while ((line = in.readLine()) != null) {
110 if (line.startsWith("#")) {
111 continue;
112 }
113 TestData next = new TestData();
114
115 Pair<Double, Double> ll = readLine("ll", in.readLine());
116 Pair<Double, Double> en = readLine("en", in.readLine());
117 Pair<Double, Double> ll2 = readLine("ll2", in.readLine());
118
119 next.code = line;
120 next.ll = new LatLon(ll.a, ll.b);
121 next.en = new EastNorth(en.a, en.b);
122 next.ll2 = new LatLon(ll2.a, ll2.b);
123
124 result.add(next);
125 }
126 return result;
127 }
128 }
129
130 private static Pair<Double, Double> readLine(String expectedName, String input) {
131 String[] fields = input.trim().split("[ ]+", -1);
132 if (fields.length != 3) throw new AssertionError();
133 if (!fields[0].equals(expectedName)) throw new AssertionError();
134 double a = Double.parseDouble(fields[1]);
135 double b = Double.parseDouble(fields[2]);
136 return Pair.create(a, b);
137 }
138
139 /**
140 * Non-regression unit test.
141 * @throws IOException if any I/O error occurs
142 */
143 @ProjectionNadGrids
144 @Test
145 void testNonRegression() throws IOException {
146 // Disable on Github Windows runners + Java 8, minor differences appeared around 2021-07-20
147 Assumptions.assumeFalse(
148 Utils.getJavaVersion() == 8
149 && Platform.determinePlatform() == Platform.WINDOWS
150 && System.getenv("GITHUB_WORKFLOW") != null);
151 List<TestData> allData = readData();
152 Set<String> dataCodes = allData.stream().map(data -> data.code).collect(Collectors.toSet());
153
154 StringBuilder fail = new StringBuilder();
155
156 for (String code : Projections.getAllProjectionCodes()) {
157 if (!dataCodes.contains(code)) {
158 fail.append("Did not find projection ").append(code).append(" in test data!\n");
159 }
160 }
161
162 final boolean java9 = Utils.getJavaVersion() >= 9;
163 for (TestData data : allData) {
164 Projection proj = Projections.getProjectionByCode(data.code);
165 if (proj == null) {
166 fail.append("Projection ").append(data.code).append(" from test data was not found!\n");
167 continue;
168 }
169 EastNorth en = proj.latlon2eastNorth(data.ll);
170 LatLon ll2 = proj.eastNorth2latlon(data.en);
171 if (!(java9 ? equalsJava9(en, data.en) : en.equals(data.en))) {
172 String error = String.format("%s (%s): Projecting latlon(%s,%s):%n" +
173 " expected: eastnorth(%s,%s),%n" +
174 " but got: eastnorth(%s,%s)!%n",
175 proj, data.code, data.ll.lat(), data.ll.lon(), data.en.east(), data.en.north(), en.east(), en.north());
176 fail.append(error);
177 }
178 if (!(java9 ? equalsJava9(ll2, data.ll2) : ll2.equals(data.ll2))) {
179 String error = String.format("%s (%s): Inverse projecting eastnorth(%s,%s):%n" +
180 " expected: latlon(%s,%s),%n" +
181 " but got: latlon(%s,%s)!%n",
182 proj, data.code, data.en.east(), data.en.north(), data.ll2.lat(), data.ll2.lon(), ll2.lat(), ll2.lon());
183 fail.append(error);
184 }
185 }
186
187 if (fail.length() > 0) {
188 System.err.println(fail);
189 fail(fail.toString());
190 }
191 }
192
193 private static boolean equalsDoubleMaxUlp(double d1, double d2) {
194 // Due to error accumulation in projection computation, the difference can reach hundreds of ULPs
195 // The worst error is 1168 ULP (followed by 816 ULP then 512 ULP) with:
196 // NAD83 / Colorado South (EPSG:26955): Projecting latlon(32.24604527892822,-125.93039495227096):
197 // expected: eastnorth(-1004398.8994415681,24167.8944844745),
198 // but got: eastnorth(-1004398.8994415683,24167.894484478747)!
199 return Math.abs(d1 - d2) <= 1200 * Math.ulp(d1);
200 }
201
202 private static boolean equalsJava9(EastNorth en1, EastNorth en2) {
203 return equalsDoubleMaxUlp(en1.east(), en2.east()) &&
204 equalsDoubleMaxUlp(en1.north(), en2.north());
205 }
206
207 private static boolean equalsJava9(LatLon ll1, LatLon ll2) {
208 return equalsDoubleMaxUlp(ll1.lat(), ll2.lat()) &&
209 equalsDoubleMaxUlp(ll1.lon(), ll2.lon());
210 }
211}
Note: See TracBrowser for help on using the repository browser.