// License: GPL. For details, see LICENSE file. import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.text.DecimalFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonNumber; import javax.json.JsonObject; import javax.json.JsonReader; import javax.json.JsonString; import javax.json.JsonValue; import org.openstreetmap.gui.jmapviewer.Coordinate; import org.openstreetmap.josm.data.Preferences; import org.openstreetmap.josm.data.imagery.ImageryInfo; import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds; import org.openstreetmap.josm.data.imagery.Shape; import org.openstreetmap.josm.data.preferences.JosmBaseDirectories; import org.openstreetmap.josm.data.projection.Projections; import org.openstreetmap.josm.data.validation.routines.DomainValidator; import org.openstreetmap.josm.io.imagery.ImageryReader; import org.openstreetmap.josm.spi.preferences.Config; import org.openstreetmap.josm.tools.Logging; import org.openstreetmap.josm.tools.OptionParser; import org.openstreetmap.josm.tools.OptionParser.OptionCount; import org.openstreetmap.josm.tools.ReflectionUtils; import org.xml.sax.SAXException; /** * Compare and analyse the differences of the editor layer index and the JOSM imagery list. * The goal is to keep both lists in sync. * * The editor layer index project (https://github.com/osmlab/editor-layer-index) * provides also a version in the JOSM format, but the GEOJSON is the original source * format, so we read that. * * How to run: * ----------- * * Main JOSM binary needs to be in classpath, e.g. * * $ java -cp ../dist/josm-custom.jar SyncEditorLayerIndex * * Add option "-h" to show the available command line flags. */ @SuppressWarnings("unchecked") public class SyncEditorLayerIndex { private static final int MAXLEN = 140; private List josmEntries; private JsonArray eliEntries; private final Map eliUrls = new HashMap<>(); private final Map josmUrls = new HashMap<>(); private final Map josmMirrors = new HashMap<>(); private static final Map oldproj = new HashMap<>(); private static final List ignoreproj = new LinkedList<>(); private static String eliInputFile = "imagery_eli.geojson"; private static String josmInputFile = "imagery_josm.imagery.xml"; private static String ignoreInputFile = "imagery_josm.ignores.txt"; private static OutputStream outputFile; private static OutputStreamWriter outputStream; private static String optionOutput; private static boolean optionShorten; private static boolean optionNoSkip; private static boolean optionXhtmlBody; private static boolean optionXhtml; private static String optionEliXml; private static String optionJosmXml; private static String optionEncoding; private static boolean optionNoEli; private Map skip = new HashMap<>(); /** * Main method. * @param args program arguments * @throws IOException if any I/O error occurs * @throws ReflectiveOperationException if any reflective operation error occurs * @throws SAXException if any SAX error occurs */ public static void main(String[] args) throws IOException, SAXException, ReflectiveOperationException { Locale.setDefault(Locale.ROOT); parseCommandLineArguments(args); Preferences pref = new Preferences(JosmBaseDirectories.getInstance()); Config.setPreferencesInstance(pref); pref.init(false); SyncEditorLayerIndex script = new SyncEditorLayerIndex(); script.setupProj(); script.loadSkip(); script.start(); script.loadJosmEntries(); if (optionJosmXml != null) { try (OutputStreamWriter stream = new OutputStreamWriter(Files.newOutputStream(Paths.get(optionJosmXml)), UTF_8)) { script.printentries(script.josmEntries, stream); } } script.loadELIEntries(); if (optionEliXml != null) { try (OutputStreamWriter stream = new OutputStreamWriter(Files.newOutputStream(Paths.get(optionEliXml)), UTF_8)) { script.printentries(script.eliEntries, stream); } } script.checkInOneButNotTheOther(); script.checkCommonEntries(); script.end(); if (outputStream != null) { outputStream.close(); } if (outputFile != null) { outputFile.close(); } } /** * Displays help on the console */ private static void showHelp() { System.out.println(getHelp()); System.exit(0); } static String getHelp() { return "usage: java -cp build SyncEditorLayerIndex\n" + "-c,--encoding output encoding (defaults to UTF-8 or cp850 on Windows)\n" + "-e,--eli_input Input file for the editor layer index (geojson). " + "Default is imagery_eli.geojson (current directory).\n" + "-h,--help show this help\n" + "-i,--ignore_input Input file for the ignore list. Default is imagery_josm.ignores.txt (current directory).\n" + "-j,--josm_input Input file for the JOSM imagery list (xml). " + "Default is imagery_josm.imagery.xml (current directory).\n" + "-m,--noeli don't show output for ELI problems\n" + "-n,--noskip don't skip known entries\n" + "-o,--output Output file, - prints to stdout (default: -)\n" + "-p,--elixml ELI entries for use in JOSM as XML file (incomplete)\n" + "-q,--josmxml JOSM entries reoutput as XML file (incomplete)\n" + "-s,--shorten shorten the output, so it is easier to read in a console window\n" + "-x,--xhtmlbody create XHTML body for display in a web page\n" + "-X,--xhtml create XHTML for display in a web page\n"; } /** * Parse command line arguments. * @param args program arguments * @throws IOException in case of I/O error */ static void parseCommandLineArguments(String[] args) throws IOException { new OptionParser("JOSM/ELI synchronization script") .addFlagParameter("help", SyncEditorLayerIndex::showHelp) .addShortAlias("help", "h") .addArgumentParameter("output", OptionCount.OPTIONAL, x -> optionOutput = x) .addShortAlias("output", "o") .addArgumentParameter("eli_input", OptionCount.OPTIONAL, x -> eliInputFile = x) .addShortAlias("eli_input", "e") .addArgumentParameter("josm_input", OptionCount.OPTIONAL, x -> josmInputFile = x) .addShortAlias("josm_input", "j") .addArgumentParameter("ignore_input", OptionCount.OPTIONAL, x -> ignoreInputFile = x) .addShortAlias("ignore_input", "i") .addFlagParameter("shorten", () -> optionShorten = true) .addShortAlias("shorten", "s") .addFlagParameter("noskip", () -> optionNoSkip = true) .addShortAlias("noskip", "n") .addFlagParameter("xhtmlbody", () -> optionXhtmlBody = true) .addShortAlias("xhtmlbody", "x") .addFlagParameter("xhtml", () -> optionXhtml = true) .addShortAlias("xhtml", "X") .addArgumentParameter("elixml", OptionCount.OPTIONAL, x -> optionEliXml = x) .addShortAlias("elixml", "p") .addArgumentParameter("josmxml", OptionCount.OPTIONAL, x -> optionJosmXml = x) .addShortAlias("josmxml", "q") .addFlagParameter("noeli", () -> optionNoEli = true) .addShortAlias("noeli", "m") .addArgumentParameter("encoding", OptionCount.OPTIONAL, x -> optionEncoding = x) .addShortAlias("encoding", "c") .parseOptionsOrExit(Arrays.asList(args)); if (optionOutput != null && !"-".equals(optionOutput)) { outputFile = Files.newOutputStream(Paths.get(optionOutput)); outputStream = new OutputStreamWriter(outputFile, optionEncoding != null ? optionEncoding : "UTF-8"); } else if (optionEncoding != null) { outputStream = new OutputStreamWriter(System.out, optionEncoding); } } void setupProj() { oldproj.put("EPSG:3359", "EPSG:3404"); oldproj.put("EPSG:3785", "EPSG:3857"); oldproj.put("EPSG:31297", "EPGS:31287"); oldproj.put("EPSG:31464", "EPSG:31468"); oldproj.put("EPSG:54004", "EPSG:3857"); oldproj.put("EPSG:102100", "EPSG:3857"); oldproj.put("EPSG:102113", "EPSG:3857"); oldproj.put("EPSG:900913", "EPGS:3857"); ignoreproj.add("EPSG:4267"); ignoreproj.add("EPSG:5221"); ignoreproj.add("EPSG:5514"); ignoreproj.add("EPSG:32019"); ignoreproj.add("EPSG:102066"); ignoreproj.add("EPSG:102067"); ignoreproj.add("EPSG:102685"); ignoreproj.add("EPSG:102711"); } void loadSkip() throws IOException { final Pattern pattern = Pattern.compile("^\\|\\| *(ELI|Ignore) *\\|\\| *\\{\\{\\{(.+)\\}\\}\\} *\\|\\|"); try (BufferedReader fr = new BufferedReader(new InputStreamReader(Files.newInputStream(Paths.get(ignoreInputFile)), UTF_8))) { String line; while ((line = fr.readLine()) != null) { Matcher res = pattern.matcher(line); if (res.matches()) { if ("Ignore".equals(res.group(1))) { skip.put(res.group(2), "green"); } else { skip.put(res.group(2), "darkgoldenrod"); } } } } } void myprintlnfinal(String s) throws IOException { if (outputStream != null) { outputStream.write(s + System.getProperty("line.separator")); } else { System.out.println(s); } } void myprintln(String s) throws IOException { if (skip.containsKey(s)) { String color = skip.get(s); skip.remove(s); if (optionXhtmlBody || optionXhtml) { s = "
"
                        + s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")+"
"; } if (!optionNoSkip) { return; } } else if (optionXhtmlBody || optionXhtml) { String color = s.startsWith("***") ? "black" : ((s.startsWith("+ ") || s.startsWith("+++ ELI")) ? "blue" : (s.startsWith("#") ? "indigo" : (s.startsWith("!") ? "orange" : "red"))); s = "
"+s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")+"
"; } if ((s.startsWith("+ ") || s.startsWith("+++ ELI") || s.startsWith("#")) && optionNoEli) { return; } myprintlnfinal(s); } void start() throws IOException { if (optionXhtml) { myprintlnfinal( "\n"); myprintlnfinal( ""+ "JOSM - ELI differences\n"); } } void end() throws IOException { for (String s : skip.keySet()) { myprintln("+++ Obsolete skip entry: " + s); } if (optionXhtml) { myprintlnfinal("\n"); } } void loadELIEntries() throws IOException { try (JsonReader jr = Json.createReader(new InputStreamReader(Files.newInputStream(Paths.get(eliInputFile)), UTF_8))) { eliEntries = jr.readObject().getJsonArray("features"); } for (JsonValue e : eliEntries) { String url = getUrlStripped(e); if (url.contains("{z}")) { myprintln("+++ ELI-URL uses {z} instead of {zoom}: "+url); url = url.replace("{z}", "{zoom}"); } if (eliUrls.containsKey(url)) { myprintln("+++ ELI-URL is not unique: "+url); } else { eliUrls.put(url, e.asJsonObject()); } JsonArray s = e.asJsonObject().get("properties").asJsonObject().getJsonArray("available_projections"); if (s != null) { String urlLc = url.toLowerCase(Locale.ENGLISH); List old = new LinkedList<>(); for (JsonValue p : s) { String proj = ((JsonString) p).getString(); if (oldproj.containsKey(proj) || ("CRS:84".equals(proj) && !urlLc.contains("version=1.3"))) { old.add(proj); } } if (!old.isEmpty()) { myprintln("+ ELI Projections "+String.join(", ", old)+" not useful: "+getDescription(e)); } } } myprintln("*** Loaded "+eliEntries.size()+" entries (ELI). ***"); } String cdata(String s) { return cdata(s, false); } String cdata(String s, boolean escape) { if (escape) { return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); } else if (s.matches("[<>&]")) return ""; return s; } String maininfo(Object entry, String offset) { String t = getType(entry); String res = offset + ""+t+"\n"; res += offset + ""+cdata(getUrl(entry))+"\n"; if (getMinZoom(entry) != null) res += offset + ""+getMinZoom(entry)+"\n"; if (getMaxZoom(entry) != null) res += offset + ""+getMaxZoom(entry)+"\n"; if ("wms".equals(t)) { List p = getProjections(entry); if (p != null) { res += offset + "\n"; for (String c : p) { res += offset + " "+c+"\n"; } res += offset + "\n"; } } return res; } void printentries(List entries, OutputStreamWriter stream) throws IOException { DecimalFormat df = new DecimalFormat("#.#######"); df.setRoundingMode(java.math.RoundingMode.CEILING); stream.write("\n"); stream.write("\n"); for (Object e : entries) { stream.write(" \n"); String t; if (isNotBlank(t = getName(e))) stream.write(" "+cdata(t, true)+"\n"); if (isNotBlank(t = getId(e))) stream.write(" "+t+"\n"); if (isNotBlank(t = getCategory(e))) stream.write(" "+t+"\n"); if (isNotBlank(t = getDate(e))) stream.write(" "+t+"\n"); if (isNotBlank(t = getCountryCode(e))) stream.write(" "+t+"\n"); if ((getDefault(e))) stream.write(" true\n"); stream.write(maininfo(e, " ")); if (isNotBlank(t = getAttributionText(e))) stream.write(" "+cdata(t, true)+"\n"); if (isNotBlank(t = getAttributionUrl(e))) stream.write(" "+cdata(t)+"\n"); if (isNotBlank(t = getLogoImage(e))) stream.write(" "+cdata(t, true)+"\n"); if (isNotBlank(t = getLogoUrl(e))) stream.write(" "+cdata(t)+"\n"); if (isNotBlank(t = getTermsOfUseText(e))) stream.write(" "+cdata(t, true)+"\n"); if (isNotBlank(t = getTermsOfUseUrl(e))) stream.write(" "+cdata(t)+"\n"); if (isNotBlank(t = getPermissionReferenceUrl(e))) stream.write(" "+cdata(t)+"\n"); if ((getValidGeoreference(e))) stream.write(" true\n"); if (isNotBlank(t = getIcon(e))) stream.write(" "+cdata(t)+"\n"); for (Entry d : getDescriptions(e).entrySet()) { stream.write(" "+d.getValue()+"\n"); } for (ImageryInfo m : getMirrors(e)) { stream.write(" \n"+maininfo(m, " ")+" \n"); } double minlat = 1000; double minlon = 1000; double maxlat = -1000; double maxlon = -1000; String shapes = ""; String sep = "\n "; try { for (Shape s: getShapes(e)) { shapes += " "; int i = 0; for (Coordinate p: s.getPoints()) { double lat = p.getLat(); double lon = p.getLon(); if (lat > maxlat) maxlat = lat; if (lon > maxlon) maxlon = lon; if (lat < minlat) minlat = lat; if (lon < minlon) minlon = lon; if ((i++ % 3) == 0) { shapes += sep + " "; } shapes += ""; } shapes += sep + "\n"; } } catch (IllegalArgumentException ignored) { Logging.trace(ignored); } if (!shapes.isEmpty()) { stream.write(" \n"); stream.write(shapes + " \n"); } stream.write(" \n"); } stream.write("\n"); stream.close(); } void loadJosmEntries() throws IOException, SAXException, ReflectiveOperationException { try (ImageryReader reader = new ImageryReader(josmInputFile)) { josmEntries = reader.parse(); } for (ImageryInfo e : josmEntries) { if (isBlank(getUrl(e))) { myprintln("+++ JOSM-Entry without URL: " + getDescription(e)); continue; } if (isBlank(getName(e))) { myprintln("+++ JOSM-Entry without Name: " + getDescription(e)); continue; } String url = getUrlStripped(e); if (url.contains("{z}")) { myprintln("+++ JOSM-URL uses {z} instead of {zoom}: "+url); url = url.replace("{z}", "{zoom}"); } if (josmUrls.containsKey(url)) { myprintln("+++ JOSM-URL is not unique: "+url); } else { josmUrls.put(url, e); } for (ImageryInfo m : e.getMirrors()) { url = getUrlStripped(m); Field origNameField = ImageryInfo.class.getDeclaredField("origName"); ReflectionUtils.setObjectsAccessible(origNameField); origNameField.set(m, m.getOriginalName().replaceAll(" mirror server( \\d+)?", "")); if (josmUrls.containsKey(url)) { myprintln("+++ JOSM-Mirror-URL is not unique: "+url); } else { josmUrls.put(url, m); josmMirrors.put(url, m); } } } myprintln("*** Loaded "+josmEntries.size()+" entries (JOSM). ***"); } void checkInOneButNotTheOther() throws IOException { List le = new LinkedList<>(eliUrls.keySet()); List lj = new LinkedList<>(josmUrls.keySet()); List ke = new LinkedList<>(le); for (String url : ke) { if (lj.contains(url)) { le.remove(url); lj.remove(url); } } if (!le.isEmpty() && !lj.isEmpty()) { ke = new LinkedList<>(le); for (String urle : ke) { JsonObject e = eliUrls.get(urle); String ide = getId(e); String urlhttps = urle.replace("http:", "https:"); if (lj.contains(urlhttps)) { myprintln("+ Missing https: "+getDescription(e)); eliUrls.put(urlhttps, eliUrls.get(urle)); eliUrls.remove(urle); le.remove(urle); lj.remove(urlhttps); } else if (isNotBlank(ide)) { List kj = new LinkedList<>(lj); for (String urlj : kj) { ImageryInfo j = josmUrls.get(urlj); String idj = getId(j); if (ide.equals(idj) && Objects.equals(getType(j), getType(e))) { myprintln("* URL for id "+idj+" differs ("+urle+"): "+getDescription(j)); le.remove(urle); lj.remove(urlj); // replace key for this entry with JOSM URL eliUrls.remove(urle); eliUrls.put(urlj, e); break; } } } } } myprintln("*** URLs found in ELI but not in JOSM ("+le.size()+"): ***"); Collections.sort(le); if (!le.isEmpty()) { for (String l : le) { myprintln("- " + getDescription(eliUrls.get(l))); } } myprintln("*** URLs found in JOSM but not in ELI ("+lj.size()+"): ***"); Collections.sort(lj); if (!lj.isEmpty()) { for (String l : lj) { myprintln("+ " + getDescription(josmUrls.get(l))); } } } void checkCommonEntries() throws IOException { doSameUrlButDifferentName(); doSameUrlButDifferentId(); doSameUrlButDifferentType(); doSameUrlButDifferentZoomBounds(); doSameUrlButDifferentCountryCode(); doSameUrlButDifferentQuality(); doSameUrlButDifferentDates(); doSameUrlButDifferentInformation(); doMismatchingShapes(); doMismatchingIcons(); doMiscellaneousChecks(); } void doSameUrlButDifferentName() throws IOException { myprintln("*** Same URL, but different name: ***"); for (String url : eliUrls.keySet()) { JsonObject e = eliUrls.get(url); if (!josmUrls.containsKey(url)) continue; ImageryInfo j = josmUrls.get(url); String ename = getName(e).replace("'", "\u2019"); String jname = getName(j).replace("'", "\u2019"); if (!ename.equals(jname)) { myprintln("* Name differs ('"+getName(e)+"' != '"+getName(j)+"'): "+getUrl(j)); } } } void doSameUrlButDifferentId() throws IOException { myprintln("*** Same URL, but different Id: ***"); for (String url : eliUrls.keySet()) { JsonObject e = eliUrls.get(url); if (!josmUrls.containsKey(url)) continue; ImageryInfo j = josmUrls.get(url); String ename = getId(e); String jname = getId(j); if (!Objects.equals(ename, jname)) { myprintln("# Id differs ('"+getId(e)+"' != '"+getId(j)+"'): "+getUrl(j)); } } } void doSameUrlButDifferentType() throws IOException { myprintln("*** Same URL, but different type: ***"); for (String url : eliUrls.keySet()) { JsonObject e = eliUrls.get(url); if (!josmUrls.containsKey(url)) continue; ImageryInfo j = josmUrls.get(url); if (!Objects.equals(getType(e), getType(j))) { myprintln("* Type differs ("+getType(e)+" != "+getType(j)+"): "+getName(j)+" - "+getUrl(j)); } } } void doSameUrlButDifferentZoomBounds() throws IOException { myprintln("*** Same URL, but different zoom bounds: ***"); for (String url : eliUrls.keySet()) { JsonObject e = eliUrls.get(url); if (!josmUrls.containsKey(url)) continue; ImageryInfo j = josmUrls.get(url); Integer eMinZoom = getMinZoom(e); Integer jMinZoom = getMinZoom(j); /* dont warn for entries copied from the base of the mirror */ if (eMinZoom == null && "wms".equals(getType(j)) && j.getName().contains(" mirror")) jMinZoom = null; if (!Objects.equals(eMinZoom, jMinZoom) && !(Objects.equals(eMinZoom, 0) && jMinZoom == null)) { myprintln("* Minzoom differs ("+eMinZoom+" != "+jMinZoom+"): "+getDescription(j)); } Integer eMaxZoom = getMaxZoom(e); Integer jMaxZoom = getMaxZoom(j); /* dont warn for entries copied from the base of the mirror */ if (eMaxZoom == null && "wms".equals(getType(j)) && j.getName().contains(" mirror")) jMaxZoom = null; if (!Objects.equals(eMaxZoom, jMaxZoom)) { myprintln("* Maxzoom differs ("+eMaxZoom+" != "+jMaxZoom+"): "+getDescription(j)); } } } void doSameUrlButDifferentCountryCode() throws IOException { myprintln("*** Same URL, but different country code: ***"); for (String url : eliUrls.keySet()) { JsonObject e = eliUrls.get(url); if (!josmUrls.containsKey(url)) continue; ImageryInfo j = josmUrls.get(url); String cce = getCountryCode(e); if ("ZZ".equals(cce)) { /* special ELI country code */ cce = null; } if (cce != null && !cce.equals(getCountryCode(j))) { myprintln("* Country code differs ("+getCountryCode(e)+" != "+getCountryCode(j)+"): "+getDescription(j)); } } } void doSameUrlButDifferentQuality() throws IOException { myprintln("*** Same URL, but different quality: ***"); for (String url : eliUrls.keySet()) { JsonObject e = eliUrls.get(url); if (!josmUrls.containsKey(url)) { String q = getQuality(e); if ("eli-best".equals(q)) { myprintln("- Quality best entry not in JOSM for "+getDescription(e)); } continue; } ImageryInfo j = josmUrls.get(url); if (!Objects.equals(getQuality(e), getQuality(j))) { myprintln("* Quality differs ("+getQuality(e)+" != "+getQuality(j)+"): "+getDescription(j)); } } } void doSameUrlButDifferentDates() throws IOException { myprintln("*** Same URL, but different dates: ***"); Pattern pattern = Pattern.compile("^(.*;)(\\d\\d\\d\\d)(-(\\d\\d)(-(\\d\\d))?)?$"); for (String url : eliUrls.keySet()) { String ed = getDate(eliUrls.get(url)); if (!josmUrls.containsKey(url)) continue; ImageryInfo j = josmUrls.get(url); String jd = getDate(j); // The forms 2015;- or -;2015 or 2015;2015 are handled equal to 2015 String ef = ed.replaceAll("\\A-;", "").replaceAll(";-\\z", "").replaceAll("\\A([0-9-]+);\\1\\z", "$1"); // ELI has a strange and inconsistent used end_date definition, so we try again with subtraction by one String ed2 = ed; Matcher m = pattern.matcher(ed); if (m.matches()) { Calendar cal = Calendar.getInstance(); cal.set(Integer.valueOf(m.group(2)), m.group(4) == null ? 0 : Integer.valueOf(m.group(4))-1, m.group(6) == null ? 1 : Integer.valueOf(m.group(6))); cal.add(Calendar.DAY_OF_MONTH, -1); ed2 = m.group(1) + cal.get(Calendar.YEAR); if (m.group(4) != null) ed2 += "-" + String.format("%02d", cal.get(Calendar.MONTH)+1); if (m.group(6) != null) ed2 += "-" + String.format("%02d", cal.get(Calendar.DAY_OF_MONTH)); } String ef2 = ed2.replaceAll("\\A-;", "").replaceAll(";-\\z", "").replaceAll("\\A([0-9-]+);\\1\\z", "$1"); if (!ed.equals(jd) && !ef.equals(jd) && !ed2.equals(jd) && !ef2.equals(jd)) { String t = "'"+ed+"'"; if (!ed.equals(ef)) { t += " or '"+ef+"'"; } if (jd.isEmpty()) { myprintln("- Missing JOSM date ("+t+"): "+getDescription(j)); } else if (!ed.isEmpty()) { myprintln("* Date differs ('"+t+"' != '"+jd+"'): "+getDescription(j)); } else if (!optionNoEli) { myprintln("+ Missing ELI date ('"+jd+"'): "+getDescription(j)); } } } } void doSameUrlButDifferentInformation() throws IOException { myprintln("*** Same URL, but different information: ***"); for (String url : eliUrls.keySet()) { if (!josmUrls.containsKey(url)) continue; JsonObject e = eliUrls.get(url); ImageryInfo j = josmUrls.get(url); compareDescriptions(e, j); comparePermissionReferenceUrls(e, j); compareAttributionUrls(e, j); compareAttributionTexts(e, j); compareProjections(e, j); compareDefaults(e, j); compareOverlays(e, j); compareNoTileHeaders(e, j); } } void compareDescriptions(JsonObject e, ImageryInfo j) throws IOException { String et = getDescriptions(e).getOrDefault("en", ""); String jt = getDescriptions(j).getOrDefault("en", ""); if (!et.equals(jt)) { if (jt.isEmpty()) { myprintln("- Missing JOSM description ("+et+"): "+getDescription(j)); } else if (!et.isEmpty()) { myprintln("* Description differs ('"+et+"' != '"+jt+"'): "+getDescription(j)); } else if (!optionNoEli) { myprintln("+ Missing ELI description ('"+jt+"'): "+getDescription(j)); } } } void comparePermissionReferenceUrls(JsonObject e, ImageryInfo j) throws IOException { String et = getPermissionReferenceUrl(e); String jt = getPermissionReferenceUrl(j); String jt2 = getTermsOfUseUrl(j); if (isBlank(jt)) jt = jt2; if (!Objects.equals(et, jt)) { if (isBlank(jt)) { myprintln("- Missing JOSM license URL ("+et+"): "+getDescription(j)); } else if (isNotBlank(et)) { String ethttps = et.replace("http:", "https:"); if (isBlank(jt2) || !(jt2.equals(ethttps) || jt2.equals(et+"/") || jt2.equals(ethttps+"/"))) { if (jt.equals(ethttps) || jt.equals(et+"/") || jt.equals(ethttps+"/")) { myprintln("+ License URL differs ('"+et+"' != '"+jt+"'): "+getDescription(j)); } else { String ja = getAttributionUrl(j); if (ja != null && (ja.equals(et) || ja.equals(ethttps) || ja.equals(et+"/") || ja.equals(ethttps+"/"))) { myprintln("+ ELI License URL in JOSM Attribution: "+getDescription(j)); } else { myprintln("* License URL differs ('"+et+"' != '"+jt+"'): "+getDescription(j)); } } } } else if (!optionNoEli) { myprintln("+ Missing ELI license URL ('"+jt+"'): "+getDescription(j)); } } } void compareAttributionUrls(JsonObject e, ImageryInfo j) throws IOException { String et = getAttributionUrl(e); String jt = getAttributionUrl(j); if (!Objects.equals(et, jt)) { if (isBlank(jt)) { myprintln("- Missing JOSM attribution URL ("+et+"): "+getDescription(j)); } else if (isNotBlank(et)) { String ethttps = et.replace("http:", "https:"); if (jt.equals(ethttps) || jt.equals(et+"/") || jt.equals(ethttps+"/")) { myprintln("+ Attribution URL differs ('"+et+"' != '"+jt+"'): "+getDescription(j)); } else { myprintln("* Attribution URL differs ('"+et+"' != '"+jt+"'): "+getDescription(j)); } } else if (!optionNoEli) { myprintln("+ Missing ELI attribution URL ('"+jt+"'): "+getDescription(j)); } } } void compareAttributionTexts(JsonObject e, ImageryInfo j) throws IOException { String et = getAttributionText(e); String jt = getAttributionText(j); if (!Objects.equals(et, jt)) { if (isBlank(jt)) { myprintln("- Missing JOSM attribution text ("+et+"): "+getDescription(j)); } else if (isNotBlank(et)) { myprintln("* Attribution text differs ('"+et+"' != '"+jt+"'): "+getDescription(j)); } else if (!optionNoEli) { myprintln("+ Missing ELI attribution text ('"+jt+"'): "+getDescription(j)); } } } void compareProjections(JsonObject e, ImageryInfo j) throws IOException { String et = getProjections(e).stream().sorted().collect(Collectors.joining(" ")); String jt = getProjections(j).stream().sorted().collect(Collectors.joining(" ")); if (!Objects.equals(et, jt)) { if (isBlank(jt)) { String t = getType(e); if ("wms_endpoint".equals(t) || "tms".equals(t)) { myprintln("+ ELI projections for type "+t+": "+getDescription(j)); } else { myprintln("- Missing JOSM projections ("+et+"): "+getDescription(j)); } } else if (isNotBlank(et)) { if ("EPSG:3857 EPSG:4326".equals(et) || "EPSG:3857".equals(et) || "EPSG:4326".equals(et)) { myprintln("+ ELI has minimal projections ('"+et+"' != '"+jt+"'): "+getDescription(j)); } else { myprintln("* Projections differ ('"+et+"' != '"+jt+"'): "+getDescription(j)); } } else if (!optionNoEli && !"tms".equals(getType(e))) { myprintln("+ Missing ELI projections ('"+jt+"'): "+getDescription(j)); } } } void compareDefaults(JsonObject e, ImageryInfo j) throws IOException { boolean ed = getDefault(e); boolean jd = getDefault(j); if (ed != jd) { if (!jd) { myprintln("- Missing JOSM default: "+getDescription(j)); } else if (!optionNoEli) { myprintln("+ Missing ELI default: "+getDescription(j)); } } } void compareOverlays(JsonObject e, ImageryInfo j) throws IOException { boolean eo = getOverlay(e); boolean jo = getOverlay(j); if (eo != jo) { if (!jo) { myprintln("- Missing JOSM overlay flag: "+getDescription(j)); } else if (!optionNoEli) { myprintln("+ Missing ELI overlay flag: "+getDescription(j)); } } } void compareNoTileHeaders(JsonObject e, ImageryInfo j) throws IOException { Map> eh = getNoTileHeader(e); Map> jh = getNoTileHeader(j); if (!Objects.equals(eh, jh)) { if (jh == null || jh.isEmpty()) { myprintln("- Missing JOSM no tile headers ("+eh+"): "+getDescription(j)); } else if (eh != null && !eh.isEmpty()) { myprintln("* No tile headers differ ('"+eh+"' != '"+jh+"'): "+getDescription(j)); } else if (!optionNoEli) { myprintln("+ Missing ELI no tile headers ('"+jh+"'): "+getDescription(j)); } } } void doMismatchingShapes() throws IOException { myprintln("*** Mismatching shapes: ***"); for (String url : josmUrls.keySet()) { ImageryInfo j = josmUrls.get(url); int num = 1; for (Shape shape : getShapes(j)) { List p = shape.getPoints(); if (!p.get(0).equals(p.get(p.size()-1))) { myprintln("+++ JOSM shape "+num+" unclosed: "+getDescription(j)); } for (int nump = 1; nump < p.size(); ++nump) { if (Objects.equals(p.get(nump-1), p.get(nump))) { myprintln("+++ JOSM shape "+num+" double point at "+(nump-1)+": "+getDescription(j)); } } ++num; } } for (String url : eliUrls.keySet()) { JsonObject e = eliUrls.get(url); int num = 1; List s = null; try { s = getShapes(e); for (Shape shape : s) { List p = shape.getPoints(); if (!p.get(0).equals(p.get(p.size()-1)) && !optionNoEli) { myprintln("+++ ELI shape "+num+" unclosed: "+getDescription(e)); } for (int nump = 1; nump < p.size(); ++nump) { if (Objects.equals(p.get(nump-1), p.get(nump))) { myprintln("+++ ELI shape "+num+" double point at "+(nump-1)+": "+getDescription(e)); } } ++num; } } catch (IllegalArgumentException err) { String desc = getDescription(e); myprintln("* Invalid data in ELI geometry for "+desc+": "+err.getMessage()); } if (s == null || !josmUrls.containsKey(url)) { continue; } ImageryInfo j = josmUrls.get(url); List js = getShapes(j); if (s.isEmpty() && !js.isEmpty()) { if (!optionNoEli) { myprintln("+ No ELI shape: "+getDescription(j)); } } else if (js.isEmpty() && !s.isEmpty()) { // don't report boundary like 5 point shapes as difference if (s.size() != 1 || s.get(0).getPoints().size() != 5) { myprintln("- No JOSM shape: "+getDescription(j)); } } else if (s.size() != js.size()) { myprintln("* Different number of shapes ("+s.size()+" != "+js.size()+"): "+getDescription(j)); } else { boolean[] edone = new boolean[s.size()]; boolean[] jdone = new boolean[js.size()]; for (int enums = 0; enums < s.size(); ++enums) { List ep = s.get(enums).getPoints(); for (int jnums = 0; jnums < js.size() && !edone[enums]; ++jnums) { List jp = js.get(jnums).getPoints(); if (ep.size() == jp.size() && !jdone[jnums]) { boolean err = false; for (int nump = 0; nump < ep.size() && !err; ++nump) { Coordinate ept = ep.get(nump); Coordinate jpt = jp.get(nump); if (Math.abs(ept.getLat()-jpt.getLat()) > 0.00001 || Math.abs(ept.getLon()-jpt.getLon()) > 0.00001) err = true; } if (!err) { edone[enums] = true; jdone[jnums] = true; break; } } } } for (int enums = 0; enums < s.size(); ++enums) { List ep = s.get(enums).getPoints(); for (int jnums = 0; jnums < js.size() && !edone[enums]; ++jnums) { List jp = js.get(jnums).getPoints(); if (ep.size() == jp.size() && !jdone[jnums]) { boolean err = false; for (int nump = 0; nump < ep.size() && !err; ++nump) { Coordinate ept = ep.get(nump); Coordinate jpt = jp.get(nump); if (Math.abs(ept.getLat()-jpt.getLat()) > 0.00001 || Math.abs(ept.getLon()-jpt.getLon()) > 0.00001) { String numtxt = Integer.toString(enums+1); if (enums != jnums) { numtxt += '/' + Integer.toString(jnums+1); } myprintln("* Different coordinate for point "+(nump+1)+" of shape "+numtxt+": "+getDescription(j)); break; } } edone[enums] = true; jdone[jnums] = true; break; } } } for (int enums = 0; enums < s.size(); ++enums) { List ep = s.get(enums).getPoints(); for (int jnums = 0; jnums < js.size() && !edone[enums]; ++jnums) { List jp = js.get(jnums).getPoints(); if (!jdone[jnums]) { String numtxt = Integer.toString(enums+1); if (enums != jnums) { numtxt += '/' + Integer.toString(jnums+1); } myprintln("* Different number of points for shape "+numtxt+" ("+ep.size()+" ! = "+jp.size()+")): " + getDescription(j)); edone[enums] = true; jdone[jnums] = true; break; } } } } } } void doMismatchingIcons() throws IOException { myprintln("*** Mismatching icons: ***"); for (String url : eliUrls.keySet()) { JsonObject e = eliUrls.get(url); if (!josmUrls.containsKey(url)) { continue; } ImageryInfo j = josmUrls.get(url); String ij = getIcon(j); String ie = getIcon(e); boolean ijok = isNotBlank(ij); boolean ieok = isNotBlank(ie); if (ijok && !ieok) { if (!optionNoEli) { myprintln("+ No ELI icon: "+getDescription(j)); } } else if (!ijok && ieok) { myprintln("- No JOSM icon: "+getDescription(j)); } else if (ijok && ieok && !Objects.equals(ij, ie) && !( (ie.startsWith("https://osmlab.github.io/editor-layer-index/") || ie.startsWith("https://raw.githubusercontent.com/osmlab/editor-layer-index/")) && ij.startsWith("data:"))) { String iehttps = ie.replace("http:", "https:"); if (ij.equals(iehttps)) { myprintln("+ Different icons: "+getDescription(j)); } else { myprintln("* Different icons: "+getDescription(j)); } } } } void doMiscellaneousChecks() throws IOException { myprintln("*** Miscellaneous checks: ***"); Map josmIds = new HashMap<>(); Collection all = Projections.getAllProjectionCodes(); DomainValidator dv = DomainValidator.getInstance(); for (String url : josmUrls.keySet()) { ImageryInfo j = josmUrls.get(url); String id = getId(j); if ("wms".equals(getType(j))) { String urlLc = url.toLowerCase(Locale.ENGLISH); if (getProjections(j).isEmpty()) { myprintln("* WMS without projections: "+getDescription(j)); } else { List unsupported = new LinkedList<>(); List old = new LinkedList<>(); for (String p : getProjectionsUnstripped(j)) { if ("CRS:84".equals(p)) { if (!urlLc.contains("version=1.3")) { myprintln("* CRS:84 without WMS 1.3: "+getDescription(j)); } } else if (oldproj.containsKey(p)) { old.add(p); } else if (!all.contains(p) && !ignoreproj.contains(p)) { unsupported.add(p); } } if (!unsupported.isEmpty()) { myprintln("* Projections "+String.join(", ", unsupported)+" not supported by JOSM: "+getDescription(j)); } for (String o : old) { myprintln("* Projection "+o+" is an old unsupported code and has been replaced by "+oldproj.get(o)+": " + getDescription(j)); } } if (urlLc.contains("version=1.3") && !urlLc.contains("crs={proj}")) { myprintln("* WMS 1.3 with strange CRS specification: "+getDescription(j)); } else if (urlLc.contains("version=1.1") && !urlLc.contains("srs={proj}")) { myprintln("* WMS 1.1 with strange SRS specification: "+getDescription(j)); } } List urls = new LinkedList<>(); if (!"scanex".equals(getType(j))) { urls.add(url); } String jt = getPermissionReferenceUrl(j); if (isNotBlank(jt) && !"Public Domain".equalsIgnoreCase(jt)) urls.add(jt); jt = getTermsOfUseUrl(j); if (isNotBlank(jt)) urls.add(jt); jt = getAttributionUrl(j); if (isNotBlank(jt)) urls.add(jt); jt = getIcon(j); if (isNotBlank(jt) && !jt.startsWith("data:image/")) urls.add(jt); Pattern patternU = Pattern.compile("^https?://([^/]+?)(:\\d+)?(/.*)?"); for (String u : urls) { if (!patternU.matcher(u).matches() || u.matches(".*[ \t]+$")) { myprintln("* Strange URL '"+u+"': "+getDescription(j)); } else { try { URL jurl = new URL(u.replaceAll("\\{switch:[^\\}]*\\}", "x")); String domain = jurl.getHost(); int port = jurl.getPort(); if (!(domain.matches("^\\d+\\.\\d+\\.\\d+\\.\\d+$")) && !dv.isValid(domain)) myprintln("* Strange Domain '"+domain+"': "+getDescription(j)); else if (80 == port || 443 == port) { myprintln("* Useless port '"+port+"': "+getDescription(j)); } } catch (MalformedURLException e) { myprintln("* Malformed URL '"+u+"': "+getDescription(j)+" => "+e.getMessage()); } } } if (josmMirrors.containsKey(url)) { continue; } if (isBlank(id)) { myprintln("* No JOSM-ID: "+getDescription(j)); } else if (josmIds.containsKey(id)) { myprintln("* JOSM-ID "+id+" not unique: "+getDescription(j)); } else { josmIds.put(id, j); } String d = getDate(j); if (isNotBlank(d)) { Pattern patternD = Pattern.compile("^(-|(\\d\\d\\d\\d)(-(\\d\\d)(-(\\d\\d))?)?)(;(-|(\\d\\d\\d\\d)(-(\\d\\d)(-(\\d\\d))?)?))?$"); Matcher m = patternD.matcher(d); if (!m.matches()) { myprintln("* JOSM-Date '"+d+"' is strange: "+getDescription(j)); } else { try { Date first = verifyDate(m.group(2), m.group(4), m.group(6)); Date second = verifyDate(m.group(9), m.group(11), m.group(13)); if (second.compareTo(first) < 0) { myprintln("* JOSM-Date '"+d+"' is strange (second earlier than first): "+getDescription(j)); } } catch (Exception e) { myprintln("* JOSM-Date '"+d+"' is strange ("+e.getMessage()+"): "+getDescription(j)); } } } if (isNotBlank(getAttributionUrl(j)) && isBlank(getAttributionText(j))) { myprintln("* Attribution link without text: "+getDescription(j)); } if (isNotBlank(getLogoUrl(j)) && isBlank(getLogoImage(j))) { myprintln("* Logo link without image: "+getDescription(j)); } if (isNotBlank(getTermsOfUseText(j)) && isBlank(getTermsOfUseUrl(j))) { myprintln("* Terms of Use text without link: "+getDescription(j)); } List js = getShapes(j); if (!js.isEmpty()) { double minlat = 1000; double minlon = 1000; double maxlat = -1000; double maxlon = -1000; for (Shape s: js) { for (Coordinate p: s.getPoints()) { double lat = p.getLat(); double lon = p.getLon(); if (lat > maxlat) maxlat = lat; if (lon > maxlon) maxlon = lon; if (lat < minlat) minlat = lat; if (lon < minlon) minlon = lon; } } ImageryBounds b = j.getBounds(); if (b.getMinLat() != minlat || b.getMinLon() != minlon || b.getMaxLat() != maxlat || b.getMaxLon() != maxlon) { myprintln("* Bounds do not match shape (is "+b.getMinLat()+","+b.getMinLon()+","+b.getMaxLat()+","+b.getMaxLon() + ", calculated ): " + getDescription(j)); } } List knownCategories = Arrays.asList("photo", "map", "historicmap", "osmbasedmap", "historicphoto", "other"); String cat = getCategory(j); if (isBlank(cat)) { myprintln("* No category: "+getDescription(j)); } else if (!knownCategories.contains(cat)) { myprintln("* Strange category "+cat+": "+getDescription(j)); } } } /* * Utility functions that allow uniform access for both ImageryInfo and JsonObject. */ static String getUrl(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getUrl(); return ((Map) e).get("properties").getString("url"); } static String getUrlStripped(Object e) { return getUrl(e).replaceAll("\\?(apikey|access_token)=.*", ""); } static String getDate(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getDate() != null ? ((ImageryInfo) e).getDate() : ""; JsonObject p = ((Map) e).get("properties"); String start = p.containsKey("start_date") ? p.getString("start_date") : ""; String end = p.containsKey("end_date") ? p.getString("end_date") : ""; if (!start.isEmpty() && !end.isEmpty()) return start+";"+end; else if (!start.isEmpty()) return start+";-"; else if (!end.isEmpty()) return "-;"+end; return ""; } static Date verifyDate(String year, String month, String day) throws ParseException { String date; if (year == null) { date = "3000-01-01"; } else { date = year + "-" + (month == null ? "01" : month) + "-" + (day == null ? "01" : day); } SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); df.setLenient(false); return df.parse(date); } static String getId(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getId(); return ((Map) e).get("properties").getString("id"); } static String getName(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getOriginalName(); return ((Map) e).get("properties").getString("name"); } static List getMirrors(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getMirrors(); return Collections.emptyList(); } static List getProjections(Object e) { List r = new ArrayList<>(); List u = getProjectionsUnstripped(e); if (u != null) { for (String p : u) { if (!oldproj.containsKey(p) && !("CRS:84".equals(p) && !(getUrlStripped(e).matches("(?i)version=1\\.3")))) { r.add(p); } } } return r; } static List getProjectionsUnstripped(Object e) { List r = null; if (e instanceof ImageryInfo) { r = ((ImageryInfo) e).getServerProjections(); } else { JsonValue s = ((Map) e).get("properties").get("available_projections"); if (s != null) { r = new ArrayList<>(); for (JsonValue p : s.asJsonArray()) { r.add(((JsonString) p).getString()); } } } return r != null ? r : Collections.emptyList(); } static List getShapes(Object e) { if (e instanceof ImageryInfo) { ImageryBounds bounds = ((ImageryInfo) e).getBounds(); if (bounds != null) { return bounds.getShapes(); } return Collections.emptyList(); } JsonValue ex = ((Map) e).get("geometry"); if (ex != null && !JsonValue.NULL.equals(ex) && !ex.asJsonObject().isNull("coordinates")) { JsonArray poly = ex.asJsonObject().getJsonArray("coordinates"); List l = new ArrayList<>(); for (JsonValue shapes: poly) { Shape s = new Shape(); for (JsonValue point: shapes.asJsonArray()) { String lon = point.asJsonArray().getJsonNumber(0).toString(); String lat = point.asJsonArray().getJsonNumber(1).toString(); s.addPoint(lat, lon); } l.add(s); } return l; } return Collections.emptyList(); } static String getType(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getImageryType().getTypeString(); return ((Map) e).get("properties").getString("type"); } static Integer getMinZoom(Object e) { if (e instanceof ImageryInfo) { int mz = ((ImageryInfo) e).getMinZoom(); return mz == 0 ? null : mz; } else { JsonNumber num = ((Map) e).get("properties").getJsonNumber("min_zoom"); if (num == null) return null; return num.intValue(); } } static Integer getMaxZoom(Object e) { if (e instanceof ImageryInfo) { int mz = ((ImageryInfo) e).getMaxZoom(); return mz == 0 ? null : mz; } else { JsonNumber num = ((Map) e).get("properties").getJsonNumber("max_zoom"); if (num == null) return null; return num.intValue(); } } static String getCountryCode(Object e) { if (e instanceof ImageryInfo) return "".equals(((ImageryInfo) e).getCountryCode()) ? null : ((ImageryInfo) e).getCountryCode(); return ((Map) e).get("properties").getString("country_code", null); } static String getQuality(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).isBestMarked() ? "eli-best" : null; return (((Map) e).get("properties").containsKey("best") && ((Map) e).get("properties").getBoolean("best")) ? "eli-best" : null; } static boolean getOverlay(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).isOverlay(); return (((Map) e).get("properties").containsKey("overlay") && ((Map) e).get("properties").getBoolean("overlay")); } static String getIcon(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getIcon(); return ((Map) e).get("properties").getString("icon", null); } static String getAttributionText(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getAttributionText(0, null, null); try { return ((Map) e).get("properties").getJsonObject("attribution").getString("text", null); } catch (NullPointerException ex) { return null; } } static String getAttributionUrl(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getAttributionLinkURL(); try { return ((Map) e).get("properties").getJsonObject("attribution").getString("url", null); } catch (NullPointerException ex) { return null; } } static String getTermsOfUseText(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getTermsOfUseText(); return null; } static String getTermsOfUseUrl(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getTermsOfUseURL(); return null; } static String getCategory(Object e) { if (e instanceof ImageryInfo) { return ((ImageryInfo) e).getImageryCategoryOriginalString(); } return null; } static String getLogoImage(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getAttributionImageRaw(); return null; } static String getLogoUrl(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getAttributionImageURL(); return null; } static String getPermissionReferenceUrl(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getPermissionReferenceURL(); return ((Map) e).get("properties").getString("license_url", null); } static Map> getNoTileHeader(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).getNoTileHeaders(); JsonObject nth = ((Map) e).get("properties").getJsonObject("no_tile_header"); return nth == null ? null : nth.keySet().stream().collect(Collectors.toMap( Function.identity(), k -> nth.getJsonArray(k).stream().map(x -> ((JsonString) x).getString()).collect(Collectors.toSet()))); } static Map getDescriptions(Object e) { Map res = new HashMap<>(); if (e instanceof ImageryInfo) { String a = ((ImageryInfo) e).getDescription(); if (a != null) res.put("en", a); } else { String a = ((Map) e).get("properties").getString("description", null); if (a != null) res.put("en", a.replaceAll("''", "'")); } return res; } static boolean getValidGeoreference(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).isGeoreferenceValid(); return false; } static boolean getDefault(Object e) { if (e instanceof ImageryInfo) return ((ImageryInfo) e).isDefaultEntry(); return ((Map) e).get("properties").getBoolean("default", false); } String getDescription(Object o) { String url = getUrl(o); String cc = getCountryCode(o); if (cc == null) { ImageryInfo j = josmUrls.get(url); if (j != null) cc = getCountryCode(j); if (cc == null) { JsonObject e = eliUrls.get(url); if (e != null) cc = getCountryCode(e); } } if (cc == null) { cc = ""; } else { cc = "["+cc+"] "; } String d = cc + getName(o) + " - " + getUrl(o); if (optionShorten) { if (d.length() > MAXLEN) d = d.substring(0, MAXLEN-1) + "..."; } return d; } }