Changeset 9609 in josm for trunk/test/unit/org/openstreetmap
- Timestamp:
- 2016-01-24T11:25:55+01:00 (9 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/test/unit/org/openstreetmap/josm/data/projection/ProjectionRefTest.java
r9130 r9609 3 3 4 4 import java.io.BufferedReader; 5 import java.io.BufferedWriter; 6 import java.io.File; 5 7 import java.io.FileInputStream; 6 8 import java.io.FileNotFoundException; 9 import java.io.FileOutputStream; 7 10 import java.io.IOException; 11 import java.io.InputStream; 8 12 import java.io.InputStreamReader; 13 import java.io.OutputStream; 14 import java.io.OutputStreamWriter; 9 15 import java.nio.charset.StandardCharsets; 16 import java.util.ArrayList; 17 import java.util.Arrays; 10 18 import java.util.Collection; 11 19 import java.util.HashMap; 20 import java.util.HashSet; 21 import java.util.LinkedHashSet; 22 import java.util.List; 12 23 import java.util.Map; 13 import java.util.Map.Entry; 14 24 import java.util.Objects; 25 import java.util.Random; 26 import java.util.Set; 27 import java.util.TreeMap; 28 import java.util.TreeSet; 29 import java.util.regex.Matcher; 30 import java.util.regex.Pattern; 31 32 import org.junit.Assert; 15 33 import org.junit.Test; 34 import org.openstreetmap.josm.data.Bounds; 16 35 import org.openstreetmap.josm.data.coor.EastNorth; 17 36 import org.openstreetmap.josm.data.coor.LatLon; 18 import org.openstreetmap.josm.gui.preferences.projection.ProjectionChoice; 19 import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; 37 import org.openstreetmap.josm.gui.preferences.projection.CodeProjectionChoice; 38 import org.openstreetmap.josm.tools.Pair; 39 import org.openstreetmap.josm.tools.Utils; 20 40 21 41 /** 22 * Test projections using reference data . (Currently provided by proj.4)42 * Test projections using reference data from external program. 23 43 * 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. 27 55 */ 28 56 public class ProjectionRefTest { 29 57 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 } 46 279 } 47 280 } 48 281 49 282 @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()); 75 313 fail.append(errorEN); 76 314 } 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; 94 348 } 95 349 }
Note:
See TracChangeset
for help on using the changeset viewer.