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}