source: josm/trunk/test/unit/org/openstreetmap/josm/data/projection/ProjectionRefTest.java@ 11648

Last change on this file since 11648 was 11648, checked in by Don-vip, 7 years ago

see #14422 - update unit test

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