source: josm/trunk/scripts/SyncEditorLayerIndex.java@ 19181

Last change on this file since 19181 was 19139, checked in by taylor.smock, 4 months ago

PMD and checkstyle fixes

  • Property svn:eol-style set to native
File size: 71.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2
3import static java.nio.charset.StandardCharsets.UTF_8;
4import static org.apache.commons.lang3.StringUtils.isBlank;
5import static org.apache.commons.lang3.StringUtils.isNotBlank;
6
7import java.io.BufferedReader;
8import java.io.BufferedWriter;
9import java.io.IOException;
10import java.io.OutputStreamWriter;
11import java.io.Writer;
12import java.lang.reflect.Field;
13import java.net.MalformedURLException;
14import java.net.URL;
15import java.nio.charset.Charset;
16import java.nio.file.Files;
17import java.nio.file.Paths;
18import java.text.DecimalFormat;
19import java.text.ParseException;
20import java.text.SimpleDateFormat;
21import java.util.ArrayList;
22import java.util.Arrays;
23import java.util.Calendar;
24import java.util.Collection;
25import java.util.Collections;
26import java.util.Date;
27import java.util.HashMap;
28import java.util.LinkedList;
29import java.util.List;
30import java.util.Locale;
31import java.util.Map;
32import java.util.Map.Entry;
33import java.util.Objects;
34import java.util.Set;
35import java.util.function.BiConsumer;
36import java.util.function.Function;
37import java.util.regex.Matcher;
38import java.util.regex.Pattern;
39import java.util.stream.Collectors;
40
41import org.openstreetmap.gui.jmapviewer.Coordinate;
42import org.openstreetmap.josm.data.Preferences;
43import org.openstreetmap.josm.data.imagery.ImageryInfo;
44import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
45import org.openstreetmap.josm.data.imagery.Shape;
46import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
47import org.openstreetmap.josm.data.preferences.JosmUrls;
48import org.openstreetmap.josm.data.projection.Projections;
49import org.openstreetmap.josm.data.sources.SourceInfo;
50import org.openstreetmap.josm.data.validation.routines.DomainValidator;
51import org.openstreetmap.josm.io.imagery.ImageryReader;
52import org.openstreetmap.josm.spi.preferences.Config;
53import org.openstreetmap.josm.tools.ImageProvider;
54import org.openstreetmap.josm.tools.JosmRuntimeException;
55import org.openstreetmap.josm.tools.Logging;
56import org.openstreetmap.josm.tools.OptionParser;
57import org.openstreetmap.josm.tools.OptionParser.OptionCount;
58import org.openstreetmap.josm.tools.ReflectionUtils;
59import org.openstreetmap.josm.tools.Utils;
60import org.xml.sax.SAXException;
61
62import jakarta.json.Json;
63import jakarta.json.JsonArray;
64import jakarta.json.JsonNumber;
65import jakarta.json.JsonObject;
66import jakarta.json.JsonReader;
67import jakarta.json.JsonString;
68import jakarta.json.JsonValue;
69
70/**
71 * Compare and analyse the differences of the editor layer index and the JOSM imagery list.
72 * The goal is to keep both lists in sync.
73 * <p>
74 * The <a href="https://github.com/osmlab/editor-layer-index">editor layer index</a> project
75 * provides also a version in the JOSM format, but the GEOJSON is the original source format, so we read that.
76 * <p>
77 * For running, the main JOSM binary needs to be in classpath, e.g.
78 * <p>
79 * {@code $ java -cp ../dist/josm-custom.jar SyncEditorLayerIndex}
80 * <p>
81 * Add option {@code -h} to show the available command line flags.
82 */
83@SuppressWarnings("unchecked")
84public class SyncEditorLayerIndex {
85
86 private static final int MAXLEN = 140;
87
88 private List<ImageryInfo> josmEntries;
89 private JsonArray eliEntries;
90 private JsonArray idEntries;
91 private JsonArray rapidEntries;
92
93 private final Map<String, JsonObject> eliUrls = new HashMap<>();
94 private final Map<String, JsonObject> idUrls = new HashMap<>();
95 private final Map<String, JsonObject> rapidUrls = new HashMap<>();
96 private final Map<String, ImageryInfo> josmUrls = new HashMap<>();
97 private final Map<String, ImageryInfo> josmMirrors = new HashMap<>();
98 private static final Map<String, String> oldproj = new HashMap<>();
99 private static final List<String> ignoreproj = new LinkedList<>();
100
101 private static String eliInputFile = "imagery_eli.geojson";
102 private static String idInputFile = "imagery_id.geojson";
103 private static String rapidInputFile = "imagery_rapid.geojson";
104 private static String josmInputFile = "imagery_josm.imagery.xml";
105 private static String ignoreInputFile = "imagery_josm.ignores.txt";
106 private static Writer outputStream;
107 private static String optionOutput;
108 private static boolean optionShorten;
109 private static boolean optionNoSkip;
110 private static boolean optionXhtmlBody;
111 private static boolean optionXhtml;
112 private static String optionEliXml;
113 private static String optionJosmXml;
114 private static String optionEncoding;
115 private static boolean optionNoEli;
116 private final Map<String, String> skip = new HashMap<>();
117 private final Map<String, String> skipStart = new HashMap<>();
118
119 /**
120 * Main method.
121 * @param args program arguments
122 * @throws IOException if any I/O error occurs
123 * @throws ReflectiveOperationException if any reflective operation error occurs
124 * @throws SAXException if any SAX error occurs
125 */
126 public static void main(String[] args) throws IOException, SAXException, ReflectiveOperationException {
127 Locale.setDefault(Locale.ROOT);
128 parseCommandLineArguments(args);
129 Config.setUrlsProvider(JosmUrls.getInstance());
130 Preferences pref = new Preferences(JosmBaseDirectories.getInstance());
131 Config.setPreferencesInstance(pref);
132 pref.init(false);
133 SyncEditorLayerIndex script = new SyncEditorLayerIndex();
134 script.setupProj();
135 script.loadSkip();
136 script.start();
137 script.loadJosmEntries();
138 if (optionJosmXml != null) {
139 try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(optionJosmXml), UTF_8)) {
140 script.printentries(script.josmEntries, writer);
141 }
142 }
143 script.loadELIEntries();
144 script.loadELIUsers();
145 if (optionEliXml != null) {
146 try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(optionEliXml), UTF_8)) {
147 script.printentries(script.eliEntries, writer);
148 }
149 }
150 script.checkInOneButNotTheOther();
151 script.checkCommonEntries();
152 script.end();
153 if (outputStream != null) {
154 outputStream.close();
155 }
156 }
157
158 /**
159 * Displays help on the console
160 */
161 private static void showHelp() {
162 System.out.println(getHelp());
163 System.exit(0);
164 }
165
166 static String getHelp() {
167 return "usage: java -cp build SyncEditorLayerIndex\n" +
168 "-c,--encoding <encoding> output encoding (defaults to UTF-8 or cp850 on Windows)\n" +
169 "-e,--eli_input <eli_input> Input file for the editor layer index (geojson). " +
170 "Default is imagery_eli.geojson (current directory).\n" +
171 "-d,--id_input <id_input> Input file for the id index (geojson). " +
172 "Default is imagery_id.geojson (current directory).\n" +
173 "-h,--help show this help\n" +
174 "-i,--ignore_input <ignore_input> Input file for the ignore list. Default is imagery_josm.ignores.txt (current directory).\n" +
175 "-j,--josm_input <josm_input> Input file for the JOSM imagery list (xml). " +
176 "Default is imagery_josm.imagery.xml (current directory).\n" +
177 "-m,--noeli don't show output for ELI, Rapid or iD problems\n" +
178 "-n,--noskip don't skip known entries\n" +
179 "-o,--output <output> Output file, - prints to stdout (default: -)\n" +
180 "-p,--elixml <elixml> ELI entries for use in JOSM as XML file (incomplete)\n" +
181 "-q,--josmxml <josmxml> JOSM entries reoutput as XML file (incomplete)\n" +
182 "-r,--rapid_input <rapid_input> Input file for the rapid index (geojson). " +
183 "Default is imagery_rapid.geojson (current directory).\n" +
184 "-s,--shorten shorten the output, so it is easier to read in a console window\n" +
185 "-x,--xhtmlbody create XHTML body for display in a web page\n" +
186 "-X,--xhtml create XHTML for display in a web page\n";
187 }
188
189 /**
190 * Parse command line arguments.
191 * @param args program arguments
192 * @throws IOException in case of I/O error
193 */
194 static void parseCommandLineArguments(String[] args) throws IOException {
195 new OptionParser("JOSM/ELI synchronization script")
196 .addFlagParameter("help", SyncEditorLayerIndex::showHelp)
197 .addShortAlias("help", "h")
198 .addArgumentParameter("output", OptionCount.OPTIONAL, x -> optionOutput = x)
199 .addShortAlias("output", "o")
200 .addArgumentParameter("eli_input", OptionCount.OPTIONAL, x -> eliInputFile = x)
201 .addShortAlias("eli_input", "e")
202 .addArgumentParameter("id_input", OptionCount.OPTIONAL, x -> idInputFile = x)
203 .addShortAlias("id_input", "d")
204 .addArgumentParameter("rapid_input", OptionCount.OPTIONAL, x -> rapidInputFile = x)
205 .addShortAlias("rapid_input", "r")
206 .addArgumentParameter("josm_input", OptionCount.OPTIONAL, x -> josmInputFile = x)
207 .addShortAlias("josm_input", "j")
208 .addArgumentParameter("ignore_input", OptionCount.OPTIONAL, x -> ignoreInputFile = x)
209 .addShortAlias("ignore_input", "i")
210 .addFlagParameter("shorten", () -> optionShorten = true)
211 .addShortAlias("shorten", "s")
212 .addFlagParameter("noskip", () -> optionNoSkip = true)
213 .addShortAlias("noskip", "n")
214 .addFlagParameter("xhtmlbody", () -> optionXhtmlBody = true)
215 .addShortAlias("xhtmlbody", "x")
216 .addFlagParameter("xhtml", () -> optionXhtml = true)
217 .addShortAlias("xhtml", "X")
218 .addArgumentParameter("elixml", OptionCount.OPTIONAL, x -> optionEliXml = x)
219 .addShortAlias("elixml", "p")
220 .addArgumentParameter("josmxml", OptionCount.OPTIONAL, x -> optionJosmXml = x)
221 .addShortAlias("josmxml", "q")
222 .addFlagParameter("noeli", () -> optionNoEli = true)
223 .addShortAlias("noeli", "m")
224 .addArgumentParameter("encoding", OptionCount.OPTIONAL, x -> optionEncoding = x)
225 .addShortAlias("encoding", "c")
226 .parseOptionsOrExit(Arrays.asList(args));
227
228 if (optionOutput != null && !"-".equals(optionOutput)) {
229 outputStream = Files.newBufferedWriter(Paths.get(optionOutput), optionEncoding != null ? Charset.forName(optionEncoding) : UTF_8);
230 } else if (optionEncoding != null) {
231 outputStream = new OutputStreamWriter(System.out, optionEncoding);
232 }
233 }
234
235 void setupProj() {
236 oldproj.put("EPSG:3359", "EPSG:3404");
237 oldproj.put("EPSG:3785", "EPSG:3857");
238 oldproj.put("EPSG:31297", "EPGS:31287");
239 oldproj.put("EPSG:31464", "EPSG:31468");
240 oldproj.put("EPSG:54004", "EPSG:3857");
241 oldproj.put("EPSG:102100", "EPSG:3857");
242 oldproj.put("EPSG:102113", "EPSG:3857");
243 oldproj.put("EPSG:900913", "EPGS:3857");
244 ignoreproj.add("EPSG:4267");
245 ignoreproj.add("EPSG:5221");
246 ignoreproj.add("EPSG:5514");
247 ignoreproj.add("EPSG:32019");
248 ignoreproj.add("EPSG:102066");
249 ignoreproj.add("EPSG:102067");
250 ignoreproj.add("EPSG:102685");
251 ignoreproj.add("EPSG:102711");
252 }
253
254 void loadSkip() throws IOException {
255 final Pattern pattern = Pattern.compile("^\\|\\| *(ELI|Ignore) *\\|\\| *\\{\\{\\{(.+)}}} *\\|\\|");
256 try (BufferedReader fr = Files.newBufferedReader(Paths.get(ignoreInputFile), UTF_8)) {
257 String line;
258
259 while ((line = fr.readLine()) != null) {
260 Matcher res = pattern.matcher(line);
261 if (res.matches()) {
262 String s = res.group(2);
263 if (s.endsWith("...")) {
264 s = s.substring(0, s.length() - 3);
265 if ("Ignore".equals(res.group(1))) {
266 skipStart.put(s, "green");
267 } else {
268 skipStart.put(s, "darkgoldenrod");
269 }
270 } else {
271 if ("Ignore".equals(res.group(1))) {
272 skip.put(s, "green");
273 } else {
274 skip.put(s, "darkgoldenrod");
275 }
276 }
277 }
278 }
279 }
280 }
281
282 void myprintlnfinal(String s) {
283 if (outputStream != null) {
284 try {
285 outputStream.write(s + System.lineSeparator());
286 } catch (IOException e) {
287 throw new JosmRuntimeException(e);
288 }
289 } else {
290 System.out.println(s);
291 }
292 }
293
294 String isSkipString(String s) {
295 if (skip.containsKey(s))
296 return skip.get(s);
297 for (Entry<String, String> str : skipStart.entrySet()) {
298 if (s.startsWith(str.getKey())) {
299 skipStart.remove(str.getKey());
300 return str.getValue();
301 }
302 }
303 return null;
304 }
305
306 void myprintln(String s) {
307 String color;
308 final String escaped = s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
309 if ((color = isSkipString(s)) != null) {
310 skip.remove(s);
311 if (optionXhtmlBody || optionXhtml) {
312 s = "<pre style=\"margin:3px;color:"+color+"\">"
313 + escaped +"</pre>";
314 }
315 if (!optionNoSkip) {
316 return;
317 }
318 } else if (optionXhtmlBody || optionXhtml) {
319 color =
320 s.startsWith("***") ? "black" :
321 ((s.startsWith("+ ") || s.startsWith("+++ ELI")) ? "blue" :
322 (s.startsWith("#") ? "indigo" :
323 (s.startsWith("!") ? "orange" :
324 (s.startsWith("~") ? "red" : "brown"))));
325 s = "<pre style=\"margin:3px;color:"+color+"\">"+ escaped +"</pre>";
326 }
327 if ((s.startsWith("+ ") || s.startsWith("+++ ELI") || s.startsWith("#")) && optionNoEli) {
328 return;
329 }
330 myprintlnfinal(s);
331 }
332
333 void start() {
334 if (optionXhtml) {
335 myprintlnfinal(
336 "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n");
337 myprintlnfinal(
338 "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>"+
339 "<title>JOSM - ELI differences</title></head><body>\n");
340 }
341 }
342
343 void end() {
344 for (String s : skip.keySet()) {
345 myprintln("+++ Obsolete skip entry: " + s);
346 }
347 for (String s : skipStart.keySet()) {
348 myprintln("+++ Obsolete skip entry: " + s + "...");
349 }
350 if (optionXhtml) {
351 myprintlnfinal("</body></html>\n");
352 }
353 }
354
355 void loadELIEntries() throws IOException {
356 try (JsonReader jr = Json.createReader(Files.newBufferedReader(Paths.get(eliInputFile), UTF_8))) {
357 eliEntries = jr.readObject().getJsonArray("features");
358 }
359
360 for (JsonValue e : eliEntries) {
361 String url = getUrlStripped(e);
362 if (url.contains("{z}")) {
363 myprintln("+++ ELI-URL uses {z} instead of {zoom}: "+getDescription(e));
364 url = url.replace("{z}", "{zoom}");
365 }
366 if (eliUrls.containsKey(url)) {
367 myprintln("+++ ELI-URL is not unique: "+url);
368 } else {
369 eliUrls.put(url, e.asJsonObject());
370 }
371 JsonArray s = e.asJsonObject().get("properties").asJsonObject().getJsonArray("available_projections");
372 if (s != null) {
373 String urlLc = url.toLowerCase(Locale.ENGLISH);
374 List<String> old = new LinkedList<>();
375 for (JsonValue p : s) {
376 String proj = ((JsonString) p).getString();
377 if (oldproj.containsKey(proj) || ("CRS:84".equals(proj) && !urlLc.contains("version=1.3"))) {
378 old.add(proj);
379 }
380 }
381 if (!old.isEmpty()) {
382 myprintln("+ ELI Projections "+String.join(", ", old)+" not useful: "+getDescription(e));
383 }
384 }
385 }
386 myprintln("*** Loaded "+eliEntries.size()+" entries (ELI). ***");
387 }
388
389 void loadELIUsers() throws IOException {
390 try (JsonReader jr = Json.createReader(Files.newBufferedReader(Paths.get(idInputFile), UTF_8))) {
391 idEntries = jr.readArray();
392 }
393 for (JsonValue e : idEntries) {
394 String url = getUrlStripped(e);
395 if (!eliUrls.containsKey(url))
396 myprintln("+++ iD-URL not in ELI: "+url);
397 idUrls.put(url, e.asJsonObject());
398 }
399 myprintln("*** Loaded "+idEntries.size()+" entries (iD). ***");
400 try (JsonReader jr = Json.createReader(Files.newBufferedReader(Paths.get(rapidInputFile), UTF_8))) {
401 rapidEntries = jr.readArray();
402 }
403 for (JsonValue e : rapidEntries) {
404 String url = getUrlStripped(e);
405 if (!eliUrls.containsKey(url))
406 myprintln("+++ Rapid-URL not in ELI: "+url);
407 rapidUrls.put(url, e.asJsonObject());
408 }
409 myprintln("*** Loaded "+rapidEntries.size()+" entries (Rapid). ***");
410 }
411
412 String cdata(String s) {
413 return cdata(s, false);
414 }
415
416 String cdata(String s, boolean escape) {
417 if (escape) {
418 return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
419 } else if (s.matches(".*[<>&].*"))
420 return "<![CDATA["+s+"]]>";
421 return s;
422 }
423
424 String maininfo(Object entry, String offset) {
425 String t = getType(entry);
426 String res = offset + "<type>"+t+"</type>\n";
427 res += offset + "<url>"+cdata(getUrl(entry))+"</url>\n";
428 if (getMinZoom(entry) != null)
429 res += offset + "<min-zoom>"+getMinZoom(entry)+"</min-zoom>\n";
430 if (getMaxZoom(entry) != null)
431 res += offset + "<max-zoom>"+getMaxZoom(entry)+"</max-zoom>\n";
432 if ("wms".equals(t)) {
433 List<String> p = getProjections(entry);
434 if (p != null) {
435 res += offset + "<projections>\n";
436 for (String c : p) {
437 res += offset + " <code>"+c+"</code>\n";
438 }
439 res += offset + "</projections>\n";
440 }
441 }
442 return res;
443 }
444
445 void printentries(List<?> entries, Writer stream) throws IOException {
446 DecimalFormat df = new DecimalFormat("#.#######");
447 df.setRoundingMode(java.math.RoundingMode.CEILING);
448 stream.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
449 stream.write("<imagery xmlns=\"http://josm.openstreetmap.de/maps-1.0\">\n");
450 for (Object e : entries) {
451 stream.write(" <entry"
452 + ("eli-best".equals(getQuality(e)) ? " eli-best=\"true\"" : "")
453 + (getOverlay(e) ? " overlay=\"true\"" : "")
454 + ">\n");
455 String t;
456 if (isNotBlank(t = getName(e)))
457 stream.write(" <name>"+cdata(t, true)+"</name>\n");
458 if (isNotBlank(t = getId(e)))
459 stream.write(" <id>"+t+"</id>\n");
460 if (isNotBlank(t = getCategory(e)))
461 stream.write(" <category>"+t+"</category>\n");
462 if (isNotBlank(t = getDate(e)))
463 stream.write(" <date>"+t+"</date>\n");
464 if (isNotBlank(t = getCountryCode(e)))
465 stream.write(" <country-code>"+t+"</country-code>\n");
466 if ((getDefault(e)))
467 stream.write(" <default>true</default>\n");
468 stream.write(maininfo(e, " "));
469 if (isNotBlank(t = getAttributionText(e)))
470 stream.write(" <attribution-text mandatory=\"true\">"+cdata(t, true)+"</attribution-text>\n");
471 if (isNotBlank(t = getAttributionUrl(e)))
472 stream.write(" <attribution-url>"+cdata(t)+"</attribution-url>\n");
473 if (isNotBlank(t = getLogoImage(e)))
474 stream.write(" <logo-image>"+cdata(t, true)+"</logo-image>\n");
475 if (isNotBlank(t = getLogoUrl(e)))
476 stream.write(" <logo-url>"+cdata(t)+"</logo-url>\n");
477 if (isNotBlank(t = getTermsOfUseText(e)))
478 stream.write(" <terms-of-use-text>"+cdata(t, true)+"</terms-of-use-text>\n");
479 if (isNotBlank(t = getTermsOfUseUrl(e)))
480 stream.write(" <terms-of-use-url>"+cdata(t)+"</terms-of-use-url>\n");
481 if (isNotBlank(t = getPermissionReferenceUrl(e)))
482 stream.write(" <permission-ref>"+cdata(t)+"</permission-ref>\n");
483 if (isNotBlank(t = getPrivacyPolicyUrl(e)))
484 stream.write(" <privacy-policy-url>"+cdata(t)+"</privacy-policy-url>\n");
485 if ((getValidGeoreference(e)))
486 stream.write(" <valid-georeference>true</valid-georeference>\n");
487 if (isNotBlank(t = getIcon(e)))
488 stream.write(" <icon>"+cdata(t)+"</icon>\n");
489 for (Entry<String, String> d : getDescriptions(e).entrySet()) {
490 stream.write(" <description lang=\""+d.getKey()+"\">"+cdata(d.getValue(), true)+"</description>\n");
491 }
492 for (ImageryInfo m : getMirrors(e)) {
493 stream.write(" <mirror>\n"+maininfo(m, " ")+" </mirror>\n");
494 }
495 double minlat = 1000;
496 double minlon = 1000;
497 double maxlat = -1000;
498 double maxlon = -1000;
499 String shapes = "";
500 String sep = "\n ";
501 try {
502 for (Shape s: getShapes(e)) {
503 shapes += " <shape>";
504 int i = 0;
505 for (Coordinate p: s.getPoints()) {
506 double lat = p.getLat();
507 double lon = p.getLon();
508 if (lat > maxlat) maxlat = lat;
509 if (lon > maxlon) maxlon = lon;
510 if (lat < minlat) minlat = lat;
511 if (lon < minlon) minlon = lon;
512 if ((i++ % 3) == 0) {
513 shapes += sep + " ";
514 }
515 shapes += "<point lat='"+df.format(lat)+"' lon='"+df.format(lon)+"'/>";
516 }
517 shapes += sep + "</shape>\n";
518 }
519 } catch (IllegalArgumentException illegalArgumentException) {
520 Logging.trace(illegalArgumentException);
521 }
522 if (!shapes.isEmpty()) {
523 stream.write(" <bounds min-lat='"+df.format(minlat)
524 +"' min-lon='"+df.format(minlon)
525 +"' max-lat='"+df.format(maxlat)
526 +"' max-lon='"+df.format(maxlon)+"'>\n");
527 stream.write(shapes + " </bounds>\n");
528 }
529 stream.write(" </entry>\n");
530 }
531 stream.write("</imagery>\n");
532 stream.close();
533 }
534
535 void loadJosmEntries() throws IOException, SAXException, ReflectiveOperationException {
536 try (ImageryReader reader = new ImageryReader(josmInputFile)) {
537 josmEntries = reader.parse();
538 }
539
540 for (ImageryInfo e : josmEntries) {
541 if (!e.isValid()) {
542 myprintln("~~~ JOSM-Entry missing fields (" + String.join(", ", e.getMissingFields()) + "): " + getDescription(e));
543 }
544 if (isBlank(getUrl(e))) {
545 myprintln("~~~ JOSM-Entry without URL: " + getDescription(e));
546 continue;
547 }
548 if (isBlank(e.getDate()) && e.getDate() != null) {
549 myprintln("~~~ JOSM-Entry with empty Date: " + getDescription(e));
550 continue;
551 }
552 if (isBlank(getName(e))) {
553 myprintln("~~~ JOSM-Entry without Name: " + getDescription(e));
554 continue;
555 }
556 String url = getUrlStripped(e);
557 if (url.contains("{z}")) {
558 myprintln("~~~ JOSM-URL uses {z} instead of {zoom}: "+getDescription(e));
559 url = url.replace("{z}", "{zoom}");
560 }
561 if (josmUrls.containsKey(url)) {
562 myprintln("~~~ JOSM-URL is not unique: "+url);
563 } else {
564 josmUrls.put(url, e);
565 }
566 for (ImageryInfo m : e.getMirrors()) {
567 url = getUrlStripped(m);
568 Field origNameField = SourceInfo.class.getDeclaredField("origName");
569 ReflectionUtils.setObjectsAccessible(origNameField);
570 origNameField.set(m, m.getOriginalName().replaceAll(" mirror server( \\d+)?", ""));
571 if (josmUrls.containsKey(url)) {
572 myprintln("~~~ JOSM-Mirror-URL is not unique: "+url);
573 } else {
574 josmUrls.put(url, m);
575 josmMirrors.put(url, m);
576 }
577 }
578 }
579 myprintln("*** Loaded "+josmEntries.size()+" entries (JOSM). ***");
580 }
581
582 // catch reordered arguments, make them uppercase, and switches to WMS version 1.3.0
583 String unifyWMS(String url) {
584 String[] x = url.replaceAll("(?i)VERSION=[0-9.]+", "VERSION=x")
585 .replaceAll("(?i)SRS=", "CRS=")
586 .replaceAll("(?i)BBOX=", "BBOX=")
587 .replaceAll("(?i)FORMAT=", "FORMAT=")
588 .replaceAll("(?i)LAYERS=", "LAYERS=")
589 .replaceAll("(?i)MAP=", "MAP=")
590 .replaceAll("(?i)REQUEST=", "REQUEST=")
591 .replaceAll("(?i)SERVICE=", "SERVICE=")
592 .replaceAll("(?i)STYLES=", "STYLES=")
593 .replaceAll("(?i)TRANSPARENT=FALSE", "TRANSPARENT=FALSE")
594 .replaceAll("(?i)TRANSPARENT=TRUE", "TRANSPARENT=TRUE")
595 .replaceAll("(?i)WIDTH=", "WIDTH=")
596 .replaceAll("(?i)HEIGHT=", "HEIGHT=")
597 .split("\\?");
598 return x[0] +"?" + Arrays.stream(x[1].split("&"))
599 .filter(s -> !s.endsWith("=")) // filter empty params
600 .sorted()
601 .collect(Collectors.joining("&"));
602 }
603
604 void checkInOneButNotTheOther() {
605 List<String> le = new LinkedList<>(eliUrls.keySet());
606 List<String> lj = new LinkedList<>(josmUrls.keySet());
607
608 for (String url : new LinkedList<>(le)) {
609 if (lj.contains(url)) {
610 le.remove(url);
611 lj.remove(url);
612 }
613 }
614
615 if (!le.isEmpty() && !lj.isEmpty()) {
616 List<String> ke = new LinkedList<>(le);
617 for (String urle : ke) {
618 JsonObject e = eliUrls.get(urle);
619 String ide = getId(e);
620 String urlhttps = urle.replace("http:", "https:");
621 if (lj.contains(urlhttps)) {
622 myprintln("+ Missing https: "+getDescription(e));
623 eliUrls.put(urlhttps, eliUrls.get(urle));
624 eliUrls.remove(urle);
625 le.remove(urle);
626 lj.remove(urlhttps);
627 } else if (isNotBlank(ide)) {
628 checkUrlsEquality(ide, e, urle, le, lj);
629 }
630 }
631 }
632
633 myprintln("*** URLs found in ELI but not in JOSM ("+le.size()+"): ***");
634 Collections.sort(le);
635 if (!le.isEmpty()) {
636 for (String l : le) {
637 String e = "";
638 if (idUrls.get(l) != null && rapidUrls.get(l) != null)
639 e = " **iD+Rapid**";
640 else if (idUrls.get(l) != null)
641 e = " **iD**";
642 else if (rapidUrls.get(l) != null)
643 e = " **Rapid**";
644 myprintln("- " + getDescription(eliUrls.get(l)) + e);
645 }
646 }
647 myprintln("*** URLs found in JOSM but not in ELI ("+lj.size()+"): ***");
648 Collections.sort(lj);
649 if (!lj.isEmpty()) {
650 for (String l : lj) {
651 myprintln("+ " + getDescription(josmUrls.get(l)));
652 }
653 }
654 }
655
656 void checkUrlsEquality(String ide, JsonObject e, String urle, List<String> le, List<String> lj) {
657 for (String urlj : new LinkedList<>(lj)) {
658 ImageryInfo j = josmUrls.get(urlj);
659 String idj = getId(j);
660
661 if (checkUrlEquality(ide, "id", idj, e, j, urle, urlj, le, lj)) {
662 return;
663 }
664 Collection<String> old = j.getOldIds();
665 if (old != null) {
666 for (String oidj : old) {
667 if (checkUrlEquality(ide, "oldid", oidj, e, j, urle, urlj, le, lj)) {
668 return;
669 }
670 }
671 }
672 }
673 }
674
675 boolean checkUrlEquality(
676 String ide, String idtype, String idj, JsonObject e, ImageryInfo j, String urle, String urlj, List<String> le, List<String> lj) {
677 if (ide.equals(idj) && Objects.equals(getType(j), getType(e))) {
678 if (getType(j).equals("wms") && unifyWMS(urle).equals(unifyWMS(urlj))) {
679 myprintln("# WMS-URL for "+idtype+" "+idj+" modified: "+getDescription(j));
680 } else {
681 myprintln("* URL for "+idtype+" "+idj+" differs ("+urle+"): "+getDescription(j));
682 }
683 le.remove(urle);
684 lj.remove(urlj);
685 // replace key for this entry with JOSM URL
686 eliUrls.remove(urle);
687 eliUrls.put(urlj, e);
688 return true;
689 }
690 return false;
691 }
692
693 void checkCommonEntries() {
694 doSameUrlButDifferentName();
695 doSameUrlButDifferentId();
696 doSameUrlButDifferentType();
697 doSameUrlButDifferentZoomBounds();
698 doSameUrlButDifferentCountryCode();
699 doSameUrlButDifferentQuality();
700 doSameUrlButDifferentDates();
701 doSameUrlButDifferentInformation();
702 doMismatchingShapes();
703 doMismatchingIcons();
704 doMismatchingCategories();
705 doMiscellaneousChecks();
706 }
707
708 void doSameUrlButDifferentName() {
709 myprintln("*** Same URL, but different name: ***");
710 for (String url : eliUrls.keySet()) {
711 JsonObject e = eliUrls.get(url);
712 if (!josmUrls.containsKey(url)) continue;
713 ImageryInfo j = josmUrls.get(url);
714 String ename = getName(e).replace("'", "\u2019");
715 String jname = getName(j).replace("'", "\u2019");
716 if (!ename.equals(jname)) {
717 myprintln("* Name differs ('"+getName(e)+"' != '"+getName(j)+"'): "+getUrl(j));
718 }
719 }
720 }
721
722 void doSameUrlButDifferentId() {
723 myprintln("*** Same URL, but different Id: ***");
724 for (String url : eliUrls.keySet()) {
725 JsonObject e = eliUrls.get(url);
726 if (!josmUrls.containsKey(url)) continue;
727 ImageryInfo j = josmUrls.get(url);
728 String ename = getId(e);
729 String jname = getId(j);
730 if (!Objects.equals(ename, jname)) {
731 myprintln("# Id differs ('"+getId(e)+"' != '"+getId(j)+"'): "+getUrl(j));
732 }
733 }
734 }
735
736 void doSameUrlButDifferentType() {
737 myprintln("*** Same URL, but different type: ***");
738 for (String url : eliUrls.keySet()) {
739 JsonObject e = eliUrls.get(url);
740 if (!josmUrls.containsKey(url)) continue;
741 ImageryInfo j = josmUrls.get(url);
742 if (!Objects.equals(getType(e), getType(j))) {
743 myprintln("* Type differs ("+getType(e)+" != "+getType(j)+"): "+getName(j)+" - "+getUrl(j));
744 }
745 }
746 }
747
748 void doSameUrlButDifferentZoomBounds() {
749 myprintln("*** Same URL, but different zoom bounds: ***");
750 for (String url : eliUrls.keySet()) {
751 JsonObject e = eliUrls.get(url);
752 if (!josmUrls.containsKey(url)) continue;
753 ImageryInfo j = josmUrls.get(url);
754
755 Integer eMinZoom = getMinZoom(e);
756 Integer jMinZoom = getMinZoom(j);
757 /* dont warn for entries copied from the base of the mirror */
758 if (eMinZoom == null && "wms".equals(getType(j)) && j.getName().contains(" mirror"))
759 jMinZoom = null;
760 if (!Objects.equals(eMinZoom, jMinZoom) && !(Objects.equals(eMinZoom, 0) && jMinZoom == null)) {
761 myprintln("* Minzoom differs ("+eMinZoom+" != "+jMinZoom+"): "+getDescription(j));
762 }
763 Integer eMaxZoom = getMaxZoom(e);
764 Integer jMaxZoom = getMaxZoom(j);
765 /* dont warn for entries copied from the base of the mirror */
766 if (eMaxZoom == null && "wms".equals(getType(j)) && j.getName().contains(" mirror"))
767 jMaxZoom = null;
768 if (!Objects.equals(eMaxZoom, jMaxZoom)) {
769 myprintln("* Maxzoom differs ("+eMaxZoom+" != "+jMaxZoom+"): "+getDescription(j));
770 }
771 }
772 }
773
774 void doSameUrlButDifferentCountryCode() {
775 myprintln("*** Same URL, but different country code: ***");
776 for (String url : eliUrls.keySet()) {
777 JsonObject e = eliUrls.get(url);
778 if (!josmUrls.containsKey(url)) continue;
779 ImageryInfo j = josmUrls.get(url);
780 String cce = getCountryCode(e);
781 if ("ZZ".equals(cce)) { /* special ELI country code */
782 cce = null;
783 }
784 if (cce != null && !cce.equals(getCountryCode(j))) {
785 myprintln("* Country code differs ("+getCountryCode(e)+" != "+getCountryCode(j)+"): "+getDescription(j));
786 }
787 }
788 }
789
790 void doSameUrlButDifferentQuality() {
791 myprintln("*** Same URL, but different quality: ***");
792 for (String url : eliUrls.keySet()) {
793 JsonObject e = eliUrls.get(url);
794 if (!josmUrls.containsKey(url)) {
795 String q = getQuality(e);
796 if ("eli-best".equals(q)) {
797 myprintln("- Quality best entry not in JOSM for "+getDescription(e));
798 }
799 continue;
800 }
801 ImageryInfo j = josmUrls.get(url);
802 if (!Objects.equals(getQuality(e), getQuality(j))) {
803 myprintln("* Quality differs ("+getQuality(e)+" != "+getQuality(j)+"): "+getDescription(j));
804 }
805 }
806 }
807
808 void doSameUrlButDifferentDates() {
809 myprintln("*** Same URL, but different dates: ***");
810 Pattern pattern = Pattern.compile("^(.*;)(\\d\\d\\d\\d)(-(\\d\\d)(-(\\d\\d))?)?$");
811 for (String url : eliUrls.keySet()) {
812 String ed = getDate(eliUrls.get(url));
813 if (!josmUrls.containsKey(url)) continue;
814 ImageryInfo j = josmUrls.get(url);
815 String jd = getDate(j);
816 // The forms 2015;- or -;2015 or 2015;2015 are handled equal to 2015
817 String ef = ed.replaceAll("\\A-;", "").replaceAll(";-\\z", "").replaceAll("\\A([0-9-]+);\\1\\z", "$1");
818 // ELI has a strange and inconsistent used end_date definition, so we try again with subtraction by one
819 String ed2 = ed;
820 Matcher m = pattern.matcher(ed);
821 if (m.matches()) {
822 Calendar cal = Calendar.getInstance();
823 cal.set(Integer.parseInt(m.group(2)),
824 m.group(4) == null ? 0 : Integer.parseInt(m.group(4))-1,
825 m.group(6) == null ? 1 : Integer.parseInt(m.group(6)));
826 cal.add(Calendar.DAY_OF_MONTH, -1);
827 ed2 = m.group(1) + cal.get(Calendar.YEAR);
828 if (m.group(4) != null)
829 ed2 += "-" + String.format("%02d", cal.get(Calendar.MONTH)+1);
830 if (m.group(6) != null)
831 ed2 += "-" + String.format("%02d", cal.get(Calendar.DAY_OF_MONTH));
832 }
833 String ef2 = ed2.replaceAll("\\A-;", "").replaceAll(";-\\z", "").replaceAll("\\A([0-9-]+);\\1\\z", "$1");
834 if (!ed.equals(jd) && !ef.equals(jd) && !ed2.equals(jd) && !ef2.equals(jd)) {
835 String t = "'"+ed+"'";
836 if (!ed.equals(ef)) {
837 t += " or '"+ef+"'";
838 }
839 if (jd.isEmpty()) {
840 myprintln("- Missing JOSM date ("+t+"): "+getDescription(j));
841 } else if (!ed.isEmpty()) {
842 myprintln("* Date differs ('"+t+"' != '"+jd+"'): "+getDescription(j));
843 } else if (!optionNoEli) {
844 myprintln("+ Missing ELI date ('"+jd+"'): "+getDescription(j));
845 }
846 }
847 }
848 }
849
850 void doSameUrlButDifferentInformation() {
851 myprintln("*** Same URL, but different information: ***");
852 for (String url : eliUrls.keySet()) {
853 if (!josmUrls.containsKey(url)) continue;
854 JsonObject e = eliUrls.get(url);
855 ImageryInfo j = josmUrls.get(url);
856
857 compareDescriptions(e, j);
858 comparePrivacyPolicyUrls(e, j);
859 comparePermissionReferenceUrls(e, j);
860 compareAttributionUrls(e, j);
861 compareAttributionTexts(e, j);
862 compareProjections(e, j);
863 compareDefaults(e, j);
864 compareOverlays(e, j);
865 compareNoTileHeaders(e, j);
866 }
867 }
868
869 void compareDescriptions(JsonObject e, ImageryInfo j) {
870 String et = getDescriptions(e).getOrDefault("en", "");
871 String jt = getDescriptions(j).getOrDefault("en", "");
872 if (!et.equals(jt)) {
873 if (jt.isEmpty()) {
874 myprintln("- Missing JOSM description ("+et+"): "+getDescription(j));
875 } else if (!et.isEmpty()) {
876 myprintln("* Description differs ('"+et+"' != '"+jt+"'): "+getDescription(j));
877 } else if (!optionNoEli) {
878 myprintln("+ Missing ELI description ('"+jt+"'): "+getDescription(j));
879 }
880 }
881 }
882
883 void comparePrivacyPolicyUrls(JsonObject e, ImageryInfo j) {
884 String et = getPrivacyPolicyUrl(e);
885 String jt = getPrivacyPolicyUrl(j);
886 if (!Objects.equals(et, jt)) {
887 if (isBlank(jt)) {
888 myprintln("- Missing JOSM privacy policy URL ("+et+"): "+getDescription(j));
889 } else if (isNotBlank(et)) {
890 myprintln("* Privacy policy URL differs ('"+et+"' != '"+jt+"'): "+getDescription(j));
891 } else if (!optionNoEli) {
892 myprintln("+ Missing ELI privacy policy URL ('"+jt+"'): "+getDescription(j));
893 }
894 }
895 }
896
897 void comparePermissionReferenceUrls(JsonObject e, ImageryInfo j) {
898 String et = getPermissionReferenceUrl(e);
899 String jt = getPermissionReferenceUrl(j);
900 String jt2 = getTermsOfUseUrl(j);
901 if (isBlank(jt)) jt = jt2;
902 if (!Objects.equals(et, jt)) {
903 if (isBlank(jt)) {
904 myprintln("- Missing JOSM license URL ("+et+"): "+getDescription(j));
905 } else if (isNotBlank(et)) {
906 String ethttps = et.replace("http:", "https:");
907 if (isBlank(jt2) || !(jt2.equals(ethttps) || jt2.equals(et+"/") || jt2.equals(ethttps+"/"))) {
908 if (jt.equals(ethttps) || jt.equals(et+"/") || jt.equals(ethttps+"/")) {
909 myprintln("+ License URL differs ('"+et+"' != '"+jt+"'): "+getDescription(j));
910 } else {
911 String ja = getAttributionUrl(j);
912 if (ja != null && (ja.equals(et) || ja.equals(ethttps) || ja.equals(et+"/") || ja.equals(ethttps+"/"))) {
913 myprintln("+ ELI License URL in JOSM Attribution: "+getDescription(j));
914 } else {
915 myprintln("* License URL differs ('"+et+"' != '"+jt+"'): "+getDescription(j));
916 }
917 }
918 }
919 } else if (!optionNoEli) {
920 myprintln("+ Missing ELI license URL ('"+jt+"'): "+getDescription(j));
921 }
922 }
923 }
924
925 void compareAttributionUrls(JsonObject e, ImageryInfo j) {
926 String et = getAttributionUrl(e);
927 String jt = getAttributionUrl(j);
928 if (!Objects.equals(et, jt)) {
929 if (isBlank(jt)) {
930 myprintln("- Missing JOSM attribution URL ("+et+"): "+getDescription(j));
931 } else if (isNotBlank(et)) {
932 String ethttps = et.replace("http:", "https:");
933 if (jt.equals(ethttps) || jt.equals(et+"/") || jt.equals(ethttps+"/")) {
934 myprintln("+ Attribution URL differs ('"+et+"' != '"+jt+"'): "+getDescription(j));
935 } else {
936 myprintln("* Attribution URL differs ('"+et+"' != '"+jt+"'): "+getDescription(j));
937 }
938 } else if (!optionNoEli) {
939 myprintln("+ Missing ELI attribution URL ('"+jt+"'): "+getDescription(j));
940 }
941 }
942 }
943
944 void compareAttributionTexts(JsonObject e, ImageryInfo j) {
945 String et = getAttributionText(e);
946 String jt = getAttributionText(j);
947 if (!Objects.equals(et, jt)) {
948 if (isBlank(jt)) {
949 myprintln("- Missing JOSM attribution text ("+et+"): "+getDescription(j));
950 } else if (isNotBlank(et)) {
951 myprintln("* Attribution text differs ('"+et+"' != '"+jt+"'): "+getDescription(j));
952 } else if (!optionNoEli) {
953 myprintln("+ Missing ELI attribution text ('"+jt+"'): "+getDescription(j));
954 }
955 }
956 }
957
958 void compareProjections(JsonObject e, ImageryInfo j) {
959 String et = getProjections(e).stream().sorted().collect(Collectors.joining(" "));
960 String jt = getProjections(j).stream().sorted().collect(Collectors.joining(" "));
961 if (!Objects.equals(et, jt)) {
962 if (isBlank(jt)) {
963 String t = getType(e);
964 if ("wms_endpoint".equals(t) || "tms".equals(t)) {
965 myprintln("+ ELI projections for type "+t+": "+getDescription(j));
966 } else {
967 myprintln("- Missing JOSM projections ("+et+"): "+getDescription(j));
968 }
969 } else if (isNotBlank(et)) {
970 if ("EPSG:3857 EPSG:4326".equals(et) || "EPSG:3857".equals(et) || "EPSG:4326".equals(et)) {
971 myprintln("+ ELI has minimal projections ('"+et+"' != '"+jt+"'): "+getDescription(j));
972 } else {
973 myprintln("* Projections differ ('"+et+"' != '"+jt+"'): "+getDescription(j));
974 }
975 } else if (!optionNoEli && !"tms".equals(getType(e))) {
976 myprintln("+ Missing ELI projections ('"+jt+"'): "+getDescription(j));
977 }
978 }
979 }
980
981 void compareDefaults(JsonObject e, ImageryInfo j) {
982 boolean ed = getDefault(e);
983 boolean jd = getDefault(j);
984 if (ed != jd) {
985 if (!jd) {
986 myprintln("- Missing JOSM default: "+getDescription(j));
987 } else if (!optionNoEli) {
988 myprintln("+ Missing ELI default: "+getDescription(j));
989 }
990 }
991 }
992
993 void compareOverlays(JsonObject e, ImageryInfo j) {
994 boolean eo = getOverlay(e);
995 boolean jo = getOverlay(j);
996 if (eo != jo) {
997 if (!jo) {
998 myprintln("- Missing JOSM overlay flag: "+getDescription(j));
999 } else if (!optionNoEli) {
1000 myprintln("+ Missing ELI overlay flag: "+getDescription(j));
1001 }
1002 }
1003 }
1004
1005 void compareNoTileHeaders(JsonObject e, ImageryInfo j) {
1006 Map<String, Set<String>> eh = getNoTileHeader(e);
1007 Map<String, Set<String>> jh = getNoTileHeader(j);
1008 if (!Objects.equals(eh, jh)) {
1009 if (Utils.isEmpty(jh)) {
1010 myprintln("- Missing JOSM no tile headers ("+eh+"): "+getDescription(j));
1011 } else if (!Utils.isEmpty(eh)) {
1012 myprintln("* No tile headers differ ('"+eh+"' != '"+jh+"'): "+getDescription(j));
1013 } else if (!optionNoEli) {
1014 myprintln("+ Missing ELI no tile headers ('"+jh+"'): "+getDescription(j));
1015 }
1016 }
1017 }
1018
1019 void doMismatchingShapes() {
1020 myprintln("*** Mismatching shapes: ***");
1021 for (String url : josmUrls.keySet()) {
1022 ImageryInfo j = josmUrls.get(url);
1023 int num = 1;
1024 for (Shape shape : getShapes(j)) {
1025 List<Coordinate> p = shape.getPoints();
1026 if (!p.get(0).equals(p.get(p.size()-1))) {
1027 myprintln("~~~ JOSM shape "+num+" unclosed: "+getDescription(j));
1028 }
1029 for (int nump = 1; nump < p.size(); ++nump) {
1030 if (Objects.equals(p.get(nump-1), p.get(nump))) {
1031 myprintln("~~~ JOSM shape "+num+" double point at "+(nump-1)+": "+getDescription(j));
1032 }
1033 }
1034 ++num;
1035 }
1036 }
1037 for (String url : eliUrls.keySet()) {
1038 JsonObject e = eliUrls.get(url);
1039 int num = 1;
1040 List<Shape> s = null;
1041 try {
1042 s = getShapes(e);
1043 for (Shape shape : s) {
1044 List<Coordinate> p = shape.getPoints();
1045 if (!p.get(0).equals(p.get(p.size()-1)) && !optionNoEli) {
1046 myprintln("+++ ELI shape "+num+" unclosed: "+getDescription(e));
1047 }
1048 for (int nump = 1; nump < p.size(); ++nump) {
1049 if (Objects.equals(p.get(nump-1), p.get(nump))) {
1050 myprintln("+++ ELI shape "+num+" double point at "+(nump-1)+": "+getDescription(e));
1051 }
1052 }
1053 ++num;
1054 }
1055 } catch (IllegalArgumentException err) {
1056 String desc = getDescription(e);
1057 myprintln("+++ ELI shape contains invalid data for "+desc+": "+err.getMessage());
1058 }
1059 if (s == null || !josmUrls.containsKey(url)) {
1060 continue;
1061 }
1062 ImageryInfo j = josmUrls.get(url);
1063 List<Shape> js = getShapes(j);
1064 if (s.isEmpty() && !js.isEmpty()) {
1065 if (!optionNoEli) {
1066 myprintln("+ No ELI shape: "+getDescription(j));
1067 }
1068 } else if (js.isEmpty() && !s.isEmpty()) {
1069 // don't report boundary like 5 point shapes as difference
1070 if (s.size() != 1 || s.get(0).getPoints().size() != 5) {
1071 myprintln("- No JOSM shape: "+getDescription(j));
1072 }
1073 } else if (s.size() != js.size()) {
1074 myprintln("* Different number of shapes ("+s.size()+" != "+js.size()+"): "+getDescription(j));
1075 } else {
1076 boolean[] edone = new boolean[s.size()];
1077 boolean[] jdone = new boolean[js.size()];
1078 for (int enums = 0; enums < s.size(); ++enums) {
1079 List<Coordinate> ep = s.get(enums).getPoints();
1080 for (int jnums = 0; jnums < js.size() && !edone[enums]; ++jnums) {
1081 List<Coordinate> jp = js.get(jnums).getPoints();
1082 if (ep.size() == jp.size() && !jdone[jnums]) {
1083 boolean err = false;
1084 for (int nump = 0; nump < ep.size() && !err; ++nump) {
1085 Coordinate ept = ep.get(nump);
1086 Coordinate jpt = jp.get(nump);
1087 if (differentCoordinate(ept.getLat(), jpt.getLat()) || differentCoordinate(ept.getLon(), jpt.getLon()))
1088 err = true;
1089 }
1090 if (!err) {
1091 edone[enums] = true;
1092 jdone[jnums] = true;
1093 break;
1094 }
1095 }
1096 }
1097 }
1098 for (int enums = 0; enums < s.size(); ++enums) {
1099 List<Coordinate> ep = s.get(enums).getPoints();
1100 for (int jnums = 0; jnums < js.size() && !edone[enums]; ++jnums) {
1101 List<Coordinate> jp = js.get(jnums).getPoints();
1102 if (ep.size() == jp.size() && !jdone[jnums]) {
1103 boolean err = false;
1104 for (int nump = 0; nump < ep.size() && !err; ++nump) {
1105 Coordinate ept = ep.get(nump);
1106 Coordinate jpt = jp.get(nump);
1107 if (differentCoordinate(ept.getLat(), jpt.getLat()) || differentCoordinate(ept.getLon(), jpt.getLon())) {
1108 String numtxt = Integer.toString(enums+1);
1109 if (enums != jnums) {
1110 numtxt += '/' + Integer.toString(jnums+1);
1111 }
1112 myprintln("* Different coordinate for point "+(nump+1)+" of shape "+numtxt+": "+getDescription(j));
1113 break;
1114 }
1115 }
1116 edone[enums] = true;
1117 jdone[jnums] = true;
1118 break;
1119 }
1120 }
1121 }
1122 for (int enums = 0; enums < s.size(); ++enums) {
1123 List<Coordinate> ep = s.get(enums).getPoints();
1124 for (int jnums = 0; jnums < js.size() && !edone[enums]; ++jnums) {
1125 List<Coordinate> jp = js.get(jnums).getPoints();
1126 if (!jdone[jnums]) {
1127 String numtxt = Integer.toString(enums+1);
1128 if (enums != jnums) {
1129 numtxt += '/' + Integer.toString(jnums+1);
1130 }
1131 myprintln("* Different number of points for shape "+numtxt+" ("+ep.size()+" ! = "+jp.size()+"): "
1132 + getDescription(j));
1133 edone[enums] = true;
1134 jdone[jnums] = true;
1135 break;
1136 }
1137 }
1138 }
1139 }
1140 }
1141 }
1142
1143 private boolean differentCoordinate(double v1, double v2) {
1144 double epsilon = 0.00001;
1145 return Math.abs(v1 - v2) > epsilon;
1146 }
1147
1148 void doMismatchingIcons() {
1149 myprintln("*** Mismatching icons: ***");
1150 doMismatching(this::compareIcons);
1151 }
1152
1153 void doMismatchingCategories() {
1154 myprintln("*** Mismatching categories: ***");
1155 doMismatching(this::compareCategories);
1156 }
1157
1158 void doMismatching(BiConsumer<ImageryInfo, JsonObject> comparator) {
1159 for (String url : eliUrls.keySet()) {
1160 if (josmUrls.containsKey(url)) {
1161 comparator.accept(josmUrls.get(url), eliUrls.get(url));
1162 }
1163 }
1164 }
1165
1166 void compareIcons(ImageryInfo j, JsonObject e) {
1167 String ij = getIcon(j);
1168 String ie = getIcon(e);
1169 boolean ijok = isNotBlank(ij);
1170 boolean ieok = isNotBlank(ie);
1171 if (ijok && !ieok) {
1172 if (!optionNoEli) {
1173 myprintln("+ No ELI icon: "+getDescription(j));
1174 }
1175 } else if (!ijok && ieok) {
1176 myprintln("- No JOSM icon: "+getDescription(j));
1177 } else if (ijok && ieok && !Objects.equals(ij, ie) && !(
1178 (ie.startsWith("https://osmlab.github.io/editor-layer-index/")
1179 || ie.startsWith("https://raw.githubusercontent.com/osmlab/editor-layer-index/")) &&
1180 ij.startsWith("data:"))) {
1181 String iehttps = ie.replace("http:", "https:");
1182 if (ij.equals(iehttps)) {
1183 myprintln("+ Different icons: "+getDescription(j));
1184 } else {
1185 myprintln("* Different icons: "+getDescription(j));
1186 }
1187 }
1188 }
1189
1190 void compareCategories(ImageryInfo j, JsonObject e) {
1191 String cj = getCategory(j);
1192 String ce = getCategory(e);
1193 boolean cjok = isNotBlank(cj);
1194 boolean ceok = isNotBlank(ce);
1195 if (cjok && !ceok) {
1196 if (!optionNoEli) {
1197 myprintln("+ No ELI category: "+getDescription(j));
1198 }
1199 } else if (!cjok && ceok) {
1200 myprintln("- No JOSM category: "+getDescription(j));
1201 } else if (cjok && ceok && !Objects.equals(cj, ce)) {
1202 myprintln("* Different categories ('"+ce+"' != '"+cj+"'): "+getDescription(j));
1203 }
1204 }
1205
1206 void doMiscellaneousChecks() {
1207 myprintln("*** Miscellaneous checks: ***");
1208 Map<String, ImageryInfo> josmIds = new HashMap<>();
1209 Collection<String> all = Projections.getAllProjectionCodes();
1210 DomainValidator dv = DomainValidator.getInstance();
1211 for (String url : josmUrls.keySet()) {
1212 ImageryInfo j = josmUrls.get(url);
1213 String id = getId(j);
1214 if ("wms".equals(getType(j))) {
1215 String urlLc = url.toLowerCase(Locale.ENGLISH);
1216 if (getProjections(j).isEmpty()) {
1217 myprintln("~ WMS without projections: "+getDescription(j));
1218 } else {
1219 List<String> unsupported = new LinkedList<>();
1220 List<String> old = new LinkedList<>();
1221 for (String p : getProjectionsUnstripped(j)) {
1222 if ("CRS:84".equals(p)) {
1223 if (!urlLc.contains("version=1.3")) {
1224 myprintln("~ CRS:84 without WMS 1.3: "+getDescription(j));
1225 }
1226 } else if (oldproj.containsKey(p)) {
1227 old.add(p);
1228 } else if (!all.contains(p) && !ignoreproj.contains(p)) {
1229 unsupported.add(p);
1230 }
1231 }
1232 if (!unsupported.isEmpty()) {
1233 myprintln("~ Projections "+String.join(", ", unsupported)+" not supported by JOSM: "+getDescription(j));
1234 }
1235 for (String o : old) {
1236 myprintln("~ Projection "+o+" is an old unsupported code and has been replaced by "+oldproj.get(o)+": "
1237 + getDescription(j));
1238 }
1239 }
1240 if (urlLc.contains("version=1.3") && !urlLc.contains("crs={proj}")) {
1241 myprintln("~ WMS 1.3 with strange CRS specification: "+getDescription(j));
1242 } else if (urlLc.contains("version=1.1") && !urlLc.contains("srs={proj}")) {
1243 myprintln("~ WMS 1.1 with strange SRS specification: "+getDescription(j));
1244 }
1245 }
1246 List<String> urls = new LinkedList<>();
1247 if (!"scanex".equals(getType(j))) {
1248 urls.add(url);
1249 }
1250 String jt = getPermissionReferenceUrl(j);
1251 if (isNotBlank(jt) && !"Public Domain".equalsIgnoreCase(jt))
1252 urls.add(jt);
1253 jt = getTermsOfUseUrl(j);
1254 if (isNotBlank(jt))
1255 urls.add(jt);
1256 jt = getAttributionUrl(j);
1257 if (isNotBlank(jt))
1258 urls.add(jt);
1259 jt = getIcon(j);
1260 if (isNotBlank(jt)) {
1261 if (!jt.startsWith("data:image/"))
1262 urls.add(jt);
1263 else {
1264 try {
1265 new ImageProvider(jt).get();
1266 } catch (RuntimeException e) {
1267 myprintln("~ Strange Icon: "+getDescription(j));
1268 }
1269 }
1270 }
1271 Pattern patternU = Pattern.compile("^https?://([^/]+?)(:\\d+)?(/.*)?");
1272 for (String u : urls) {
1273 if (!patternU.matcher(u).matches() || u.matches(".*[ \t]+$")) {
1274 myprintln("~ Strange URL '"+u+"': "+getDescription(j));
1275 } else {
1276 try {
1277 URL jurl = new URL(u.replaceAll("\\{switch:[^}]*}", "x"));
1278 String domain = jurl.getHost();
1279 int port = jurl.getPort();
1280 if (!(domain.matches("^\\d+\\.\\d+\\.\\d+\\.\\d+$")) && !dv.isValid(domain))
1281 myprintln("~ Strange Domain '"+domain+"': "+getDescription(j));
1282 else if (80 == port || 443 == port) {
1283 myprintln("~ Useless port '"+port+"': "+getDescription(j));
1284 }
1285 } catch (MalformedURLException e) {
1286 myprintln("~ Malformed URL '"+u+"': "+getDescription(j)+" => "+e.getMessage());
1287 }
1288 }
1289 }
1290
1291 if (josmMirrors.containsKey(url)) {
1292 continue;
1293 }
1294 if (isBlank(id)) {
1295 myprintln("~ No JOSM-ID: "+getDescription(j));
1296 } else if (josmIds.containsKey(id)) {
1297 myprintln("~ JOSM-ID "+id+" not unique: "+getDescription(j));
1298 } else {
1299 josmIds.put(id, j);
1300 }
1301 String d = getDate(j);
1302 if (isNotBlank(d)) {
1303 Pattern patternD = Pattern.compile("^(-|(\\d\\d\\d\\d)(-(\\d\\d)(-(\\d\\d))?)?)(;(-|(\\d\\d\\d\\d)(-(\\d\\d)(-(\\d\\d))?)?))?$");
1304 Matcher m = patternD.matcher(d);
1305 if (!m.matches()) {
1306 myprintln("~ JOSM-Date '"+d+"' is strange: "+getDescription(j));
1307 } else {
1308 try {
1309 Date first = verifyDate(m.group(2), m.group(4), m.group(6));
1310 Date second = verifyDate(m.group(9), m.group(11), m.group(13));
1311 if (second.compareTo(first) < 0) {
1312 myprintln("~ JOSM-Date '"+d+"' is strange (second earlier than first): "+getDescription(j));
1313 }
1314 } catch (Exception e) {
1315 myprintln("~ JOSM-Date '"+d+"' is strange ("+e.getMessage()+"): "+getDescription(j));
1316 }
1317 }
1318 }
1319 if (isNotBlank(getAttributionUrl(j)) && isBlank(getAttributionText(j))) {
1320 myprintln("~ Attribution link without text: "+getDescription(j));
1321 }
1322 if (isNotBlank(getLogoUrl(j)) && isBlank(getLogoImage(j))) {
1323 myprintln("~ Logo link without image: "+getDescription(j));
1324 }
1325 if (isNotBlank(getTermsOfUseText(j)) && isBlank(getTermsOfUseUrl(j))) {
1326 myprintln("~ Terms of Use text without link: "+getDescription(j));
1327 }
1328 List<Shape> js = getShapes(j);
1329 if (!js.isEmpty()) {
1330 double minlat = 1000;
1331 double minlon = 1000;
1332 double maxlat = -1000;
1333 double maxlon = -1000;
1334 for (Shape s: js) {
1335 for (Coordinate p: s.getPoints()) {
1336 double lat = p.getLat();
1337 double lon = p.getLon();
1338 if (lat > maxlat) maxlat = lat;
1339 if (lon > maxlon) maxlon = lon;
1340 if (lat < minlat) minlat = lat;
1341 if (lon < minlon) minlon = lon;
1342 }
1343 }
1344 ImageryBounds b = j.getBounds();
1345 if (differentCoordinate(b.getMinLat(), minlat)
1346 || differentCoordinate(b.getMinLon(), minlon)
1347 || differentCoordinate(b.getMaxLat(), maxlat)
1348 || differentCoordinate(b.getMaxLon(), maxlon)) {
1349 myprintln("~ Bounds do not match shape (is "+b.getMinLat()+","+b.getMinLon()+","+b.getMaxLat()+","+b.getMaxLon()
1350 + ", calculated <bounds min-lat='"+minlat+"' min-lon='"+minlon+"' max-lat='"+maxlat+"' max-lon='"+maxlon+"'>): "
1351 + getDescription(j));
1352 }
1353 }
1354 List<String> knownCategories = Arrays.asList(
1355 "photo", "elevation", "map", "historicmap", "osmbasedmap", "historicphoto", "qa", "other");
1356 String cat = getCategory(j);
1357 if (isBlank(cat)) {
1358 myprintln("~ No category: "+getDescription(j));
1359 } else if (!knownCategories.contains(cat)) {
1360 myprintln("~ Strange category "+cat+": "+getDescription(j));
1361 }
1362 }
1363 }
1364
1365 /*
1366 * Utility functions that allow uniform access for both ImageryInfo and JsonObject.
1367 */
1368
1369 static String getUrl(Object e) {
1370 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getUrl();
1371 JsonObject p = ((Map<String, JsonObject>) e).get("properties");
1372 if (p != null)
1373 return p.getString("url");
1374 else
1375 return ((JsonObject) e).getString("template");
1376 }
1377
1378 static String getUrlStripped(Object e) {
1379 return getUrl(e).replaceAll("\\?(apikey|access_token)=.*", "");
1380 }
1381
1382 static String getDate(Object e) {
1383 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getDate() != null ? ((ImageryInfo) e).getDate() : "";
1384 JsonObject p = ((Map<String, JsonObject>) e).get("properties");
1385 String start = p.containsKey("start_date") ? p.getString("start_date") : "";
1386 String end = p.containsKey("end_date") ? p.getString("end_date") : "";
1387 if (!start.isEmpty() && !end.isEmpty())
1388 return start+";"+end;
1389 else if (!start.isEmpty())
1390 return start+";-";
1391 else if (!end.isEmpty())
1392 return "-;"+end;
1393 return "";
1394 }
1395
1396 static Date verifyDate(String year, String month, String day) throws ParseException {
1397 String date;
1398 if (year == null) {
1399 date = "3000-01-01";
1400 } else {
1401 date = year + "-" + (month == null ? "01" : month) + "-" + (day == null ? "01" : day);
1402 }
1403 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
1404 df.setLenient(false);
1405 return df.parse(date);
1406 }
1407
1408 static String getId(Object e) {
1409 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getId();
1410 return ((Map<String, JsonObject>) e).get("properties").getString("id");
1411 }
1412
1413 static String getName(Object e) {
1414 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getOriginalName();
1415 return ((Map<String, JsonObject>) e).get("properties").getString("name");
1416 }
1417
1418 static List<ImageryInfo> getMirrors(Object e) {
1419 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getMirrors();
1420 return Collections.emptyList();
1421 }
1422
1423 static List<String> getProjections(Object e) {
1424 List<String> r = new ArrayList<>();
1425 List<String> u = getProjectionsUnstripped(e);
1426 for (String p : u) {
1427 if (!oldproj.containsKey(p) && !("CRS:84".equals(p) && !(getUrlStripped(e).matches("(?i)version=1\\.3")))) {
1428 r.add(p);
1429 }
1430 }
1431 return r;
1432 }
1433
1434 static List<String> getProjectionsUnstripped(Object e) {
1435 List<String> r = null;
1436 if (e instanceof ImageryInfo) {
1437 r = ((ImageryInfo) e).getServerProjections();
1438 } else {
1439 JsonValue s = ((Map<String, JsonObject>) e).get("properties").get("available_projections");
1440 if (s != null) {
1441 r = new ArrayList<>();
1442 for (JsonValue p : s.asJsonArray()) {
1443 r.add(((JsonString) p).getString());
1444 }
1445 }
1446 }
1447 return r != null ? r : Collections.emptyList();
1448 }
1449
1450 static void addJsonShapes(List<Shape> l, JsonArray a) {
1451 if (a.get(0).asJsonArray().get(0) instanceof JsonArray) {
1452 for (JsonValue sub: a.asJsonArray()) {
1453 addJsonShapes(l, sub.asJsonArray());
1454 }
1455 } else {
1456 Shape s = new Shape();
1457 for (JsonValue point: a.asJsonArray()) {
1458 JsonArray ar = point.asJsonArray();
1459 String lon = ar.getJsonNumber(0).toString();
1460 String lat = ar.getJsonNumber(1).toString();
1461 s.addPoint(lat, lon);
1462 }
1463 l.add(s);
1464 }
1465 }
1466
1467 static List<Shape> getShapes(Object e) {
1468 if (e instanceof ImageryInfo) {
1469 ImageryBounds bounds = ((ImageryInfo) e).getBounds();
1470 if (bounds != null) {
1471 return bounds.getShapes();
1472 }
1473 return Collections.emptyList();
1474 }
1475 JsonValue ex = ((Map<String, JsonValue>) e).get("geometry");
1476 if (ex != null && !JsonValue.NULL.equals(ex) && !ex.asJsonObject().isNull("coordinates")) {
1477 JsonArray poly = ex.asJsonObject().getJsonArray("coordinates");
1478 List<Shape> l = new ArrayList<>();
1479 for (JsonValue shapes: poly) {
1480 addJsonShapes(l, shapes.asJsonArray());
1481 }
1482 return l;
1483 }
1484 return Collections.emptyList();
1485 }
1486
1487 static String getType(Object e) {
1488 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getImageryType().getTypeString();
1489 return ((Map<String, JsonObject>) e).get("properties").getString("type");
1490 }
1491
1492 static Integer getMinZoom(Object e) {
1493 if (e instanceof ImageryInfo) {
1494 int mz = ((ImageryInfo) e).getMinZoom();
1495 return mz == 0 ? null : mz;
1496 } else {
1497 JsonNumber num = ((Map<String, JsonObject>) e).get("properties").getJsonNumber("min_zoom");
1498 if (num == null) return null;
1499 return num.intValue();
1500 }
1501 }
1502
1503 static Integer getMaxZoom(Object e) {
1504 if (e instanceof ImageryInfo) {
1505 int mz = ((ImageryInfo) e).getMaxZoom();
1506 return mz == 0 ? null : mz;
1507 } else {
1508 JsonNumber num = ((Map<String, JsonObject>) e).get("properties").getJsonNumber("max_zoom");
1509 if (num == null) return null;
1510 return num.intValue();
1511 }
1512 }
1513
1514 static String getCountryCode(Object e) {
1515 if (e instanceof ImageryInfo) return "".equals(((ImageryInfo) e).getCountryCode()) ? null : ((ImageryInfo) e).getCountryCode();
1516 return ((Map<String, JsonObject>) e).get("properties").getString("country_code", null);
1517 }
1518
1519 static String getQuality(Object e) {
1520 if (e instanceof ImageryInfo) return ((ImageryInfo) e).isBestMarked() ? "eli-best" : null;
1521 return (((Map<String, JsonObject>) e).get("properties").containsKey("best")
1522 && ((Map<String, JsonObject>) e).get("properties").getBoolean("best")) ? "eli-best" : null;
1523 }
1524
1525 static boolean getOverlay(Object e) {
1526 if (e instanceof ImageryInfo) return ((ImageryInfo) e).isOverlay();
1527 return (((Map<String, JsonObject>) e).get("properties").containsKey("overlay")
1528 && ((Map<String, JsonObject>) e).get("properties").getBoolean("overlay"));
1529 }
1530
1531 static String getIcon(Object e) {
1532 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getIcon();
1533 return ((Map<String, JsonObject>) e).get("properties").getString("icon", null);
1534 }
1535
1536 static String getAttributionText(Object e) {
1537 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getAttributionText(0, null, null);
1538 try {
1539 return ((Map<String, JsonObject>) e).get("properties").getJsonObject("attribution").getString("text", null);
1540 } catch (NullPointerException ex) {
1541 return null;
1542 }
1543 }
1544
1545 static String getAttributionUrl(Object e) {
1546 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getAttributionLinkURL();
1547 try {
1548 return ((Map<String, JsonObject>) e).get("properties").getJsonObject("attribution").getString("url", null);
1549 } catch (NullPointerException ex) {
1550 return null;
1551 }
1552 }
1553
1554 static String getTermsOfUseText(Object e) {
1555 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getTermsOfUseText();
1556 return null;
1557 }
1558
1559 static String getTermsOfUseUrl(Object e) {
1560 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getTermsOfUseURL();
1561 return null;
1562 }
1563
1564 static String getCategory(Object e) {
1565 if (e instanceof ImageryInfo) {
1566 return ((ImageryInfo) e).getImageryCategoryOriginalString();
1567 }
1568 return ((Map<String, JsonObject>) e).get("properties").getString("category", null);
1569 }
1570
1571 static String getLogoImage(Object e) {
1572 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getAttributionImageRaw();
1573 return null;
1574 }
1575
1576 static String getLogoUrl(Object e) {
1577 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getAttributionImageURL();
1578 return null;
1579 }
1580
1581 static String getPermissionReferenceUrl(Object e) {
1582 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getPermissionReferenceURL();
1583 return ((Map<String, JsonObject>) e).get("properties").getString("license_url", null);
1584 }
1585
1586 static String getPrivacyPolicyUrl(Object e) {
1587 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getPrivacyPolicyURL();
1588 return ((Map<String, JsonObject>) e).get("properties").getString("privacy_policy_url", null);
1589 }
1590
1591 static Map<String, Set<String>> getNoTileHeader(Object e) {
1592 if (e instanceof ImageryInfo) return ((ImageryInfo) e).getNoTileHeaders();
1593 JsonObject nth = ((Map<String, JsonObject>) e).get("properties").getJsonObject("no_tile_header");
1594 return nth == null ? null : nth.keySet().stream().collect(Collectors.toMap(
1595 Function.identity(),
1596 k -> nth.getJsonArray(k).stream().map(x -> ((JsonString) x).getString()).collect(Collectors.toSet())));
1597 }
1598
1599 static Map<String, String> getDescriptions(Object e) {
1600 Map<String, String> res = new HashMap<>();
1601 if (e instanceof ImageryInfo) {
1602 String a = ((ImageryInfo) e).getDescription();
1603 if (a != null) res.put("en", a);
1604 } else {
1605 String a = ((Map<String, JsonObject>) e).get("properties").getString("description", null);
1606 if (a != null) res.put("en", a.replaceAll("''", "'"));
1607 }
1608 return res;
1609 }
1610
1611 static boolean getValidGeoreference(Object e) {
1612 if (e instanceof ImageryInfo) return ((ImageryInfo) e).isGeoreferenceValid();
1613 return false;
1614 }
1615
1616 static boolean getDefault(Object e) {
1617 if (e instanceof ImageryInfo) return ((ImageryInfo) e).isDefaultEntry();
1618 return ((Map<String, JsonObject>) e).get("properties").getBoolean("default", false);
1619 }
1620
1621 String getDescription(Object o) {
1622 String url = getUrl(o);
1623 String cc = getCountryCode(o);
1624 if (cc == null) {
1625 ImageryInfo j = josmUrls.get(url);
1626 if (j != null) cc = getCountryCode(j);
1627 if (cc == null) {
1628 JsonObject e = eliUrls.get(url);
1629 if (e != null) cc = getCountryCode(e);
1630 }
1631 }
1632 if (cc == null) {
1633 cc = "";
1634 } else {
1635 cc = "["+cc+"] ";
1636 }
1637 String name = getName(o);
1638 String id = getId(o);
1639 String d = cc;
1640 if (!Utils.isEmpty(name)) {
1641 d += name;
1642 if (!Utils.isEmpty(id))
1643 d += " ["+id+"]";
1644 } else if (!Utils.isEmpty(url))
1645 d += url;
1646 if (optionShorten) {
1647 if (d.length() > MAXLEN) d = d.substring(0, MAXLEN-1) + "...";
1648 }
1649 return d;
1650 }
1651}
Note: See TracBrowser for help on using the repository browser.