Allow for a central repository for MapWithAI-plugin compatible servers

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
pull/1/head v0.3.0
Taylor Smock 2019-12-12 09:05:40 -07:00
rodzic a14502d1d2
commit 31c60293f6
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 625F6A74A3E4311A
7 zmienionych plików z 307 dodań i 107 usunięć

Wyświetl plik

@ -0,0 +1,246 @@
package org.openstreetmap.josm.plugins.mapwithai.backend;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonException;
import javax.json.JsonObject;
import javax.json.JsonValue;
import javax.json.stream.JsonParser;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.BBox;
import org.openstreetmap.josm.io.CachedFile;
import org.openstreetmap.josm.plugins.mapwithai.backend.commands.conflation.DataUrl;
import org.openstreetmap.josm.tools.Logging;
import org.openstreetmap.josm.tools.Territories;
public class DataAvailability {
/** This points to a list of default sources that can be used with MapWithAI */
public static final String DEFAULT_SERVER_URL = "https://gokaart.gitlab.io/JOSM_MapWithAI/json/sources.json";
/** A map of tag -&gt; message of possible data types */
protected static final Map<String, String> POSSIBLE_DATA_POINTS = new TreeMap<>();
private static final String PROVIDES = "provides";
/**
* Map&lt;Source,
* Map&lt;(url|parameters|countries|license|osm_compatible|permission_url),
* Object&gt;&gt;
*/
protected static final Map<String, Map<String, Object>> COUNTRY_MAP = new HashMap<>();
/**
* This holds classes that can give availability of data for a specific service
*/
private static final List<Class<? extends DataAvailability>> DATA_SOURCES = new ArrayList<>();
static {
DATA_SOURCES.add(MapWithAIAvailability.class);
}
/**
* A map of countries to a map of available types
* ({@code Map<Country, Map<Type, IsAvailable>>}
*/
protected static final Map<String, Map<String, Boolean>> COUNTRIES = new HashMap<>();
private static class InstanceHelper {
static DataAvailability instance = new DataAvailability();
}
protected DataAvailability() {
if (DataAvailability.class.equals(this.getClass())) {
initialize();
}
}
/**
* Initialize the class
*/
private static void initialize() {
try (CachedFile jsonFile = new CachedFile(DEFAULT_SERVER_URL);
JsonParser jsonParser = Json.createParser(jsonFile.getContentReader());) {
jsonFile.setMaxAge(604_800);
jsonParser.next();
JsonObject jsonObject = jsonParser.getObject();
boolean initializePreferences = MapWithAIPreferenceHelper.getMapWithAIUrl().isEmpty();
for (Entry<String, JsonValue> entry : jsonObject.entrySet()) {
if (initializePreferences) {
DataUrl url = new DataUrl(entry.getKey(), entry.getValue().asJsonObject().getString("url", ""),
false, entry.getValue().asJsonObject().getJsonArray("parameters").toString());
MapWithAIPreferenceHelper.setMapWithAIUrl(url, false, true);
}
Logging.error("{0}: {1}", entry.getKey(), entry.getValue());
if (JsonValue.ValueType.OBJECT.equals(entry.getValue().getValueType())
&& entry.getValue().asJsonObject().containsKey("countries")) {
JsonValue countries = entry.getValue().asJsonObject().get("countries");
parseCountries(COUNTRIES, countries, entry.getValue());
}
}
} catch (JsonException | IOException e) {
Logging.debug(e);
}
DATA_SOURCES.forEach(clazz -> {
try {
clazz.getConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
Logging.debug(e);
}
});
}
/**
* Parse a JSON Value for country information
*
* @param countriesMap The countries map (will be modified)
* @param countries The country object (JsonObject)
* @param information The information for the source
*/
private static void parseCountries(Map<String, Map<String, Boolean>> countriesMap, JsonValue countries,
JsonValue information) {
if (JsonValue.ValueType.ARRAY.equals(countries.getValueType())) {
parseCountriesArray(countriesMap, countries.asJsonArray(), information);
} else if (JsonValue.ValueType.OBJECT.equals(countries.getValueType())) {
parseCountriesObject(countriesMap, countries.asJsonObject(), information);
}
}
/**
* Parse a JsonObject for countries
*
* @param countriesMap The countries map (will be modified)
* @param countryObject The country object (JsonObject)
* @param information The information for the source
*/
private static void parseCountriesObject(Map<String, Map<String, Boolean>> countriesMap, JsonObject countryObject,
JsonValue information) {
for (Entry<String, JsonValue> entry : countryObject.entrySet()) {
Map<String, Boolean> providesMap = countriesMap.getOrDefault(entry.getKey(), new TreeMap<>());
countriesMap.putIfAbsent(entry.getKey(), providesMap);
if (JsonValue.ValueType.ARRAY.equals(entry.getValue().getValueType())) {
for (String provide : entry.getValue().asJsonArray().parallelStream()
.filter(c -> JsonValue.ValueType.STRING.equals(c.getValueType())).map(JsonValue::toString)
.map(DataAvailability::stripQuotes).collect(Collectors.toList())) {
providesMap.put(provide, true);
}
}
if (providesMap.isEmpty() && JsonValue.ValueType.OBJECT.equals(information.getValueType())
&& information.asJsonObject().containsKey(PROVIDES)
&& JsonValue.ValueType.ARRAY.equals(information.asJsonObject().get(PROVIDES).getValueType())) {
for (String provide : information.asJsonObject().getJsonArray(PROVIDES).stream()
.filter(val -> JsonValue.ValueType.STRING.equals(val.getValueType())).map(JsonValue::toString)
.map(DataAvailability::stripQuotes).collect(Collectors.toList())) {
providesMap.put(provide, Boolean.TRUE);
}
}
}
}
/**
* Parse a JsonArray for countries
*
* @param countriesMap The countries map (will be modified)
* @param countryArray The country array (JsonArray)
* @param information The information for the source
*/
private static void parseCountriesArray(Map<String, Map<String, Boolean>> countriesMap, JsonArray countryArray,
JsonValue information) {
List<String> array = countryArray.parallelStream()
.filter(c -> JsonValue.ValueType.STRING.equals(c.getValueType())).map(JsonValue::toString)
.map(DataAvailability::stripQuotes).collect(Collectors.toList());
if (JsonValue.ValueType.OBJECT.equals(information.getValueType())
&& information.asJsonObject().containsKey(PROVIDES)) {
List<String> provides = information.asJsonObject().getJsonArray(PROVIDES).parallelStream()
.filter(p -> JsonValue.ValueType.STRING.equals(p.getValueType())).map(JsonValue::toString)
.map(DataAvailability::stripQuotes).collect(Collectors.toList());
for (String countryValue : array) {
for (String provide : provides) {
Map<String, Boolean> providesMap = countriesMap.getOrDefault(countryValue, new TreeMap<>());
countriesMap.putIfAbsent(countryValue, providesMap);
providesMap.put(provide, true);
}
}
}
}
/**
* Strip double quotes (") from a string
*
* @param string A string that may have quotes at the beginning, the end, or
* both
* @return A string that doesn't have quotes at the beginning or end
*/
public static String stripQuotes(String string) {
return string.replaceAll("(^\"|\"$)", "");
}
/**
* @return the unique instance
*/
public static DataAvailability getInstance() {
if (InstanceHelper.instance == null || COUNTRIES.isEmpty()) {
InstanceHelper.instance = new DataAvailability();
}
return InstanceHelper.instance;
}
/**
* @param bbox An area that may have data
* @return True if one of the corners of the {@code bbox} is in a country with
* available data.
*/
public boolean hasData(BBox bbox) {
final List<LatLon> corners = new ArrayList<>();
corners.add(bbox.getBottomRight());
corners.add(new LatLon(bbox.getBottomRightLat(), bbox.getTopLeftLon()));
corners.add(bbox.getTopLeft());
corners.add(new LatLon(bbox.getTopLeftLat(), bbox.getBottomRightLon()));
return corners.parallelStream().anyMatch(this::hasData);
}
/**
* @param latLon A point that may have data from MapWithAI
* @return true if it is in an ares with data from MapWithAI
*/
public boolean hasData(LatLon latLon) {
boolean returnBoolean = false;
for (final Entry<String, Map<String, Boolean>> entry : COUNTRIES.entrySet()) {
Logging.debug(entry.getKey());
if (Territories.isIso3166Code(entry.getKey(), latLon)) {
returnBoolean = entry.getValue().entrySet().parallelStream().anyMatch(Entry::getValue);
break;
}
}
return returnBoolean;
}
/**
* Get data types that may be visible around a point
*
* @param latLon The point of interest
* @return A map that may have available data types (or be empty)
*/
public static Map<String, Boolean> getDataTypes(LatLon latLon) {
return COUNTRIES.entrySet().parallelStream().filter(entry -> Territories.isIso3166Code(entry.getKey(), latLon))
.map(Entry::getValue).findFirst().orElse(Collections.emptyMap());
}
/**
* Get the URL that this class is responsible for
*
* @return The url (e.g., example.com/addresses/{bbox}), or null if generic
*/
public String getUrl() {
return null;
}
}

Wyświetl plik

@ -9,7 +9,6 @@ import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.TreeMap;
import java.util.stream.Collectors;
@ -118,19 +117,16 @@ public class MapWithAIAction extends JosmAction {
}
final StringBuilder message = new StringBuilder();
message.append(MapWithAIPlugin.NAME).append(": ");
final MapWithAIAvailability availability = MapWithAIAvailability.getInstance();
DataAvailability.getInstance(); // force initialization, if it hasn't already occured
final Map<String, Boolean> availableTypes = new TreeMap<>();
for (final Bounds bound : bounds) {
availability.getDataTypes(bound.getCenter())
DataAvailability.getDataTypes(bound.getCenter())
.forEach((type, available) -> availableTypes.merge(type, available, Boolean::logicalOr));
}
final List<String> types = availableTypes.entrySet().stream().filter(Entry::getValue)
.map(entry -> MapWithAIAvailability.getPossibleDataTypesAndMessages().get(entry.getKey()))
.collect(Collectors.toList());
if (types.isEmpty()) {
if (availableTypes.isEmpty()) {
message.append(tr("No data available"));
} else {
message.append("Data available: ").append(String.join(", ", types));
message.append("Data available: ").append(String.join(", ", availableTypes.keySet()));
}
notification.setContent(message.toString());

Wyświetl plik

@ -4,12 +4,10 @@ package org.openstreetmap.josm.plugins.mapwithai.backend;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
@ -22,8 +20,6 @@ import javax.json.JsonObject;
import javax.json.JsonValue;
import javax.json.stream.JsonParser;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.BBox;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.io.CachedFile;
@ -31,18 +27,11 @@ import org.openstreetmap.josm.plugins.mapwithai.MapWithAIPlugin;
import org.openstreetmap.josm.tools.Logging;
import org.openstreetmap.josm.tools.Territories;
public final class MapWithAIAvailability {
public final class MapWithAIAvailability extends DataAvailability {
private static String rapidReleases = "https://raw.githubusercontent.com/facebookmicrosites/Open-Mapping-At-Facebook/master/data/rapid_releases.geojson";
/** A map of country to a map of available types */
private static final Map<String, Map<String, Boolean>> COUNTRIES = new HashMap<>();
private static final Map<String, String> POSSIBLE_DATA_POINTS = new TreeMap<>();
/** Original country, replacement countries */
private static final Map<String, Collection<String>> COUNTRY_NAME_FIX = new HashMap<>();
private static class InstanceHelper {
static MapWithAIAvailability instance = new MapWithAIAvailability();
}
static {
COUNTRY_NAME_FIX.put("Egypt", Collections.singleton("Egypt, Arab Rep."));
COUNTRY_NAME_FIX.put("Dem. Rep. Congo", Collections.singleton("Congo, Dem. Rep."));
@ -65,11 +54,12 @@ public final class MapWithAIAvailability {
COUNTRY_NAME_FIX.put("Congo", Collections.singleton("Congo, Rep."));
COUNTRY_NAME_FIX.put("North Macedonia", Collections.singleton("Macedonia, FYR"));
COUNTRY_NAME_FIX.put("Venezuela", Collections.singleton("Venezuela, RB"));
POSSIBLE_DATA_POINTS.put("roads", "RapiD roads available");
POSSIBLE_DATA_POINTS.put("buildings", "MS buildings available");
POSSIBLE_DATA_POINTS.put("highway", "RapiD roads available");
POSSIBLE_DATA_POINTS.put("building", "MS buildings available");
}
private MapWithAIAvailability() {
public MapWithAIAvailability() {
super();
try (CachedFile cachedRapidReleases = new CachedFile(rapidReleases);
JsonParser parser = Json.createParser(cachedRapidReleases.getContentReader())) {
cachedRapidReleases.setMaxAge(604_800);
@ -95,16 +85,6 @@ public final class MapWithAIAvailability {
}
}
/**
* @return the unique instance
*/
public static MapWithAIAvailability getInstance() {
if (COUNTRIES.isEmpty()) {
InstanceHelper.instance = new MapWithAIAvailability();
}
return InstanceHelper.instance;
}
private static Map<String, Map<String, Boolean>> parseForCountries(JsonArray countries) {
final Map<String, Map<String, Boolean>> returnCountries = new TreeMap<>();
Territories.initialize();
@ -146,59 +126,15 @@ public final class MapWithAIAvailability {
return COUNTRY_NAME_FIX.containsKey(name) ? COUNTRY_NAME_FIX.get(name) : Collections.singleton(name);
}
/**
* @param bbox An area that may have data
* @return True if one of the corners of the {@code bbox} is in a country with
* available data.
*/
public boolean hasData(BBox bbox) {
final List<LatLon> corners = new ArrayList<>();
corners.add(bbox.getBottomRight());
corners.add(new LatLon(bbox.getBottomRightLat(), bbox.getTopLeftLon()));
corners.add(bbox.getTopLeft());
corners.add(new LatLon(bbox.getTopLeftLat(), bbox.getBottomRightLon()));
return corners.parallelStream().anyMatch(this::hasData);
}
/**
* @param latLon A point that may have data from MapWithAI
* @return true if it is in an ares with data from MapWithAI
*/
public boolean hasData(LatLon latLon) {
boolean returnBoolean = false;
for (final Entry<String, Map<String, Boolean>> entry : COUNTRIES.entrySet()) {
Logging.debug(entry.getKey());
if (Territories.isIso3166Code(entry.getKey(), latLon)) {
returnBoolean = entry.getValue().entrySet().parallelStream().anyMatch(Entry::getValue);
break;
}
}
return returnBoolean;
}
/**
* Get data types that may be visible around a point
*
* @param latLon The point of interest
* @return A map that may have available data types (or be empty)
*/
public Map<String, Boolean> getDataTypes(LatLon latLon) {
return COUNTRIES.entrySet().parallelStream().filter(entry -> Territories.isIso3166Code(entry.getKey(), latLon))
.map(Entry::getValue).findFirst().orElse(Collections.emptyMap());
}
/**
* @return A map of possible data types with their messages
*/
public static Map<String, String> getPossibleDataTypesAndMessages() {
return POSSIBLE_DATA_POINTS;
}
/**
* @param url The URL where the MapWithAI data releases are.
*/
public static void setReleaseUrl(String url) {
rapidReleases = url;
}
@Override
public String getUrl() {
return MapWithAIPreferenceHelper.DEFAULT_MAPWITHAI_API;
}
}

Wyświetl plik

@ -8,12 +8,14 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeSet;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;
import javax.json.JsonObject;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

Wyświetl plik

@ -11,6 +11,7 @@ import java.util.stream.Collectors;
import org.openstreetmap.josm.data.osm.Tag;
import org.openstreetmap.josm.plugins.mapwithai.MapWithAIPlugin;
import org.openstreetmap.josm.plugins.mapwithai.backend.commands.conflation.DataUrl;
import org.openstreetmap.josm.spi.preferences.Config;
public final class MapWithAIPreferenceHelper {
@ -69,6 +70,14 @@ public final class MapWithAIPreferenceHelper {
public static List<Map<String, String>> getMapWithAIURLs() {
final List<Map<String, String>> returnMap = Config.getPref().getListOfMaps(API_MAP_CONFIG, new ArrayList<>())
.stream().map(TreeMap::new).collect(Collectors.toList());
if (MapWithAIDataUtils.getLayer(false) != null) {
TreeMap<String, String> layerMap = new TreeMap<>();
layerMap.put("url", MapWithAIDataUtils.getLayer(false).getMapWithAIUrl());
if (layerMap.get("url") != null && !layerMap.get("url").trim().isEmpty() && returnMap.parallelStream()
.noneMatch(map -> map.getOrDefault("url", "").equals(layerMap.get("url")))) {
returnMap.add(layerMap);
}
}
if (returnMap.isEmpty()) {
final List<String> defaultAPIs = Collections.singletonList(DEFAULT_MAPWITHAI_API);
final List<String> defaultList = Config.getPref().getList(API_CONFIG).isEmpty() ? defaultAPIs
@ -137,24 +146,29 @@ public final class MapWithAIPreferenceHelper {
* sessions
*/
public static void setMapWithAIUrl(String source, String url, boolean enabled, boolean permanent) {
final MapWithAILayer layer = MapWithAIDataUtils.getLayer(false);
final String setUrl = url;
setMapWithAIUrl(new DataUrl(source, url, permanent), enabled, permanent);
}
final List<Map<String, String>> urls = new ArrayList<>(getMapWithAIURLs());
Map<String, String> addOrModifyMap = urls.parallelStream()
.filter(map -> map.getOrDefault(URL_STRING, "").equals(url)).findFirst().orElse(new TreeMap<>());
if (addOrModifyMap.isEmpty()) {
urls.add(addOrModifyMap);
} else {
urls.remove(addOrModifyMap);
addOrModifyMap = new TreeMap<>(addOrModifyMap);
urls.add(addOrModifyMap);
public static void setMapWithAIUrl(DataUrl dataUrl, boolean enabled, boolean permanent) {
final MapWithAILayer layer = MapWithAIDataUtils.getLayer(false);
final String setUrl = dataUrl.getMap().getOrDefault("url", DEFAULT_MAPWITHAI_API);
if (permanent) {
final List<Map<String, String>> urls = new ArrayList<>(getMapWithAIURLs());
Map<String, String> addOrModifyMap = urls.parallelStream()
.filter(map -> map.getOrDefault(URL_STRING, "").equals(setUrl)).findFirst().orElse(new TreeMap<>());
if (addOrModifyMap.isEmpty()) {
urls.add(addOrModifyMap);
} else {
urls.remove(addOrModifyMap);
addOrModifyMap = new TreeMap<>(addOrModifyMap);
urls.add(addOrModifyMap);
}
addOrModifyMap.putAll(dataUrl.getMap());
addOrModifyMap.put(ENABLED_STRING, Boolean.toString(enabled));
setMapWithAIURLs(urls);
}
addOrModifyMap.put(URL_STRING, url);
addOrModifyMap.put(SOURCE_STRING, source);
addOrModifyMap.put(ENABLED_STRING, Boolean.toString(permanent));
setMapWithAIURLs(urls);
if ((layer != null) && !permanent && enabled) {
if (layer != null && !permanent && enabled) {
layer.setMapWithAIUrl(setUrl);
}
}

Wyświetl plik

@ -22,6 +22,7 @@ import org.openstreetmap.josm.gui.Notification;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.testutils.JOSMTestRules;
import org.openstreetmap.josm.tools.Territories;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@ -35,6 +36,7 @@ public class MapWithAIActionTest {
@Before
public void setUp() {
action = new MapWithAIAction();
Territories.initialize();
}
@Test

Wyświetl plik

@ -20,7 +20,7 @@ import com.github.tomakehurst.wiremock.WireMockServer;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
public class MapWithAIAvailabilityTest {
private MapWithAIAvailability instance;
private DataAvailability instance;
@Rule
@SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
@ -34,7 +34,7 @@ public class MapWithAIAvailabilityTest {
MapWithAIAvailability.setReleaseUrl(
wireMock.baseUrl() + "/facebookmicrosites/Open-Mapping-At-Facebook/master/data/rapid_releases.geojson");
Territories.initialize();
instance = MapWithAIAvailability.getInstance();
instance = DataAvailability.getInstance();
LatLon temp = new LatLon(40, -100);
await().atMost(Durations.TEN_SECONDS).until(() -> Territories.isIso3166Code("US", temp));
}
@ -60,12 +60,16 @@ public class MapWithAIAvailabilityTest {
@Test
public void testgetDataLatLon() {
Assert.assertTrue(instance.getDataTypes(new LatLon(0, 0)).isEmpty());
Assert.assertTrue(instance.getDataTypes(new LatLon(40, -100)).get("roads"));
Assert.assertTrue(instance.getDataTypes(new LatLon(40, -100)).get("buildings"));
Assert.assertFalse(instance.getDataTypes(new LatLon(45.424722, -75.695)).get("roads"));
Assert.assertTrue(instance.getDataTypes(new LatLon(45.424722, -75.695)).get("buildings"));
Assert.assertTrue(instance.getDataTypes(new LatLon(19.433333, -99.133333)).get("roads"));
Assert.assertFalse(instance.getDataTypes(new LatLon(19.433333, -99.133333)).get("buildings"));
Assert.assertTrue(DataAvailability.getDataTypes(new LatLon(0, 0)).isEmpty());
Assert.assertTrue(DataAvailability.getDataTypes(new LatLon(40, -100)).getOrDefault("highway", false));
Assert.assertTrue(DataAvailability.getDataTypes(new LatLon(40, -100)).getOrDefault("building", false));
Assert.assertFalse(
DataAvailability.getDataTypes(new LatLon(45.424722, -75.695)).getOrDefault("highway", false));
Assert.assertTrue(
DataAvailability.getDataTypes(new LatLon(45.424722, -75.695)).getOrDefault("building", false));
Assert.assertTrue(
DataAvailability.getDataTypes(new LatLon(19.433333, -99.133333)).getOrDefault("highway", false));
Assert.assertFalse(
DataAvailability.getDataTypes(new LatLon(19.433333, -99.133333)).getOrDefault("building", false));
}
}