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

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

see #16129 - add new projections and support for new format of ESRI file

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