source: josm/trunk/src/org/openstreetmap/josm/tools/Territories.java@ 17333

Last change on this file since 17333 was 16595, checked in by simon04, 4 years ago

Add unit test for Territories.getRegionalTaginfoUrls

  • Property svn:eol-style set to native
File size: 10.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static java.util.Optional.ofNullable;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.io.IOException;
8import java.io.InputStream;
9import java.util.ArrayList;
10import java.util.Arrays;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashMap;
14import java.util.List;
15import java.util.Locale;
16import java.util.Map;
17import java.util.Map.Entry;
18import java.util.Objects;
19import java.util.Set;
20import java.util.TreeMap;
21import java.util.stream.Collectors;
22import java.util.stream.Stream;
23
24import javax.json.Json;
25import javax.json.JsonArray;
26import javax.json.JsonString;
27import javax.json.JsonValue;
28import javax.json.stream.JsonParser;
29import javax.json.stream.JsonParser.Event;
30import javax.json.stream.JsonParsingException;
31
32import org.openstreetmap.josm.data.coor.LatLon;
33import org.openstreetmap.josm.data.osm.DataSet;
34import org.openstreetmap.josm.data.osm.Node;
35import org.openstreetmap.josm.data.osm.OsmPrimitive;
36import org.openstreetmap.josm.data.osm.Relation;
37import org.openstreetmap.josm.data.osm.TagMap;
38import org.openstreetmap.josm.data.osm.Way;
39import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
40import org.openstreetmap.josm.io.CachedFile;
41import org.openstreetmap.josm.io.IllegalDataException;
42import org.openstreetmap.josm.io.OsmReader;
43import org.openstreetmap.josm.spi.preferences.Config;
44
45/**
46 * Look up territories ISO3166 codes at a certain place.
47 */
48public final class Territories {
49
50 /** Internal OSM filename */
51 public static final String FILENAME = "boundaries.osm";
52
53 private static final String ISO3166_1 = "ISO3166-1:alpha2";
54 private static final String ISO3166_2 = "ISO3166-2";
55 private static final String ISO3166_1_LC = ISO3166_1.toLowerCase(Locale.ENGLISH);
56 private static final String ISO3166_2_LC = ISO3166_2.toLowerCase(Locale.ENGLISH);
57 private static final String TAGINFO = "taginfo";
58
59 private static DataSet dataSet;
60
61 static volatile Map<String, GeoPropertyIndex<Boolean>> iso3166Cache;
62 static volatile Map<String, TaginfoRegionalInstance> taginfoCache;
63 static volatile Map<String, TaginfoRegionalInstance> taginfoGeofabrikCache;
64 static volatile Map<String, TagMap> customTagsCache;
65
66 private static final List<String> KNOWN_KEYS = Arrays.asList(ISO3166_1, ISO3166_2, TAGINFO, "type", "driving_side", "note");
67
68 private Territories() {
69 // Hide implicit public constructor for utility classes
70 }
71
72 /**
73 * Get all known ISO3166-1 and ISO3166-2 codes.
74 *
75 * @return the ISO3166-1 and ISO3166-2 codes for the given location
76 */
77 public static synchronized Set<String> getKnownIso3166Codes() {
78 return iso3166Cache.keySet();
79 }
80
81 /**
82 * Returns the {@link GeoPropertyIndex} for the given ISO3166-1 or ISO3166-2 code.
83 * @param code the ISO3166-1 or ISO3166-2 code
84 * @return the {@link GeoPropertyIndex} for the given {@code code}
85 * @since 14484
86 */
87 public static GeoPropertyIndex<Boolean> getGeoPropertyIndex(String code) {
88 return iso3166Cache.get(code);
89 }
90
91 /**
92 * Determine, if a point is inside a territory with the given ISO3166-1
93 * or ISO3166-2 code.
94 *
95 * @param code the ISO3166-1 or ISO3166-2 code
96 * @param ll the coordinates of the point
97 * @return true, if the point is inside a territory with the given code
98 */
99 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}
Note: See TracBrowser for help on using the repository browser.