001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import static java.util.Optional.ofNullable; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.io.IOException; 008import java.io.InputStream; 009import java.util.ArrayList; 010import java.util.Arrays; 011import java.util.Collection; 012import java.util.Collections; 013import java.util.HashMap; 014import java.util.List; 015import java.util.Locale; 016import java.util.Map; 017import java.util.Map.Entry; 018import java.util.Objects; 019import java.util.Set; 020import java.util.TreeMap; 021import java.util.stream.Collectors; 022import java.util.stream.Stream; 023 024import javax.json.Json; 025import javax.json.JsonArray; 026import javax.json.JsonString; 027import javax.json.JsonValue; 028import javax.json.stream.JsonParser; 029import javax.json.stream.JsonParser.Event; 030import javax.json.stream.JsonParsingException; 031 032import org.openstreetmap.josm.data.coor.LatLon; 033import org.openstreetmap.josm.data.osm.DataSet; 034import org.openstreetmap.josm.data.osm.Node; 035import org.openstreetmap.josm.data.osm.OsmPrimitive; 036import org.openstreetmap.josm.data.osm.Relation; 037import org.openstreetmap.josm.data.osm.TagMap; 038import org.openstreetmap.josm.data.osm.Way; 039import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 040import org.openstreetmap.josm.io.CachedFile; 041import org.openstreetmap.josm.io.IllegalDataException; 042import org.openstreetmap.josm.io.OsmReader; 043import org.openstreetmap.josm.spi.preferences.Config; 044 045/** 046 * Look up territories ISO3166 codes at a certain place. 047 */ 048public final class Territories { 049 050 /** Internal OSM filename */ 051 public static final String FILENAME = "boundaries.osm"; 052 053 private static final String ISO3166_1 = "ISO3166-1:alpha2"; 054 private static final String ISO3166_2 = "ISO3166-2"; 055 private static final String ISO3166_1_LC = ISO3166_1.toLowerCase(Locale.ENGLISH); 056 private static final String ISO3166_2_LC = ISO3166_2.toLowerCase(Locale.ENGLISH); 057 private static final String TAGINFO = "taginfo"; 058 059 private static DataSet dataSet; 060 061 static volatile Map<String, GeoPropertyIndex<Boolean>> iso3166Cache; 062 static volatile Map<String, TaginfoRegionalInstance> taginfoCache; 063 static volatile Map<String, TaginfoRegionalInstance> taginfoGeofabrikCache; 064 static volatile Map<String, TagMap> customTagsCache; 065 066 private static final List<String> KNOWN_KEYS = Arrays.asList(ISO3166_1, ISO3166_2, TAGINFO, "type", "driving_side", "note"); 067 068 private Territories() { 069 // Hide implicit public constructor for utility classes 070 } 071 072 /** 073 * Get all known ISO3166-1 and ISO3166-2 codes. 074 * 075 * @return the ISO3166-1 and ISO3166-2 codes for the given location 076 */ 077 public static synchronized Set<String> getKnownIso3166Codes() { 078 return iso3166Cache.keySet(); 079 } 080 081 /** 082 * Returns the {@link GeoPropertyIndex} for the given ISO3166-1 or ISO3166-2 code. 083 * @param code the ISO3166-1 or ISO3166-2 code 084 * @return the {@link GeoPropertyIndex} for the given {@code code} 085 * @since 14484 086 */ 087 public static GeoPropertyIndex<Boolean> getGeoPropertyIndex(String code) { 088 return iso3166Cache.get(code); 089 } 090 091 /** 092 * Determine, if a point is inside a territory with the given ISO3166-1 093 * or ISO3166-2 code. 094 * 095 * @param code the ISO3166-1 or ISO3166-2 code 096 * @param ll the coordinates of the point 097 * @return true, if the point is inside a territory with the given code 098 */ 099 public static synchronized boolean isIso3166Code(String code, LatLon ll) { 100 GeoPropertyIndex<Boolean> gpi = iso3166Cache.get(code); 101 if (gpi == null) { 102 Logging.warn(tr("Unknown territory id: {0}", code)); 103 return false; 104 } 105 return Boolean.TRUE.equals(gpi.get(ll)); // avoid NPE, see #16491 106 } 107 108 /** 109 * Returns the original territories dataset. Be extra cautious when manipulating it! 110 * @return the original territories dataset 111 * @since 15565 112 */ 113 public static synchronized DataSet getOriginalDataSet() { 114 return dataSet; 115 } 116 117 /** 118 * Initializes territories. 119 * TODO: Synchronization can be refined inside the {@link GeoPropertyIndex} as most look-ups are read-only. 120 * @see #initializeInternalData() 121 */ 122 public static synchronized void initialize() { 123 initializeInternalData(); 124 initializeExternalData(); 125 } 126 127 /** 128 * Initializes territories using the internal data only. 129 */ 130 public static synchronized void initializeInternalData() { 131 iso3166Cache = new HashMap<>(); 132 taginfoCache = new TreeMap<>(); 133 customTagsCache = new TreeMap<>(); 134 Collection<Way> traffic = new ArrayList<>(); 135 try (CachedFile cf = new CachedFile("resource://data/" + FILENAME); 136 InputStream is = cf.getInputStream()) { 137 dataSet = OsmReader.parseDataSet(is, null); 138 for (OsmPrimitive osm : dataSet.allPrimitives()) { 139 if (osm instanceof Node) { 140 continue; 141 } 142 String iso1 = osm.get(ISO3166_1); 143 String iso2 = osm.get(ISO3166_2); 144 if (iso1 != null || iso2 != null) { 145 TagMap tags = osm.getKeys(); 146 KNOWN_KEYS.forEach(tags::remove); 147 GeoProperty<Boolean> gp; 148 if (osm instanceof Way) { 149 gp = new DefaultGeoProperty(Collections.singleton((Way) osm)); 150 } else { 151 gp = new DefaultGeoProperty((Relation) osm); 152 } 153 GeoPropertyIndex<Boolean> gpi = new GeoPropertyIndex<>(gp, 24); 154 addInCache(iso1, gpi, tags); 155 addInCache(iso2, gpi, tags); 156 if (iso1 != null) { 157 String taginfo = osm.get(TAGINFO); 158 if (taginfo != null) { 159 taginfoCache.put(iso1, new TaginfoRegionalInstance(taginfo, Collections.singleton(iso1))); 160 } 161 } 162 } 163 RightAndLefthandTraffic.appendLeftDrivingBoundaries(osm, traffic); 164 } 165 RightAndLefthandTraffic.initialize(new DefaultGeoProperty(traffic)); 166 } catch (IOException | IllegalDataException ex) { 167 throw new JosmRuntimeException(ex); 168 } finally { 169 if (dataSet != null) 170 MultipolygonCache.getInstance().clear(dataSet); 171 if (!Logging.isDebugEnabled()) { 172 // unset dataSet to save memory, see #18907 173 dataSet = null; 174 } else { 175 Logging.debug("Retaining {0} to allow editing via advanced preferences", FILENAME); 176 } 177 } 178 } 179 180 private static void addInCache(String code, GeoPropertyIndex<Boolean> gpi, TagMap tags) { 181 if (code != null) { 182 iso3166Cache.put(code, gpi); 183 if (!tags.isEmpty()) { 184 customTagsCache.put(code, tags); 185 } 186 } 187 } 188 189 private static void initializeExternalData() { 190 initializeExternalData("Geofabrik", 191 Config.getUrls().getJOSMWebsite() + "/remote/geofabrik-index-v1-nogeom.json"); 192 } 193 194 static void initializeExternalData(String source, String path) { 195 taginfoGeofabrikCache = new TreeMap<>(); 196 try (CachedFile cf = new CachedFile(path); InputStream is = cf.getInputStream(); JsonParser json = Json.createParser(is)) { 197 while (json.hasNext()) { 198 Event event = json.next(); 199 if (event == Event.START_OBJECT) { 200 for (JsonValue feature : json.getObject().getJsonArray("features")) { 201 ofNullable(feature.asJsonObject().getJsonObject("properties")).ifPresent(props -> 202 ofNullable(props.getJsonObject("urls")).ifPresent(urls -> 203 ofNullable(urls.getString(TAGINFO)).ifPresent(taginfo -> { 204 JsonArray iso1 = props.getJsonArray(ISO3166_1_LC); 205 JsonArray iso2 = props.getJsonArray(ISO3166_2_LC); 206 if (iso1 != null) { 207 readExternalTaginfo(taginfoGeofabrikCache, taginfo, iso1, source); 208 } else if (iso2 != null) { 209 readExternalTaginfo(taginfoGeofabrikCache, taginfo, iso2, source); 210 } 211 }))); 212 } 213 } 214 } 215 } catch (IOException | JsonParsingException e) { 216 Logging.debug(e); 217 Logging.warn(tr("Failed to parse external taginfo data at {0}: {1}", path, e.getMessage())); 218 } 219 } 220 221 private static void readExternalTaginfo(Map<String, TaginfoRegionalInstance> cache, String taginfo, JsonArray jsonCodes, String source) { 222 Set<String> isoCodes = jsonCodes.getValuesAs(JsonString.class).stream().map(JsonString::getString).collect(Collectors.toSet()); 223 isoCodes.forEach(s -> cache.put(s, new TaginfoRegionalInstance(taginfo, isoCodes, source))); 224 } 225 226 /** 227 * Returns regional taginfo instances for the given location. 228 * @param ll lat/lon where to look. 229 * @return regional taginfo instances for the given location (code / url) 230 * @since 15876 231 */ 232 public static List<TaginfoRegionalInstance> getRegionalTaginfoUrls(LatLon ll) { 233 if (iso3166Cache == null) { 234 return Collections.emptyList(); 235 } 236 return iso3166Cache.entrySet().parallelStream().distinct() 237 .filter(e -> Boolean.TRUE.equals(e.getValue().get(ll))) 238 .map(Entry<String, GeoPropertyIndex<Boolean>>::getKey) 239 .distinct() 240 .flatMap(code -> Stream.of(taginfoCache, taginfoGeofabrikCache).map(cache -> cache.get(code))) 241 .filter(Objects::nonNull) 242 .collect(Collectors.toList()); 243 } 244 245 /** 246 * Returns the map of custom tags for a territory with the given ISO3166-1 or ISO3166-2 code. 247 * 248 * @param code the ISO3166-1 or ISO3166-2 code 249 * @return the map of custom tags for a territory with the given ISO3166-1 or ISO3166-2 code, or {@code null} 250 * @since 16109 251 */ 252 public static TagMap getCustomTags(String code) { 253 return code != null ? customTagsCache.get(code) : null; 254 } 255}