2021-12-23 10:42:24 +00:00
|
|
|
package com.onthegomap.planetiler.util;
|
2021-09-10 00:46:20 +00:00
|
|
|
|
|
|
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
2021-12-23 10:42:24 +00:00
|
|
|
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
2021-09-10 00:46:20 +00:00
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.InputStream;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.Locale;
|
|
|
|
import java.util.Map;
|
|
|
|
import java.util.Set;
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
import java.util.stream.Stream;
|
|
|
|
import javax.annotation.concurrent.ThreadSafe;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A utility to search <a href="https://download.geofabrik.de/">Geofabrik Download Server</a> for a {@code .osm.pbf}
|
|
|
|
* download URL by name.
|
|
|
|
*
|
|
|
|
* @see <a href="https://download.geofabrik.de/technical.html">Geofabrik JSON index technical details</a>
|
|
|
|
*/
|
|
|
|
@ThreadSafe
|
|
|
|
public class Geofabrik {
|
|
|
|
|
|
|
|
private static volatile IndexJson index = null;
|
|
|
|
private static final ObjectMapper objectMapper = new ObjectMapper()
|
|
|
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetches the Geofabrik index and searches for a {@code .osm.pbf} resource to download where ID or name field
|
|
|
|
* contains all the tokens in {@code searchQuery}.
|
|
|
|
* <p>
|
2022-03-09 02:08:03 +00:00
|
|
|
* If an exact match is found, returns that. Otherwise, looks for a resource that contains {@code searchQuery} as a
|
2021-09-10 00:46:20 +00:00
|
|
|
* substring.
|
|
|
|
* <p>
|
|
|
|
* The index is only fetched once and cached after that.
|
|
|
|
*
|
|
|
|
* @param searchQuery the tokens to search for
|
2021-12-23 10:42:24 +00:00
|
|
|
* @param config planetiler config with user-agent and timeout to use when downloading files
|
2021-09-10 00:46:20 +00:00
|
|
|
* @return the URL of a {@code .osm.pbf} file with name or ID matching {@code searchQuery}
|
|
|
|
* @throws IllegalArgumentException if no matches, or more than one match is found.
|
|
|
|
*/
|
2021-12-23 10:42:24 +00:00
|
|
|
public static String getDownloadUrl(String searchQuery, PlanetilerConfig config) {
|
2021-10-20 01:57:47 +00:00
|
|
|
IndexJson index = getAndCacheIndex(config);
|
2021-09-10 00:46:20 +00:00
|
|
|
return searchIndexForDownloadUrl(searchQuery, index);
|
|
|
|
}
|
|
|
|
|
2021-12-23 10:42:24 +00:00
|
|
|
private synchronized static IndexJson getAndCacheIndex(PlanetilerConfig config) {
|
2021-09-10 00:46:20 +00:00
|
|
|
if (index == null) {
|
2022-03-09 02:08:03 +00:00
|
|
|
try (
|
|
|
|
InputStream inputStream = Downloader.openStream("https://download.geofabrik.de/index-v1-nogeom.json",
|
|
|
|
config)
|
|
|
|
) {
|
2021-09-10 00:46:20 +00:00
|
|
|
index = parseIndexJson(inputStream);
|
|
|
|
} catch (IOException e) {
|
|
|
|
throw new IllegalStateException(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Set<String> tokenize(String in) {
|
|
|
|
return Stream.of(in.toLowerCase(Locale.ROOT).split("[^a-z]+")).collect(Collectors.toSet());
|
|
|
|
}
|
|
|
|
|
|
|
|
static IndexJson parseIndexJson(InputStream indexJsonContent) throws IOException {
|
|
|
|
return objectMapper.readValue(indexJsonContent, IndexJson.class);
|
|
|
|
}
|
|
|
|
|
|
|
|
static String searchIndexForDownloadUrl(String searchQuery, IndexJson index) {
|
|
|
|
Set<String> searchTokens = tokenize(searchQuery);
|
|
|
|
List<PropertiesJson> approx = new ArrayList<>();
|
|
|
|
List<PropertiesJson> exact = new ArrayList<>();
|
|
|
|
for (var feature : index.features) {
|
|
|
|
PropertiesJson properties = feature.properties;
|
|
|
|
if (properties.urls.containsKey("pbf")) {
|
|
|
|
if (tokenize(properties.id).equals(searchTokens) ||
|
|
|
|
tokenize(properties.name).equals(searchTokens)) {
|
|
|
|
exact.add(properties);
|
|
|
|
} else if (tokenize(properties.name).containsAll(searchTokens)) {
|
|
|
|
approx.add(properties);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (exact.size() > 1) {
|
|
|
|
throw new IllegalArgumentException(
|
|
|
|
"Multiple exact matches for '" + searchQuery + "': " + exact.stream().map(d -> d.id).collect(
|
|
|
|
Collectors.joining(", ")));
|
|
|
|
} else if (exact.size() == 1) {
|
|
|
|
return exact.get(0).urls.get("pbf");
|
|
|
|
} else {
|
|
|
|
if (approx.size() > 1) {
|
|
|
|
throw new IllegalArgumentException(
|
|
|
|
"Multiple approximate matches for '" + searchQuery + "': " + approx.stream().map(d -> d.id).collect(
|
|
|
|
Collectors.joining(", ")));
|
|
|
|
} else if (approx.size() == 1) {
|
|
|
|
return approx.get(0).urls.get("pbf");
|
|
|
|
} else {
|
|
|
|
throw new IllegalArgumentException("No matches for '" + searchQuery + "'");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 01:45:56 +00:00
|
|
|
record PropertiesJson(String id, String parent, String name, Map<String, String> urls) {}
|
2021-09-10 00:46:20 +00:00
|
|
|
|
2022-02-24 01:45:56 +00:00
|
|
|
record FeatureJson(PropertiesJson properties) {}
|
2021-09-10 00:46:20 +00:00
|
|
|
|
2022-02-24 01:45:56 +00:00
|
|
|
record IndexJson(List<FeatureJson> features) {}
|
2021-09-10 00:46:20 +00:00
|
|
|
}
|