001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.IOException; 007import java.io.InputStream; 008import java.util.ArrayList; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.HashMap; 012import java.util.Map; 013import java.util.Map.Entry; 014import java.util.Set; 015import java.util.TreeMap; 016import java.util.stream.Collectors; 017 018import org.openstreetmap.josm.data.coor.LatLon; 019import org.openstreetmap.josm.data.osm.DataSet; 020import org.openstreetmap.josm.data.osm.OsmPrimitive; 021import org.openstreetmap.josm.data.osm.Relation; 022import org.openstreetmap.josm.data.osm.Way; 023import org.openstreetmap.josm.io.CachedFile; 024import org.openstreetmap.josm.io.IllegalDataException; 025import org.openstreetmap.josm.io.OsmReader; 026 027/** 028 * Look up territories ISO3166 codes at a certain place. 029 */ 030public final class Territories { 031 032 /** Internal OSM filename */ 033 public static final String FILENAME = "boundaries.osm"; 034 035 private static final String ISO3166_1 = "ISO3166-1:alpha2"; 036 private static final String ISO3166_2 = "ISO3166-2"; 037 private static final String TAGINFO = "taginfo"; 038 039 private static DataSet dataSet; 040 041 private static volatile Map<String, GeoPropertyIndex<Boolean>> iso3166Cache; 042 private static volatile Map<String, String> taginfoCache; 043 044 private Territories() { 045 // Hide implicit public constructor for utility classes 046 } 047 048 /** 049 * Get all known ISO3166-1 and ISO3166-2 codes. 050 * 051 * @return the ISO3166-1 and ISO3166-2 codes for the given location 052 */ 053 public static synchronized Set<String> getKnownIso3166Codes() { 054 return iso3166Cache.keySet(); 055 } 056 057 /** 058 * Returns the {@link GeoPropertyIndex} for the given ISO3166-1 or ISO3166-2 code. 059 * @param code the ISO3166-1 or ISO3166-2 code 060 * @return the {@link GeoPropertyIndex} for the given {@code code} 061 * @since 14484 062 */ 063 public static GeoPropertyIndex<Boolean> getGeoPropertyIndex(String code) { 064 return iso3166Cache.get(code); 065 } 066 067 /** 068 * Determine, if a point is inside a territory with the given ISO3166-1 069 * or ISO3166-2 code. 070 * 071 * @param code the ISO3166-1 or ISO3166-2 code 072 * @param ll the coordinates of the point 073 * @return true, if the point is inside a territory with the given code 074 */ 075 public static synchronized boolean isIso3166Code(String code, LatLon ll) { 076 GeoPropertyIndex<Boolean> gpi = iso3166Cache.get(code); 077 if (gpi == null) { 078 Logging.warn(tr("Unknown territory id: {0}", code)); 079 return false; 080 } 081 return Boolean.TRUE.equals(gpi.get(ll)); // avoid NPE, see #16491 082 } 083 084 /** 085 * Returns the original territories dataset. Be extra cautious when manipulating it! 086 * @return the original territories dataset 087 * @since 15565 088 */ 089 public static synchronized DataSet getOriginalDataSet() { 090 return dataSet; 091 } 092 093 /** 094 * Returns a copy of the territories dataset. 095 * @return a copy of the territories dataset 096 */ 097 public static synchronized DataSet getDataSet() { 098 return new DataSet(dataSet); 099 } 100 101 /** 102 * Initializes territories. 103 * TODO: Synchronization can be refined inside the {@link GeoPropertyIndex} as most look-ups are read-only. 104 */ 105 public static synchronized void initialize() { 106 iso3166Cache = new HashMap<>(); 107 taginfoCache = new TreeMap<>(); 108 try (CachedFile cf = new CachedFile("resource://data/" + FILENAME); 109 InputStream is = cf.getInputStream()) { 110 dataSet = OsmReader.parseDataSet(is, null); 111 Collection<OsmPrimitive> candidates = new ArrayList<>(dataSet.getWays()); 112 candidates.addAll(dataSet.getRelations()); 113 for (OsmPrimitive osm : candidates) { 114 String iso1 = osm.get(ISO3166_1); 115 String iso2 = osm.get(ISO3166_2); 116 if (iso1 != null || iso2 != null) { 117 GeoProperty<Boolean> gp; 118 if (osm instanceof Way) { 119 gp = new DefaultGeoProperty(Collections.singleton((Way) osm)); 120 } else { 121 gp = new DefaultGeoProperty((Relation) osm); 122 } 123 GeoPropertyIndex<Boolean> gpi = new GeoPropertyIndex<>(gp, 24); 124 if (iso1 != null) { 125 iso3166Cache.put(iso1, gpi); 126 String taginfo = osm.get(TAGINFO); 127 if (taginfo != null) { 128 taginfoCache.put(iso1, taginfo); 129 } 130 } 131 if (iso2 != null) { 132 iso3166Cache.put(iso2, gpi); 133 } 134 } 135 } 136 } catch (IOException | IllegalDataException ex) { 137 throw new JosmRuntimeException(ex); 138 } 139 } 140 141 /** 142 * Returns a map of national taginfo instances for the given location. 143 * @param ll lat/lon where to look. 144 * @return a map of national taginfo instances for the given location (code / url) 145 * @since 15565 146 */ 147 public static Map<String, String> getNationalTaginfoUrls(LatLon ll) { 148 Map<String, String> result = new TreeMap<>(); 149 if (iso3166Cache != null) { 150 for (String code : iso3166Cache.entrySet().parallelStream().distinct() 151 .filter(e -> Boolean.TRUE.equals(e.getValue().get(ll))) 152 .map(Entry<String, GeoPropertyIndex<Boolean>>::getKey) 153 .collect(Collectors.toSet())) { 154 String taginfo = taginfoCache.get(code); 155 if (taginfo != null) { 156 result.put(code, taginfo); 157 } 158 } 159 } 160 return result; 161 } 162}